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') { if (pathname === '/laudos') {
return { return {
element: <ReportsPage navigate={navigate} />, element: <ReportsPage navigate={navigate} />,
title: 'Laudos', title: 'Relatorios medicos',
withShell: true, withShell: true,
} }
} }

View File

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

View File

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

View File

@@ -11,8 +11,6 @@ import {
} from 'date-fns' } from 'date-fns'
import { ptBR } from 'date-fns/locale' 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 { AgendaDailyView } from '../components/calendar/AgendaDailyView.jsx'
import { AgendaWeeklyView } from '../components/calendar/AgendaWeeklyView.jsx' import { AgendaWeeklyView } from '../components/calendar/AgendaWeeklyView.jsx'
import { AgendaMonthlyView } from '../components/calendar/AgendaMonthlyView.jsx' import { AgendaMonthlyView } from '../components/calendar/AgendaMonthlyView.jsx'
@@ -131,7 +129,7 @@ export function AgendaPage({ navigate }) {
</section> </section>
{error ? ( {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"> <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> <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> <p className="mt-2 text-sm leading-6 text-[#fca5a5]">{error}</p>
@@ -142,14 +140,13 @@ export function AgendaPage({ navigate }) {
</section> </section>
) : ( ) : (
<section className="grid gap-6 xl:grid-cols-1"> <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 className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div> <div>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<h2 className="text-base font-bold leading-6 text-[#e5e5e5]"> <h2 className="text-base font-bold leading-6 text-[#e5e5e5]">
{format(baseDate, "EEEE, dd 'de' MMMM", { locale: ptBR })} {format(baseDate, "EEEE, dd 'de' MMMM", { locale: ptBR })}
</h2> </h2>
<FeatureBadge status="live" />
</div> </div>
<p className="mt-1 text-sm leading-5 text-[#a3a3a3]"> <p className="mt-1 text-sm leading-5 text-[#a3a3a3]">
Visualização: {activeView.toLowerCase()} | {visibleAppointments.length} registros visíveis Visualização: {activeView.toLowerCase()} | {visibleAppointments.length} registros visíveis

View File

@@ -301,43 +301,43 @@ async function deletePatient(patientId) {
) : null} ) : null}
<div className="overflow-x-auto rounded-lg border border-[#404040]"> <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]"> <thead className="bg-[#171717] text-xs font-semibold uppercase text-[#a3a3a3]">
<tr> <tr>
<th className="px-6 py-4">Nome</th> <th className="w-[24%] px-6 py-4">Nome</th>
<th className="px-6 py-4">Telefone</th> <th className="w-[14%] px-6 py-4">Telefone</th>
<th className="px-6 py-4">Cidade</th> <th className="w-[12%] px-6 py-4">Cidade</th>
<th className="px-6 py-4">Estado</th> <th className="w-[8%] px-6 py-4">Estado</th>
<th className="px-6 py-4">Ultimo atendimento</th> <th className="w-[16%] px-6 py-4">Ultimo atendimento</th>
<th className="px-6 py-4">Proximo atendimento</th> <th className="w-[18%] px-6 py-4">Proximo atendimento</th>
<th className="px-6 py-4 text-right">Acoes</th> <th className="sticky right-0 w-[8.5rem] bg-[#171717] px-6 py-4 text-right">Acoes</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-[#404040] bg-[#262626]"> <tbody className="divide-y divide-[#404040] bg-[#262626]">
{paginatedPatients.length ? ( {paginatedPatients.length ? (
paginatedPatients.map((patient) => ( paginatedPatients.map((patient) => (
<tr className="transition hover:bg-[#303030]" key={patient.id}> <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"> <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)} {patient.name.charAt(0)}
</span> </span>
<span> <span className="min-w-0">
<span className="block font-medium text-[#e5e5e5] transition hover:text-[#3b82f6]"> <span className="block whitespace-normal break-words font-medium text-[#e5e5e5] transition hover:text-[#3b82f6]">
{patient.name} {patient.name}
</span> </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' : ''} {patient.insurance || 'Sem convenio'} {patient.vip ? ' | VIP' : ''}
</span> </span>
</span> </span>
</button> </button>
</td> </td>
<td className="px-6 py-4 text-[#a3a3a3]">{patient.phone}</td> <td className="px-6 py-4 align-top whitespace-normal break-words text-[#a3a3a3]">{patient.phone}</td>
<td className="px-6 py-4 text-[#a3a3a3]">{patient.city}</td> <td className="px-6 py-4 align-top whitespace-normal break-words text-[#a3a3a3]">{patient.city}</td>
<td className="px-6 py-4 text-[#a3a3a3]">{patient.state}</td> <td className="px-6 py-4 align-top 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 align-top whitespace-normal break-words 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="px-6 py-4 align-top whitespace-normal break-words text-[#a3a3a3]">{patient.nextVisit || 'Nenhum atendimento agendado'}</td>
<td className="relative px-6 py-4 text-right"> <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 <button
aria-label={`Acoes de ${patient.name}`} aria-label={`Acoes de ${patient.name}`}
className="rounded p-1 text-[#a3a3a3] transition hover:bg-[#333333] hover:text-[#e5e5e5]" 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"> <div className="grid gap-3">
{[ {[
{ date: patient.nextVisit, status: 'Agendada', description: `Retorno para ${patient.condition}` }, { 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) => ( ].map((visit) => (
<div className="rounded-xl border border-[#404040] bg-[#171717] p-4" key={`${visit.date}-${visit.status}`}> <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"> <div className="flex flex-wrap items-start justify-between gap-3">
@@ -814,7 +814,7 @@ function PatientDocuments({ patient }) {
{patient.exams.map((exam) => ( {patient.exams.map((exam) => (
<div className="rounded-xl border border-[#404040] bg-[#171717] p-4" key={exam}> <div className="rounded-xl border border-[#404040] bg-[#171717] p-4" key={exam}>
<p className="font-semibold text-[#f5f5f5]">{exam}</p> <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"> <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 A revisar
</span> </span>
@@ -1276,4 +1276,4 @@ function maskCEPInput(event) {
.replace(/\D/g, '') .replace(/\D/g, '')
.replace(/(\d{5})(\d)/, '$1-$2') .replace(/(\d{5})(\d)/, '$1-$2')
.replace(/(-\d{3})\d+?$/, '$1') .replace(/(-\d{3})\d+?$/, '$1')
} }

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 { reportMapper } from '../mappers/reportMapper.js'
import { fetchJsonWithFallback, normalizeCollection, normalizeItem } from './repositoryUtils.js' import { getResponseError, normalizeItem } from './repositoryUtils.js'
export const reportRepository = { export const reportRepository = {
async getInitialReports() { async getInitialReports(filters = {}) {
const data = await fetchJsonWithFallback( const query = new URLSearchParams()
[ query.set('select', '*')
{ query.set('order', filters.order || 'created_at.desc')
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.',
)
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) { async create(uiData) {
const data = await fetchJsonWithFallback( const response = await fetch(`${apiConfig.restUrl}/reports`, {
[ method: 'POST',
{ headers: getAuthenticatedHeaders({ Prefer: 'return=representation' }),
url: apiEndpoint('/reports'), body: JSON.stringify(reportMapper.toApi(uiData)),
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.',
)
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) { async update(id, uiData) {
const data = await fetchJsonWithFallback( const response = await fetch(`${apiConfig.restUrl}/reports?id=eq.${id}`, {
[ method: 'PATCH',
{ headers: getAuthenticatedHeaders({ Prefer: 'return=representation' }),
url: apiEndpoint(`/reports/${id}`), body: JSON.stringify(reportMapper.toApi(uiData)),
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.',
)
return reportMapper.toUi(normalizeItem(data, ['report', 'relatorio', 'laudo', 'data'])) if (!response.ok) {
}, throw new Error(await getResponseError(response, 'Falha ao atualizar relatorio medico.'))
}
getTemplates() { const data = await response.json()
return [ return reportMapper.toUi(normalizeItem(data))
{
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',
]
}, },
} }