diff --git a/src/hooks/useAgenda.js b/src/hooks/useAgenda.js index 8258b33..c43294d 100644 --- a/src/hooks/useAgenda.js +++ b/src/hooks/useAgenda.js @@ -214,6 +214,11 @@ export function useAgenda() { async function handleCreate(event) { event.preventDefault() + if (!form.patientId) { + alert('Selecione um paciente para criar o agendamento.') + return + } + const targetProfessionalId = agendaScope === 'doctor' ? currentProfessional?.id : form.professionalId diff --git a/src/index.css b/src/index.css index 9872ace..fddd4ef 100644 --- a/src/index.css +++ b/src/index.css @@ -43,3 +43,133 @@ button:disabled { #root { min-height: 100vh; } + +:root[data-theme='light'] { + color: #333333; + background: #eef2f7; + color-scheme: light; +} + +[data-theme='light'] body { + background: #eef2f7; + color: #333333; +} + +[data-theme='light'] input, +[data-theme='light'] select, +[data-theme='light'] textarea { + color-scheme: light; +} + +[data-theme='light'] aside.bg-\[\#262626\] { + background-color: #f3f4f6; +} + +[data-theme='light'] .bg-\[\#0a0a0a\], +[data-theme='light'] .bg-\[\#171717\] { + background-color: #eef2f7; +} + +[data-theme='light'] .bg-\[\#1a1a1a\] { + background-color: #f9fafb; +} + +[data-theme='light'] .bg-\[\#262626\] { + background-color: #ffffff; +} + +[data-theme='light'] .bg-\[\#1f1f1f\], +[data-theme='light'] .bg-\[\#202020\] { + background-color: #f3f4f6; +} + +[data-theme='light'] .bg-\[\#2a2a2a\], +[data-theme='light'] .bg-\[\#303030\], +[data-theme='light'] .bg-\[\#333333\] { + background-color: #e8edf4; +} + +[data-theme='light'] .bg-\[\#3b82f6\] { + background-color: #3b82f6; +} + +[data-theme='light'] .bg-\[\#2563eb\], +[data-theme='light'] .hover\:bg-\[\#2563eb\]:hover, +[data-theme='light'] .hover\:bg-\[\#3478ed\]:hover { + background-color: #2563eb; +} + +[data-theme='light'] .hover\:bg-\[\#2a2a2a\]:hover, +[data-theme='light'] .hover\:bg-\[\#303030\]:hover, +[data-theme='light'] .hover\:bg-\[\#333333\]:hover { + background-color: #e8edf4; +} + +[data-theme='light'] .disabled\:bg-\[\#303030\]:disabled { + background-color: #eef2f7; +} + +[data-theme='light'] .border-\[\#404040\], +[data-theme='light'] .divide-\[\#404040\] > :not([hidden]) ~ :not([hidden]) { + border-color: #d6dee8; +} + +[data-theme='light'] .border-\[\#525252\], +[data-theme='light'] .hover\:border-\[\#525252\]:hover { + border-color: #d1d5db; +} + +[data-theme='light'] .hover\:border-\[\#404040\]:hover, +[data-theme='light'] .disabled\:border-\[\#404040\]:disabled { + border-color: #d6dee8; +} + +[data-theme='light'] .text-\[\#f5f5f5\], +[data-theme='light'] .text-\[\#e5e5e5\], +[data-theme='light'] .hover\:text-\[\#e5e5e5\]:hover { + color: #333333; +} + +[data-theme='light'] .text-\[\#d4d4d4\], +[data-theme='light'] .text-\[\#b8b8b8\] { + color: #4b5563; +} + +[data-theme='light'] .text-\[\#a3a3a3\], +[data-theme='light'] .text-\[\#737373\], +[data-theme='light'] .disabled\:text-\[\#737373\]:disabled { + color: #6b7280; +} + +[data-theme='light'] .text-\[\#51a2ff\] { + color: #1d4ed8; +} + +[data-theme='light'] .placeholder\:text-\[\#737373\]::placeholder, +[data-theme='light'] .placeholder\:text-\[\#a3a3a3\]::placeholder { + color: #6b7280; +} + +[data-theme='light'] [class*='[color-scheme:dark]'] { + color-scheme: light; +} + +[data-theme='light'] svg [stroke='#303030'] { + stroke: #e5e7eb; +} + +[data-theme='light'] svg [stroke='#1d4ed8'] { + stroke: #3b82f6; +} + +[data-theme='light'] svg [fill='#262626'] { + fill: #ffffff; +} + +[data-theme='light'] svg [fill='#a3a3a3'] { + fill: #6b7280; +} + +[data-theme='light'] svg [fill='#171717'] { + fill: #f9fafb; +} diff --git a/src/main.jsx b/src/main.jsx index b9a1a6d..0ca7c6c 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -2,6 +2,9 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.jsx' +import { applyTheme, getStoredTheme } from './utils/theme.js' + +applyTheme(getStoredTheme()) createRoot(document.getElementById('root')).render( diff --git a/src/mappers/reportMapper.js b/src/mappers/reportMapper.js index e378126..2d0abf9 100644 --- a/src/mappers/reportMapper.js +++ b/src/mappers/reportMapper.js @@ -27,6 +27,7 @@ export const reportMapper = { toApi(uiData) { return cleanPayload({ patient_id: uiData.patientId, + order_number: emptyToUndefined(uiData.orderNumber), status: normalizeApiStatus(uiData.status), exam: emptyToUndefined(uiData.exam), requested_by: emptyToUndefined(uiData.requestedBy), @@ -38,6 +39,8 @@ export const reportMapper = { hide_date: Boolean(uiData.hideDate), hide_signature: Boolean(uiData.hideSignature), due_at: emptyToUndefined(uiData.dueAt), + created_by: emptyToUndefined(uiData.createdBy), + updated_by: emptyToUndefined(uiData.updatedBy), }) }, } diff --git a/src/pages/AgendaPage.jsx b/src/pages/AgendaPage.jsx index de484be..bb3769f 100644 --- a/src/pages/AgendaPage.jsx +++ b/src/pages/AgendaPage.jsx @@ -31,6 +31,8 @@ const viewFilters = [ { label: 'Mês', value: 'Mes' }, ] +const appointmentTypeOptions = ['Retorno', 'Primeira consulta', 'Exame', 'Avaliação pre-op'] + export function AgendaPage({ navigate }) { const [modalPatientSearch, setModalPatientSearch] = useState('') const [modalDoctorSearch, setModalDoctorSearch] = useState('') @@ -84,29 +86,35 @@ export function AgendaPage({ navigate }) { ), ].sort((a, b) => a.localeCompare(b, 'pt-BR')) const filteredPatients = (() => { - const query = modalPatientSearch.trim().toLowerCase() + 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 = modalDoctorSearch.trim().toLowerCase() + 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 selectedPatient = patients.find((patient) => String(patient.id) === String(form.patientId)) + const selectedProfessional = professionals.find((professional) => String(professional.id) === String(form.professionalId)) return (
@@ -318,23 +326,25 @@ export function AgendaPage({ navigate }) { setModalPatientSearch(event.target.value)} + 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} + value={modalPatientSearch || getPatientLabel(selectedPatient)} + /> + { + updateForm('patientId', patient.id) + setModalPatientSearch(getPatientLabel(patient)) + }} + selectedId={form.patientId} /> -
@@ -389,33 +399,42 @@ export function AgendaPage({ navigate }) { ) : ( <> setModalDoctorSearch(event.target.value)} + 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} + value={modalDoctorSearch || selectedProfessional?.name || ''} + /> + 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} /> - )} - updateForm('type', event.target.value)} value={form.type} - /> + > + {appointmentTypeOptions.map((type) => ( + + ))} +
@@ -473,3 +492,44 @@ function DarkModal({ children, onClose, open, title }) {
) } + +function SearchResults({ emptyText, getDescription, getLabel, items, onSelect, selectedId }) { + return ( +
+ {items.length ? ( + items.map((item) => { + const isSelected = String(item.id) === String(selectedId) + return ( + + ) + }) + ) : ( +

{emptyText}

+ )} +
+ ) +} + +function getPatientLabel(patient) { + return patient?.name || patient?.full_name || patient?.nome || '' +} + +function normalizeSearch(value) { + return String(value || '') + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim() + .toLowerCase() +} diff --git a/src/pages/AuthPages.jsx b/src/pages/AuthPages.jsx index ef0bf93..82e8903 100644 --- a/src/pages/AuthPages.jsx +++ b/src/pages/AuthPages.jsx @@ -3,7 +3,7 @@ import { useState } from 'react' import { authRepository } from '../repositories/authRepository.js' import { BrandLogo } from '../components/Brand.jsx' -import { FeatureBadge, FeatureCallout } from '../components/FeatureState.jsx' +import { FeatureCallout } from '../components/FeatureState.jsx' import loginClinicImage from '../assets/figma/login-clinic.png' const mockCredentials = [ @@ -164,7 +164,7 @@ export function LoginPage({ navigate }) { {credentialsOpen ? (

- Credenciais mockadas + Credenciais de acesso

{mockCredentials.map((credential) => ( @@ -190,11 +190,10 @@ export function LoginPage({ navigate }) {
diff --git a/src/pages/MedicalRecordsPage.jsx b/src/pages/MedicalRecordsPage.jsx index 862c1e0..efc1dbb 100644 --- a/src/pages/MedicalRecordsPage.jsx +++ b/src/pages/MedicalRecordsPage.jsx @@ -1,7 +1,8 @@ -import { useMemo, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { FeatureCallout } from '../components/FeatureState.jsx' import { medicalRecordRepository } from '../repositories/medicalRecordRepository.js' +import { patientRepository } from '../repositories/patientRepository.js' const inputClass = @@ -12,9 +13,27 @@ const cardClass = 'rounded-2xl border border-[#404040] bg-[#262626] shadow-sm' export function MedicalRecordsPage() { const recordTypes = medicalRecordRepository.getRecordTypes() const [records, setRecords] = useState(() => medicalRecordRepository.getInitialRecords()) + const [patients, setPatients] = useState([]) const [search, setSearch] = useState('') const [editorOpen, setEditorOpen] = useState(false) + useEffect(() => { + let active = true + + patientRepository + .getDirectoryRows() + .then((data) => { + if (active) setPatients(data || []) + }) + .catch(() => { + if (active) setPatients([]) + }) + + return () => { + active = false + } + }, []) + const filteredRecords = useMemo(() => { return records.filter((record) => { const matchesSearch = [record.patient, record.cid, record.doctor] @@ -81,6 +100,7 @@ export function MedicalRecordsPage() { setEditorOpen(false)} onSave={handleCreateRecord} + patients={patients} recordTypes={recordTypes} /> ) : null} @@ -149,8 +169,10 @@ function IconButton({ label, name }) { ) } -function RecordEditorModal({ onClose, onSave, recordTypes }) { +function RecordEditorModal({ onClose, onSave, patients, recordTypes }) { + const [patientSearch, setPatientSearch] = useState('') const [formData, setFormData] = useState({ + patientId: '', patient: '', date: '', type: 'Primeira Consulta', @@ -168,6 +190,31 @@ function RecordEditorModal({ onClose, onSave, recordTypes }) { setFormData((currentData) => ({ ...currentData, [name]: value })) } + const filteredPatients = (() => { + const query = normalizeSearch(patientSearch) + if (!query) return patients + + return patients.filter((patient) => + [patient.name, patient.full_name, patient.nome, patient.cpf, patient.document, patient.email] + .filter(Boolean) + .join(' ') + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase() + .includes(query), + ) + })() + + function selectPatient(patient) { + const name = getPatientName(patient) + setFormData((currentData) => ({ + ...currentData, + patientId: patient.id, + patient: name, + })) + setPatientSearch(name) + } + function handleSubmit(event) { event.preventDefault() const submitter = event.nativeEvent.submitter @@ -199,11 +246,36 @@ function RecordEditorModal({ onClose, onSave, recordTypes }) { { + setPatientSearch(event.target.value) + setFormData((currentData) => ({ ...currentData, patientId: '', patient: '' })) + }} placeholder="Buscar paciente..." - value={formData.patient} + type="search" + value={patientSearch || formData.patient} /> +
+ {filteredPatients.length ? ( + filteredPatients.slice(0, 6).map((patient) => { + const selected = String(patient.id) === String(formData.patientId) + return ( + + ) + }) + ) : ( +

Nenhum paciente encontrado.

+ )} +
{ - buildPatientRows(role) + buildPatientRows() .then((data) => setRows(data)) .catch((err) => setError(err.message)) .finally(() => setLoading(false)) - }, [role]) + }, []) const editingPatient = rows.find((patient) => patient.id === editingId) const hasAdvancedFilters = city || state || ageMin || ageMax || lastVisitSince @@ -180,13 +178,13 @@ export function PatientsPage({ navigate, role }) { try { if (isNew) { - const [created] = await patientRepository.create(patient) + const created = normalizeCreatedPatient(await patientRepository.create(patient)) const newRow = { ...patient, - id: created.id, - detailId: created.id, - name: created.full_name || patient.name, - phone: created.phone_mobile || patient.phone, + 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 { @@ -368,14 +366,14 @@ export function PatientsPage({ navigate, role }) { {patient.name} - {patient.insurance || 'Sem convenio'} {patient.vip ? ' | VIP' : ''} + {patient.insurance || missingValue('Convênio')} {patient.vip ? ' | VIP' : ''} - {patient.phone} - {patient.city} - {patient.state} + {patient.phone || missingValue('Telefone')} + {patient.city || missingValue('Cidade')} + {patient.state || missingValue('Estado')} {patient.lastVisit || 'Ainda não houve atendimento'} {patient.nextVisit || 'Nenhum atendimento agendado'} @@ -398,7 +396,7 @@ export function PatientsPage({ navigate, role }) { onClick={() => setOpenMenuId(null)} type="button" /> -
+
openDetail(patient)} /> {canEditPatients ? openForm(patient.id)} /> : null}
- - + +
@@ -1197,11 +1195,15 @@ function InfoRow({ label, value }) { return (
{label}
-
{value || 'Não informado'}
+
{value || missingValue(label)}
) } +function missingValue(label) { + return `${label} não informado` +} + function formatDisplayDate(value) { if (!value) return '' const [year, month, day] = String(value).split('-') @@ -1296,8 +1298,8 @@ function PageButton({ children, disabled, onClick }) { function ActionItem({ danger = false, icon, label, onClick }) { return ( + )) + ) : ( +

Nenhum médico encontrado.

+ )} +
@@ -632,7 +661,6 @@ function ReportEditorModal({ editor, onChange, onClose, onSave, patientOptions,