modified: index.html
modified: src/App.jsx modified: src/components/AppShell.jsx modified: src/components/featureStateStyles.js modified: src/config/permissions.js modified: src/hooks/useAgenda.js modified: src/mappers/reportMapper.js modified: src/pages/AgendaPage.jsx modified: src/pages/AnalyticsPage.jsx modified: src/pages/AuthPages.jsx modified: src/pages/HomePage.jsx modified: src/pages/MedicalRecordsPage.jsx modified: src/pages/MessagesPage.jsx modified: src/pages/PatientsPage.jsx modified: src/pages/ReportsPage.jsx modified: src/pages/SettingsPage.jsx deleted: src/pages/TeamPage.jsx modified: src/pages/UsersPage.jsx modified: src/repositories/availabilityRepository.js modified: src/repositories/patientRepository.js modified: src/repositories/professionalRepository.js modified: src/repositories/reportRepository.js modified: src/repositories/settingsRepository.js
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { hasCapability } from '../config/permissions.js'
|
||||
import { hasCapability, normalizeRole } from '../config/permissions.js'
|
||||
import { patientRepository } from '../repositories/patientRepository.js'
|
||||
import { professionalRepository } from '../repositories/professionalRepository.js'
|
||||
import { profileRepository } from '../repositories/profileRepository.js'
|
||||
const ITEMS_PER_PAGE = 25
|
||||
|
||||
const darkInput =
|
||||
@@ -15,6 +17,38 @@ const patientTabs = [
|
||||
{ label: 'Documentos', value: 'documentos' },
|
||||
]
|
||||
|
||||
const BRAZILIAN_STATES = [
|
||||
{ value: 'AC', label: 'Acre' },
|
||||
{ value: 'AL', label: 'Alagoas' },
|
||||
{ value: 'AP', label: 'Amapá' },
|
||||
{ value: 'AM', label: 'Amazonas' },
|
||||
{ value: 'BA', label: 'Bahia' },
|
||||
{ value: 'CE', label: 'Ceará' },
|
||||
{ value: 'DF', label: 'Distrito Federal' },
|
||||
{ value: 'ES', label: 'Espírito Santo' },
|
||||
{ value: 'GO', label: 'Goiás' },
|
||||
{ value: 'MA', label: 'Maranhão' },
|
||||
{ value: 'MT', label: 'Mato Grosso' },
|
||||
{ value: 'MS', label: 'Mato Grosso do Sul' },
|
||||
{ value: 'MG', label: 'Minas Gerais' },
|
||||
{ value: 'PA', label: 'Pará' },
|
||||
{ value: 'PB', label: 'Paraíba' },
|
||||
{ value: 'PR', label: 'Paraná' },
|
||||
{ value: 'PE', label: 'Pernambuco' },
|
||||
{ value: 'PI', label: 'Piauí' },
|
||||
{ value: 'RJ', label: 'Rio de Janeiro' },
|
||||
{ value: 'RN', label: 'Rio Grande do Norte' },
|
||||
{ value: 'RS', label: 'Rio Grande do Sul' },
|
||||
{ value: 'RO', label: 'Rondônia' },
|
||||
{ value: 'RR', label: 'Roraima' },
|
||||
{ value: 'SC', label: 'Santa Catarina' },
|
||||
{ value: 'SP', label: 'São Paulo' },
|
||||
{ value: 'SE', label: 'Sergipe' },
|
||||
{ value: 'TO', label: 'Tocantins' },
|
||||
]
|
||||
|
||||
const INSURANCE_OPTIONS = ['Unimed', 'Bradesco Saúde', 'Amil']
|
||||
|
||||
export function PatientsPage({ navigate, role }) {
|
||||
const [rows, setRows] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -36,22 +70,29 @@ export function PatientsPage({ navigate, role }) {
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
useEffect(() => {
|
||||
buildPatientRows()
|
||||
buildPatientRows(role)
|
||||
.then((data) => setRows(data))
|
||||
.catch((err) => setError(err.message))
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
}, [role])
|
||||
|
||||
const editingPatient = rows.find((patient) => patient.id === editingId)
|
||||
const insuranceOptions = useMemo(() => [...new Set(rows.map((patient) => patient.insurance).filter(Boolean))], [rows])
|
||||
const stateOptions = useMemo(() => [...new Set(rows.map((patient) => patient.state).filter(Boolean))], [rows])
|
||||
const hasAdvancedFilters = city || state || ageMin || ageMax || lastVisitSince
|
||||
const canEditPatients = hasCapability(role, 'canEditPatients')
|
||||
const canHardDeletePatients = hasCapability(role, 'hardDeletePatients')
|
||||
|
||||
const filteredPatients = useMemo(() => {
|
||||
return rows.filter((patient) => {
|
||||
const haystack = [patient.name, patient.cpf, patient.document, patient.insurance, patient.phone]
|
||||
const haystack = [
|
||||
patient.name,
|
||||
patient.cpf,
|
||||
patient.document,
|
||||
patient.insurance,
|
||||
patient.phone,
|
||||
patient.email,
|
||||
patient.city,
|
||||
patient.state,
|
||||
patient.motherName,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
@@ -60,7 +101,7 @@ export function PatientsPage({ navigate, role }) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (insurance && patient.insurance !== insurance) {
|
||||
if (insurance && normalizeFilterValue(patient.insurance) !== normalizeFilterValue(insurance)) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -72,15 +113,16 @@ export function PatientsPage({ navigate, role }) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (birthday === 'Hoje' && patient.birthday !== '07/04') {
|
||||
const patientBirthday = getPatientBirthday(patient)
|
||||
if (birthday === 'Hoje' && patientBirthday !== getTodayBirthday()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (birthday === 'Neste mes' && !patient.birthday?.endsWith('/04')) {
|
||||
if (birthday === 'Neste mês' && !patientBirthday.endsWith(`/${getCurrentMonth()}`)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (city && !patient.city.toLowerCase().includes(city.toLowerCase())) {
|
||||
if (city && !String(patient.city || '').toLowerCase().includes(city.toLowerCase())) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -88,15 +130,16 @@ export function PatientsPage({ navigate, role }) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (ageMin && patient.age < Number(ageMin)) {
|
||||
const patientAge = Number(patient.age) || 0
|
||||
if (ageMin && patientAge < Number(ageMin)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (ageMax && patient.age > Number(ageMax)) {
|
||||
if (ageMax && patientAge > Number(ageMax)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (lastVisitSince && patient.lastVisitIso && patient.lastVisitIso < lastVisitSince) {
|
||||
if (lastVisitSince && (!patient.lastVisitIso || patient.lastVisitIso < lastVisitSince)) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -164,24 +207,6 @@ export function PatientsPage({ navigate, role }) {
|
||||
setView('list')
|
||||
}
|
||||
|
||||
async function deletePatient(patientId) {
|
||||
if (!canHardDeletePatients) {
|
||||
window.alert('Você não tem permissão para excluir pacientes.')
|
||||
return
|
||||
}
|
||||
|
||||
if (window.confirm('Tem certeza que deseja excluir este paciente?')) {
|
||||
try {
|
||||
await patientRepository.remove(patientId)
|
||||
setRows((currentRows) => currentRows.filter((patient) => patient.id !== patientId))
|
||||
} catch (err) {
|
||||
window.alert(`Erro ao excluir paciente: ${err.message}`)
|
||||
}
|
||||
setOpenMenuId(null)
|
||||
setPage(1)
|
||||
}
|
||||
}
|
||||
|
||||
function openDetail(patient) {
|
||||
setOpenMenuId(null)
|
||||
if (patient.detailId) {
|
||||
@@ -258,7 +283,7 @@ async function deletePatient(patientId) {
|
||||
setInsurance(value)
|
||||
setPage(1)
|
||||
}}
|
||||
options={insuranceOptions}
|
||||
options={INSURANCE_OPTIONS}
|
||||
value={insurance}
|
||||
/>
|
||||
|
||||
@@ -282,7 +307,7 @@ async function deletePatient(patientId) {
|
||||
setBirthday(value)
|
||||
setPage(1)
|
||||
}}
|
||||
options={['Hoje', 'Neste mes']}
|
||||
options={['Hoje', 'Neste mês']}
|
||||
value={birthday}
|
||||
/>
|
||||
<button
|
||||
@@ -357,7 +382,10 @@ async function deletePatient(patientId) {
|
||||
<button
|
||||
aria-label={`Ações de ${patient.name}`}
|
||||
className="rounded p-1 text-[#a3a3a3] transition hover:bg-[#333333] hover:text-[#e5e5e5]"
|
||||
onClick={() => setOpenMenuId(openMenuId === patient.id ? null : patient.id)}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
setOpenMenuId(openMenuId === patient.id ? null : patient.id)
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<PatientIcon className="size-5" name="more" />
|
||||
@@ -366,11 +394,11 @@ async function deletePatient(patientId) {
|
||||
<>
|
||||
<button
|
||||
aria-label="Fechar menu"
|
||||
className="fixed inset-0 z-10 cursor-default"
|
||||
className="fixed inset-0 z-40 cursor-default"
|
||||
onClick={() => setOpenMenuId(null)}
|
||||
type="button"
|
||||
/>
|
||||
<div className="absolute right-8 top-10 z-20 w-48 rounded-lg border border-[#404040] bg-[#303030] py-1 text-left shadow-lg">
|
||||
<div className="absolute right-4 top-12 z-50 w-48 rounded-lg border border-[#404040] bg-[#303030] py-1 text-left shadow-lg">
|
||||
<ActionItem icon="file" label="Ver detalhes" onClick={() => openDetail(patient)} />
|
||||
{canEditPatients ? <ActionItem icon="edit" label="Editar" onClick={() => openForm(patient.id)} /> : null}
|
||||
<ActionItem
|
||||
@@ -381,9 +409,6 @@ async function deletePatient(patientId) {
|
||||
navigate('/agenda')
|
||||
}}
|
||||
/>
|
||||
{canHardDeletePatients ? (
|
||||
<ActionItem danger icon="trash" label="Excluir" onClick={() => deletePatient(patient.id)} />
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
@@ -449,7 +474,7 @@ async function deletePatient(patientId) {
|
||||
setLastVisitSince={setLastVisitSince}
|
||||
setState={setState}
|
||||
state={state}
|
||||
stateOptions={stateOptions}
|
||||
stateOptions={BRAZILIAN_STATES}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -462,8 +487,18 @@ function PatientEditor({ existingIds, onCancel, onSave, patient, saving }) {
|
||||
detailId: patient?.detailId || null,
|
||||
name: patient?.name || '',
|
||||
cpf: patient?.cpf || '',
|
||||
birthDate: patient?.birthDate || patient?.birth_date || '',
|
||||
motherName: patient?.motherName || patient?.mother_name || '',
|
||||
fatherName: patient?.fatherName || patient?.father_name || '',
|
||||
ethnicity: patient?.ethnicity || '',
|
||||
maritalStatus: patient?.maritalStatus || patient?.marital_status || '',
|
||||
phone: patient?.phone || '',
|
||||
phoneSecondary: patient?.phoneSecondary || patient?.phone_secondary || '',
|
||||
email: patient?.email || '',
|
||||
zipCode: patient?.zipCode || patient?.zip_code || '',
|
||||
addressStreet: patient?.addressStreet || patient?.address_street || patient?.address || '',
|
||||
addressNumber: patient?.addressNumber || patient?.address_number || '',
|
||||
addressComplement: patient?.addressComplement || patient?.address_complement || '',
|
||||
city: patient?.city || '',
|
||||
state: patient?.state || '',
|
||||
insurance: patient?.insurance || '',
|
||||
@@ -471,12 +506,14 @@ function PatientEditor({ existingIds, onCancel, onSave, patient, saving }) {
|
||||
age: patient?.age || '',
|
||||
condition: patient?.condition || '',
|
||||
birthday: patient?.birthday || '',
|
||||
notesText: patient?.notesText || patient?.notes_text || '',
|
||||
vip: Boolean(patient?.vip),
|
||||
lastVisit: patient?.lastVisit || null,
|
||||
nextVisit: patient?.nextVisit || null,
|
||||
lastVisitIso: patient?.lastVisitIso || null,
|
||||
}))
|
||||
const [attachmentsOpen, setAttachmentsOpen] = useState(false)
|
||||
const isNewPatient = !patient
|
||||
|
||||
function handleChange(event) {
|
||||
const { checked, name, type, value } = event.target
|
||||
@@ -490,6 +527,14 @@ function PatientEditor({ existingIds, onCancel, onSave, patient, saving }) {
|
||||
nextValue = maskPhone(value)
|
||||
}
|
||||
|
||||
if (name === 'phoneSecondary') {
|
||||
nextValue = maskPhone(value)
|
||||
}
|
||||
|
||||
if (name === 'zipCode') {
|
||||
nextValue = maskCEP(value)
|
||||
}
|
||||
|
||||
setFormData((currentData) => ({ ...currentData, [name]: nextValue }))
|
||||
}
|
||||
|
||||
@@ -501,19 +546,46 @@ function PatientEditor({ existingIds, onCancel, onSave, patient, saving }) {
|
||||
return
|
||||
}
|
||||
|
||||
const requiredFields = [
|
||||
['cpf', 'CPF'],
|
||||
['age', 'idade'],
|
||||
['birthDate', 'data de nascimento'],
|
||||
['motherName', 'nome da mãe'],
|
||||
['email', 'email'],
|
||||
['phone', 'celular'],
|
||||
['zipCode', 'CEP'],
|
||||
['addressStreet', 'endereço'],
|
||||
['addressNumber', 'número'],
|
||||
['city', 'cidade'],
|
||||
['state', 'estado'],
|
||||
['plan', 'plano'],
|
||||
]
|
||||
if (isNewPatient) {
|
||||
const missingFields = requiredFields
|
||||
.filter(([field]) => !String(formData[field] || '').trim())
|
||||
.map(([, label]) => label)
|
||||
|
||||
if (missingFields.length) {
|
||||
window.alert(`Preencha os campos obrigatórios: ${missingFields.join(', ')}.`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
onSave({
|
||||
...formData,
|
||||
id: formData.id || uniqueSlug(formData.name, existingIds),
|
||||
age: Number(formData.age) || 0,
|
||||
birthday: formData.birthday || '07/04',
|
||||
city: formData.city || 'Cidade não informada',
|
||||
birthday: formData.birthday || formatBirthday(formData.birthDate),
|
||||
city: formData.city,
|
||||
document: formData.cpf ? `CPF ${formData.cpf}` : 'CPF não informado',
|
||||
insurance: formData.insurance || 'Particular',
|
||||
insurance: formData.insurance,
|
||||
lastVisit: formData.lastVisit || 'Ainda não houve atendimento',
|
||||
nextVisit: formData.nextVisit || null,
|
||||
phone: formData.phone || 'Telefone não informado',
|
||||
plan: formData.insurance || formData.plan || 'Particular',
|
||||
state: formData.state || 'UF',
|
||||
phone: formData.phone,
|
||||
plan: formData.plan,
|
||||
state: formData.state,
|
||||
address: formatAddress(formData),
|
||||
notes: formData.notesText ? [formData.notesText] : [],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -552,43 +624,43 @@ function PatientEditor({ existingIds, onCancel, onSave, patient, saving }) {
|
||||
|
||||
<div className="grid grid-cols-1 gap-x-6 gap-y-6 md:grid-cols-12">
|
||||
<DarkField className="md:col-span-6" label="Nome *">
|
||||
<input className={darkInput} name="name" onChange={handleChange} required value={formData.name} />
|
||||
<input className={darkInput} name="name" onChange={handleChange} required={isNewPatient} value={formData.name} />
|
||||
</DarkField>
|
||||
<DarkField className="md:col-span-3" label="CPF">
|
||||
<input className={darkInput} maxLength={14} name="cpf" onChange={handleChange} value={formData.cpf} />
|
||||
<DarkField className="md:col-span-3" label={requiredLabel('CPF')}>
|
||||
<input className={darkInput} maxLength={14} name="cpf" onChange={handleChange} required={isNewPatient} value={formData.cpf} />
|
||||
</DarkField>
|
||||
<DarkField className="md:col-span-3" label="Idade">
|
||||
<input className={darkInput} min="0" name="age" onChange={handleChange} type="number" value={formData.age} />
|
||||
<DarkField className="md:col-span-3" label={requiredLabel('Idade')}>
|
||||
<input className={darkInput} min="0" name="age" onChange={handleChange} required={isNewPatient} type="number" value={formData.age} />
|
||||
</DarkField>
|
||||
<DarkField className="md:col-span-3" label="Data de Nascimento">
|
||||
<input className={`${darkInput} [color-scheme:dark]`} type="date" />
|
||||
<DarkField className="md:col-span-3" label={requiredLabel('Data de Nascimento')}>
|
||||
<input className={`${darkInput} [color-scheme:dark]`} name="birthDate" onChange={handleChange} required={isNewPatient} type="date" value={formData.birthDate} />
|
||||
</DarkField>
|
||||
<DarkField className="md:col-span-3" label="Aniversario">
|
||||
<DarkField className="md:col-span-3" label="Aniversário">
|
||||
<input className={darkInput} maxLength={5} name="birthday" onChange={handleChange} placeholder="07/04" value={formData.birthday} />
|
||||
</DarkField>
|
||||
<DarkField className="md:col-span-3" label="Etnia">
|
||||
<select className={darkInput} defaultValue="">
|
||||
<select className={darkInput} name="ethnicity" onChange={handleChange} value={formData.ethnicity}>
|
||||
<option value="">Selecione</option>
|
||||
<option>Indígena</option>
|
||||
<option>Não Indígena</option>
|
||||
</select>
|
||||
</DarkField>
|
||||
<DarkField className="md:col-span-3" label="Estado civil">
|
||||
<select className={darkInput} defaultValue="">
|
||||
<select className={darkInput} name="maritalStatus" onChange={handleChange} value={formData.maritalStatus}>
|
||||
<option value="">Selecione</option>
|
||||
<option>Solteiro(a)</option>
|
||||
<option>Casado(a)</option>
|
||||
<option>Divorciado(a)</option>
|
||||
</select>
|
||||
</DarkField>
|
||||
<DarkField className="md:col-span-6" label="Nome da mae">
|
||||
<input className={darkInput} />
|
||||
<DarkField className="md:col-span-6" label={requiredLabel('Nome da mãe')}>
|
||||
<input className={darkInput} name="motherName" onChange={handleChange} required={isNewPatient} value={formData.motherName} />
|
||||
</DarkField>
|
||||
<DarkField className="md:col-span-6" label="Nome do pai">
|
||||
<input className={darkInput} />
|
||||
<input className={darkInput} name="fatherName" onChange={handleChange} value={formData.fatherName} />
|
||||
</DarkField>
|
||||
<DarkField className="md:col-span-12" label="Observacoes">
|
||||
<textarea className={`${darkInput} min-h-24 py-2`} />
|
||||
<textarea className={`${darkInput} min-h-24 py-2`} name="notesText" onChange={handleChange} value={formData.notesText} />
|
||||
</DarkField>
|
||||
<div className="md:col-span-12">
|
||||
<button
|
||||
@@ -610,14 +682,14 @@ function PatientEditor({ existingIds, onCancel, onSave, patient, saving }) {
|
||||
<section className={darkCard}>
|
||||
<h2 className="mb-6 text-lg font-semibold text-[#e5e5e5]">Contato</h2>
|
||||
<div className="grid grid-cols-1 gap-x-6 gap-y-6 md:grid-cols-12">
|
||||
<DarkField className="md:col-span-4" label="E-mail">
|
||||
<input className={darkInput} name="email" onChange={handleChange} type="email" value={formData.email} />
|
||||
<DarkField className="md:col-span-4" label={requiredLabel('E-mail')}>
|
||||
<input className={darkInput} name="email" onChange={handleChange} required={isNewPatient} type="email" value={formData.email} />
|
||||
</DarkField>
|
||||
<DarkField className="md:col-span-4" label="Celular">
|
||||
<input className={darkInput} maxLength={15} name="phone" onChange={handleChange} value={formData.phone} />
|
||||
<DarkField className="md:col-span-4" label={requiredLabel('Celular')}>
|
||||
<input className={darkInput} maxLength={15} name="phone" onChange={handleChange} required={isNewPatient} value={formData.phone} />
|
||||
</DarkField>
|
||||
<DarkField className="md:col-span-4" label="Telefone 2">
|
||||
<input className={darkInput} />
|
||||
<input className={darkInput} maxLength={15} name="phoneSecondary" onChange={handleChange} value={formData.phoneSecondary} />
|
||||
</DarkField>
|
||||
</div>
|
||||
</section>
|
||||
@@ -625,22 +697,29 @@ function PatientEditor({ existingIds, onCancel, onSave, patient, saving }) {
|
||||
<section className={darkCard}>
|
||||
<h2 className="mb-6 text-lg font-semibold text-[#e5e5e5]">Endereço</h2>
|
||||
<div className="grid grid-cols-1 gap-x-6 gap-y-6 md:grid-cols-12">
|
||||
<DarkField className="md:col-span-3" label="CEP">
|
||||
<input className={darkInput} maxLength={9} onChange={maskCEPInput} placeholder="_____-___" />
|
||||
<DarkField className="md:col-span-3" label={requiredLabel('CEP')}>
|
||||
<input className={darkInput} maxLength={9} name="zipCode" onChange={handleChange} placeholder="_____-___" required={isNewPatient} value={formData.zipCode} />
|
||||
</DarkField>
|
||||
<DarkField className="md:col-span-5" label="Endereço">
|
||||
<input className={darkInput} />
|
||||
<DarkField className="md:col-span-5" label={requiredLabel('Endereço')}>
|
||||
<input className={darkInput} name="addressStreet" onChange={handleChange} required={isNewPatient} value={formData.addressStreet} />
|
||||
</DarkField>
|
||||
<DarkField className="md:col-span-4" label="Cidade">
|
||||
<input className={darkInput} name="city" onChange={handleChange} value={formData.city} />
|
||||
<DarkField className="md:col-span-2" label={requiredLabel('Número')}>
|
||||
<input className={darkInput} name="addressNumber" onChange={handleChange} required={isNewPatient} value={formData.addressNumber} />
|
||||
</DarkField>
|
||||
<DarkField className="md:col-span-4" label="Estado">
|
||||
<select className={darkInput} name="state" onChange={handleChange} value={formData.state}>
|
||||
<DarkField className="md:col-span-6" label="Complemento">
|
||||
<input className={darkInput} name="addressComplement" onChange={handleChange} value={formData.addressComplement} />
|
||||
</DarkField>
|
||||
<DarkField className="md:col-span-4" label={requiredLabel('Cidade')}>
|
||||
<input className={darkInput} name="city" onChange={handleChange} required={isNewPatient} value={formData.city} />
|
||||
</DarkField>
|
||||
<DarkField className="md:col-span-4" label={requiredLabel('Estado')}>
|
||||
<select className={darkInput} name="state" onChange={handleChange} required={isNewPatient} value={formData.state}>
|
||||
<option value="">Selecione</option>
|
||||
<option value="PE">Pernambuco</option>
|
||||
<option value="SE">Sergipe</option>
|
||||
<option value="SP">São Paulo</option>
|
||||
<option value="RJ">Rio de Janeiro</option>
|
||||
{BRAZILIAN_STATES.map((stateOption) => (
|
||||
<option key={stateOption.value} value={stateOption.value}>
|
||||
{stateOption.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</DarkField>
|
||||
</div>
|
||||
@@ -649,17 +728,18 @@ function PatientEditor({ existingIds, onCancel, onSave, patient, saving }) {
|
||||
<section className={darkCard}>
|
||||
<h2 className="mb-6 text-lg font-semibold text-[#e5e5e5]">Informações de convenio</h2>
|
||||
<div className="grid grid-cols-1 gap-x-6 gap-y-6 md:grid-cols-12">
|
||||
<DarkField className="md:col-span-6" label="Convenio">
|
||||
<DarkField className="md:col-span-6" label="Convênio">
|
||||
<select className={darkInput} name="insurance" onChange={handleChange} value={formData.insurance}>
|
||||
<option value="">Selecione</option>
|
||||
<option value="Unimed">Unimed</option>
|
||||
<option value="Bradesco Saude">Bradesco Saude</option>
|
||||
<option value="Amil">Amil</option>
|
||||
<option value="Particular">Particular</option>
|
||||
{INSURANCE_OPTIONS.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</DarkField>
|
||||
<DarkField className="md:col-span-6" label="Plano">
|
||||
<input className={darkInput} name="plan" onChange={handleChange} value={formData.plan} />
|
||||
<DarkField className="md:col-span-6" label={requiredLabel('Plano')}>
|
||||
<input className={darkInput} name="plan" onChange={handleChange} required={isNewPatient} value={formData.plan} />
|
||||
</DarkField>
|
||||
<label className="flex w-fit cursor-pointer items-center gap-2 text-sm text-[#e5e5e5] md:col-span-12">
|
||||
<input className="size-4 accent-[#3b82f6]" checked={formData.vip} name="vip" onChange={handleChange} type="checkbox" />
|
||||
@@ -690,8 +770,57 @@ function PatientEditor({ existingIds, onCancel, onSave, patient, saving }) {
|
||||
)
|
||||
}
|
||||
|
||||
export function PatientDetailPage({ navigate, patient }) {
|
||||
export function PatientDetailPage({ navigate, patient, role }) {
|
||||
const [activeTab, setActiveTab] = useState('resumo')
|
||||
const [localPatient, setLocalPatient] = useState(patient)
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [messageShortcutOpen, setMessageShortcutOpen] = useState(false)
|
||||
const [appointmentShortcutOpen, setAppointmentShortcutOpen] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const canEditPatients = hasCapability(role, 'canEditPatients')
|
||||
const canHardDeletePatients = hasCapability(role, 'hardDeletePatients')
|
||||
|
||||
async function savePatient(updatedPatient) {
|
||||
if (!canEditPatients) return
|
||||
|
||||
setSaving(true)
|
||||
try {
|
||||
await patientRepository.update(updatedPatient.id, updatedPatient)
|
||||
setLocalPatient((current) => ({ ...current, ...updatedPatient }))
|
||||
setEditing(false)
|
||||
} catch (err) {
|
||||
window.alert(`Erro ao salvar paciente: ${err.message}`)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function deletePatient() {
|
||||
if (!canHardDeletePatients) return
|
||||
|
||||
if (!window.confirm('Tem certeza que deseja excluir este paciente definitivamente?')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await patientRepository.remove(localPatient.id)
|
||||
navigate('/pacientes')
|
||||
} catch (err) {
|
||||
window.alert(`Erro ao excluir paciente: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (editing) {
|
||||
return (
|
||||
<PatientEditor
|
||||
existingIds={[localPatient.id]}
|
||||
onCancel={() => setEditing(false)}
|
||||
onSave={savePatient}
|
||||
patient={localPatient}
|
||||
saving={saving}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl space-y-6">
|
||||
@@ -706,24 +835,33 @@ export function PatientDetailPage({ navigate, patient }) {
|
||||
</button>
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-[#3b82f6]">Dados do Paciente</p>
|
||||
<h1 className="mt-1 text-2xl font-bold tracking-tight text-[#f5f5f5]">{patient.name}</h1>
|
||||
<h1 className="mt-1 text-2xl font-bold tracking-tight text-[#f5f5f5]">{localPatient.name}</h1>
|
||||
<p className="mt-1 text-sm text-[#b8b8b8]">
|
||||
{patient.condition} • {patient.status} • {patient.document}
|
||||
{localPatient.condition} • {localPatient.status} • {localPatient.document}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{canEditPatients ? (
|
||||
<button
|
||||
className="h-10 rounded-sm border border-[#404040] bg-[#262626] px-4 text-sm font-semibold text-[#e5e5e5] transition hover:bg-[#303030]"
|
||||
onClick={() => setEditing(true)}
|
||||
type="button"
|
||||
>
|
||||
Editar dados
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
className="h-10 rounded-sm border border-[#404040] bg-[#262626] px-4 text-sm font-semibold text-[#e5e5e5] transition hover:bg-[#303030]"
|
||||
onClick={() => navigate('/comunicacao')}
|
||||
onClick={() => setMessageShortcutOpen(true)}
|
||||
type="button"
|
||||
>
|
||||
Enviar mensagem
|
||||
</button>
|
||||
<button
|
||||
className="h-10 rounded-sm bg-[#3b82f6] px-4 text-sm font-semibold text-white transition hover:bg-[#2563eb]"
|
||||
onClick={() => navigate('/agenda')}
|
||||
onClick={() => setAppointmentShortcutOpen(true)}
|
||||
type="button"
|
||||
>
|
||||
Novo retorno
|
||||
@@ -732,10 +870,10 @@ export function PatientDetailPage({ navigate, patient }) {
|
||||
</header>
|
||||
|
||||
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<SummaryTile label="Idade" value={`${patient.age} anos`} />
|
||||
<SummaryTile label="Risco" value={patient.risk} tone={riskColor(patient.risk)} />
|
||||
<SummaryTile label="Última consulta" value={patient.lastVisit} />
|
||||
<SummaryTile label="Próxima consulta" value={patient.nextVisit} />
|
||||
<SummaryTile label="Idade" value={`${localPatient.age} anos`} />
|
||||
<SummaryTile label="Risco" value={localPatient.risk} tone={riskColor(localPatient.risk)} />
|
||||
<SummaryTile label="Última consulta" value={localPatient.lastVisit || 'Ainda não houve atendimento'} />
|
||||
<SummaryTile label="Próxima consulta" value={localPatient.nextVisit || 'Nenhum atendimento agendado'} />
|
||||
</section>
|
||||
|
||||
<section className={darkCard}>
|
||||
@@ -757,48 +895,235 @@ export function PatientDetailPage({ navigate, patient }) {
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
{activeTab === 'resumo' ? <PatientSummary patient={patient} /> : null}
|
||||
{activeTab === 'consultas' ? <PatientVisits navigate={navigate} patient={patient} /> : null}
|
||||
{activeTab === 'documentos' ? <PatientDocuments patient={patient} /> : null}
|
||||
{activeTab === 'resumo' ? <PatientSummary patient={localPatient} /> : null}
|
||||
{activeTab === 'consultas' ? <PatientVisits navigate={navigate} patient={localPatient} /> : null}
|
||||
{activeTab === 'documentos' ? <PatientDocuments patient={localPatient} /> : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{canHardDeletePatients ? (
|
||||
<section className="rounded-2xl border border-red-500/30 bg-red-500/10 p-6">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h2 className="text-sm font-bold text-red-300">Zona de exclusão</h2>
|
||||
<p className="mt-1 text-sm text-red-100/80">Remove definitivamente o paciente e seus dados locais carregados.</p>
|
||||
</div>
|
||||
<button
|
||||
className="h-10 rounded-sm border border-red-500/40 bg-red-500/10 px-4 text-sm font-semibold text-red-300 transition hover:bg-red-500/20"
|
||||
onClick={deletePatient}
|
||||
type="button"
|
||||
>
|
||||
Excluir paciente
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{messageShortcutOpen ? (
|
||||
<PatientMessageShortcutModal
|
||||
onClose={() => setMessageShortcutOpen(false)}
|
||||
patient={localPatient}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{appointmentShortcutOpen ? (
|
||||
<PatientAppointmentShortcutModal
|
||||
onClose={() => setAppointmentShortcutOpen(false)}
|
||||
patient={localPatient}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PatientSummary({ patient }) {
|
||||
const notes = Array.isArray(patient.notes) ? patient.notes : []
|
||||
|
||||
return (
|
||||
<div className="grid gap-6 lg:grid-cols-[1fr_320px]">
|
||||
<div>
|
||||
<div className="grid gap-6 lg:grid-cols-[1fr_360px]">
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-xl font-bold text-[#f5f5f5]">Resumo clínico</h2>
|
||||
<div className="mt-4 grid gap-3">
|
||||
{patient.notes.map((note) => (
|
||||
<div className="grid gap-3">
|
||||
{notes.length ? notes.map((note) => (
|
||||
<p className="rounded-xl border border-[#404040] bg-[#171717] p-4 text-sm leading-6 text-[#b8b8b8]" key={note}>
|
||||
{note}
|
||||
</p>
|
||||
))}
|
||||
)) : (
|
||||
<p className="rounded-xl border border-[#404040] bg-[#171717] p-4 text-sm leading-6 text-[#b8b8b8]">
|
||||
Nenhuma observação clínica registrada.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<PatientInfoSection
|
||||
items={[
|
||||
['CPF', patient.cpf || patient.document],
|
||||
['Data de nascimento', formatDisplayDate(patient.birthDate || patient.birth_date)],
|
||||
['Nome da mãe', patient.motherName],
|
||||
['Nome do pai', patient.fatherName],
|
||||
['Etnia', patient.ethnicity],
|
||||
['Estado civil', patient.maritalStatus],
|
||||
]}
|
||||
title="Dados pessoais"
|
||||
/>
|
||||
<PatientInfoSection
|
||||
items={[
|
||||
['CEP', patient.zipCode],
|
||||
['Endereço', patient.addressStreet],
|
||||
['Número', patient.addressNumber],
|
||||
['Complemento', patient.addressComplement],
|
||||
['Cidade', patient.city],
|
||||
['Estado', patient.state],
|
||||
]}
|
||||
title="Endereço"
|
||||
/>
|
||||
<PatientInfoSection
|
||||
items={[
|
||||
['Convênio', patient.insurance],
|
||||
['Plano', patient.plan],
|
||||
['VIP', patient.vip ? 'Sim' : 'Não'],
|
||||
]}
|
||||
title="Informações de convênio"
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-xl border border-[#404040] bg-[#171717] p-4">
|
||||
<h3 className="font-bold text-[#f5f5f5]">Contato e equipe</h3>
|
||||
<dl className="mt-4 grid gap-3 text-sm">
|
||||
<InfoRow label="Telefone" value={patient.phone} />
|
||||
<InfoRow label="Telefone 2" value={patient.phoneSecondary} />
|
||||
<InfoRow label="E-mail" value={patient.email} />
|
||||
<InfoRow label="Endereço" value={patient.address} />
|
||||
<InfoRow label="Equipe" value={patient.team.join(', ')} />
|
||||
<InfoRow label="Equipe" value={(patient.team || []).join(', ')} />
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PatientMessageShortcutModal({ onClose, patient }) {
|
||||
const [message, setMessage] = useState('')
|
||||
const [channel, setChannel] = useState('whatsapp')
|
||||
|
||||
function handleSubmit(event) {
|
||||
event.preventDefault()
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<ShortcutModal onClose={onClose} title="Nova mensagem">
|
||||
<form className="space-y-4" onSubmit={handleSubmit}>
|
||||
<DarkField label="Paciente">
|
||||
<input className={darkInput} readOnly value={patient.name || ''} />
|
||||
</DarkField>
|
||||
<DarkField label="Canal">
|
||||
<select className={darkInput} onChange={(event) => setChannel(event.target.value)} value={channel}>
|
||||
<option value="whatsapp">WhatsApp</option>
|
||||
<option value="sms">SMS</option>
|
||||
<option value="email">E-mail</option>
|
||||
</select>
|
||||
</DarkField>
|
||||
<DarkField label="Mensagem">
|
||||
<textarea
|
||||
className={`${darkInput} min-h-28 py-2`}
|
||||
onChange={(event) => setMessage(event.target.value)}
|
||||
value={message}
|
||||
/>
|
||||
</DarkField>
|
||||
<ShortcutActions disabled={!message.trim()} onClose={onClose} submitLabel="Enviar" />
|
||||
</form>
|
||||
</ShortcutModal>
|
||||
)
|
||||
}
|
||||
|
||||
function PatientAppointmentShortcutModal({ onClose, patient }) {
|
||||
const [date, setDate] = useState('')
|
||||
const [time, setTime] = useState('')
|
||||
const [type, setType] = useState('Retorno')
|
||||
|
||||
function handleSubmit(event) {
|
||||
event.preventDefault()
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<ShortcutModal onClose={onClose} title="Novo agendamento">
|
||||
<form className="space-y-4" onSubmit={handleSubmit}>
|
||||
<DarkField label="Paciente">
|
||||
<input className={darkInput} readOnly value={patient.name || ''} />
|
||||
</DarkField>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<DarkField label="Data">
|
||||
<input className={`${darkInput} [color-scheme:dark]`} onChange={(event) => setDate(event.target.value)} type="date" value={date} />
|
||||
</DarkField>
|
||||
<DarkField label="Horário">
|
||||
<input className={`${darkInput} [color-scheme:dark]`} onChange={(event) => setTime(event.target.value)} type="time" value={time} />
|
||||
</DarkField>
|
||||
</div>
|
||||
<DarkField label="Tipo">
|
||||
<input className={darkInput} onChange={(event) => setType(event.target.value)} value={type} />
|
||||
</DarkField>
|
||||
<ShortcutActions disabled={!date || !time} onClose={onClose} submitLabel="Salvar" />
|
||||
</form>
|
||||
</ShortcutModal>
|
||||
)
|
||||
}
|
||||
|
||||
function ShortcutModal({ children, onClose, title }) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4">
|
||||
<div className="w-full max-w-xl rounded-2xl border border-[#404040] bg-[#262626] shadow-2xl">
|
||||
<div className="flex items-center justify-between border-b border-[#404040] px-5 py-4">
|
||||
<h2 className="text-lg font-bold text-[#f5f5f5]">{title}</h2>
|
||||
<button className="grid size-9 place-items-center rounded-sm text-[#a3a3a3] hover:bg-[#303030]" onClick={onClose} type="button">
|
||||
<PatientIcon className="size-5" name="x" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-5">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ShortcutActions({ disabled, onClose, submitLabel }) {
|
||||
return (
|
||||
<div className="flex justify-end gap-3 border-t border-[#404040] pt-4">
|
||||
<button className="h-10 rounded-sm border border-[#404040] px-4 text-sm font-semibold text-[#e5e5e5]" onClick={onClose} type="button">
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
className="h-10 rounded-sm bg-[#3b82f6] px-4 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={disabled}
|
||||
type="submit"
|
||||
>
|
||||
{submitLabel}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PatientInfoSection({ items, title }) {
|
||||
return (
|
||||
<section className="rounded-xl border border-[#404040] bg-[#171717] p-4">
|
||||
<h3 className="font-bold text-[#f5f5f5]">{title}</h3>
|
||||
<dl className="mt-4 grid gap-3 text-sm md:grid-cols-2">
|
||||
{items.map(([label, value]) => (
|
||||
<InfoRow key={label} label={label} value={value || 'Não informado'} />
|
||||
))}
|
||||
</dl>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function PatientVisits({ navigate, patient }) {
|
||||
return (
|
||||
<div className="grid gap-3">
|
||||
{[
|
||||
{ date: patient.nextVisit, status: 'Agendada', description: `Retorno para ${patient.condition}` },
|
||||
{ date: patient.lastVisit, status: 'Finalizada', description: 'Consulta registrada no historico do paciente.' },
|
||||
].map((visit) => (
|
||||
patient.nextVisit
|
||||
? { date: patient.nextVisit, status: 'Agendada', description: `Retorno para ${patient.condition}` }
|
||||
: null,
|
||||
patient.lastVisit
|
||||
? { date: patient.lastVisit, status: 'Finalizada', description: 'Consulta registrada no histórico do paciente.' }
|
||||
: null,
|
||||
].filter(Boolean).map((visit) => (
|
||||
<div className="rounded-xl border border-[#404040] bg-[#171717] p-4" key={`${visit.date}-${visit.status}`}>
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
@@ -815,6 +1140,11 @@ function PatientVisits({ navigate, patient }) {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{!patient.nextVisit && !patient.lastVisit ? (
|
||||
<div className="rounded-xl border border-[#404040] bg-[#171717] p-4 text-sm text-[#a3a3a3]">
|
||||
Nenhum agendamento encontrado para este paciente.
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
className="h-10 justify-self-start rounded-sm border border-[#404040] bg-[#303030] px-4 text-sm font-semibold text-[#e5e5e5] transition hover:border-[#3b82f6]"
|
||||
onClick={() => navigate('/consultas')}
|
||||
@@ -827,9 +1157,11 @@ function PatientVisits({ navigate, patient }) {
|
||||
}
|
||||
|
||||
function PatientDocuments({ patient }) {
|
||||
const exams = Array.isArray(patient.exams) ? patient.exams : []
|
||||
|
||||
return (
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
{patient.exams.map((exam) => (
|
||||
{exams.length ? exams.map((exam) => (
|
||||
<div className="rounded-xl border border-[#404040] bg-[#171717] p-4" key={exam}>
|
||||
<p className="font-semibold text-[#f5f5f5]">{exam}</p>
|
||||
<p className="mt-2 text-sm text-[#a3a3a3]">Pendente de revisão.</p>
|
||||
@@ -837,7 +1169,11 @@ function PatientDocuments({ patient }) {
|
||||
A revisar
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
)) : (
|
||||
<div className="rounded-xl border border-[#404040] bg-[#171717] p-4 text-sm text-[#a3a3a3]">
|
||||
Nenhum documento encontrado.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -861,11 +1197,17 @@ function InfoRow({ label, value }) {
|
||||
return (
|
||||
<div>
|
||||
<dt className="font-semibold text-[#737373]">{label}</dt>
|
||||
<dd className="mt-1 text-[#e5e5e5]">{value}</dd>
|
||||
<dd className="mt-1 text-[#e5e5e5]">{value || 'Não informado'}</dd>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function formatDisplayDate(value) {
|
||||
if (!value) return ''
|
||||
const [year, month, day] = String(value).split('-')
|
||||
return year && month && day ? `${day}/${month}/${year}` : value
|
||||
}
|
||||
|
||||
function riskColor(risk) {
|
||||
if (risk === 'Alto') {
|
||||
return 'bg-red-500/20 text-red-400'
|
||||
@@ -878,6 +1220,32 @@ function riskColor(risk) {
|
||||
return 'bg-emerald-500/20 text-emerald-400'
|
||||
}
|
||||
|
||||
function normalizeFilterValue(value) {
|
||||
return String(value || '')
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
function getPatientBirthday(patient) {
|
||||
if (patient.birthday) return patient.birthday
|
||||
const birthDate = patient.birthDate || patient.birth_date
|
||||
if (!birthDate) return ''
|
||||
|
||||
const [, month, day] = String(birthDate).split('-')
|
||||
return month && day ? `${day}/${month}` : ''
|
||||
}
|
||||
|
||||
function getTodayBirthday() {
|
||||
const today = new Date()
|
||||
return `${String(today.getDate()).padStart(2, '0')}/${getCurrentMonth()}`
|
||||
}
|
||||
|
||||
function getCurrentMonth() {
|
||||
return String(new Date().getMonth() + 1).padStart(2, '0')
|
||||
}
|
||||
|
||||
function PatientSelect({ className = '', icon, label, onChange, options, value }) {
|
||||
return (
|
||||
<div className={`relative ${className}`}>
|
||||
@@ -940,6 +1308,14 @@ function ActionItem({ danger = false, icon, label, onClick }) {
|
||||
)
|
||||
}
|
||||
|
||||
function requiredLabel(label) {
|
||||
return (
|
||||
<>
|
||||
{label} <span className="text-red-400">*</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function DarkField({ children, className = '', label }) {
|
||||
return (
|
||||
<label className={`block ${className}`}>
|
||||
@@ -1002,8 +1378,8 @@ function AdvancedFilterModal({
|
||||
<select className={darkInput} onChange={(event) => setState(event.target.value)} value={state}>
|
||||
<option value="">Todos</option>
|
||||
{stateOptions.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -1015,7 +1391,6 @@ function AdvancedFilterModal({
|
||||
className={darkInput}
|
||||
min="0"
|
||||
onChange={(event) => setAgeMin(event.target.value)}
|
||||
placeholder="0"
|
||||
type="number"
|
||||
value={ageMin}
|
||||
/>
|
||||
@@ -1025,7 +1400,6 @@ function AdvancedFilterModal({
|
||||
className={darkInput}
|
||||
min="0"
|
||||
onChange={(event) => setAgeMax(event.target.value)}
|
||||
placeholder="120"
|
||||
type="number"
|
||||
value={ageMax}
|
||||
/>
|
||||
@@ -1246,8 +1620,22 @@ function PatientIcon({ className = 'size-4', name }) {
|
||||
)
|
||||
}
|
||||
|
||||
async function buildPatientRows() {
|
||||
return patientRepository.getDirectoryRows()
|
||||
async function buildPatientRows(role) {
|
||||
if (normalizeRole(role) !== 'medico') {
|
||||
return patientRepository.getDirectoryRows()
|
||||
}
|
||||
|
||||
const [profile, professionals] = await Promise.all([
|
||||
profileRepository.getCurrentUserProfile(),
|
||||
professionalRepository.getAll(),
|
||||
])
|
||||
const currentProfessional = professionalRepository.resolveCurrentProfessional(profile, professionals)
|
||||
|
||||
if (!currentProfessional?.id) {
|
||||
throw new Error('Não foi possível vincular o médico logado a um profissional da base.')
|
||||
}
|
||||
|
||||
return patientRepository.getDirectoryRows({ doctorId: currentProfessional.id })
|
||||
}
|
||||
|
||||
function uniqueSlug(value, existingIds) {
|
||||
@@ -1289,9 +1677,28 @@ function maskPhone(value) {
|
||||
.replace(/(-\d{4})\d+?$/, '$1')
|
||||
}
|
||||
|
||||
function maskCEPInput(event) {
|
||||
event.target.value = event.target.value
|
||||
function maskCEP(value) {
|
||||
return value
|
||||
.replace(/\D/g, '')
|
||||
.replace(/(\d{5})(\d)/, '$1-$2')
|
||||
.replace(/(-\d{3})\d+?$/, '$1')
|
||||
}
|
||||
|
||||
function formatBirthday(birthDate) {
|
||||
if (!birthDate) return ''
|
||||
const [, month, day] = birthDate.split('-')
|
||||
return day && month ? `${day}/${month}` : ''
|
||||
}
|
||||
|
||||
function formatAddress(patient) {
|
||||
return [
|
||||
patient.addressStreet,
|
||||
patient.addressNumber,
|
||||
patient.addressComplement,
|
||||
patient.city,
|
||||
patient.state,
|
||||
patient.zipCode,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(', ')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user