refactor(principal): remove legenda global do AppShell

This commit is contained in:
EdilbertoC
2026-04-28 14:00:14 -03:00
parent 000abb39ac
commit 77079e173c
7 changed files with 770 additions and 915 deletions

View File

@@ -145,7 +145,7 @@ function resolveRoute(pathname, navigate) {
if (pathname === '/laudos') {
return {
element: <ReportsPage navigate={navigate} />,
title: 'Laudos',
title: 'Relatorios medicos',
withShell: true,
}
}

View File

@@ -2,14 +2,13 @@ import { useEffect, useMemo, useState } from 'react'
import { profileRepository } from '../repositories/profileRepository.js'
import { BrandLogo } from './Brand.jsx'
import { FeatureLegend } from './FeatureState.jsx'
const navItems = [
{ href: '/inicio', label: 'Painel', icon: 'pulse', activePaths: ['/inicio', '/home', '/dashboard'] },
{ href: '/agenda', label: 'Agenda', icon: 'calendar' },
{ href: '/pacientes', label: 'Pacientes', icon: 'users', exact: true },
{ href: '/prontuario', label: 'Prontuario', icon: 'file' },
{ href: '/laudos', label: 'Laudos', icon: 'clipboard' },
{ href: '/laudos', label: 'Relatorios medicos', icon: 'clipboard' },
{
href: '/camunicacao',
label: 'Comunicacao',
@@ -26,7 +25,7 @@ const titles = {
'/dashboard': 'Painel',
'/agenda': 'Agenda',
'/consultas': 'Consultas',
'/laudos': 'Laudos',
'/laudos': 'Relatorios medicos',
'/pacientes': 'Pacientes',
'/prontuario': 'Prontuario',
'/camunicacao': 'Comunicacao',
@@ -198,9 +197,6 @@ export function AppShell({ children, currentPath, navigate, routeTitle }) {
</header>
<main className="w-full px-4 py-6 md:px-8 md:py-8" id="app-content">
<div className="mb-6">
<FeatureLegend />
</div>
<div className="sr-only" aria-live="polite">
{pageTitle}
</div>

View File

@@ -2,72 +2,60 @@ export const reportMapper = {
toUi(apiData) {
if (!apiData) return null
const patient = apiData.patient || apiData.paciente || apiData.patients || {}
const doctor = apiData.doctor || apiData.medico || apiData.professional || apiData.doctors || {}
const createdAt = apiData.created_at || apiData.createdAt || apiData.data_criacao || apiData.date
const status = normalizeStatus(apiData.status || apiData.situacao)
return {
id: String(apiData.id || apiData.report_id || apiData.laudo_id),
patientId: apiData.patientId || apiData.patient_id || apiData.paciente_id || patient.id || '',
patient: apiData.patientName || apiData.patient_name || patient.full_name || patient.nome || patient.name || 'Paciente',
date: createdAt ? new Date(createdAt).toLocaleDateString('pt-BR') : 'Sem data',
doctor: apiData.doctorName || apiData.doctor_name || apiData.medico_nome || doctor.name || doctor.nome || 'Medico(a)',
author: apiData.author || apiData.autor || doctor.name || doctor.nome || 'Medico(a)',
type: apiData.type || apiData.report_type || apiData.tipo || apiData.tipo_laudo || 'Laudo medico',
status,
content: apiData.content || apiData.conteudo || apiData.text || '',
cid: apiData.cid || '',
tags: apiData.tags || [],
verified: apiData.verified ?? apiData.verificado ?? status !== 'rascunho',
showDate: apiData.showDate ?? apiData.exibir_data ?? true,
signDigital: apiData.signDigital ?? apiData.assinatura_digital ?? true,
versions: normalizeVersions(apiData.versions || apiData.versoes),
id: String(apiData.id || ''),
orderNumber: apiData.order_number || '',
patientId: apiData.patient_id || '',
status: normalizeStatus(apiData.status),
exam: apiData.exam || '',
requestedBy: apiData.requested_by || '',
cidCode: apiData.cid_code || '',
diagnosis: apiData.diagnosis || '',
conclusion: apiData.conclusion || '',
contentHtml: apiData.content_html || '',
contentJson: apiData.content_json ?? null,
hideDate: Boolean(apiData.hide_date),
hideSignature: Boolean(apiData.hide_signature),
dueAt: apiData.due_at || '',
createdBy: apiData.created_by || '',
updatedBy: apiData.updated_by || '',
createdAt: apiData.created_at || '',
updatedAt: apiData.updated_at || '',
}
},
toApi(uiData, dialect = 'api') {
if (dialect === 'supabase') {
return {
patient_id: uiData.patientId,
report_type: uiData.type,
content: uiData.content,
status: uiData.status,
cid: uiData.cid || null,
}
}
return {
toApi(uiData) {
return cleanPayload({
patient_id: uiData.patientId,
paciente_id: uiData.patientId,
report_type: uiData.type,
tipo: uiData.type,
content: uiData.content,
conteudo: uiData.content,
status: uiData.status,
cid: uiData.cid || null,
}
status: normalizeApiStatus(uiData.status),
exam: emptyToUndefined(uiData.exam),
requested_by: emptyToUndefined(uiData.requestedBy),
cid_code: emptyToUndefined(uiData.cidCode),
diagnosis: emptyToUndefined(uiData.diagnosis),
conclusion: emptyToUndefined(uiData.conclusion),
content_html: emptyToUndefined(uiData.contentHtml),
content_json: uiData.contentJson === undefined ? undefined : uiData.contentJson,
hide_date: Boolean(uiData.hideDate),
hide_signature: Boolean(uiData.hideSignature),
due_at: emptyToUndefined(uiData.dueAt),
})
},
}
function normalizeStatus(status) {
if (!status) return 'rascunho'
const normalized = String(status).toLowerCase()
if (['finalizado', 'liberado', 'assinado'].includes(normalized)) return 'finalizado'
if (['enviado', 'entregue'].includes(normalized)) return 'enviado'
return 'rascunho'
return status === 'draft' ? 'draft' : 'draft'
}
function normalizeVersions(versions) {
if (Array.isArray(versions) && versions.length) return versions
return [
{
version: 1,
action: 'Criado',
user: 'Sistema',
summary: 'Registro importado da API',
},
]
function normalizeApiStatus(status) {
return status === 'draft' ? 'draft' : 'draft'
}
function emptyToUndefined(value) {
return value === '' || value === null ? undefined : value
}
function cleanPayload(payload) {
return Object.fromEntries(
Object.entries(payload).filter(([, value]) => value !== undefined),
)
}

View File

@@ -11,8 +11,6 @@ import {
} from 'date-fns'
import { ptBR } from 'date-fns/locale'
import { FeatureBadge } from '../components/FeatureState.jsx'
import { featurePanelClass } from '../components/featureStateStyles.js'
import { AgendaDailyView } from '../components/calendar/AgendaDailyView.jsx'
import { AgendaWeeklyView } from '../components/calendar/AgendaWeeklyView.jsx'
import { AgendaMonthlyView } from '../components/calendar/AgendaMonthlyView.jsx'
@@ -131,7 +129,7 @@ export function AgendaPage({ navigate }) {
</section>
{error ? (
<section className={`rounded-2xl border bg-[#262626] p-5 shadow-[0_1px_3px_rgba(0,0,0,0.2)] ${featurePanelClass('live')}`}>
<section className="rounded-2xl border border-[#404040] bg-[#262626] p-5 shadow-[0_1px_3px_rgba(0,0,0,0.2)]">
<div className="rounded-xl border border-dashed border-[#7f1d1d] bg-[#2a1111] p-6">
<h2 className="text-base font-bold text-[#fecaca]">Nao foi possivel liberar a agenda</h2>
<p className="mt-2 text-sm leading-6 text-[#fca5a5]">{error}</p>
@@ -142,14 +140,13 @@ export function AgendaPage({ navigate }) {
</section>
) : (
<section className="grid gap-6 xl:grid-cols-1">
<div className={`rounded-2xl border bg-[#262626] p-5 shadow-[0_1px_3px_rgba(0,0,0,0.2)] ${featurePanelClass('live')}`}>
<div className="rounded-2xl border border-[#404040] bg-[#262626] p-5 shadow-[0_1px_3px_rgba(0,0,0,0.2)]">
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<div className="flex flex-wrap items-center gap-2">
<h2 className="text-base font-bold leading-6 text-[#e5e5e5]">
{format(baseDate, "EEEE, dd 'de' MMMM", { locale: ptBR })}
</h2>
<FeatureBadge status="live" />
</div>
<p className="mt-1 text-sm leading-5 text-[#a3a3a3]">
Visualização: {activeView.toLowerCase()} | {visibleAppointments.length} registros visíveis

View File

@@ -301,43 +301,43 @@ async function deletePatient(patientId) {
) : null}
<div className="overflow-x-auto rounded-lg border border-[#404040]">
<table className="w-full whitespace-nowrap text-left text-sm">
<table className="w-full min-w-full table-fixed text-left text-sm">
<thead className="bg-[#171717] text-xs font-semibold uppercase text-[#a3a3a3]">
<tr>
<th className="px-6 py-4">Nome</th>
<th className="px-6 py-4">Telefone</th>
<th className="px-6 py-4">Cidade</th>
<th className="px-6 py-4">Estado</th>
<th className="px-6 py-4">Ultimo atendimento</th>
<th className="px-6 py-4">Proximo atendimento</th>
<th className="px-6 py-4 text-right">Acoes</th>
<th className="w-[24%] px-6 py-4">Nome</th>
<th className="w-[14%] px-6 py-4">Telefone</th>
<th className="w-[12%] px-6 py-4">Cidade</th>
<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>
</tr>
</thead>
<tbody className="divide-y divide-[#404040] bg-[#262626]">
{paginatedPatients.length ? (
paginatedPatients.map((patient) => (
<tr className="transition hover:bg-[#303030]" key={patient.id}>
<td className="px-6 py-4">
<td className="px-6 py-4 align-top">
<button className="flex items-center gap-3 text-left" onClick={() => openDetail(patient)} type="button">
<span className="grid size-8 place-items-center rounded-full bg-[#333333] text-xs font-bold text-[#3b82f6]">
<span className="grid size-8 shrink-0 place-items-center rounded-full bg-[#333333] text-xs font-bold text-[#3b82f6]">
{patient.name.charAt(0)}
</span>
<span>
<span className="block font-medium text-[#e5e5e5] transition hover:text-[#3b82f6]">
<span className="min-w-0">
<span className="block whitespace-normal break-words font-medium text-[#e5e5e5] transition hover:text-[#3b82f6]">
{patient.name}
</span>
<span className="mt-0.5 block text-xs text-[#a3a3a3]">
<span className="mt-0.5 block whitespace-normal break-words text-xs text-[#a3a3a3]">
{patient.insurance || 'Sem convenio'} {patient.vip ? ' | VIP' : ''}
</span>
</span>
</button>
</td>
<td className="px-6 py-4 text-[#a3a3a3]">{patient.phone}</td>
<td className="px-6 py-4 text-[#a3a3a3]">{patient.city}</td>
<td className="px-6 py-4 text-[#a3a3a3]">{patient.state}</td>
<td className="px-6 py-4 text-[#a3a3a3]">{patient.lastVisit || 'Ainda nao houve atendimento'}</td>
<td className="px-6 py-4 text-[#a3a3a3]">{patient.nextVisit || 'Nenhum atendimento agendado'}</td>
<td className="relative px-6 py-4 text-right">
<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.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}`}
className="rounded p-1 text-[#a3a3a3] transition hover:bg-[#333333] hover:text-[#e5e5e5]"
@@ -779,7 +779,7 @@ function PatientVisits({ navigate, patient }) {
<div className="grid gap-3">
{[
{ date: patient.nextVisit, status: 'Agendada', description: `Retorno para ${patient.condition}` },
{ date: patient.lastVisit, status: 'Finalizada', description: 'Consulta registrada no historico local.' },
{ date: patient.lastVisit, status: 'Finalizada', description: 'Consulta registrada no historico do paciente.' },
].map((visit) => (
<div className="rounded-xl border border-[#404040] bg-[#171717] p-4" key={`${visit.date}-${visit.status}`}>
<div className="flex flex-wrap items-start justify-between gap-3">
@@ -814,7 +814,7 @@ function PatientDocuments({ patient }) {
{patient.exams.map((exam) => (
<div className="rounded-xl border border-[#404040] bg-[#171717] p-4" key={exam}>
<p className="font-semibold text-[#f5f5f5]">{exam}</p>
<p className="mt-2 text-sm text-[#a3a3a3]">Pendente de revisão mockada.</p>
<p className="mt-2 text-sm text-[#a3a3a3]">Pendente de revisão.</p>
<span className="mt-4 inline-flex rounded bg-amber-500/20 px-2.5 py-1 text-xs font-bold text-amber-400">
A revisar
</span>

File diff suppressed because it is too large Load Diff

View File

@@ -1,142 +1,64 @@
import { apiConfig, apiEndpoint, getAuthenticatedHeaders } from '../config/api.js'
import { apiConfig, getAuthenticatedHeaders } from '../config/api.js'
import { reportMapper } from '../mappers/reportMapper.js'
import { fetchJsonWithFallback, normalizeCollection, normalizeItem } from './repositoryUtils.js'
import { getResponseError, normalizeItem } from './repositoryUtils.js'
export const reportRepository = {
async getInitialReports() {
const data = await fetchJsonWithFallback(
[
{
url: apiEndpoint('/reports'),
options: { headers: getAuthenticatedHeaders() },
},
{
url: `${apiConfig.restUrl}/reports?select=*,patients(full_name),doctors(name)`,
options: { headers: getAuthenticatedHeaders() },
},
],
'Falha ao buscar laudos da API.',
)
async getInitialReports(filters = {}) {
const query = new URLSearchParams()
query.set('select', '*')
query.set('order', filters.order || 'created_at.desc')
return normalizeCollection(data, ['reports', 'relatorios', 'laudos', 'data']).map(reportMapper.toUi)
if (filters.patientId) {
query.set('patient_id', `eq.${filters.patientId}`)
}
if (filters.status) {
query.set('status', `eq.${filters.status}`)
}
if (filters.createdBy) {
query.set('created_by', `eq.${filters.createdBy}`)
}
const response = await fetch(`${apiConfig.restUrl}/reports?${query.toString()}`, {
headers: getAuthenticatedHeaders(),
})
if (!response.ok) {
throw new Error(await getResponseError(response, 'Falha ao buscar relatorios medicos.'))
}
const data = await response.json()
return (Array.isArray(data) ? data : []).map(reportMapper.toUi)
},
async create(uiData) {
const data = await fetchJsonWithFallback(
[
{
url: apiEndpoint('/reports'),
options: {
method: 'POST',
headers: getAuthenticatedHeaders(),
body: JSON.stringify(reportMapper.toApi(uiData)),
},
},
{
url: `${apiConfig.restUrl}/reports`,
options: {
method: 'POST',
headers: getAuthenticatedHeaders({ Prefer: 'return=representation' }),
body: JSON.stringify(reportMapper.toApi(uiData, 'supabase')),
},
},
],
'Falha ao salvar laudo.',
)
const response = await fetch(`${apiConfig.restUrl}/reports`, {
method: 'POST',
headers: getAuthenticatedHeaders({ Prefer: 'return=representation' }),
body: JSON.stringify(reportMapper.toApi(uiData)),
})
return reportMapper.toUi(normalizeItem(data, ['report', 'relatorio', 'laudo', 'data']))
if (!response.ok) {
throw new Error(await getResponseError(response, 'Falha ao criar relatorio medico.'))
}
const data = await response.json()
return reportMapper.toUi(normalizeItem(data))
},
async update(id, uiData) {
const data = await fetchJsonWithFallback(
[
{
url: apiEndpoint(`/reports/${id}`),
options: {
method: 'PATCH',
headers: getAuthenticatedHeaders(),
body: JSON.stringify(reportMapper.toApi({ ...uiData, id })),
},
},
{
url: apiEndpoint('/reports'),
options: {
method: 'PATCH',
headers: getAuthenticatedHeaders(),
body: JSON.stringify({ id, ...reportMapper.toApi(uiData) }),
},
},
{
url: `${apiConfig.restUrl}/reports?id=eq.${id}`,
options: {
method: 'PATCH',
headers: getAuthenticatedHeaders({ Prefer: 'return=representation' }),
body: JSON.stringify(reportMapper.toApi(uiData, 'supabase')),
},
},
],
'Falha ao atualizar o laudo.',
)
const response = await fetch(`${apiConfig.restUrl}/reports?id=eq.${id}`, {
method: 'PATCH',
headers: getAuthenticatedHeaders({ Prefer: 'return=representation' }),
body: JSON.stringify(reportMapper.toApi(uiData)),
})
return reportMapper.toUi(normalizeItem(data, ['report', 'relatorio', 'laudo', 'data']))
},
if (!response.ok) {
throw new Error(await getResponseError(response, 'Falha ao atualizar relatorio medico.'))
}
getTemplates() {
return [
{
id: 't1',
name: 'Atestado Medico Padrao',
type: 'Atestado Medico',
description: 'Atestado simples para repouso, consulta e CID.',
content:
'Atesto para os devidos fins que o(a) paciente [NOME DO PACIENTE] esteve em consulta medica nesta data, necessitando de [DIAS] dias de repouso por motivo de saude (CID: [CODIGO]).',
},
{
id: 't2',
name: 'Encaminhamento Especializado',
type: 'Encaminhamento',
description: 'Encaminhamento para avaliacao de especialidade.',
content:
'Encaminho o(a) paciente [NOME DO PACIENTE] para avaliacao da especialidade de [ESPECIALIDADE] devido ao quadro clinico de [SINTOMAS/DIAGNOSTICO PREVIO].\n\nConduta mantida ate o momento: [MEDICACOES]',
},
{
id: 't3',
name: 'Laudo de Evolucao Diaria',
type: 'Evolucao Clinica',
description: 'Modelo para evolucao clinica diaria.',
content:
'Paciente evolui [BEM/MAL], [COM/SEM] queixas no momento.\nSinais vitais: PA [VALOR], FC [VALOR] bpm, SatO2 [VALOR]%.\nExame fisico: [DESCRICAO].\nConduta: [MANTER/ALTERAR TRATAMENTO OPCOES].',
},
{
id: 't4',
name: 'Receituario de Uso Continuo',
type: 'Receituario Fixado',
description: 'Lista de medicamentos de uso continuo.',
content:
'Uso continuo:\n1. [MEDICAMENTO] - [DOSE] - Tomar [POSOLOGIA]\n2. [MEDICAMENTO] - [DOSE] - Tomar [POSOLOGIA]',
},
]
},
getAdminUsers() {
return ['Dr. Henrique Cardoso', 'Dra. Marina Lopes', 'Dra. Ana Silva']
},
getCurrentUser() {
return 'Dr. Henrique Cardoso'
},
getDoctors() {
return ['Dr. Henrique Cardoso', 'Dra. Marina Lopes', 'Dra. Ana Silva', 'Dr. Roberto Santos']
},
getReportTypes() {
return [
'Atestado Medico',
'Encaminhamento',
'Evolucao Clinica',
'Receituario Fixado',
'Laudo de Procedimento',
]
const data = await response.json()
return reportMapper.toUi(normalizeItem(data))
},
}