MelissaPaciente Fase 3: Tab Perfil completa (6 sections stacked + anchors)

EXTENSAO: src/features/patients/utils/patientFormatters.js
- +5 formatters: pickField (compartilhado), onlyDigits, fmtCPF (000.000.000-00),
  fmtRG (passthrough), fmtPhoneMobile ((XX) 9XXXX-XXXX), fmtGender
  (Masculino/Feminino/Nao-binario/Outro), fmtMarital (Solteiro/Casado/
  Divorciado/Viuvo/Uniao estavel).

MELISSAPACIENTE.VUE — script
- 30+ field computeds usando pickField (cobre snake_case + camelCase):
  birthValue, telefone/Alternativo, email/Alternativo, genero, estadoCivil,
  naturalidade, ondeNosConheceu, encaminhadoPor, observacoes, notasInternas
  + 8 campos de endereco + 5 dados adicionais + 4 responsavel.
- groupNames/groupLabel/groupCountLabel pra bloco Origem.
- scrollToProfileSection(key): liga sidebar sub-nav -> scrollIntoView do
  anchor #mpa-perfil-XXX. Em mobile fecha o drawer.

MELISSAPACIENTE.VUE — Tab Perfil reescrita
Diferente do PatientProntuario legacy que usa PrimeVue Accordion (1 painel
aberto por vez), o Melissa nativo mostra os 6 cards stacked com scroll
suave do sidebar sub-nav. Mais legivel em desktop, mais rapido de escanear.

- 1. Informacoes Pessoais: 2-col com Dados de cadastro (nome/data nasc
  com idade inline/genero/estado civil/CPF/RG/naturalidade) + Contato +
  Origem (grupos/tags chips/onde nos conheceu/encaminhado por). tel: e
  mailto: links onde ha valor. Observacoes full-width quando preenchido.
- 2. Endereco: grid 2-col com 8 fields.
- 3. Dados Adicionais: grid 2-col com escolaridade/profissao/parente/grau/
  tel parente.
- 4. Responsavel: 1-col com nome/CPF/tel + observacao block textual.
- 5. Anotacoes Internas: card com hint lock + textblock min-height.
- 6. Sessoes: lista compacta scrollable (max-height 360px) com titulo/
  data/duracao/modalidade chips + tag status.

CSS: ~250L novos pros componentes (mpa-fields/field-row/field-grid-2/
field-block/sess/sess-list). Pattern visual Melissa: cards com label
uppercase, separadores horizontais sutis, links primary, monospace pra
CPF/RG/CEP.

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:43:03 -03:00
parent ab7526b8d7
commit 4fc0e3a02b
3 changed files with 691 additions and 22 deletions
+47
View File
@@ -50,6 +50,53 @@ Touched: none
## [2026-05-08 00:00] session | Melissa cfg-* nativas + temas + cronometro DB
Touched: none
## [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
com anchors no MelissaPaciente. Diferente do PatientProntuario legacy que
usava PrimeVue Accordion (1 painel aberto por vez), o Melissa nativo
mostra todos os 6 cards stacked com scroll suave do sidebar sub-nav pra
cada anchor. Mais legivel em desktop, mais rapido pra escanear.
EXTENSAO de patientFormatters.js: +5 formatters
- pickField (ja existia computed local; agora helper compartilhavel)
- onlyDigits, fmtCPF (000.000.000-00), fmtRG (passthrough), fmtPhoneMobile
((XX) 9XXXX-XXXX), fmtGender (Masculino/Feminino/Nao-binario/Outro),
fmtMarital (Solteiro/Casado/Divorciado/Viuvo/Uniao estavel).
MELISSAPACIENTE.VUE — script:
- 30+ field computeds usando pickField (cobre snake_case + camelCase do
schema): birthValue, telefone/Alternativo, email/Alternativo, genero,
estadoCivil, naturalidade, ondeNosConheceu, encaminhadoPor, observacoes,
notasInternas + 8 campos de endereco (cep/pais/cidade/estado/endereco/
numero/bairro/complemento) + 5 dados adicionais (escolaridade/profissao/
nomeParente/grauParentesco/telefoneParente) + 4 responsavel.
- groupNames/groupLabel/groupCountLabel pra Origem.
- scrollToProfileSection(key) que liga sidebar sub-nav -> nextTick ->
scrollIntoView do anchor #mpa-perfil-XXX. Em mobile fecha o drawer.
MELISSAPACIENTE.VUE — template Tab Perfil:
- 1. Informacoes Pessoais: 2-col (Dados de cadastro: nome/data nasc com
idade/genero/estado civil/CPF/RG/naturalidade) + (Contato: tel/tel-alt
com tel: links + e-mail principal/alt com mailto: + Origem: grupos/tags
chips/onde nos conheceu/encaminhado por). Observacoes full-width quando
preenchido.
- 2. Endereco: grid 2-col com 8 fields (CEP/pais/cidade/estado/endereco/
numero/bairro/complemento).
- 3. Dados Adicionais: grid 2-col com escolaridade/profissao/parente/grau/
tel parente.
- 4. Responsavel: 1-col com nome/CPF/tel + observacao block textual.
- 5. Anotacoes Internas: card com hint lock + textblock min-height.
- 6. Sessoes: lista compacta scrollable (max-height 360px) com titulo/
data/duracao/modalidade chips + tag status.
CSS: ~250L novos pros componentes (mpa-fields/field-row/field-grid-2/
field-block/sess/sess-list). Pattern visual Melissa: cards com label
uppercase, separadores horizontais sutis, links com cor primary, monospace
pra CPF/RG/CEP.
ESLint: 0 errors da minha mudanca.
## [2026-05-08 13:00] session | MelissaPaciente Fase 2 — Tab Visao Geral completa
Touched: none
Detalhes: Reescreveu a aba Visao Geral do MelissaPaciente substituindo o
@@ -40,6 +40,82 @@ export function dash(v) {
return s || '—';
}
/**
* Pega a primeira chave nao vazia de um objeto (snake_case ou camelCase).
* Usado pra resolver discrepancias de schema (ex: 'data_nascimento' vs 'birth_date').
*/
export function pickField(obj, keys = []) {
for (const k of keys) {
const v = obj?.[k];
if (v !== null && v !== undefined && String(v).trim()) return v;
}
return null;
}
export function onlyDigits(v) {
return String(v ?? '').replace(/\D/g, '');
}
/**
* Formata CPF: 00000000000 -> 000.000.000-00
*/
export function fmtCPF(v) {
const d = onlyDigits(v);
if (!d) return '—';
if (d.length !== 11) return d;
return `${d.slice(0, 3)}.${d.slice(3, 6)}.${d.slice(6, 9)}-${d.slice(9)}`;
}
/**
* Formata RG (genericamente — varia por estado):
* 00.000.000-0 / 0000000000 → mantem digitos com pontos a cada 3 a partir da direita.
*/
export function fmtRG(v) {
const s = String(v ?? '').trim();
if (!s) return '—';
return s;
}
/**
* Formata telefone celular pt-br: (XX) 9XXXX-XXXX ou (XX) XXXX-XXXX.
*/
export function fmtPhoneMobile(v) {
const d = onlyDigits(v);
if (!d) return '—';
if (d.length === 11) return `(${d.slice(0, 2)}) ${d.slice(2, 7)}-${d.slice(7, 11)}`;
if (d.length === 10) return `(${d.slice(0, 2)}) ${d.slice(2, 6)}-${d.slice(6, 10)}`;
return d;
}
/**
* Mapeia variantes de genero pra label legivel.
*/
export function fmtGender(v) {
const s = String(v ?? '').trim();
if (!s) return '—';
const x = s.toLowerCase();
if (['m', 'masc', 'masculino', 'male', 'man', 'homem'].includes(x)) return 'Masculino';
if (['f', 'fem', 'feminino', 'female', 'woman', 'mulher'].includes(x)) return 'Feminino';
if (['nb', 'nao-binario', 'não-binário', 'nonbinary', 'non-binary'].includes(x)) return 'Não-binário';
if (['outro', 'other'].includes(x)) return 'Outro';
return s;
}
/**
* Mapeia variantes de estado civil pra label pt-br.
*/
export function fmtMarital(v) {
const s = String(v ?? '').trim();
if (!s) return '—';
const x = s.toLowerCase();
if (['solteiro', 'solteira', 'single'].includes(x)) return 'Solteiro(a)';
if (['casado', 'casada', 'married'].includes(x)) return 'Casado(a)';
if (['divorciado', 'divorciada', 'divorced'].includes(x)) return 'Divorciado(a)';
if (['viuvo', 'viúva', 'viuvo(a)', 'widowed'].includes(x)) return 'Viúvo(a)';
if (['uniao estavel', 'união estável', 'civil union'].includes(x)) return 'União estável';
return s;
}
export function fmtDateBR(v) {
const d = parseDateLoose(v);
if (!d) return v ? dash(v) : '—';
+564 -18
View File
@@ -18,7 +18,7 @@
*
* Prefixo CSS: .mpa-* (Melissa PAciente).
*/
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue';
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue';
import { useToast } from 'primevue/usetoast';
import MelissaConfigList from './MelissaConfigList.vue';
import { usePatientDetail } from '@/features/patients/composables/usePatientDetail';
@@ -27,11 +27,17 @@ import { usePatientFinancial } from '@/features/patients/composables/usePatientF
import { usePatientMessages } from '@/features/patients/composables/usePatientMessages';
import { usePatientDocuments } from '@/features/patients/composables/usePatientDocuments';
import {
pickField,
calcAge,
fmtRelative,
fmtDateBR,
fmtDateTimeBR,
fmtCurrency,
fmtCPF,
fmtRG,
fmtGender,
fmtMarital,
fmtPhoneMobile,
sessionDuration,
STATUS_LABEL,
STATUS_SEVERITY,
@@ -95,6 +101,16 @@ const PROFILE_SECTIONS = [
{ key: 'sess', label: 'Sessões', icon: 'pi pi-calendar' }
];
const activeProfileSection = ref('pessoais');
function scrollToProfileSection(key) {
activeProfileSection.value = key;
if (isMobile.value) drawerOpen.value = false;
nextTick(() => {
const el = document.getElementById(`mpa-perfil-${key}`);
if (el && typeof el.scrollIntoView === 'function') {
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
}
// ── Helpers de exibicao ────────────────────────────────────
function dash(v) {
@@ -127,17 +143,48 @@ function tagStyle(t) {
return tagStyleHelper(t);
}
// ── Notas + observacoes (campos opcionais do paciente) ────
function pickField(obj, keys = []) {
for (const k of keys) {
const v = obj?.[k];
if (v !== null && v !== undefined && String(v).trim()) return v;
}
return null;
}
// ── Field computeds (cobre snake_case e camelCase do schema)
const birthValue = computed(() => pickField(patientData.value, ['data_nascimento', 'birth_date']));
const telefone = computed(() => pickField(patientData.value, ['telefone', 'phone']));
const telefoneAlternativo = computed(() => pickField(patientData.value, ['telefone_alternativo', 'phone_alt', 'phoneAlt']));
const emailPrincipal = computed(() => pickField(patientData.value, ['email_principal', 'email']));
const emailAlternativo = computed(() => pickField(patientData.value, ['email_alternativo', 'email_alt', 'emailAlt']));
const genero = computed(() => pickField(patientData.value, ['genero', 'gender']));
const estadoCivil = computed(() => pickField(patientData.value, ['estado_civil', 'marital_status']));
const naturalidade = computed(() => pickField(patientData.value, ['naturalidade', 'birthplace', 'place_of_birth']));
const ondeNosConheceu = computed(() => pickField(patientData.value, ['onde_nos_conheceu', 'lead_source']));
const encaminhadoPor = computed(() => pickField(patientData.value, ['encaminhado_por', 'referred_by']));
const observacoes = computed(() => pickField(patientData.value, ['observacoes', 'notes_short']));
const notasInternas = computed(() => pickField(patientData.value, ['notas_internas', 'notes']));
// Endereço
const cep = computed(() => pickField(patientData.value, ['cep', 'postal_code']));
const pais = computed(() => pickField(patientData.value, ['pais', 'country']));
const cidade = computed(() => pickField(patientData.value, ['cidade', 'city']));
const estado = computed(() => pickField(patientData.value, ['estado', 'state']));
const endereco = computed(() => pickField(patientData.value, ['endereco', 'address_line']));
const numero = computed(() => pickField(patientData.value, ['numero', 'address_number']));
const bairro = computed(() => pickField(patientData.value, ['bairro', 'neighborhood']));
const complemento = computed(() => pickField(patientData.value, ['complemento', 'address_complement']));
// Dados Adicionais
const escolaridade = computed(() => pickField(patientData.value, ['escolaridade', 'education', 'education_level']));
const profissao = computed(() => pickField(patientData.value, ['profissao', 'profession']));
const nomeParente = computed(() => pickField(patientData.value, ['nome_parente', 'relative_name']));
const grauParentesco = computed(() => pickField(patientData.value, ['grau_parentesco', 'relative_relation']));
const telefoneParente = computed(() => pickField(patientData.value, ['telefone_parente', 'relative_phone']));
// Responsável (pra menores)
const nomeResponsavel = computed(() => pickField(patientData.value, ['nome_responsavel', 'guardian_name']));
const cpfResponsavel = computed(() => pickField(patientData.value, ['cpf_responsavel', 'guardian_cpf']));
const telefoneResponsavel = computed(() => pickField(patientData.value, ['telefone_responsavel', 'guardian_phone']));
const observacaoResponsavel = computed(() => pickField(patientData.value, ['observacao_responsavel', 'guardian_note']));
// Grupos como string label (Origem na Informacoes Pessoais)
const groupNames = computed(() => detail.groups.value.map((g) => g?.name).filter(Boolean));
const groupLabel = computed(() => groupNames.value.length ? groupNames.value.join(', ') : '—');
const groupCountLabel = computed(() => groupNames.value.length <= 1 ? 'Grupo' : 'Grupos');
// ── KPIs Visao Geral (Fase 2) ──────────────────────────────
const kpiSessoes = computed(() => sessionsHook.totalSessoes.value);
const kpiRealizadas = computed(() => sessionsHook.totalRealizadas.value);
@@ -349,7 +396,7 @@ void toast;
type="button"
class="mpa-tab-btn mpa-tab-btn--sub"
:class="{ 'is-active': activeProfileSection === s.key }"
@click="activeProfileSection = s.key"
@click="scrollToProfileSection(s.key)"
>
<i :class="s.icon" class="mpa-tab-btn__sub-icon" />
<span class="mpa-tab-btn__label">{{ s.label }}</span>
@@ -625,23 +672,323 @@ void toast;
</div>
</div>
<!-- ABA: Perfil -->
<!-- ABA: Perfil (Fase 3 6 sections stacked com anchors) -->
<div v-else-if="activeTab === 'perfil'" class="mpa-tab">
<div class="mpa-w">
<!-- 1. Informacoes Pessoais -->
<section id="mpa-perfil-pessoais" class="mpa-w">
<div class="mpa-w__head">
<div class="mpa-w__icon mpa-w__icon--blue"><i class="pi pi-user" /></div>
<div class="mpa-w__icon mpa-w__icon--blue"><i class="pi pi-pencil" /></div>
<div class="mpa-w__title">
<div class="mpa-w__title-text">Perfil Fase 3</div>
<div class="mpa-w__sub">Accordion 6 seções (pessoais, endereço, dados, responsável, anotações, sessões)</div>
<div class="mpa-w__title-text">1. Informações Pessoais</div>
<div class="mpa-w__sub">Cadastro, contato e origem</div>
</div>
</div>
<div class="mpa-w__body">
<p class="mpa-placeholder">
Em desenvolvimento <strong>Fase 3</strong>. Seção atual:
<code>{{ activeProfileSection }}</code>.
<div class="mpa-fields-grid">
<!-- Dados de cadastro -->
<div class="mpa-fields">
<p class="mpa-fields__label">Dados de cadastro</p>
<div class="mpa-field-row">
<span class="mpa-field-row__k">Nome completo</span>
<span class="mpa-field-row__v">{{ dash(nomeCompleto) }}</span>
</div>
<div class="mpa-field-row">
<span class="mpa-field-row__k">Data de nascimento</span>
<span class="mpa-field-row__v">
{{ fmtDateBR(birthValue) }}
<span v-if="calcAge(birthValue) != null" class="mpa-field-row__v-dim">
({{ calcAge(birthValue) }} a)
</span>
</span>
</div>
<div class="mpa-field-row">
<span class="mpa-field-row__k">Gênero</span>
<span class="mpa-field-row__v">{{ fmtGender(genero) }}</span>
</div>
<div class="mpa-field-row">
<span class="mpa-field-row__k">Estado civil</span>
<span class="mpa-field-row__v">{{ fmtMarital(estadoCivil) }}</span>
</div>
<div class="mpa-field-row">
<span class="mpa-field-row__k">CPF</span>
<span class="mpa-field-row__v mpa-field-row__v--mono">{{ fmtCPF(patientData.cpf) }}</span>
</div>
<div class="mpa-field-row">
<span class="mpa-field-row__k">RG</span>
<span class="mpa-field-row__v mpa-field-row__v--mono">{{ fmtRG(patientData.rg) }}</span>
</div>
<div class="mpa-field-row mpa-field-row--last">
<span class="mpa-field-row__k">Naturalidade</span>
<span class="mpa-field-row__v">{{ dash(naturalidade) }}</span>
</div>
</div>
<!-- Contato + Origem -->
<div class="mpa-fields-stack">
<div class="mpa-fields">
<p class="mpa-fields__label">Contato</p>
<div class="mpa-field-row">
<span class="mpa-field-row__k">Telefone / Celular</span>
<a v-if="telefone" :href="`tel:${telefone}`" class="mpa-field-row__v mpa-field-row__v--link">
{{ fmtPhoneMobile(telefone) }}
</a>
<span v-else class="mpa-field-row__v"></span>
</div>
<div class="mpa-field-row">
<span class="mpa-field-row__k">Telefone alternativo</span>
<a v-if="telefoneAlternativo" :href="`tel:${telefoneAlternativo}`" class="mpa-field-row__v mpa-field-row__v--link">
{{ fmtPhoneMobile(telefoneAlternativo) }}
</a>
<span v-else class="mpa-field-row__v"></span>
</div>
<div class="mpa-field-row">
<span class="mpa-field-row__k">E-mail principal</span>
<a v-if="emailPrincipal" :href="`mailto:${emailPrincipal}`" class="mpa-field-row__v mpa-field-row__v--link mpa-field-row__v--truncate">
{{ emailPrincipal }}
</a>
<span v-else class="mpa-field-row__v"></span>
</div>
<div class="mpa-field-row mpa-field-row--last">
<span class="mpa-field-row__k">E-mail alternativo</span>
<a v-if="emailAlternativo" :href="`mailto:${emailAlternativo}`" class="mpa-field-row__v mpa-field-row__v--link mpa-field-row__v--truncate">
{{ emailAlternativo }}
</a>
<span v-else class="mpa-field-row__v"></span>
</div>
</div>
<div class="mpa-fields">
<p class="mpa-fields__label">Origem</p>
<div class="mpa-field-row">
<span class="mpa-field-row__k">{{ groupCountLabel }}</span>
<span class="mpa-field-row__v">{{ groupLabel }}</span>
</div>
<div class="mpa-field-row mpa-field-row--align-start">
<span class="mpa-field-row__k">Tags</span>
<div class="mpa-field-row__chips">
<span v-for="t in detail.tags.value" :key="t.id" class="mpa-chip" :style="tagStyle(t)">
{{ t.name }}
</span>
<span v-if="!detail.tags.value.length" class="mpa-field-row__v"></span>
</div>
</div>
<div class="mpa-field-row">
<span class="mpa-field-row__k">Onde nos conheceu?</span>
<span class="mpa-field-row__v">{{ dash(ondeNosConheceu) }}</span>
</div>
<div class="mpa-field-row mpa-field-row--last">
<span class="mpa-field-row__k">Encaminhado por</span>
<span class="mpa-field-row__v">{{ dash(encaminhadoPor) }}</span>
</div>
</div>
</div>
</div>
<!-- Observacoes (full-width quando preenchido) -->
<div v-if="observacoes" class="mpa-fields mpa-fields--full">
<p class="mpa-fields__label">Observações</p>
<p class="mpa-fields__textblock">{{ observacoes }}</p>
</div>
</div>
</section>
<!-- 2. Endereco -->
<section id="mpa-perfil-endereco" class="mpa-w">
<div class="mpa-w__head">
<div class="mpa-w__icon mpa-w__icon--blue"><i class="pi pi-map-marker" /></div>
<div class="mpa-w__title">
<div class="mpa-w__title-text">2. Endereço</div>
<div class="mpa-w__sub">Localização do paciente</div>
</div>
</div>
<div class="mpa-w__body">
<div class="mpa-fields">
<p class="mpa-fields__label">Localização</p>
<div class="mpa-field-grid-2">
<div class="mpa-field-row">
<span class="mpa-field-row__k">CEP</span>
<span class="mpa-field-row__v mpa-field-row__v--mono">{{ dash(cep) }}</span>
</div>
<div class="mpa-field-row">
<span class="mpa-field-row__k">País</span>
<span class="mpa-field-row__v">{{ dash(pais) }}</span>
</div>
<div class="mpa-field-row">
<span class="mpa-field-row__k">Cidade</span>
<span class="mpa-field-row__v">{{ dash(cidade) }}</span>
</div>
<div class="mpa-field-row">
<span class="mpa-field-row__k">Estado</span>
<span class="mpa-field-row__v">{{ dash(estado) }}</span>
</div>
<div class="mpa-field-row">
<span class="mpa-field-row__k">Endereço</span>
<span class="mpa-field-row__v">{{ dash(endereco) }}</span>
</div>
<div class="mpa-field-row">
<span class="mpa-field-row__k">Número</span>
<span class="mpa-field-row__v">{{ dash(numero) }}</span>
</div>
<div class="mpa-field-row">
<span class="mpa-field-row__k">Bairro</span>
<span class="mpa-field-row__v">{{ dash(bairro) }}</span>
</div>
<div class="mpa-field-row mpa-field-row--last">
<span class="mpa-field-row__k">Complemento</span>
<span class="mpa-field-row__v">{{ dash(complemento) }}</span>
</div>
</div>
</div>
</div>
</section>
<!-- 3. Dados Adicionais -->
<section id="mpa-perfil-adicional" class="mpa-w">
<div class="mpa-w__head">
<div class="mpa-w__icon mpa-w__icon--blue"><i class="pi pi-tags" /></div>
<div class="mpa-w__title">
<div class="mpa-w__title-text">3. Dados Adicionais</div>
<div class="mpa-w__sub">Formação e contato familiar</div>
</div>
</div>
<div class="mpa-w__body">
<div class="mpa-fields">
<p class="mpa-fields__label">Formação & família</p>
<div class="mpa-field-grid-2">
<div class="mpa-field-row">
<span class="mpa-field-row__k">Escolaridade</span>
<span class="mpa-field-row__v">{{ dash(escolaridade) }}</span>
</div>
<div class="mpa-field-row">
<span class="mpa-field-row__k">Profissão</span>
<span class="mpa-field-row__v">{{ dash(profissao) }}</span>
</div>
<div class="mpa-field-row">
<span class="mpa-field-row__k">Nome de um parente</span>
<span class="mpa-field-row__v">{{ dash(nomeParente) }}</span>
</div>
<div class="mpa-field-row">
<span class="mpa-field-row__k">Grau de parentesco</span>
<span class="mpa-field-row__v">{{ dash(grauParentesco) }}</span>
</div>
<div class="mpa-field-row mpa-field-row--last">
<span class="mpa-field-row__k">Telefone do parente</span>
<a v-if="telefoneParente" :href="`tel:${telefoneParente}`" class="mpa-field-row__v mpa-field-row__v--link">
{{ fmtPhoneMobile(telefoneParente) }}
</a>
<span v-else class="mpa-field-row__v"></span>
</div>
</div>
</div>
</div>
</section>
<!-- 4. Responsavel -->
<section id="mpa-perfil-resp" class="mpa-w">
<div class="mpa-w__head">
<div class="mpa-w__icon mpa-w__icon--blue"><i class="pi pi-users" /></div>
<div class="mpa-w__title">
<div class="mpa-w__title-text">4. Responsável</div>
<div class="mpa-w__sub">Para pacientes menores de idade</div>
</div>
</div>
<div class="mpa-w__body">
<div class="mpa-fields">
<p class="mpa-fields__label">Dados do responsável</p>
<div class="mpa-field-row">
<span class="mpa-field-row__k">Nome</span>
<span class="mpa-field-row__v">{{ dash(nomeResponsavel) }}</span>
</div>
<div class="mpa-field-row">
<span class="mpa-field-row__k">CPF</span>
<span class="mpa-field-row__v mpa-field-row__v--mono">{{ fmtCPF(cpfResponsavel) }}</span>
</div>
<div class="mpa-field-row" :class="{ 'mpa-field-row--last': !observacaoResponsavel }">
<span class="mpa-field-row__k">Telefone</span>
<a v-if="telefoneResponsavel" :href="`tel:${telefoneResponsavel}`" class="mpa-field-row__v mpa-field-row__v--link">
{{ fmtPhoneMobile(telefoneResponsavel) }}
</a>
<span v-else class="mpa-field-row__v"></span>
</div>
<div v-if="observacaoResponsavel" class="mpa-field-block mpa-field-block--last">
<p class="mpa-field-block__label">Observação</p>
<p class="mpa-field-block__text">{{ observacaoResponsavel }}</p>
</div>
</div>
</div>
</section>
<!-- 5. Anotacoes Internas -->
<section id="mpa-perfil-anot" class="mpa-w">
<div class="mpa-w__head">
<div class="mpa-w__icon mpa-w__icon--blue"><i class="pi pi-file-edit" /></div>
<div class="mpa-w__title">
<div class="mpa-w__title-text">5. Anotações Internas</div>
<div class="mpa-w__sub">Visível pra você</div>
</div>
</div>
<div class="mpa-w__body">
<div class="mpa-fields">
<div class="mpa-fields__lock-hint">
<i class="pi pi-lock" />
Campo interno não aparece no cadastro externo.
</div>
<p class="mpa-fields__textblock mpa-fields__textblock--min">
{{ notasInternas ? notasInternas : '—' }}
</p>
</div>
</div>
</section>
<!-- 6. Sessoes (mini lista completa fica na aba Agenda) -->
<section id="mpa-perfil-sess" class="mpa-w">
<div class="mpa-w__head">
<div class="mpa-w__icon mpa-w__icon--blue"><i class="pi pi-calendar" /></div>
<div class="mpa-w__title">
<div class="mpa-w__title-text">6. Sessões</div>
<div class="mpa-w__sub">{{ kpiSessoes }} no histórico abertura completa na aba Agenda</div>
</div>
</div>
<div class="mpa-w__body">
<div v-if="sessionsHook.loading.value" class="mpa-empty">
<i class="pi pi-spin pi-spinner mr-2" /> Carregando sessões
</div>
<div v-else-if="!sessionsHook.sessions.value.length" class="mpa-empty mpa-empty--rich">
<div class="mpa-empty__icon"><i class="pi pi-calendar" /></div>
<div class="mpa-empty__title">Nenhuma sessão</div>
<div class="mpa-empty__sub">As sessões agendadas com este paciente aparecerão aqui.</div>
</div>
<div v-else class="mpa-sess-list">
<div
v-for="s in sessionsHook.sessions.value"
:key="s.id"
class="mpa-sess"
>
<div class="mpa-sess__main">
<p class="mpa-sess__title">
{{ s.titulo_custom || s.titulo || (s.tipo ? s.tipo : 'Sessão') }}
</p>
<div class="mpa-sess__meta">
<span><i class="pi pi-calendar" />{{ fmtDateTimeBR(s.inicio_em) }}</span>
<span v-if="sessionDuration(s.inicio_em, s.fim_em)">
<i class="pi pi-clock" />{{ sessionDuration(s.inicio_em, s.fim_em) }}
</span>
<span v-if="s.modalidade">
<i :class="s.modalidade === 'online' ? 'pi pi-video' : 'pi pi-map-marker'" />
{{ s.modalidade === 'online' ? 'Online' : 'Presencial' }}
</span>
</div>
<p v-if="s.observacoes" class="mpa-sess__obs">{{ s.observacoes }}</p>
</div>
<Tag
:value="STATUS_LABEL[s.status] || s.status || 'Agendado'"
:severity="STATUS_SEVERITY[s.status] || 'info'"
class="mpa-sess__tag"
/>
</div>
</div>
</div>
</section>
</div>
<!-- ABA: Prontuario -->
@@ -1524,6 +1871,205 @@ void toast;
-webkit-box-orient: vertical;
}
/* ═══════ Tab Perfil — fields stacked grid ═══════ */
.mpa-fields-grid {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
@media (min-width: 720px) {
.mpa-fields-grid {
grid-template-columns: 1fr 1fr;
}
}
.mpa-fields-stack {
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.mpa-fields {
border-radius: 10px;
border: 1px solid var(--m-border);
background: var(--m-bg-medium);
padding: 12px 14px;
}
.mpa-fields--full {
margin-top: 12px;
grid-column: 1 / -1;
}
.mpa-fields__label {
font-size: 0.66rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--m-text-muted);
margin-bottom: 8px;
opacity: 0.85;
}
.mpa-fields__textblock {
font-size: 0.85rem;
color: var(--m-text);
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.mpa-fields__textblock--min { min-height: 3.5rem; }
.mpa-fields__lock-hint {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.7rem;
color: var(--m-text-muted);
opacity: 0.75;
margin-bottom: 10px;
}
.mpa-fields__lock-hint > i { font-size: 0.66rem; }
/* Field row generico (k = label esq, v = valor dir) */
.mpa-field-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
padding: 6px 0;
border-bottom: 1px solid var(--m-border);
font-size: 0.82rem;
}
.mpa-field-row--last { border-bottom: none; }
.mpa-field-row--align-start { align-items: flex-start; }
.mpa-field-row__k {
color: var(--m-text-muted);
flex-shrink: 0;
}
.mpa-field-row__v {
color: var(--m-text);
font-weight: 600;
text-align: right;
word-break: break-word;
}
.mpa-field-row__v--mono {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 0.78rem;
}
.mpa-field-row__v--link {
color: var(--p-primary-color);
text-decoration: none;
}
.mpa-field-row__v--link:hover { text-decoration: underline; }
.mpa-field-row__v--truncate {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
}
.mpa-field-row__v-dim {
color: var(--m-text-muted);
font-weight: 400;
margin-left: 4px;
}
.mpa-field-row__chips {
display: flex;
flex-wrap: wrap;
gap: 4px;
justify-content: flex-end;
}
/* Grid 2-col pra blocos de endereco/dados (8 fields em 2 colunas) */
.mpa-field-grid-2 {
display: grid;
grid-template-columns: 1fr;
}
@media (min-width: 600px) {
.mpa-field-grid-2 {
grid-template-columns: 1fr 1fr;
column-gap: 18px;
}
.mpa-field-grid-2 > .mpa-field-row {
border-bottom: 1px solid var(--m-border);
}
/* Ultima da esq tira borda quando ultima do grid; --last e atribuida no template */
.mpa-field-grid-2 > .mpa-field-row--last { border-bottom: none; }
}
/* Bloco textual full-width dentro de .mpa-fields (ex: Observacao Responsavel) */
.mpa-field-block {
padding: 10px 0;
border-top: 1px solid var(--m-border);
}
.mpa-field-block--last { border-bottom: none; }
.mpa-field-block__label {
font-size: 0.7rem;
font-weight: 700;
color: var(--m-text-muted);
margin-bottom: 4px;
}
.mpa-field-block__text {
font-size: 0.82rem;
color: var(--m-text);
line-height: 1.5;
white-space: pre-wrap;
}
/* Mini lista de sessoes (panel 6 da aba Perfil) */
.mpa-sess-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 360px;
overflow-y: auto;
padding-right: 2px;
}
.mpa-sess {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid var(--m-border);
background: var(--m-bg-medium);
}
.mpa-sess__main {
flex: 1;
min-width: 0;
}
.mpa-sess__title {
font-size: 0.85rem;
font-weight: 700;
color: var(--m-text);
line-height: 1.2;
margin-bottom: 4px;
}
.mpa-sess__meta {
display: flex;
flex-wrap: wrap;
gap: 4px 10px;
font-size: 0.72rem;
color: var(--m-text-muted);
}
.mpa-sess__meta > span {
display: inline-flex;
align-items: center;
gap: 4px;
}
.mpa-sess__meta > span > i { font-size: 0.66rem; opacity: 0.6; }
.mpa-sess__obs {
font-size: 0.74rem;
color: var(--m-text-muted);
line-height: 1.4;
margin-top: 4px;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.mpa-sess__tag {
flex-shrink: 0;
font-size: 0.66rem !important;
}
/* ═══════ Notas e observacoes ═══════ */
.mpa-notes {
display: flex;