forked from RiseUP/riseup_squad_03
new file: public/favicon.svg
deleted: src/assets/hero.png modified: src/components/AppShell.jsx modified: src/components/calendar/AgendaDailyView.jsx modified: src/components/calendar/AgendaMonthlyView.jsx modified: src/components/calendar/AgendaWeeklyView.jsx modified: src/hooks/useAgenda.js modified: src/index.css modified: src/mappers/appointmentMapper.js modified: src/mappers/reportMapper.js modified: src/pages/AgendaPage.jsx modified: src/pages/AuthPages.jsx modified: src/pages/HomePage.jsx modified: src/pages/MessagesPage.jsx modified: src/pages/PatientsPage.jsx modified: src/pages/ProfilePage.jsx modified: src/pages/ReportsPage.jsx modified: src/pages/SettingsPage.jsx modified: src/repositories/appointmentRepository.js modified: src/repositories/settingsRepository.js
This commit is contained in:
@@ -1,20 +1,20 @@
|
||||
import {
|
||||
addDays,
|
||||
subDays,
|
||||
addWeeks,
|
||||
subWeeks,
|
||||
addMonths,
|
||||
subMonths,
|
||||
addWeeks,
|
||||
endOfWeek,
|
||||
format,
|
||||
startOfWeek,
|
||||
subDays,
|
||||
subMonths,
|
||||
subWeeks,
|
||||
} from 'date-fns'
|
||||
import { ptBR } from 'date-fns/locale'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { AgendaDailyView } from '../components/calendar/AgendaDailyView.jsx'
|
||||
import { AgendaWeeklyView } from '../components/calendar/AgendaWeeklyView.jsx'
|
||||
import { AgendaMonthlyView } from '../components/calendar/AgendaMonthlyView.jsx'
|
||||
import { AgendaWeeklyView } from '../components/calendar/AgendaWeeklyView.jsx'
|
||||
import { useAgenda } from '../hooks/useAgenda.js'
|
||||
import { formatLocalDateInput, parseLocalDate } from '../utils/agendaDate.js'
|
||||
|
||||
@@ -23,6 +23,7 @@ const statusFilters = [
|
||||
{ label: 'Confirmadas', value: 'Confirmada' },
|
||||
{ label: 'Em triagem', value: 'Em triagem' },
|
||||
{ label: 'Aguardando', value: 'Aguardando' },
|
||||
{ label: 'Canceladas', value: 'Cancelada' },
|
||||
]
|
||||
|
||||
const viewFilters = [
|
||||
@@ -32,8 +33,9 @@ const viewFilters = [
|
||||
]
|
||||
|
||||
const appointmentTypeOptions = ['Retorno', 'Primeira consulta', 'Exame', 'Avaliação pre-op']
|
||||
const appointmentStatusOptions = ['Confirmada', 'Em triagem', 'Aguardando']
|
||||
|
||||
export function AgendaPage({ navigate }) {
|
||||
export function AgendaPage() {
|
||||
const [modalPatientSearch, setModalPatientSearch] = useState('')
|
||||
const [modalDoctorSearch, setModalDoctorSearch] = useState('')
|
||||
const {
|
||||
@@ -57,10 +59,14 @@ export function AgendaPage({ navigate }) {
|
||||
unitFilter,
|
||||
setUnitFilter,
|
||||
modalOpen,
|
||||
setModalOpen,
|
||||
editingAppointment,
|
||||
form,
|
||||
updateForm,
|
||||
handleCreate,
|
||||
openCreateModal,
|
||||
openAppointmentModal,
|
||||
closeAppointmentModal,
|
||||
handleSubmitAppointment,
|
||||
handleCancelAppointment,
|
||||
visibleAppointments,
|
||||
availableSlots,
|
||||
slotsLoading,
|
||||
@@ -79,42 +85,41 @@ export function AgendaPage({ navigate }) {
|
||||
const weekEnd = endOfWeek(baseDate, { weekStartsOn: 0 })
|
||||
const isDoctorScope = agendaScope === 'doctor'
|
||||
const unitOptions = [
|
||||
...new Set(
|
||||
professionals
|
||||
.map((professional) => professional.unit)
|
||||
.filter(Boolean),
|
||||
),
|
||||
...new Set(professionals.map((professional) => professional.unit).filter(Boolean)),
|
||||
].sort((a, b) => a.localeCompare(b, 'pt-BR'))
|
||||
const filteredPatients = (() => {
|
||||
const query = normalizeSearch(modalPatientSearch)
|
||||
if (!query) return patients
|
||||
|
||||
return patients.filter((patient) =>
|
||||
[patient.name, patient.full_name, patient.nome, patient.cpf, patient.email]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase()
|
||||
.includes(query),
|
||||
)
|
||||
})()
|
||||
const filteredProfessionals = (() => {
|
||||
const query = normalizeSearch(modalDoctorSearch)
|
||||
if (!query) return professionals
|
||||
|
||||
return professionals.filter((professional) =>
|
||||
[professional.name, professional.email, professional.unit]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase()
|
||||
.includes(query),
|
||||
)
|
||||
})()
|
||||
const filteredPatients = filterBySearch(patients, modalPatientSearch, (patient) => [
|
||||
patient.name,
|
||||
patient.full_name,
|
||||
patient.nome,
|
||||
patient.cpf,
|
||||
patient.email,
|
||||
])
|
||||
const filteredProfessionals = filterBySearch(professionals, modalDoctorSearch, (professional) => [
|
||||
professional.name,
|
||||
professional.email,
|
||||
professional.unit,
|
||||
])
|
||||
const selectedPatient = patients.find((patient) => String(patient.id) === String(form.patientId))
|
||||
const selectedProfessional = professionals.find((professional) => String(professional.id) === String(form.professionalId))
|
||||
const timeOptions = getTimeOptions(form.time, availableSlots)
|
||||
|
||||
function openCreate(options = {}) {
|
||||
setModalPatientSearch('')
|
||||
setModalDoctorSearch('')
|
||||
openCreateModal(options)
|
||||
}
|
||||
|
||||
function openManage(appointment) {
|
||||
setModalPatientSearch('')
|
||||
setModalDoctorSearch('')
|
||||
openAppointmentModal(appointment)
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
setModalPatientSearch('')
|
||||
setModalDoctorSearch('')
|
||||
closeAppointmentModal()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex max-w-[1180px] flex-col gap-8 text-[#e5e5e5]">
|
||||
@@ -169,7 +174,7 @@ export function AgendaPage({ navigate }) {
|
||||
<button
|
||||
className="h-9 rounded-sm border border-[#3b82f6] bg-[#3b82f6] px-4 text-sm font-semibold text-white shadow-[0_10px_15px_rgba(59,130,246,0.16)] transition hover:bg-[#3478ed] disabled:cursor-not-allowed disabled:border-[#404040] disabled:bg-[#303030] disabled:text-[#737373] disabled:shadow-none"
|
||||
disabled={!canCreateAppointment}
|
||||
onClick={() => setModalOpen(true)}
|
||||
onClick={() => openCreate()}
|
||||
type="button"
|
||||
>
|
||||
+ Novo agendamento
|
||||
@@ -283,7 +288,7 @@ export function AgendaPage({ navigate }) {
|
||||
<AgendaWeeklyView
|
||||
baseDate={baseDate}
|
||||
appointments={visibleAppointments}
|
||||
onAppointmentClick={(appointment) => navigate(`/pacientes/${appointment.patientId}`)}
|
||||
onAppointmentClick={openManage}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -300,9 +305,11 @@ export function AgendaPage({ navigate }) {
|
||||
|
||||
{activeView === 'Dia' && (
|
||||
<AgendaDailyView
|
||||
baseDate={baseDate}
|
||||
appointments={visibleAppointments}
|
||||
onAppointmentClick={(appointment) => navigate(`/pacientes/${appointment.patientId}`)}
|
||||
baseDate={baseDate}
|
||||
canCreateAppointment={canCreateAppointment}
|
||||
onAppointmentClick={openManage}
|
||||
onSlotCreate={(time) => openCreate({ time })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -310,54 +317,93 @@ export function AgendaPage({ navigate }) {
|
||||
</section>
|
||||
)}
|
||||
|
||||
<DarkModal onClose={() => setModalOpen(false)} open={modalOpen} title="Novo agendamento">
|
||||
<form className="grid gap-4" onSubmit={handleCreate}>
|
||||
<DarkField label="Dia do agendamento">
|
||||
<input
|
||||
className="h-11 rounded-md border border-[#404040] bg-[#303030] px-3 text-sm text-[#e5e5e5] outline-none [color-scheme:dark] focus:border-[#3b82f6]"
|
||||
onChange={(event) => {
|
||||
const parsedDate = parseLocalDate(event.target.value)
|
||||
if (parsedDate) setBaseDate(parsedDate)
|
||||
}}
|
||||
type="date"
|
||||
value={formatLocalDateInput(baseDate)}
|
||||
/>
|
||||
</DarkField>
|
||||
<DarkModal onClose={closeModal} open={modalOpen} title={editingAppointment ? 'Gerenciar agendamento' : 'Novo agendamento'}>
|
||||
<form className="grid gap-4" onSubmit={handleSubmitAppointment}>
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<div className="grid content-start gap-4">
|
||||
<DarkField label="Paciente">
|
||||
<input
|
||||
className="h-10 rounded-md border border-[#404040] bg-[#303030] px-3 text-sm text-[#e5e5e5] outline-none transition placeholder:text-[#737373] focus:border-[#3b82f6]"
|
||||
onChange={(event) => {
|
||||
setModalPatientSearch(event.target.value)
|
||||
updateForm('patientId', '')
|
||||
}}
|
||||
placeholder="Pesquisar paciente"
|
||||
type="search"
|
||||
value={modalPatientSearch || getPatientLabel(selectedPatient)}
|
||||
/>
|
||||
<SearchResults
|
||||
emptyText="Nenhum paciente encontrado."
|
||||
getLabel={getPatientLabel}
|
||||
items={filteredPatients.slice(0, 5)}
|
||||
onSelect={(patient) => {
|
||||
updateForm('patientId', patient.id)
|
||||
setModalPatientSearch(getPatientLabel(patient))
|
||||
}}
|
||||
selectedId={form.patientId}
|
||||
/>
|
||||
</DarkField>
|
||||
|
||||
<DarkField label="Paciente">
|
||||
<input
|
||||
className="h-10 rounded-md border border-[#404040] bg-[#303030] px-3 text-sm text-[#e5e5e5] outline-none transition placeholder:text-[#737373] focus:border-[#3b82f6]"
|
||||
onChange={(event) => {
|
||||
setModalPatientSearch(event.target.value)
|
||||
updateForm('patientId', '')
|
||||
}}
|
||||
placeholder="Pesquisar paciente"
|
||||
type="search"
|
||||
value={modalPatientSearch || getPatientLabel(selectedPatient)}
|
||||
/>
|
||||
<SearchResults
|
||||
emptyText="Nenhum paciente encontrado."
|
||||
getLabel={getPatientLabel}
|
||||
items={filteredPatients.slice(0, 6)}
|
||||
onSelect={(patient) => {
|
||||
updateForm('patientId', patient.id)
|
||||
setModalPatientSearch(getPatientLabel(patient))
|
||||
}}
|
||||
selectedId={form.patientId}
|
||||
/>
|
||||
</DarkField>
|
||||
<DarkField label="Profissional">
|
||||
{isDoctorScope ? (
|
||||
<input
|
||||
className="h-11 rounded-md border border-[#404040] bg-[#262626] px-3 text-sm text-[#a3a3a3] outline-none"
|
||||
disabled
|
||||
readOnly
|
||||
value={currentProfessional?.name || 'Médico não vinculado'}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<input
|
||||
className="h-10 rounded-md border border-[#404040] bg-[#303030] px-3 text-sm text-[#e5e5e5] outline-none transition placeholder:text-[#737373] focus:border-[#3b82f6]"
|
||||
onChange={(event) => {
|
||||
setModalDoctorSearch(event.target.value)
|
||||
updateForm('professionalId', '')
|
||||
}}
|
||||
placeholder="Pesquisar médico"
|
||||
type="search"
|
||||
value={modalDoctorSearch || selectedProfessional?.name || ''}
|
||||
/>
|
||||
<SearchResults
|
||||
emptyText="Nenhum médico encontrado."
|
||||
getDescription={(professional) => professional.unit || professional.email}
|
||||
getLabel={(professional) => professional.name}
|
||||
items={filteredProfessionals.slice(0, 5)}
|
||||
onSelect={(professional) => {
|
||||
updateForm('professionalId', professional.id)
|
||||
setModalDoctorSearch(professional.name)
|
||||
}}
|
||||
selectedId={form.professionalId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</DarkField>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<DarkField label="Horário">
|
||||
{availableSlots.length ? (
|
||||
<div className="grid content-start gap-4">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<DarkField label="Dia">
|
||||
<input
|
||||
className="h-11 rounded-md border border-[#404040] bg-[#303030] px-3 text-sm text-[#e5e5e5] outline-none [color-scheme:dark] focus:border-[#3b82f6]"
|
||||
onChange={(event) => {
|
||||
const parsedDate = parseLocalDate(event.target.value)
|
||||
if (parsedDate) setBaseDate(parsedDate)
|
||||
}}
|
||||
type="date"
|
||||
value={formatLocalDateInput(baseDate)}
|
||||
/>
|
||||
</DarkField>
|
||||
|
||||
<DarkField label="Horário">
|
||||
{timeOptions.length ? (
|
||||
<select
|
||||
className="h-11 rounded-md border border-[#404040] bg-[#303030] px-3 text-sm text-[#e5e5e5] outline-none focus:border-[#3b82f6]"
|
||||
onChange={(event) => updateForm('time', event.target.value)}
|
||||
value={form.time}
|
||||
>
|
||||
{availableSlots.map((slot) => (
|
||||
<option key={slot.time} value={slot.time}>
|
||||
{slot.time}
|
||||
{timeOptions.map((time) => (
|
||||
<option key={time} value={time}>
|
||||
{time}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -369,88 +415,99 @@ export function AgendaPage({ navigate }) {
|
||||
value={form.time}
|
||||
/>
|
||||
)}
|
||||
{slotsLoading ? (
|
||||
<span className="text-xs font-normal text-[#a3a3a3]">Calculando horários...</span>
|
||||
) : null}
|
||||
{slotsError ? (
|
||||
<span className="text-xs font-normal text-amber-400">{slotsError}</span>
|
||||
) : null}
|
||||
</DarkField>
|
||||
<DarkField label="Formato">
|
||||
<select
|
||||
className="h-11 rounded-md border border-[#404040] bg-[#303030] px-3 text-sm text-[#e5e5e5] outline-none focus:border-[#3b82f6]"
|
||||
onChange={(event) => updateForm('mode', event.target.value)}
|
||||
value={form.mode}
|
||||
>
|
||||
<option>Teleconsulta</option>
|
||||
<option>Presencial</option>
|
||||
</select>
|
||||
</DarkField>
|
||||
{slotsLoading ? <span className="text-xs font-normal text-[#a3a3a3]">Calculando horários...</span> : null}
|
||||
{slotsError ? <span className="text-xs font-normal text-amber-400">{slotsError}</span> : null}
|
||||
</DarkField>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<DarkField label="Formato">
|
||||
<select
|
||||
className="h-11 rounded-md border border-[#404040] bg-[#303030] px-3 text-sm text-[#e5e5e5] outline-none focus:border-[#3b82f6]"
|
||||
onChange={(event) => updateForm('mode', event.target.value)}
|
||||
value={form.mode}
|
||||
>
|
||||
<option>Teleconsulta</option>
|
||||
<option>Presencial</option>
|
||||
</select>
|
||||
</DarkField>
|
||||
|
||||
<DarkField label="Status">
|
||||
<select
|
||||
className="h-11 rounded-md border border-[#404040] bg-[#303030] px-3 text-sm text-[#e5e5e5] outline-none focus:border-[#3b82f6]"
|
||||
onChange={(event) => updateForm('status', event.target.value)}
|
||||
value={form.status}
|
||||
>
|
||||
{!appointmentStatusOptions.includes(form.status) && form.status ? (
|
||||
<option value={form.status}>{form.status}</option>
|
||||
) : null}
|
||||
{appointmentStatusOptions.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</DarkField>
|
||||
</div>
|
||||
|
||||
<DarkField label="Tipo de consulta">
|
||||
<select
|
||||
className="h-11 rounded-md border border-[#404040] bg-[#303030] px-3 text-sm text-[#e5e5e5] outline-none focus:border-[#3b82f6]"
|
||||
onChange={(event) => updateForm('type', event.target.value)}
|
||||
value={form.type}
|
||||
>
|
||||
{appointmentTypeOptions.map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{type}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</DarkField>
|
||||
|
||||
<DarkField label="Observações">
|
||||
<textarea
|
||||
className="min-h-24 resize-y rounded-md border border-[#404040] bg-[#303030] px-3 py-2 text-sm leading-5 text-[#e5e5e5] outline-none transition placeholder:text-[#737373] focus:border-[#3b82f6]"
|
||||
onChange={(event) => updateForm('notes', event.target.value)}
|
||||
placeholder="Observações sobre o agendamento"
|
||||
value={form.notes}
|
||||
/>
|
||||
</DarkField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DarkField label="Profissional">
|
||||
{isDoctorScope ? (
|
||||
<input
|
||||
className="h-11 rounded-md border border-[#404040] bg-[#262626] px-3 text-sm text-[#a3a3a3] outline-none"
|
||||
disabled
|
||||
readOnly
|
||||
value={currentProfessional?.name || 'Médico não vinculado'}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<input
|
||||
className="h-10 rounded-md border border-[#404040] bg-[#303030] px-3 text-sm text-[#e5e5e5] outline-none transition placeholder:text-[#737373] focus:border-[#3b82f6]"
|
||||
onChange={(event) => {
|
||||
setModalDoctorSearch(event.target.value)
|
||||
updateForm('professionalId', '')
|
||||
}}
|
||||
placeholder="Pesquisar médico"
|
||||
type="search"
|
||||
value={modalDoctorSearch || selectedProfessional?.name || ''}
|
||||
/>
|
||||
<SearchResults
|
||||
emptyText="Nenhum médico encontrado."
|
||||
getDescription={(professional) => professional.unit || professional.email}
|
||||
getLabel={(professional) => professional.name}
|
||||
items={filteredProfessionals.slice(0, 6)}
|
||||
onSelect={(professional) => {
|
||||
updateForm('professionalId', professional.id)
|
||||
setModalDoctorSearch(professional.name)
|
||||
}}
|
||||
selectedId={form.professionalId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</DarkField>
|
||||
|
||||
<DarkField label="Tipo de consulta">
|
||||
<select
|
||||
className="h-11 rounded-md border border-[#404040] bg-[#303030] px-3 text-sm text-[#e5e5e5] outline-none focus:border-[#3b82f6]"
|
||||
onChange={(event) => updateForm('type', event.target.value)}
|
||||
value={form.type}
|
||||
>
|
||||
{appointmentTypeOptions.map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{type}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</DarkField>
|
||||
{editingAppointment ? (
|
||||
<div className="rounded-xl border border-[#404040] bg-[#1f1f1f] px-4 py-3 text-sm text-[#a3a3a3]">
|
||||
<p>
|
||||
Agendamento de {selectedPatient ? getPatientLabel(selectedPatient) : 'paciente não informado'} às {form.time}.
|
||||
</p>
|
||||
<p className="mt-1">Status atual: {form.status}</p>
|
||||
{form.notes ? <p className="mt-1">Observações: {form.notes}</p> : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-wrap justify-end gap-3 pt-2">
|
||||
{editingAppointment ? (
|
||||
<button
|
||||
className="mr-auto h-10 rounded-sm border border-red-500/40 bg-red-950/20 px-4 text-sm font-semibold text-red-200 transition hover:bg-red-950/35"
|
||||
onClick={handleCancelAppointment}
|
||||
type="button"
|
||||
>
|
||||
Cancelar agendamento
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
className="h-10 rounded-sm border border-[#404040] bg-[#303030] px-4 text-sm font-semibold text-[#e5e5e5] transition hover:bg-[#333333]"
|
||||
onClick={() => setModalOpen(false)}
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
>
|
||||
Cancelar
|
||||
Fechar
|
||||
</button>
|
||||
<button
|
||||
className="h-10 rounded-sm border border-[#3b82f6] bg-[#3b82f6] px-4 text-sm font-semibold text-white transition hover:bg-[#3478ed] disabled:cursor-not-allowed disabled:border-[#404040] disabled:bg-[#303030] disabled:text-[#737373]"
|
||||
disabled={!canCreateAppointment}
|
||||
type="submit"
|
||||
>
|
||||
Salvar
|
||||
{editingAppointment ? 'Salvar alterações' : 'Salvar'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -469,13 +526,11 @@ function DarkField({ children, label }) {
|
||||
}
|
||||
|
||||
function DarkModal({ children, onClose, open, title }) {
|
||||
if (!open) {
|
||||
return null
|
||||
}
|
||||
if (!open) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center bg-black/60 p-4 sm:items-center">
|
||||
<div className="w-full max-w-xl rounded-2xl border border-[#404040] bg-[#262626] shadow-2xl">
|
||||
<div className="w-full max-w-4xl rounded-2xl border border-[#404040] bg-[#262626] shadow-2xl">
|
||||
<div className="flex items-center justify-between gap-4 border-b border-[#404040] px-5 py-4">
|
||||
<h2 className="text-lg font-bold text-[#e5e5e5]">{title}</h2>
|
||||
<button
|
||||
@@ -526,6 +581,30 @@ function getPatientLabel(patient) {
|
||||
return patient?.name || patient?.full_name || patient?.nome || ''
|
||||
}
|
||||
|
||||
function filterBySearch(items, search, getValues) {
|
||||
const query = normalizeSearch(search)
|
||||
if (!query) return items
|
||||
|
||||
return items.filter((item) =>
|
||||
getValues(item)
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase()
|
||||
.includes(query),
|
||||
)
|
||||
}
|
||||
|
||||
function getTimeOptions(selectedTime, slots) {
|
||||
return [
|
||||
...new Set([
|
||||
selectedTime,
|
||||
...slots.map((slot) => slot.time),
|
||||
].filter(Boolean)),
|
||||
].sort()
|
||||
}
|
||||
|
||||
function normalizeSearch(value) {
|
||||
return String(value || '')
|
||||
.normalize('NFD')
|
||||
|
||||
Reference in New Issue
Block a user