import { useEffect, useMemo, useState } from 'react' import { hasCapability } from '../config/permissions.js' import { patientRepository } from '../repositories/patientRepository.js' const ITEMS_PER_PAGE = 25 const darkInput = 'h-10 w-full rounded-lg border border-[#404040] bg-[#1a1a1a] px-3 text-sm text-[#e5e5e5] outline-none transition placeholder:text-[#737373] focus:border-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6]' const darkLabel = 'mb-1.5 block text-xs font-medium text-[#e5e5e5]' const darkCard = 'rounded-2xl border border-[#404040] bg-[#262626] p-6 shadow-sm' const patientTabs = [ { label: 'Resumo', value: 'resumo' }, { label: 'Consultas', value: 'consultas' }, { 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) const [error, setError] = useState(null) const [saving, setSaving] = useState(false) const [view, setView] = useState('list') const [editingId, setEditingId] = useState(null) const [search, setSearch] = useState('') const [insurance, setInsurance] = useState('') const [vip, setVip] = useState('') const [birthday, setBirthday] = useState('') const [city, setCity] = useState('') const [state, setState] = useState('') const [ageMin, setAgeMin] = useState('') const [ageMax, setAgeMax] = useState('') const [lastVisitSince, setLastVisitSince] = useState('') const [advancedOpen, setAdvancedOpen] = useState(false) const [openMenuId, setOpenMenuId] = useState(null) const [page, setPage] = useState(1) useEffect(() => { buildPatientRows() .then((data) => setRows(data)) .catch((err) => setError(err.message)) .finally(() => setLoading(false)) }, []) const editingPatient = rows.find((patient) => patient.id === editingId) const hasAdvancedFilters = city || state || ageMin || ageMax || lastVisitSince const canEditPatients = hasCapability(role, 'canEditPatients') const filteredPatients = useMemo(() => { return rows.filter((patient) => { const haystack = [ patient.name, patient.cpf, patient.document, patient.insurance, patient.phone, patient.email, patient.city, patient.state, patient.motherName, ] .filter(Boolean) .join(' ') .toLowerCase() if (search && !haystack.includes(search.toLowerCase())) { return false } if (insurance && normalizeFilterValue(patient.insurance) !== normalizeFilterValue(insurance)) { return false } if (vip === 'Sim' && !patient.vip) { return false } if (vip === 'Não' && patient.vip) { return false } const patientBirthday = getPatientBirthday(patient) if (birthday === 'Hoje' && patientBirthday !== getTodayBirthday()) { return false } if (birthday === 'Neste mês' && !patientBirthday.endsWith(`/${getCurrentMonth()}`)) { return false } if (city && !String(patient.city || '').toLowerCase().includes(city.toLowerCase())) { return false } if (state && patient.state !== state) { return false } const patientAge = Number(patient.age) || 0 if (ageMin && patientAge < Number(ageMin)) { return false } if (ageMax && patientAge > Number(ageMax)) { return false } if (lastVisitSince && (!patient.lastVisitIso || patient.lastVisitIso < lastVisitSince)) { return false } return true }) }, [ageMax, ageMin, birthday, city, insurance, lastVisitSince, rows, search, state, vip]) const totalPages = Math.max(1, Math.ceil(filteredPatients.length / ITEMS_PER_PAGE)) const currentPage = Math.min(page, totalPages) const startIndex = (currentPage - 1) * ITEMS_PER_PAGE const paginatedPatients = filteredPatients.slice(startIndex, startIndex + ITEMS_PER_PAGE) function resetAdvancedFilters() { setCity('') setState('') setAgeMin('') setAgeMax('') setLastVisitSince('') setAdvancedOpen(false) setPage(1) } function openForm(patientId = null) { if (!canEditPatients) return setEditingId(patientId) setOpenMenuId(null) setView('form') } async function savePatient(patient) { if (!canEditPatients) { window.alert('Você não tem permissão para salvar pacientes.') return } const isNew = !rows.some((item) => item.id === patient.id) setSaving(true) try { if (isNew) { const created = normalizeCreatedPatient(await patientRepository.create(patient)) const newRow = { ...patient, id: created?.id || patient.id, detailId: created?.id || patient.detailId || patient.id, name: created?.full_name || created?.name || patient.name, phone: created?.phone_mobile || created?.phone || patient.phone, } setRows((currentRows) => [newRow, ...currentRows]) } else { await patientRepository.update(patient.id, patient) setRows((currentRows) => currentRows.map((item) => (item.id === patient.id ? patient : item)) ) } } catch (err) { window.alert(`Erro ao salvar paciente: ${err.message}`) return } finally { setSaving(false) } setEditingId(null) setPage(1) setView('list') } function openDetail(patient) { setOpenMenuId(null) if (patient.detailId) { navigate(`/pacientes/${patient.detailId}`) return } openForm(patient.id) } if (loading) { return

Carregando pacientes...

} if (error) { return

Erro ao carregar pacientes: {error}

} if (view === 'form') { return ( patient.id)} onCancel={() => { setEditingId(null) setView('list') }} onSave={savePatient} patient={editingPatient} saving={saving} /> ) } return (

Pacientes

Gerencie as informações de seus pacientes

{canEditPatients ? ( ) : null}
{ setSearch(event.target.value) setPage(1) }} placeholder="Buscar por nome ou documento..." value={search} />
{ setInsurance(value) setPage(1) }} options={INSURANCE_OPTIONS} value={insurance} /> { setVip(value) setPage(1) }} options={['Sim', 'Não']} value={vip} />
{ setBirthday(value) setPage(1) }} options={['Hoje', 'Neste mês']} value={birthday} />
{hasAdvancedFilters ? (
Filtros ativos: {city ? setCity('')} /> : null} {state ? setState('')} /> : null} {ageMin ? setAgeMin('')} /> : null} {ageMax ? setAgeMax('')} /> : null} {lastVisitSince ? ( setLastVisitSince('')} /> ) : null}
) : null}
{paginatedPatients.length ? ( paginatedPatients.map((patient) => ( )) ) : ( )}
Nome Telefone Cidade Estado Ultimo atendimento Proximo atendimento Ações
{patient.phone || missingValue('Telefone')} {patient.city || missingValue('Cidade')} {patient.state || missingValue('Estado')} {patient.lastVisit || 'Ainda não houve atendimento'} {patient.nextVisit || 'Nenhum atendimento agendado'} {openMenuId === patient.id ? ( <>
Nenhum paciente encontrado.

Mostrando {filteredPatients.length ? startIndex + 1 : 0}- {Math.min(startIndex + ITEMS_PER_PAGE, filteredPatients.length)} de {filteredPatients.length} pacientes

setPage(currentPage - 1)}> {Array.from({ length: totalPages }, (_, index) => index + 1).map((pageNumber) => ( ))} setPage(currentPage + 1)}>
{advancedOpen ? ( { setPage(1) setAdvancedOpen(false) }} onClear={resetAdvancedFilters} onClose={() => setAdvancedOpen(false)} setAgeMax={setAgeMax} setAgeMin={setAgeMin} setCity={setCity} setLastVisitSince={setLastVisitSince} setState={setState} state={state} stateOptions={BRAZILIAN_STATES} /> ) : null}
) } function PatientEditor({ existingIds, onCancel, onSave, patient, saving }) { const [formData, setFormData] = useState(() => ({ id: patient?.id || '', 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 || '', plan: patient?.plan || '', 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 let nextValue = type === 'checkbox' ? checked : value if (name === 'cpf') { nextValue = maskCPF(value) } if (name === 'phone') { nextValue = maskPhone(value) } if (name === 'phoneSecondary') { nextValue = maskPhone(value) } if (name === 'zipCode') { nextValue = maskCEP(value) } setFormData((currentData) => ({ ...currentData, [name]: nextValue })) } function handleSubmit(event) { event.preventDefault() if (!formData.name.trim()) { window.alert('O nome e obrigatorio.') 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 || formatBirthday(formData.birthDate), city: formData.city, document: formData.cpf ? `CPF ${formData.cpf}` : 'CPF não informado', insurance: formData.insurance, lastVisit: formData.lastVisit || 'Ainda não houve atendimento', nextVisit: formData.nextVisit || null, phone: formData.phone, plan: formData.plan, state: formData.state, address: formatAddress(formData), notes: formData.notesText ? [formData.notesText] : [], }) } return (

Paciente

Gerencie as informações de seus pacientes

Dados do Paciente