forked from RiseUP/riseup_squad_03
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 { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { normalizeRole } from '../config/permissions.js'
|
||||
import { patientRepository } from '../repositories/patientRepository.js'
|
||||
import { professionalRepository } from '../repositories/professionalRepository.js'
|
||||
import { profileRepository } from '../repositories/profileRepository.js'
|
||||
import { reportRepository } from '../repositories/reportRepository.js'
|
||||
|
||||
const ITEMS_PER_PAGE = 25
|
||||
@@ -12,6 +14,11 @@ const statusConfig = {
|
||||
pill: 'bg-amber-500/20 text-amber-400',
|
||||
stat: 'text-amber-400',
|
||||
},
|
||||
finalized: {
|
||||
label: 'Finalizado',
|
||||
pill: 'bg-emerald-500/20 text-emerald-400',
|
||||
stat: 'text-emerald-400',
|
||||
},
|
||||
}
|
||||
|
||||
const orderOptions = [
|
||||
@@ -44,13 +51,17 @@ const emptyEditor = {
|
||||
dueAt: '',
|
||||
}
|
||||
|
||||
export function ReportsPage() {
|
||||
export function ReportsPage({ role }) {
|
||||
const [reports, setReports] = useState([])
|
||||
const [patients, setPatients] = useState([])
|
||||
const [professionals, setProfessionals] = useState([])
|
||||
const [viewerProfile, setViewerProfile] = useState(null)
|
||||
const [currentProfessional, setCurrentProfessional] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [scopeLoading, setScopeLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const isDoctorRole = normalizeRole(role) === 'medico'
|
||||
|
||||
const [filterPatientId, setFilterPatientId] = useState('')
|
||||
const [filterStatus, setFilterStatus] = useState('')
|
||||
@@ -121,6 +132,11 @@ export function ReportsPage() {
|
||||
value: enrichedReports.filter((report) => report.status === 'draft').length,
|
||||
className: statusConfig.draft.stat,
|
||||
},
|
||||
{
|
||||
label: 'Finalizados',
|
||||
value: enrichedReports.filter((report) => report.status === 'finalized').length,
|
||||
className: statusConfig.finalized.stat,
|
||||
},
|
||||
],
|
||||
[enrichedReports],
|
||||
)
|
||||
@@ -131,14 +147,30 @@ export function ReportsPage() {
|
||||
const paginatedReports = enrichedReports.slice(startIndex, startIndex + ITEMS_PER_PAGE)
|
||||
|
||||
const loadReports = useCallback(async () => {
|
||||
if (scopeLoading) return
|
||||
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const doctorPatientIds = isDoctorRole
|
||||
? patientOptions.map((patient) => patient.id).filter(Boolean)
|
||||
: []
|
||||
const createdByValues = isDoctorRole
|
||||
? uniqueValues([
|
||||
viewerProfile?.id,
|
||||
viewerProfile?.doctorId,
|
||||
currentProfessional?.userId,
|
||||
currentProfessional?.id,
|
||||
])
|
||||
: []
|
||||
|
||||
const data = await reportRepository.getInitialReports({
|
||||
patientId: filterPatientId || undefined,
|
||||
patientIds: !filterPatientId && doctorPatientIds.length ? doctorPatientIds : undefined,
|
||||
status: filterStatus || undefined,
|
||||
createdBy: filterCreatedBy || undefined,
|
||||
createdBy: !isDoctorRole ? filterCreatedBy || undefined : undefined,
|
||||
createdByValues: isDoctorRole && !doctorPatientIds.length ? createdByValues : undefined,
|
||||
order: filterOrder,
|
||||
})
|
||||
|
||||
@@ -146,36 +178,54 @@ export function ReportsPage() {
|
||||
setPage(1)
|
||||
} catch (loadError) {
|
||||
console.error(loadError)
|
||||
setError(loadError.message || 'Erro ao carregar relatórios médicos.')
|
||||
setError(loadError.message || 'Erro ao carregar relatórios.')
|
||||
setReports([])
|
||||
setPage(1)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [filterCreatedBy, filterOrder, filterPatientId, filterStatus])
|
||||
}, [currentProfessional, filterCreatedBy, filterOrder, filterPatientId, filterStatus, isDoctorRole, patientOptions, scopeLoading, viewerProfile])
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
|
||||
Promise.all([
|
||||
patientRepository.getAll(),
|
||||
professionalRepository.getAll(),
|
||||
])
|
||||
.then(([patientData, professionalData]) => {
|
||||
async function loadAuxiliaryData() {
|
||||
setScopeLoading(true)
|
||||
|
||||
try {
|
||||
const [professionalData, currentProfile] = await Promise.all([
|
||||
professionalRepository.getAll(),
|
||||
profileRepository.getCurrentUserProfile(),
|
||||
])
|
||||
|
||||
if (!active) return
|
||||
|
||||
const resolvedProfessional = professionalRepository.resolveCurrentProfessional(currentProfile, professionalData || [])
|
||||
const patientData = isDoctorRole && resolvedProfessional?.id
|
||||
? await patientRepository.getDirectoryRows({ doctorId: resolvedProfessional.id })
|
||||
: await patientRepository.getAll()
|
||||
|
||||
if (!active) return
|
||||
|
||||
setViewerProfile(currentProfile)
|
||||
setCurrentProfessional(resolvedProfessional)
|
||||
setPatients(patientData || [])
|
||||
setProfessionals(professionalData || [])
|
||||
})
|
||||
.catch((loadError) => {
|
||||
} catch (loadError) {
|
||||
if (!active) return
|
||||
console.error(loadError)
|
||||
setError(loadError.message || 'Erro ao carregar dados auxiliares.')
|
||||
})
|
||||
} finally {
|
||||
if (active) setScopeLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadAuxiliaryData()
|
||||
|
||||
return () => {
|
||||
active = false
|
||||
}
|
||||
}, [])
|
||||
}, [isDoctorRole])
|
||||
|
||||
useEffect(() => {
|
||||
loadReports()
|
||||
@@ -238,7 +288,7 @@ export function ReportsPage() {
|
||||
setEditorOpen(false)
|
||||
await loadReports()
|
||||
} catch (saveError) {
|
||||
alert(saveError.message || 'Erro ao salvar relatório médico.')
|
||||
alert(saveError.message || 'Erro ao salvar relatório.')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
@@ -248,8 +298,8 @@ export function ReportsPage() {
|
||||
<div className="mx-auto max-w-7xl space-y-6 text-[#e5e5e5]">
|
||||
<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]">Relatórios médicos</h1>
|
||||
<p className="mt-1 text-sm text-[#a3a3a3]">Consulta, criação e edição de relatórios médicos.</p>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-[#e5e5e5]">Relatórios</h1>
|
||||
<p className="mt-1 text-sm text-[#a3a3a3]">Consulta, criação e edição de relatórios.</p>
|
||||
</div>
|
||||
<button
|
||||
className="inline-flex h-10 items-center justify-center gap-2 rounded-lg bg-[#3b82f6] px-4 text-sm font-medium text-white transition hover:bg-[#2563eb]"
|
||||
@@ -303,6 +353,7 @@ export function ReportsPage() {
|
||||
>
|
||||
<option value="">Todos os status</option>
|
||||
<option value="draft">Rascunho</option>
|
||||
<option value="finalized">Finalizado</option>
|
||||
</select>
|
||||
</FilterField>
|
||||
|
||||
@@ -365,7 +416,7 @@ export function ReportsPage() {
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td className="px-4 py-8 text-center text-sm text-[#a3a3a3]" colSpan={7}>
|
||||
Carregando relatórios médicos...
|
||||
Carregando relatórios...
|
||||
</td>
|
||||
</tr>
|
||||
) : paginatedReports.length ? (
|
||||
@@ -438,6 +489,8 @@ export function ReportsPage() {
|
||||
}
|
||||
|
||||
function ReportRow({ onEdit, onView, report }) {
|
||||
const currentStatus = statusConfig[report.status] || statusConfig.draft
|
||||
|
||||
return (
|
||||
<tr className="transition hover:bg-[#303030]">
|
||||
<td className="px-4 py-3 align-top text-[#a3a3a3]">{report.orderNumber || '-'}</td>
|
||||
@@ -451,8 +504,8 @@ function ReportRow({ onEdit, onView, report }) {
|
||||
<td className="px-4 py-3 align-top whitespace-normal break-words text-[#a3a3a3]">{report.requestedBy || '-'}</td>
|
||||
<td className="px-4 py-3 align-top text-[#a3a3a3]">{formatDate(report.createdAt)}</td>
|
||||
<td className="px-4 py-3 align-top">
|
||||
<span className={`rounded px-2 py-1 text-[10px] font-bold ${statusConfig[report.status].pill}`}>
|
||||
{statusConfig[report.status].label}
|
||||
<span className={`rounded px-2 py-1 text-[10px] font-bold ${currentStatus.pill}`}>
|
||||
{currentStatus.label}
|
||||
</span>
|
||||
</td>
|
||||
<td className="sticky right-0 bg-[#262626] px-4 py-3 text-right shadow-[-10px_0_12px_-12px_rgba(0,0,0,0.75)]">
|
||||
@@ -480,7 +533,7 @@ function ReportEditorModal({ editor, onChange, onClose, onSave, patientOptions,
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-[#404040] px-6 py-4">
|
||||
<h2 className="text-lg font-bold text-[#e5e5e5]">
|
||||
{editor.id ? 'Editar relatório médico' : 'Novo relatório médico'}
|
||||
{editor.id ? 'Editar relatório' : 'Novo relatório'}
|
||||
</h2>
|
||||
<button className="rounded-lg p-1.5 transition hover:bg-[#2a2a2a]" onClick={onClose} type="button">
|
||||
<ReportIcon className="size-4 text-[#a3a3a3]" name="x" />
|
||||
@@ -504,6 +557,7 @@ function ReportEditorModal({ editor, onChange, onClose, onSave, patientOptions,
|
||||
<DarkField label="Status">
|
||||
<select className={inputClass} onChange={(event) => updateField('status', event.target.value)} value={editor.status}>
|
||||
<option value="draft">Rascunho</option>
|
||||
<option value="finalized">Finalizado</option>
|
||||
</select>
|
||||
</DarkField>
|
||||
</div>
|
||||
@@ -574,11 +628,11 @@ function ReportEditorModal({ editor, onChange, onClose, onSave, patientOptions,
|
||||
/>
|
||||
</DarkField>
|
||||
|
||||
<DarkField label="Conteúdo HTML">
|
||||
<DarkField label="Complemento">
|
||||
<textarea
|
||||
className={`${textareaClass} min-h-72`}
|
||||
onChange={(event) => updateField('contentHtml', event.target.value)}
|
||||
placeholder="<p>Conteúdo do relatório</p>"
|
||||
placeholder="Complemento em texto simples"
|
||||
value={editor.contentHtml}
|
||||
/>
|
||||
</DarkField>
|
||||
@@ -631,6 +685,8 @@ function ReportEditorModal({ editor, onChange, onClose, onSave, patientOptions,
|
||||
}
|
||||
|
||||
function ReportViewModal({ onClose, report }) {
|
||||
const currentStatus = statusConfig[report.status] || statusConfig.draft
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4" onClick={onClose}>
|
||||
<div
|
||||
@@ -639,7 +695,7 @@ function ReportViewModal({ onClose, report }) {
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-[#404040] px-6 py-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-[#e5e5e5]">Relatório médico</h2>
|
||||
<h2 className="text-lg font-bold text-[#e5e5e5]">Relatório</h2>
|
||||
<p className="mt-1 text-xs text-[#a3a3a3]">{report.orderNumber || 'Sem número'} </p>
|
||||
</div>
|
||||
<button className="rounded-lg p-1.5 transition hover:bg-[#2a2a2a]" onClick={onClose} type="button">
|
||||
@@ -653,7 +709,7 @@ function ReportViewModal({ onClose, report }) {
|
||||
<DetailCard label="Solicitante" value={report.requestedBy || '-'} />
|
||||
<DetailCard label="Criado em" value={formatDate(report.createdAt)} />
|
||||
<DetailCard label="Criado por" value={report.createdByName} />
|
||||
<DetailCard label="Status" value={statusConfig[report.status].label} />
|
||||
<DetailCard label="Status" value={currentStatus.label} />
|
||||
<DetailCard label="Prazo" value={formatDateTime(report.dueAt)} />
|
||||
</div>
|
||||
|
||||
@@ -677,14 +733,11 @@ function ReportViewModal({ onClose, report }) {
|
||||
</div>
|
||||
|
||||
<div className="mt-6 rounded-xl border border-[#404040] bg-[#1a1a1a] p-5">
|
||||
<p className="mb-3 text-xs font-semibold uppercase tracking-wide text-[#a3a3a3]">Conteúdo HTML</p>
|
||||
<p className="mb-3 text-xs font-semibold uppercase tracking-wide text-[#a3a3a3]">Complemento</p>
|
||||
{report.contentHtml ? (
|
||||
<div
|
||||
className="prose prose-invert max-w-none text-sm text-[#e5e5e5]"
|
||||
dangerouslySetInnerHTML={{ __html: report.contentHtml }}
|
||||
/>
|
||||
<p className="whitespace-pre-wrap text-sm leading-6 text-[#e5e5e5]">{report.contentHtml}</p>
|
||||
) : (
|
||||
<p className="text-sm text-[#a3a3a3]">Nenhum conteúdo HTML informado.</p>
|
||||
<p className="text-sm text-[#a3a3a3]">Nenhum complemento informado.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -789,6 +842,10 @@ function toDateTimeLocal(value) {
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`
|
||||
}
|
||||
|
||||
function uniqueValues(values) {
|
||||
return [...new Set(values.map((value) => String(value || '').trim()).filter(Boolean))]
|
||||
}
|
||||
|
||||
function ReportIcon({ className = 'size-4', name }) {
|
||||
const common = {
|
||||
className,
|
||||
|
||||
Reference in New Issue
Block a user