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:
@@ -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) : '—';
|
||||
|
||||
@@ -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,16 +143,47 @@ 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;
|
||||
}
|
||||
const observacoes = computed(() => pickField(patientData.value, ['observacoes', 'notes_short']));
|
||||
const notasInternas = computed(() => pickField(patientData.value, ['notas_internas', 'notes']));
|
||||
// ── 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);
|
||||
@@ -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>.
|
||||
</p>
|
||||
<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>
|
||||
</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 só 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;
|
||||
|
||||
Reference in New Issue
Block a user