MelissaPaciente Fase 4: Tab Prontuario MVP (evolucao via session.observacoes)

O legacy PatientProntuario.vue tem a aba Prontuario como PLACEHOLDER
("Em breve" rich empty state). O MVP entregue aqui SUPERA o legacy: usa
agenda_eventos.observacoes como nota evolutiva — funcional ja hoje sem
precisar de schema novo.

ESTADO + COMPUTEDS adicionados ao MelissaPaciente.vue:
- pronFilter ref ('com-evolucao' default) + PRON_FILTERS com 5 opcoes
  (Com evolucao / Todas / Realizadas / Faltas / Cancelamentos)
- pronSessions computed: filtra sessoes por status/presenca de observacoes
- sessoesComEvolucao computed: count de sessoes com observacoes nao-vazia

TEMPLATE Tab Prontuario (substitui placeholder Fase 1):
- Hint banner explicativo no topo (icon info + "Prontuario em construcao")
- 4 mini-stats em grid: com evolucao / realizadas / faltas / total
- 5 filter chips redondas — selecao default 'com-evolucao' filtra so
  sessoes que tem nota
- Empty states contextuais (sem sessoes / sem evolucao / filtro vazio)
- Lista de sessoes:
  - border-left colorida por status (verde/vermelho/amarelo/cinza)
  - head com data + relative + chips status/modalidade/duracao
  - block "Evolucao" destacado quando tem observacoes (bg medium + border
    primary + label uppercase + texto pre-wrap)
  - "Sem evolucao registrada" italico cinza quando nao tem
- Roadmap card (border dashed) listando 4 features futuras: anamnese
  estruturada / plano terapeutico / evolucao por temas / assinatura
  digital + LGPD Art. 18.

CSS: ~200L novos. Padrao Melissa (chips estilo MelissaTags, border-left
adaptativa, label uppercase nos blocks de evolucao).

ESLint: 0 errors da minha mudanca.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-08 09:46:58 -03:00
parent 4fc0e3a02b
commit 1278e93b01
2 changed files with 475 additions and 7 deletions
+43
View File
@@ -50,6 +50,49 @@ Touched: none
## [2026-05-08 00:00] session | Melissa cfg-* nativas + temas + cronometro DB
Touched: none
## [2026-05-08 15:30] session | MelissaPaciente Fase 4 — Tab Prontuario MVP
Touched: none
Detalhes: O legacy PatientProntuario.vue tem a aba Prontuario como
PLACEHOLDER ("Em breve"). MVP entregue aqui supera o legacy: usa
agenda_eventos.observacoes como nota evolutiva (pq nao tem schema de
anamnese/clinical_notes ainda).
ESTADO + COMPUTEDS adicionados:
- pronFilter ref ('com-evolucao' default) + PRON_FILTERS array com 5
opcoes (Com evolucao, Todas, Realizadas, Faltas, Cancelamentos).
- pronSessions computed: filtra sessions por status/observacoes presentes.
- sessoesComEvolucao computed: count de sessoes com observacoes nao-vazia.
TEMPLATE Tab Prontuario (substitui placeholder Fase 1):
- Hint banner top: "Prontuario em construcao", explica que usa observacoes
de sessoes como historico evolutivo.
- 4 mini-stats em grid responsivo: com evolucao / realizadas / faltas /
total. Cada uma colorida + icone + value 800.
- 5 filter chips redondas (estilo Melissa): com-evolucao default; troca
pra todas/realiz/falt/cancel.
- Empty state contextual:
- Se nao tem sessoes: "Quando atender este paciente..."
- Se filtro 'com-evolucao' e zero: "Use o campo Observacoes ao editar
sessao..."
- Outro filtro: "Tente outro filtro acima."
- Lista de sessoes (pron-list) com:
- border-left colorida por status (verde realiz / vermelho falta /
amarelo cancel / cinza default)
- head com data + relative + chips status/modalidade/duracao
- titulo opcional (titulo_custom || titulo)
- block "Evolucao" quando tem observacoes (background medium, border-
left primary, label uppercase com icone, texto pre-wrap)
- mensagem "Sem evolucao registrada" italico cinza quando nao tem
- Roadmap card (border-dashed) listando 4 features futuras: anamnese
estruturada / plano terapeutico / evolucao por temas / assinatura
digital + LGPD Art. 18.
CSS: ~200L novos pros componentes (mpa-pron-hint/stats/filters/list/
item/roadmap). Padrao visual Melissa: chips redondas estilo MelissaTags,
border-left adaptativa, monospace inutilizado.
ESLint: 0 errors da minha mudanca.
## [2026-05-08 14:30] session | MelissaPaciente Fase 3 — Tab Perfil (6 sections stacked)
Touched: none
Detalhes: Substituiu o placeholder da aba Perfil por 6 sections stacked
+431 -6
View File
@@ -91,6 +91,16 @@ function selectTab(key) {
if (isMobile.value) drawerOpen.value = false;
}
// Filtros da aba Prontuario (MVP — usa session.observacoes como evolucao)
const pronFilter = ref('com-evolucao'); // com-evolucao | todas | realiz | falt | cancel
const PRON_FILTERS = [
{ value: 'com-evolucao', label: 'Com evolução', icon: 'pi pi-file-edit' },
{ value: 'todas', label: 'Todas', icon: 'pi pi-list' },
{ value: 'realiz', label: 'Realizadas', icon: 'pi pi-check-circle' },
{ value: 'falt', label: 'Faltas', icon: 'pi pi-user-minus' },
{ value: 'cancel', label: 'Cancelamentos', icon: 'pi pi-ban' }
];
// Sub-nav da aba Perfil
const PROFILE_SECTIONS = [
{ key: 'pessoais', label: 'Informações Pessoais', icon: 'pi pi-pencil' },
@@ -185,6 +195,31 @@ const groupNames = computed(() => detail.groups.value.map((g) => g?.name).filter
const groupLabel = computed(() => groupNames.value.length ? groupNames.value.join(', ') : '—');
const groupCountLabel = computed(() => groupNames.value.length <= 1 ? 'Grupo' : 'Grupos');
// ── Tab Prontuario: lista filtrada de sessoes ──────────────
// MVP enquanto anamnese/evolucao_clinica nao existem no schema:
// usa agenda_eventos.observacoes como nota evolutiva.
const pronSessions = computed(() => {
const all = sessionsHook.sessions.value;
if (pronFilter.value === 'todas') return all;
if (pronFilter.value === 'com-evolucao') {
return all.filter((s) => s.observacoes && String(s.observacoes).trim());
}
if (pronFilter.value === 'realiz') {
return all.filter((s) => /realiz|present/i.test(String(s.status || '')));
}
if (pronFilter.value === 'falt') {
return all.filter((s) => /falt/i.test(String(s.status || '')));
}
if (pronFilter.value === 'cancel') {
return all.filter((s) => /cancel|remarca/i.test(String(s.status || '')));
}
return all;
});
const pronSessionsCount = computed(() => pronSessions.value.length);
const sessoesComEvolucao = computed(() =>
sessionsHook.sessions.value.filter((s) => s.observacoes && String(s.observacoes).trim()).length
);
// ── KPIs Visao Geral (Fase 2) ──────────────────────────────
const kpiSessoes = computed(() => sessionsHook.totalSessoes.value);
const kpiRealizadas = computed(() => sessionsHook.totalRealizadas.value);
@@ -991,20 +1026,184 @@ void toast;
</section>
</div>
<!-- ABA: Prontuario -->
<!-- ABA: Prontuario (Fase 4 MVP evolucao via session.observacoes) -->
<div v-else-if="activeTab === 'pron'" class="mpa-tab">
<div class="mpa-w">
<!-- Header explicativo -->
<div class="mpa-pron-hint">
<i class="pi pi-info-circle" />
<div>
<strong>Prontuário em construção.</strong>
Por enquanto mostra as <strong>observações que você anota nas sessões</strong>
como histórico evolutivo. Anamnese estruturada, plano terapêutico e
evolução por temas chegam quando o módulo clínico for liberado.
</div>
</div>
<!-- Mini-stats -->
<div class="mpa-pron-stats">
<article class="mpa-pron-stat" style="--c:#06b6d4">
<i class="pi pi-file-edit" />
<div>
<div class="mpa-pron-stat__value">{{ sessoesComEvolucao }}</div>
<div class="mpa-pron-stat__label">com evolução</div>
</div>
</article>
<article class="mpa-pron-stat" style="--c:#10b981">
<i class="pi pi-check-circle" />
<div>
<div class="mpa-pron-stat__value">{{ sessionsHook.totalRealizadas.value }}</div>
<div class="mpa-pron-stat__label">realizadas</div>
</div>
</article>
<article class="mpa-pron-stat" style="--c:#f87171">
<i class="pi pi-user-minus" />
<div>
<div class="mpa-pron-stat__value">{{ sessionsHook.totalFaltas.value }}</div>
<div class="mpa-pron-stat__label">faltas</div>
</div>
</article>
<article class="mpa-pron-stat" style="--c:#94a3b8">
<i class="pi pi-calendar" />
<div>
<div class="mpa-pron-stat__value">{{ sessionsHook.totalSessoes.value }}</div>
<div class="mpa-pron-stat__label">total</div>
</div>
</article>
</div>
<!-- Filtros -->
<div class="mpa-pron-filters" role="tablist">
<button
v-for="f in PRON_FILTERS"
:key="f.value"
type="button"
role="tab"
:aria-selected="pronFilter === f.value"
class="mpa-pron-filter"
:class="{ 'is-active': pronFilter === f.value }"
@click="pronFilter = f.value"
>
<i :class="f.icon" />
<span>{{ f.label }}</span>
</button>
</div>
<!-- Lista de evolução -->
<div v-if="sessionsHook.loading.value" class="mpa-empty">
<i class="pi pi-spin pi-spinner mr-2" /> Carregando
</div>
<div v-else-if="!pronSessionsCount" class="mpa-empty mpa-empty--rich">
<div class="mpa-empty__icon"><i class="pi pi-file-edit" /></div>
<div class="mpa-empty__title">
<template v-if="!sessionsHook.sessions.value.length">
Sem sessões registradas
</template>
<template v-else-if="pronFilter === 'com-evolucao'">
Nenhuma sessão tem evolução escrita ainda
</template>
<template v-else>
Nenhuma sessão neste filtro
</template>
</div>
<div class="mpa-empty__sub">
<template v-if="pronFilter === 'com-evolucao' && sessionsHook.sessions.value.length">
Use o campo <strong>Observações</strong> ao editar uma sessão pra registrar
como ela transcorreu vai aparecer aqui como nota evolutiva.
</template>
<template v-else-if="!sessionsHook.sessions.value.length">
Quando você atender este paciente, as sessões e evoluções aparecerão aqui.
</template>
<template v-else>
Tente outro filtro acima ou veja "Todas" pra listar o histórico completo.
</template>
</div>
</div>
<div v-else class="mpa-pron-list">
<article
v-for="s in pronSessions"
:key="s.id"
class="mpa-pron-item"
:data-status="String(s.status || 'agendado').toLowerCase()"
>
<div class="mpa-pron-item__head">
<div class="mpa-pron-item__when">
<span class="mpa-pron-item__date">{{ fmtDateTimeBR(s.inicio_em) }}</span>
<span class="mpa-pron-item__rel">{{ fmtRelative(s.inicio_em) }}</span>
</div>
<div class="mpa-pron-item__chips">
<Tag
:value="STATUS_LABEL[s.status] || s.status || 'Agendado'"
:severity="STATUS_SEVERITY[s.status] || 'info'"
class="mpa-pron-item__tag"
/>
<span v-if="s.modalidade" class="mpa-tl__chip">
<i :class="s.modalidade === 'online' ? 'pi pi-video' : 'pi pi-map-marker'" />
{{ s.modalidade === 'online' ? 'Online' : 'Presencial' }}
</span>
<span v-if="sessionDuration(s.inicio_em, s.fim_em)" class="mpa-tl__chip mpa-tl__chip--dim">
<i class="pi pi-clock" /> {{ sessionDuration(s.inicio_em, s.fim_em) }}
</span>
</div>
</div>
<div v-if="s.titulo_custom || s.titulo" class="mpa-pron-item__title">
{{ s.titulo_custom || s.titulo }}
</div>
<div v-if="s.observacoes" class="mpa-pron-item__evol">
<div class="mpa-pron-item__evol-label">
<i class="pi pi-file-edit" />
Evolução
</div>
<p class="mpa-pron-item__evol-text">{{ s.observacoes }}</p>
</div>
<div v-else class="mpa-pron-item__noevol">
<i class="pi pi-circle" />
Sem evolução registrada
</div>
</article>
</div>
<!-- Roadmap card "em breve" -->
<section class="mpa-w mpa-pron-roadmap">
<div class="mpa-w__head">
<div class="mpa-w__icon mpa-w__icon--cyan"><i class="pi pi-file-edit" /></div>
<div class="mpa-w__icon mpa-w__icon--cyan"><i class="pi pi-sparkles" /></div>
<div class="mpa-w__title">
<div class="mpa-w__title-text">Prontuário Fase 4</div>
<div class="mpa-w__sub">Anamnese + sessões clínicas + evoluções</div>
<div class="mpa-w__title-text">Em breve no prontuário</div>
<div class="mpa-w__sub">Roadmap clínico previsto</div>
</div>
</div>
<div class="mpa-w__body">
<p class="mpa-placeholder">Em desenvolvimento <strong>Fase 4</strong>.</p>
<ul class="mpa-pron-roadmap__list">
<li>
<i class="pi pi-clipboard" />
<div>
<strong>Anamnese estruturada</strong>
<span>Modelo configurável por terapeuta com seções (queixa, história, hipótese diagnóstica, objetivos).</span>
</div>
</li>
<li>
<i class="pi pi-bullseye" />
<div>
<strong>Plano terapêutico</strong>
<span>Objetivos com prazo + acompanhamento de progresso ao longo das sessões.</span>
</div>
</li>
<li>
<i class="pi pi-tag" />
<div>
<strong>Evolução por temas</strong>
<span>Tagging das notas pra cruzar evolução com objetivos e gerar relatórios.</span>
</div>
</li>
<li>
<i class="pi pi-shield" />
<div>
<strong>Assinatura digital + LGPD Art. 18</strong>
<span>Notas imutáveis com hash de auditoria, exportação compatível CFP.</span>
</div>
</li>
</ul>
</div>
</section>
</div>
<!-- ABA: Agenda -->
@@ -2070,6 +2269,232 @@ void toast;
font-size: 0.66rem !important;
}
/* ═══════ Tab Prontuario (Fase 4 MVP) ═══════ */
.mpa-pron-hint {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px 14px;
border-radius: 10px;
background: color-mix(in srgb, #06b6d4 9%, transparent);
border: 1px solid color-mix(in srgb, #06b6d4 28%, transparent);
color: var(--m-text);
font-size: 0.78rem;
line-height: 1.5;
}
.mpa-pron-hint > i {
color: #06b6d4;
font-size: 1rem;
margin-top: 2px;
flex-shrink: 0;
}
.mpa-pron-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 8px;
}
.mpa-pron-stat {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 10px;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
border-left: 3px solid var(--c, var(--p-primary-color));
}
.mpa-pron-stat > i {
color: var(--c, var(--p-primary-color));
font-size: 1.05rem;
flex-shrink: 0;
}
.mpa-pron-stat__value {
font-size: 1.05rem;
font-weight: 800;
color: var(--m-text);
line-height: 1.1;
}
.mpa-pron-stat__label {
font-size: 0.7rem;
color: var(--m-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
margin-top: 2px;
}
.mpa-pron-filters {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.mpa-pron-filter {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 6px 12px;
border-radius: 999px;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
color: var(--m-text);
cursor: pointer;
font-family: inherit;
font-size: 0.78rem;
font-weight: 600;
transition: all 120ms ease;
}
.mpa-pron-filter > i { font-size: 0.7rem; }
.mpa-pron-filter:hover {
background: var(--m-bg-soft-hover);
border-color: var(--m-border-strong);
}
.mpa-pron-filter.is-active {
background: var(--p-primary-color);
border-color: var(--p-primary-color);
color: var(--p-primary-contrast-color, #fff);
}
.mpa-pron-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.mpa-pron-item {
border-radius: 12px;
border: 1px solid var(--m-border);
background: var(--m-bg-soft);
padding: 12px 14px;
border-left: 3px solid var(--m-text-muted);
transition: border-color 120ms ease;
}
.mpa-pron-item[data-status*="realiz"],
.mpa-pron-item[data-status*="present"] { border-left-color: rgb(34, 197, 94); }
.mpa-pron-item[data-status*="falt"] { border-left-color: rgb(239, 68, 68); }
.mpa-pron-item[data-status*="cancel"],
.mpa-pron-item[data-status*="remarc"] { border-left-color: rgb(245, 158, 11); }
.mpa-pron-item__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 6px;
}
.mpa-pron-item__when {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.mpa-pron-item__date {
font-size: 0.85rem;
font-weight: 700;
color: var(--m-text);
}
.mpa-pron-item__rel {
font-size: 0.7rem;
color: var(--m-text-muted);
}
.mpa-pron-item__chips {
display: flex;
align-items: center;
gap: 5px;
flex-wrap: wrap;
}
.mpa-pron-item__tag { font-size: 0.66rem !important; }
.mpa-pron-item__title {
font-size: 0.82rem;
font-weight: 600;
color: var(--m-text);
margin-bottom: 6px;
}
.mpa-pron-item__evol {
margin-top: 8px;
padding: 10px 12px;
border-radius: 8px;
background: var(--m-bg-medium);
border-left: 2px solid var(--p-primary-color);
}
.mpa-pron-item__evol-label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 0.66rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--p-primary-color);
margin-bottom: 4px;
}
.mpa-pron-item__evol-label > i { font-size: 0.62rem; }
.mpa-pron-item__evol-text {
font-size: 0.85rem;
color: var(--m-text);
line-height: 1.55;
white-space: pre-wrap;
word-break: break-word;
}
.mpa-pron-item__noevol {
margin-top: 6px;
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.7rem;
color: var(--m-text-muted);
opacity: 0.65;
font-style: italic;
}
.mpa-pron-item__noevol > i { font-size: 0.5rem; }
.mpa-pron-roadmap {
margin-top: 6px;
border-style: dashed;
background: transparent;
}
.mpa-pron-roadmap__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
.mpa-pron-roadmap__list > li {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 8px 0;
border-bottom: 1px solid var(--m-border);
}
.mpa-pron-roadmap__list > li:last-child {
border-bottom: none;
padding-bottom: 0;
}
.mpa-pron-roadmap__list > li > i {
color: var(--p-primary-color);
font-size: 0.95rem;
margin-top: 2px;
flex-shrink: 0;
width: 24px;
text-align: center;
}
.mpa-pron-roadmap__list > li > div {
flex: 1;
min-width: 0;
}
.mpa-pron-roadmap__list strong {
display: block;
font-size: 0.85rem;
color: var(--m-text);
margin-bottom: 2px;
}
.mpa-pron-roadmap__list span {
font-size: 0.74rem;
color: var(--m-text-muted);
line-height: 1.4;
}
/* ═══════ Notas e observacoes ═══════ */
.mpa-notes {
display: flex;