modo-claro

modified:   src/hooks/useAgenda.js
modified:   src/index.css
modified:   src/main.jsx
modified:   src/mappers/reportMapper.js
modified:   src/pages/AgendaPage.jsx
modified:   src/pages/AuthPages.jsx
modified:   src/pages/MedicalRecordsPage.jsx
modified:   src/pages/PatientsPage.jsx
modified:   src/pages/ReportsPage.jsx
modified:   src/pages/SettingsPage.jsx
modified:   src/repositories/analyticsRepository.js
modified:   src/repositories/authRepository.js
modified:   src/repositories/patientRepository.js
modified:   src/repositories/reportRepository.js
modified:   src/repositories/repositoryUtils.js
new file:   src/utils/theme.js
new file:   vercel.json
This commit is contained in:
2026-05-07 05:51:07 -03:00
parent 64d9527318
commit db7a2fe8f5
17 changed files with 669 additions and 121 deletions

View File

@@ -37,6 +37,7 @@ const cardClass = 'rounded-2xl border border-[#404040] bg-[#262626] shadow-sm'
const emptyEditor = {
id: null,
orderNumber: '',
patientId: '',
status: 'draft',
exam: '',
@@ -242,6 +243,7 @@ export function ReportsPage({ role }) {
function openEdit(report) {
setEditor({
id: report.id,
orderNumber: report.orderNumber,
patientId: String(report.patientId || ''),
status: report.status,
exam: report.exam,
@@ -264,6 +266,7 @@ export function ReportsPage({ role }) {
setSaving(true)
const payload = {
orderNumber: editor.id ? editor.orderNumber : `REL-${Date.now()}`,
patientId: editor.patientId,
status: editor.status,
exam: editor.exam,
@@ -276,6 +279,8 @@ export function ReportsPage({ role }) {
hideDate: editor.hideDate,
hideSignature: editor.hideSignature,
dueAt: editor.dueAt ? new Date(editor.dueAt).toISOString() : '',
createdBy: editor.id ? undefined : viewerProfile?.id || currentProfessional?.userId || currentProfessional?.id || undefined,
updatedBy: viewerProfile?.id || currentProfessional?.userId || currentProfessional?.id || undefined,
}
try {
@@ -519,7 +524,11 @@ function ReportRow({ onEdit, onView, report }) {
}
function ReportEditorModal({ editor, onChange, onClose, onSave, patientOptions, professionalOptions, saving }) {
const [requesterSearch, setRequesterSearch] = useState(editor.requestedBy || '')
const isValid = Boolean(editor.patientId)
const filteredRequesterOptions = professionalOptions
.filter((professional) => normalizeSearch(professional.name).includes(normalizeSearch(requesterSearch)))
.slice(0, 6)
function updateField(field, value) {
onChange((current) => ({ ...current, [field]: value }))
@@ -573,19 +582,39 @@ function ReportEditorModal({ editor, onChange, onClose, onSave, patientOptions,
</DarkField>
<DarkField label="Solicitante">
<div>
<div className="space-y-2">
<input
className={inputClass}
list="report-requested-by-suggestions"
onChange={(event) => updateField('requestedBy', event.target.value)}
placeholder="Nome do solicitante"
value={editor.requestedBy}
onChange={(event) => {
setRequesterSearch(event.target.value)
updateField('requestedBy', event.target.value)
}}
placeholder="Pesquisar médico"
type="search"
value={requesterSearch}
/>
<datalist id="report-requested-by-suggestions">
{professionalOptions.map((professional) => (
<option key={professional.id} value={professional.name} />
))}
</datalist>
<div className="max-h-36 overflow-y-auto rounded-md border border-[#404040] bg-[#1a1a1a] p-1">
{filteredRequesterOptions.length ? (
filteredRequesterOptions.map((professional) => (
<button
className={`flex w-full items-center justify-between gap-3 rounded-sm px-3 py-2 text-left text-sm font-medium transition hover:bg-[#303030] ${
editor.requestedBy === professional.name ? 'text-[#51a2ff]' : 'text-[#e5e5e5]'
}`}
key={professional.id || professional.createdByValue || professional.name}
onClick={() => {
setRequesterSearch(professional.name)
updateField('requestedBy', professional.name)
}}
type="button"
>
<span className="truncate">{professional.name}</span>
{editor.requestedBy === professional.name ? <ReportIcon className="size-3.5" name="check" /> : null}
</button>
))
) : (
<p className="px-3 py-2 text-sm text-[#a3a3a3]">Nenhum médico encontrado.</p>
)}
</div>
</div>
</DarkField>
</div>
@@ -632,7 +661,6 @@ function ReportEditorModal({ editor, onChange, onClose, onSave, patientOptions,
<textarea
className={`${textareaClass} min-h-72`}
onChange={(event) => updateField('contentHtml', event.target.value)}
placeholder="Complemento em texto simples"
value={editor.contentHtml}
/>
</DarkField>
@@ -698,9 +726,19 @@ function ReportViewModal({ onClose, report }) {
<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">
<ReportIcon className="size-4 text-[#a3a3a3]" name="x" />
</button>
<div className="flex items-center gap-2">
<button
className="inline-flex h-9 items-center gap-2 rounded-lg border border-[#404040] bg-[#1a1a1a] px-3 text-xs font-semibold text-[#e5e5e5] transition hover:bg-[#2a2a2a]"
onClick={() => printReportAsPdf(report, currentStatus)}
type="button"
>
<ReportIcon className="size-4" name="print" />
Imprimir PDF
</button>
<button className="rounded-lg p-1.5 transition hover:bg-[#2a2a2a]" onClick={onClose} type="button">
<ReportIcon className="size-4 text-[#a3a3a3]" name="x" />
</button>
</div>
</div>
<div className="flex-1 overflow-y-auto p-6">
@@ -846,6 +884,94 @@ function uniqueValues(values) {
return [...new Set(values.map((value) => String(value || '').trim()).filter(Boolean))]
}
function normalizeSearch(value) {
return String(value || '')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.trim()
.toLowerCase()
}
function printReportAsPdf(report, status) {
const printWindow = window.open('', '_blank', 'noopener,noreferrer,width=900,height=1100')
if (!printWindow) {
window.print()
return
}
printWindow.document.write(`
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<title>Relatório ${escapeHtml(report.orderNumber || '')}</title>
<style>
* { box-sizing: border-box; }
body { color: #171717; font-family: Arial, sans-serif; margin: 40px; }
h1 { font-size: 24px; margin: 0 0 4px; }
.muted { color: #525252; font-size: 12px; }
.grid { display: grid; gap: 12px; grid-template-columns: repeat(2, minmax(0, 1fr)); margin-top: 24px; }
.box { border: 1px solid #d4d4d4; border-radius: 8px; padding: 12px; }
.label { color: #525252; font-size: 10px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
.value { font-size: 13px; margin-top: 6px; white-space: pre-wrap; }
.section { margin-top: 20px; }
@media print { body { margin: 24mm; } button { display: none; } }
</style>
</head>
<body>
<h1>Relatório</h1>
<p class="muted">${escapeHtml(report.orderNumber || 'Sem número')}</p>
<div class="grid">
${printDetail('Paciente', report.patientName)}
${printDetail('Solicitante', report.requestedBy || '-')}
${printDetail('Criado em', formatDate(report.createdAt))}
${printDetail('Criado por', report.createdByName)}
${printDetail('Status', status.label)}
${printDetail('Prazo', formatDateTime(report.dueAt))}
</div>
<div class="grid">
${printDetail('Exame', report.exam || '-')}
${printDetail('CID-10', report.cidCode || '-')}
</div>
<div class="section box">
<p class="label">Diagnóstico</p>
<p class="value">${escapeHtml(report.diagnosis || '-')}</p>
</div>
<div class="section box">
<p class="label">Conclusão</p>
<p class="value">${escapeHtml(report.conclusion || '-')}</p>
</div>
<div class="section box">
<p class="label">Complemento</p>
<p class="value">${escapeHtml(report.contentHtml || 'Nenhum complemento informado.')}</p>
</div>
</body>
</html>
`)
printWindow.document.close()
printWindow.focus()
printWindow.print()
}
function printDetail(label, value) {
return `
<div class="box">
<p class="label">${escapeHtml(label)}</p>
<p class="value">${escapeHtml(value || '-')}</p>
</div>
`
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
function ReportIcon({ className = 'size-4', name }) {
const common = {
className,
@@ -924,6 +1050,24 @@ function ReportIcon({ className = 'size-4', name }) {
)
}
if (name === 'print') {
return (
<svg {...common}>
<path d="M7 8V4h10v4" />
<path d="M7 17H5a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-2" />
<path d="M7 14h10v7H7zM17 12h.01" />
</svg>
)
}
if (name === 'check') {
return (
<svg {...common}>
<path d="m5 12 4 4L19 6" />
</svg>
)
}
return (
<svg {...common}>
<path d="m6 9 6 6 6-6" />