modified: src/App.jsx

modified:   src/components/AppShell.jsx
modified:   src/config/api.js
modified:   src/config/permissions.js
modified:   src/data/mockData.js
modified:   src/hooks/useAgenda.js
modified:   src/hooks/useAuth.js
modified:   src/mappers/appointmentMapper.js
modified:   src/pages/AgendaPage.jsx
modified:   src/pages/AuthPages.jsx
modified:   src/pages/HomePage.jsx
modified:   src/pages/MedicalRecordsPage.jsx
modified:   src/pages/MessagesPage.jsx
modified:   src/pages/NotFoundPage.jsx
modified:   src/pages/PatientsPage.jsx
modified:   src/pages/ReportsPage.jsx
modified:   src/pages/TeamPage.jsx
modified:   src/pages/UsersPage.jsx
modified:   src/pages/VisitsPage.jsx
modified:   src/repositories/authRepository.js
new file:   src/repositories/availabilityRepository.js
modified:   src/repositories/communicationRepository.js
modified:   src/repositories/patientRepository.js
modified:   src/repositories/professionalRepository.js
modified:   src/repositories/profileRepository.js
modified:   src/repositories/reportRepository.js
modified:   src/repositories/repositoryUtils.js
modified:   src/repositories/settingsRepository.js
modified:   src/repositories/userRepository.js
modified:   src/repositories/visitRepository.js
This commit is contained in:
2026-05-06 01:09:36 -03:00
parent bb5200664a
commit 666b3b5c0e
30 changed files with 1038 additions and 376 deletions

View File

@@ -1,5 +1,6 @@
import { useEffect, useMemo, useState } from 'react'
import { hasCapability } from '../config/permissions.js'
import { patientRepository } from '../repositories/patientRepository.js'
const ITEMS_PER_PAGE = 25
@@ -14,7 +15,7 @@ const patientTabs = [
{ label: 'Documentos', value: 'documentos' },
]
export function PatientsPage({ navigate }) {
export function PatientsPage({ navigate, role }) {
const [rows, setRows] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
@@ -45,6 +46,8 @@ export function PatientsPage({ navigate }) {
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) => {
@@ -65,7 +68,7 @@ export function PatientsPage({ navigate }) {
return false
}
if (vip === 'Nao' && patient.vip) {
if (vip === 'Não' && patient.vip) {
return false
}
@@ -117,12 +120,18 @@ export function PatientsPage({ navigate }) {
}
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)
@@ -156,6 +165,11 @@ export function PatientsPage({ navigate }) {
}
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)
@@ -206,16 +220,18 @@ async function deletePatient(patientId) {
<div className="flex flex-col items-start justify-between gap-4 md:flex-row md:items-center">
<div>
<h1 className="text-2xl font-bold tracking-tight text-[#e5e5e5]">Pacientes</h1>
<p className="mt-1 text-sm text-[#a3a3a3]">Gerencie as informacoes de seus pacientes</p>
<p className="mt-1 text-sm text-[#a3a3a3]">Gerencie as informações de seus pacientes</p>
</div>
<button
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded-lg bg-[#3b82f6] px-4 text-sm font-medium text-white shadow-sm transition hover:bg-[#2563eb] md:w-auto"
onClick={() => openForm()}
type="button"
>
<PatientIcon name="user-plus" />
Adicionar
</button>
{canEditPatients ? (
<button
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded-lg bg-[#3b82f6] px-4 text-sm font-medium text-white shadow-sm transition hover:bg-[#2563eb] md:w-auto"
onClick={() => openForm()}
type="button"
>
<PatientIcon name="user-plus" />
Adicionar
</button>
) : null}
</div>
<section className="rounded-2xl border border-[#404040] bg-[#262626] px-6 py-8 shadow-sm xl:py-14">
@@ -253,7 +269,7 @@ async function deletePatient(patientId) {
setVip(value)
setPage(1)
}}
options={['Sim', 'Nao']}
options={['Sim', 'Não']}
value={vip}
/>
@@ -310,7 +326,7 @@ async function deletePatient(patientId) {
<th className="w-[8%] px-6 py-4">Estado</th>
<th className="w-[16%] px-6 py-4">Ultimo atendimento</th>
<th className="w-[18%] px-6 py-4">Proximo atendimento</th>
<th className="sticky right-0 w-[8.5rem] bg-[#171717] px-6 py-4 text-right">Acoes</th>
<th className="sticky right-0 w-[8.5rem] bg-[#171717] px-6 py-4 text-right">Ações</th>
</tr>
</thead>
<tbody className="divide-y divide-[#404040] bg-[#262626]">
@@ -335,11 +351,11 @@ async function deletePatient(patientId) {
<td className="px-6 py-4 align-top whitespace-normal break-words text-[#a3a3a3]">{patient.phone}</td>
<td className="px-6 py-4 align-top whitespace-normal break-words text-[#a3a3a3]">{patient.city}</td>
<td className="px-6 py-4 align-top text-[#a3a3a3]">{patient.state}</td>
<td className="px-6 py-4 align-top whitespace-normal break-words text-[#a3a3a3]">{patient.lastVisit || 'Ainda nao houve atendimento'}</td>
<td className="px-6 py-4 align-top whitespace-normal break-words text-[#a3a3a3]">{patient.lastVisit || 'Ainda não houve atendimento'}</td>
<td className="px-6 py-4 align-top whitespace-normal break-words text-[#a3a3a3]">{patient.nextVisit || 'Nenhum atendimento agendado'}</td>
<td className="relative sticky right-0 bg-[#262626] px-6 py-4 text-right shadow-[-10px_0_12px_-12px_rgba(0,0,0,0.75)]">
<button
aria-label={`Acoes de ${patient.name}`}
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)}
type="button"
@@ -356,7 +372,7 @@ async function deletePatient(patientId) {
/>
<div className="absolute right-8 top-10 z-20 w-48 rounded-lg border border-[#404040] bg-[#303030] py-1 text-left shadow-lg">
<ActionItem icon="file" label="Ver detalhes" onClick={() => openDetail(patient)} />
<ActionItem icon="edit" label="Editar" onClick={() => openForm(patient.id)} />
{canEditPatients ? <ActionItem icon="edit" label="Editar" onClick={() => openForm(patient.id)} /> : null}
<ActionItem
icon="calendar"
label="Marcar consulta"
@@ -365,7 +381,9 @@ async function deletePatient(patientId) {
navigate('/agenda')
}}
/>
<ActionItem danger icon="trash" label="Excluir" onClick={() => deletePatient(patient.id)} />
{canHardDeletePatients ? (
<ActionItem danger icon="trash" label="Excluir" onClick={() => deletePatient(patient.id)} />
) : null}
</div>
</>
) : null}
@@ -488,12 +506,12 @@ function PatientEditor({ existingIds, onCancel, onSave, patient, saving }) {
id: formData.id || uniqueSlug(formData.name, existingIds),
age: Number(formData.age) || 0,
birthday: formData.birthday || '07/04',
city: formData.city || 'Cidade nao informada',
document: formData.cpf ? `CPF ${formData.cpf}` : 'CPF nao informado',
city: formData.city || 'Cidade não informada',
document: formData.cpf ? `CPF ${formData.cpf}` : 'CPF não informado',
insurance: formData.insurance || 'Particular',
lastVisit: formData.lastVisit || 'Ainda nao houve atendimento',
lastVisit: formData.lastVisit || 'Ainda não houve atendimento',
nextVisit: formData.nextVisit || null,
phone: formData.phone || 'Telefone nao informado',
phone: formData.phone || 'Telefone não informado',
plan: formData.insurance || formData.plan || 'Particular',
state: formData.state || 'UF',
})
@@ -512,7 +530,7 @@ function PatientEditor({ existingIds, onCancel, onSave, patient, saving }) {
</button>
<div>
<h1 className="text-2xl font-bold tracking-tight text-[#e5e5e5]">Paciente</h1>
<p className="mt-1 text-sm text-[#a3a3a3]">Gerencie as informacoes de seus pacientes</p>
<p className="mt-1 text-sm text-[#a3a3a3]">Gerencie as informações de seus pacientes</p>
</div>
</div>
</div>
@@ -551,8 +569,8 @@ function PatientEditor({ existingIds, onCancel, onSave, patient, saving }) {
<DarkField className="md:col-span-3" label="Etnia">
<select className={darkInput} defaultValue="">
<option value="">Selecione</option>
<option>Indigena</option>
<option>Nao Indigena</option>
<option>Indígena</option>
<option>Não Indígena</option>
</select>
</DarkField>
<DarkField className="md:col-span-3" label="Estado civil">
@@ -605,12 +623,12 @@ function PatientEditor({ existingIds, onCancel, onSave, patient, saving }) {
</section>
<section className={darkCard}>
<h2 className="mb-6 text-lg font-semibold text-[#e5e5e5]">Endereco</h2>
<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>
<DarkField className="md:col-span-5" label="Endereco">
<DarkField className="md:col-span-5" label="Endereço">
<input className={darkInput} />
</DarkField>
<DarkField className="md:col-span-4" label="Cidade">
@@ -621,7 +639,7 @@ function PatientEditor({ existingIds, onCancel, onSave, patient, saving }) {
<option value="">Selecione</option>
<option value="PE">Pernambuco</option>
<option value="SE">Sergipe</option>
<option value="SP">Sao Paulo</option>
<option value="SP">São Paulo</option>
<option value="RJ">Rio de Janeiro</option>
</select>
</DarkField>
@@ -629,7 +647,7 @@ function PatientEditor({ existingIds, onCancel, onSave, patient, saving }) {
</section>
<section className={darkCard}>
<h2 className="mb-6 text-lg font-semibold text-[#e5e5e5]">Informacoes de convenio</h2>
<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">
<select className={darkInput} name="insurance" onChange={handleChange} value={formData.insurance}>