Files
agenciapsilmno/src/features/patients/prontuario/PatientProntuario.vue.bak
T
Leonardo 957e912a7f Melissa polish + Prontuario Visao Geral + agenda historico
Sprints B (05-03) e C (05-04) acumulados:

- NotificationDrawer/Item redesign (visual mais limpo, ações inline)
- Dock pins compose (useMelissaDockPins) + cache store global (melissaCacheStore)
- MelissaAgenda: timeline FullCalendar parity + cards resumo, histórico
  card com useMelissaAgendaHistorico, MelissaEventoPanel ajustado
- useFeriados: cache opt-in pra evitar fetch redundante de feriados
- PatientProntuario: aba Visão Geral nova; PatientConversationsTab polish
- AgendaClinicMosaic / AgendaTerapeutaPage / useAgendaSettings: ajustes
  de paridade com Melissa
- DocumentsListPage: pequenos ajustes
- DB migration 20260504000001: fix do trigger pra status 'excluido' nas
  cancel_notifications

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 09:11:55 -03:00

1176 lines
73 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/features/patients/prontuario/PatientProntuario.vue
| Data: 2026
| Local: São Carlos/SP — Brasil
|--------------------------------------------------------------------------
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue';
import Chip from 'primevue/chip';
import Accordion from 'primevue/accordion';
import AccordionPanel from 'primevue/accordionpanel';
import AccordionHeader from 'primevue/accordionheader';
import AccordionContent from 'primevue/accordioncontent';
import Popover from 'primevue/popover';
import { useToast } from 'primevue/usetoast';
import { supabase } from '@/lib/supabase/client';
import DocumentsListPage from '@/features/documents/DocumentsListPage.vue';
import PatientConversationsTab from './PatientConversationsTab.vue';
// ── PROPS / EMITS ──────────────────────────────────────────────
const props = defineProps({
modelValue: { type: Boolean, default: false },
patient: { type: Object, default: () => ({}) },
});
const emit = defineEmits(['update:modelValue', 'close', 'edit']);
const toast = useToast();
const model = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v),
});
// ── UTILS ──────────────────────────────────────────────────────
function isEmpty(v) {
if (v === null || v === undefined) return true;
return !String(v).trim();
}
function dash(v) {
const s = String(v ?? '').trim();
return s || '—';
}
function pick(obj, keys = []) {
for (const k of keys) {
const v = obj?.[k];
if (!isEmpty(v)) return v;
}
return null;
}
// ── TABS PRINCIPAIS ────────────────────────────────────────────
const activeTab = ref(0);
const mainTabs = [
{ label: 'Perfil', icon: 'pi pi-user' },
{ label: 'Prontuário', icon: 'pi pi-file-edit' },
{ label: 'Agenda', icon: 'pi pi-calendar' },
{ label: 'Financeiro', icon: 'pi pi-wallet' },
{ label: 'Documentos', icon: 'pi pi-folder' },
{ label: 'Conversas', icon: 'pi pi-whatsapp' },
];
// ── ACCORDION DA ABA PERFIL ────────────────────────────────────
const accordionValues = ['0', '1', '2', '3', '4', '5'];
const activeAccValues = ref(['0']);
const activeAccValue = computed(() => activeAccValues.value?.[0] ?? null);
const panelHeaderRefs = ref([]);
function setPanelHeaderRef(el, idx) {
if (!el) return;
panelHeaderRefs.value[idx] = el;
}
const allOpen = computed(() =>
accordionValues.every((v) => activeAccValues.value.includes(v))
);
function toggleAllAccordions() {
activeAccValues.value = allOpen.value ? [] : [...accordionValues];
}
async function openPanel(i) {
activeTab.value = 0;
const v = String(i);
activeAccValues.value = [v];
await nextTick();
const headerRef = panelHeaderRefs.value?.[i];
const el = headerRef?.$el ?? headerRef;
if (el && typeof el.scrollIntoView === 'function') {
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
const profileNavItems = [
{ value: '0', label: 'Informações Pessoais', icon: 'pi pi-pencil' },
{ value: '1', label: 'Endereço', icon: 'pi pi-map-marker' },
{ value: '2', label: 'Dados Adicionais', icon: 'pi pi-tags' },
{ value: '3', label: 'Responsável', icon: 'pi pi-users' },
{ value: '4', label: 'Anotações', icon: 'pi pi-file-edit' },
{ value: '5', label: 'Sessões', icon: 'pi pi-calendar' },
];
const navPopover = ref(null);
function toggleNav(event) { navPopover.value?.toggle(event); }
function selectNav(item) {
openPanel(Number(item.value));
navPopover.value?.hide();
}
const selectedNav = computed(() =>
profileNavItems.find((i) => i.value === activeAccValue.value) || null
);
// Responsivo
const isCompact = ref(false);
let mql = null, mqlHandler = null;
function syncCompact() { isCompact.value = !!mql?.matches; }
onMounted(() => {
mql = window.matchMedia('(max-width: 1199px)');
mqlHandler = () => syncCompact();
mql.addEventListener?.('change', mqlHandler);
mql.addListener?.(mqlHandler);
syncCompact();
});
onBeforeUnmount(() => {
mql?.removeEventListener?.('change', mqlHandler);
mql?.removeListener?.(mqlHandler);
});
// ── DATA LOAD ──────────────────────────────────────────────────
const loading = ref(false);
const loadError = ref('');
const patientFull = ref(null);
const groups = ref([]);
const tags = ref([]);
const sessions = ref([]);
const sessionsLoading = ref(false);
const patientData = computed(() => patientFull.value || props.patient || {});
// Avatar
const avatarUrl = computed(() =>
patientData.value?.avatar_url || patientData.value?.avatar || null
);
// Grupos
const groupNames = computed(() =>
(groups.value || []).map((g) => g?.name).filter(Boolean)
);
const groupLabel = computed(() => {
const n = groupNames.value.length;
if (n === 0) return '—';
return groupNames.value.join(', ');
});
const groupCountLabel = computed(() =>
groupNames.value.length <= 1 ? 'Grupo' : 'Grupos'
);
// ── COMPUTED FIELDS ────────────────────────────────────────────
const birthValue = computed(() => pick(patientData.value, ['data_nascimento', 'birth_date']));
const nomeCompleto = computed(() => pick(patientData.value, ['nome_completo', 'name']));
const telefone = computed(() => pick(patientData.value, ['telefone', 'phone']));
const emailPrincipal = computed(() => pick(patientData.value, ['email_principal', 'email']));
const emailAlternativo = computed(() => pick(patientData.value, ['email_alternativo', 'email_alt', 'emailAlt']));
const telefoneAlternativo = computed(() => pick(patientData.value, ['telefone_alternativo', 'phone_alt', 'phoneAlt']));
const genero = computed(() => pick(patientData.value, ['genero', 'gender']));
const estadoCivil = computed(() => pick(patientData.value, ['estado_civil', 'marital_status']));
const naturalidade = computed(() => pick(patientData.value, ['naturalidade', 'birthplace', 'place_of_birth']));
const observacoes = computed(() => pick(patientData.value, ['observacoes', 'notes_short']));
const ondeNosConheceu = computed(() => pick(patientData.value, ['onde_nos_conheceu', 'lead_source']));
const encaminhadoPor = computed(() => pick(patientData.value, ['encaminhado_por', 'referred_by']));
const statusPaciente = computed(() => pick(patientData.value, ['status']));
const convenio = computed(() => pick(patientData.value, ['convenio']));
const patientScope = computed(() => pick(patientData.value, ['patient_scope']));
const riscoElevado = computed(() => !!patientData.value?.risco_elevado);
const cep = computed(() => pick(patientData.value, ['cep', 'postal_code']));
const pais = computed(() => pick(patientData.value, ['pais', 'country']));
const cidade = computed(() => pick(patientData.value, ['cidade', 'city']));
const estado = computed(() => pick(patientData.value, ['estado', 'state']));
const endereco = computed(() => pick(patientData.value, ['endereco', 'address_line']));
const numero = computed(() => pick(patientData.value, ['numero', 'address_number']));
const bairro = computed(() => pick(patientData.value, ['bairro', 'neighborhood']));
const complemento = computed(() => pick(patientData.value, ['complemento', 'address_complement']));
const escolaridade = computed(() => pick(patientData.value, ['escolaridade', 'education', 'education_level']));
const profissao = computed(() => pick(patientData.value, ['profissao', 'profession']));
const nomeParente = computed(() => pick(patientData.value, ['nome_parente', 'relative_name']));
const grauParentesco = computed(() => pick(patientData.value, ['grau_parentesco', 'relative_relation']));
const telefoneParente = computed(() => pick(patientData.value, ['telefone_parente', 'relative_phone']));
const nomeResponsavel = computed(() => pick(patientData.value, ['nome_responsavel', 'guardian_name']));
const cpfResponsavel = computed(() => pick(patientData.value, ['cpf_responsavel', 'guardian_cpf']));
const telefoneResponsavel = computed(() => pick(patientData.value, ['telefone_responsavel', 'guardian_phone']));
const observacaoResponsavel = computed(() => pick(patientData.value, ['observacao_responsavel', 'guardian_note']));
const notasInternas = computed(() => pick(patientData.value, ['notas_internas', 'notes']));
// Métricas (lidas do patientData ou fallback null)
const totalSessoes = computed(() => patientData.value?.total_sessoes ?? null);
const comparecimentoPct = computed(() => patientData.value?.comparecimento_pct ?? null);
const ltvTotal = computed(() => patientData.value?.ltv_total ?? null);
const diasUltimaSessao = computed(() => patientData.value?.dias_ultima_sessao ?? null);
// ── FORMATTERS ─────────────────────────────────────────────────
function nameInitials(name) {
if (!name) return '?';
return String(name).split(' ').filter(Boolean).slice(0, 2)
.map((w) => w[0].toUpperCase()).join('');
}
const avatarInitials = computed(() => nameInitials(nomeCompleto.value));
function onlyDigits(v) { return String(v ?? '').replace(/\D/g, ''); }
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,11)}`;
}
function fmtRG(v) {
const s = String(v ?? '').toUpperCase().replace(/[^0-9X]/g, '');
if (!s) return '—';
if (s.length === 9) return `${s.slice(0,2)}.${s.slice(2,5)}.${s.slice(5,8)}-${s.slice(8)}`;
if (s.length === 8) return `${s.slice(0,2)}.${s.slice(2,5)}.${s.slice(5,8)}`;
return s;
}
function parseDateLoose(v) {
if (!v) return null;
const s = String(v).trim();
if (/^\d{4}-\d{2}-\d{2}/.test(s)) {
const d = new Date(s.slice(0, 10));
return isNaN(d) ? null : d;
}
let m = s.match(/^(\d{2})[/-](\d{2})[/-](\d{4})$/);
if (m) {
const d = new Date(Number(m[3]), Number(m[2]) - 1, Number(m[1]));
return isNaN(d) ? null : d;
}
const d = new Date(s);
return isNaN(d) ? null : d;
}
function fmtDateBR(v) {
const d = parseDateLoose(v);
if (!d) return v ? dash(v) : '—';
return `${String(d.getDate()).padStart(2,'0')}/${String(d.getMonth()+1).padStart(2,'0')}/${d.getFullYear()}`;
}
function calcAge(v) {
const d = parseDateLoose(v);
if (!d) return null;
const now = new Date();
let age = now.getFullYear() - d.getFullYear();
const m = now.getMonth() - d.getMonth();
if (m < 0 || (m === 0 && now.getDate() < d.getDate())) age--;
return age;
}
const ageLabel = computed(() => {
const age = calcAge(birthValue.value);
return age == null ? '—' : `${age} anos`;
});
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;
}
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;
}
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;
}
function fmtCurrency(v) {
if (!v && v !== 0) return '—';
return `R$ ${Number(v).toLocaleString('pt-BR')}`;
}
function fmtDateTimeBR(iso) {
if (!iso) return '—';
const d = new Date(iso);
if (isNaN(d)) return iso;
const dd = String(d.getDate()).padStart(2,'0');
const mm = String(d.getMonth()+1).padStart(2,'0');
const hh = String(d.getHours()).padStart(2,'0');
const mi = String(d.getMinutes()).padStart(2,'0');
return `${dd}/${mm}/${d.getFullYear()} ${hh}:${mi}`;
}
function sessionDuration(inicio, fim) {
if (!inicio || !fim) return null;
const diff = new Date(fim) - new Date(inicio);
if (diff <= 0) return null;
const min = Math.round(diff / 60000);
if (min < 60) return `${min} min`;
const h = Math.floor(min / 60);
const m = min % 60;
return m ? `${h}h ${m}min` : `${h}h`;
}
// ── TAG COLORS ─────────────────────────────────────────────────
function normalizeHexColor(c) {
const s = String(c ?? '').trim();
if (!s) return '';
if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(s)) return s;
if (/^([0-9a-f]{3}|[0-9a-f]{6})$/i.test(s)) return `#${s}`;
return s;
}
function hexToRgb(hex) {
const h = String(hex || '').replace('#','').trim();
if (!/^([0-9a-f]{3}|[0-9a-f]{6})$/i.test(h)) return null;
const full = h.length === 3 ? h.split('').map(ch => ch+ch).join('') : h;
const r = parseInt(full.slice(0,2), 16);
const g = parseInt(full.slice(2,4), 16);
const b = parseInt(full.slice(4,6), 16);
if ([r,g,b].some(n => isNaN(n))) return null;
return { r, g, b };
}
function relativeLuminance({ r, g, b }) {
const srgb = [r,g,b].map(v => v/255).map(c => c <= 0.03928 ? c/12.92 : Math.pow((c+0.055)/1.055, 2.4));
return 0.2126*srgb[0] + 0.7152*srgb[1] + 0.0722*srgb[2];
}
function bestTextColor(bg) {
const rgb = hexToRgb(normalizeHexColor(bg));
if (!rgb) return '#0f172a';
return relativeLuminance(rgb) < 0.45 ? '#ffffff' : '#0f172a';
}
function tagStyle(t) {
const bg = normalizeHexColor(t?.color || t?.cor);
if (!bg) return {};
return { background: bg, color: bestTextColor(bg), borderColor: 'transparent' };
}
// ── SESSION STATUS ─────────────────────────────────────────────
const STATUS_LABEL = {
agendado: 'Agendado', realizado: 'Realizado', faltou: 'Faltou',
cancelado: 'Cancelado', remarcado: 'Remarcado', bloqueado: 'Bloqueado',
};
const STATUS_SEVERITY = {
agendado: 'info', realizado: 'success', faltou: 'danger',
cancelado: 'warn', remarcado: 'secondary', bloqueado: 'secondary',
};
// ── SUPABASE ───────────────────────────────────────────────────
async function loadSessions(patientId) {
sessionsLoading.value = true;
sessions.value = [];
try {
const { data, error } = await supabase
.from('agenda_eventos')
.select('id, inicio_em, fim_em, status, modalidade, tipo, titulo, titulo_custom, observacoes')
.eq('patient_id', patientId)
.order('inicio_em', { ascending: false })
.limit(100);
if (error) throw error;
sessions.value = data || [];
} catch { sessions.value = []; }
finally { sessionsLoading.value = false; }
}
async function getPatientById(id) {
const { data, error } = await supabase.from('patients').select('*').eq('id', id).maybeSingle();
if (error) throw error;
return data;
}
async function getPatientRelations(id) {
const { data: g, error: ge } = await supabase.from('patient_group_patient').select('patient_group_id').eq('patient_id', id);
if (ge) throw ge;
const { data: t, error: te } = await supabase.from('patient_patient_tag').select('tag_id').eq('patient_id', id);
if (te) throw te;
return {
groupIds: (g || []).map(x => x.patient_group_id).filter(Boolean),
tagIds: (t || []).map(x => x.tag_id).filter(Boolean),
};
}
async function getGroupsByIds(ids) {
if (!ids?.length) return [];
const { data, error } = await supabase.from('patient_groups').select('id, nome').in('id', ids).order('nome', { ascending: true });
if (error) throw error;
return (data || []).map(g => ({ id: g.id, name: g.nome }));
}
async function getTagsByIds(ids) {
if (!ids?.length) return [];
const { data, error } = await supabase.from('patient_tags').select('id, nome, cor').in('id', ids).order('nome', { ascending: true });
if (error) throw error;
return (data || []).map(t => ({ id: t.id, name: t.nome, color: t.cor }));
}
async function loadDetail(id) {
loadError.value = '';
loading.value = true;
patientFull.value = null;
groups.value = [];
tags.value = [];
try {
const [p, rel] = await Promise.all([getPatientById(id), getPatientRelations(id)]);
if (!p) throw new Error('Paciente não retornou dados (RLS bloqueando ou ID não existe no banco).');
patientFull.value = p;
const [g, t] = await Promise.all([
getGroupsByIds(rel.groupIds || []),
getTagsByIds(rel.tagIds || []),
loadSessions(id),
]);
groups.value = g;
tags.value = t;
} catch (e) {
loadError.value = e?.message || 'Falha ao buscar dados no Supabase.';
toast.add({ severity: 'error', summary: 'Erro ao carregar', detail: loadError.value, life: 4500 });
} finally {
loading.value = false;
}
}
watch(
[() => props.modelValue, () => props.patient?.id],
async ([open, id], [prevOpen, prevId]) => {
if (!open || !id) return;
if (open === prevOpen && id === prevId) return;
activeTab.value = 0;
activeAccValues.value = ['0'];
await loadDetail(id);
},
{ immediate: true }
);
// ── AÇÕES ──────────────────────────────────────────────────────
function close() {
model.value = false;
emit('close');
}
function editPatient() {
const id = patientData.value?.id;
if (!id) return;
close();
emit('edit', String(id));
}
async function copyResumo() {
const txt = `Paciente: ${dash(nomeCompleto.value)}
Idade: ${ageLabel.value}
${groupCountLabel.value}: ${groupLabel.value}
Telefone: ${fmtPhoneMobile(telefone.value)}
Email: ${dash(emailPrincipal.value)}
CPF: ${fmtCPF(patientData.value?.cpf)}
RG: ${fmtRG(patientData.value?.rg)}
Nascimento: ${fmtDateBR(birthValue.value)}
Tags: ${(tags.value || []).map(t => t.name).filter(Boolean).join(', ') || '—'}`;
try {
await navigator.clipboard.writeText(txt);
toast.add({ severity: 'success', summary: 'Copiado', detail: 'Resumo copiado para a área de transferência.', life: 2200 });
} catch {
toast.add({ severity: 'error', summary: 'Falha', detail: 'Não foi possível copiar.', life: 3000 });
}
}
</script>
<template>
<Dialog
v-model:visible="model"
modal
:draggable="false"
maximizable
:style="{ width: '96vw', maxWidth: '1400px' }"
:contentStyle="{ padding: 0 }"
pt:mask:class="backdrop-blur-sm"
pt:header:class="border-b border-[var(--surface-border)] px-4 py-3"
pt:title:class="text-[0.95rem] font-bold text-[var(--text-color)] truncate"
@hide="close"
>
<!-- ── HEADER DO DIALOG ────────────────────────────────── -->
<template #header>
<div class="flex items-center gap-3 min-w-0 flex-1">
<!-- Avatar mini -->
<div v-if="avatarUrl"
class="w-8 h-8 rounded-full overflow-hidden shrink-0 border border-[var(--surface-border)]">
<img :src="avatarUrl" class="w-full h-full object-cover" />
</div>
<div v-else
class="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center shrink-0">
<span class="text-[0.68rem] font-bold text-indigo-700">{{ avatarInitials }}</span>
</div>
<!-- Nome + sub-linha -->
<div class="min-w-0 flex-1">
<p class="text-[0.92rem] font-bold text-[var(--text-color)] truncate leading-tight">
{{ dash(nomeCompleto) }}
</p>
<div class="flex items-center gap-2 flex-wrap mt-0.5">
<span class="text-[0.72rem] text-[var(--text-color-secondary)]">
{{ ageLabel }}<template v-if="patientData.pronomes"> · {{ patientData.pronomes }}</template>
</span>
<!-- Badge risco elevado no header -->
<span v-if="riscoElevado"
class="inline-flex items-center gap-1 text-[0.65rem] font-semibold
text-red-600 bg-red-50 border border-red-200 px-1.5 py-0.5 rounded-full">
<i class="pi pi-exclamation-circle text-[0.6rem]" />
Risco elevado
</span>
</div>
</div>
<!-- Botão + Sessão no header -->
<Button label="+ Sessão" size="small" class="shrink-0 mr-2" />
</div>
</template>
<!-- ══ CONTEÚDO ════════════════════════════════════════════ -->
<div class="bg-[var(--surface-ground)] min-h-[60vh]">
<!-- Loading -->
<div v-if="loading"
class="flex items-center justify-center py-20 gap-2 text-sm text-[var(--text-color-secondary)]">
<i class="pi pi-spin pi-spinner" /> Carregando…
</div>
<!-- Erro -->
<div v-else-if="loadError" class="p-4">
<div class="flex items-start gap-3 rounded-xl border border-red-200 bg-red-50 px-4 py-3">
<i class="pi pi-exclamation-circle text-red-500 mt-0.5 shrink-0" />
<div>
<p class="text-sm font-semibold text-red-700">Falha ao carregar</p>
<p class="text-xs text-red-500 mt-0.5">{{ loadError }}</p>
</div>
</div>
</div>
<div v-else class="grid grid-cols-1 xl:grid-cols-[240px_1fr] gap-3 p-3 md:p-4">
<!-- ══ SIDEBAR ══════════════════════════════════════ -->
<aside class="rounded-xl border border-[var(--surface-border)]
bg-[var(--surface-card)] p-4 shadow-sm
xl:sticky xl:top-2 xl:self-start space-y-0">
<!-- Banner risco elevado (sidebar) -->
<div v-if="riscoElevado"
class="flex items-start gap-2 rounded-lg border border-red-200
bg-red-50 px-3 py-2 mb-4">
<i class="pi pi-exclamation-circle text-red-500 text-sm mt-0.5 shrink-0" />
<p class="text-[0.73rem] font-semibold text-red-700 leading-snug">
Risco elevado sinalizado
</p>
</div>
<!-- Avatar + nome + badges -->
<div class="flex items-center gap-3 pb-4 mb-4
border-b border-[var(--surface-border)]
xl:flex-col xl:items-center xl:gap-2 xl:text-center">
<!-- Avatar -->
<div class="shrink-0 w-16 h-16 xl:w-20 xl:h-20 rounded-full overflow-hidden">
<img v-if="avatarUrl" :src="avatarUrl" alt="avatar"
class="w-full h-full object-cover" />
<div v-else class="w-full h-full bg-indigo-100 flex items-center justify-center">
<span class="text-xl font-bold text-indigo-700 tracking-tight">
{{ avatarInitials }}
</span>
</div>
</div>
<!-- Info -->
<div class="flex-1 min-w-0 xl:text-center">
<p class="text-sm font-bold text-[var(--text-color)] truncate">
{{ dash(nomeCompleto) }}
</p>
<p class="text-xs text-[var(--text-color-secondary)] mt-0.5">
{{ ageLabel }}
<template v-if="patientData.pronomes"> · {{ patientData.pronomes }}</template>
</p>
<p v-if="naturalidade || estado"
class="text-xs text-[var(--text-color-secondary)]">
{{ dash(naturalidade) }}<template v-if="estado">, {{ estado }}</template>
</p>
<!-- Status + Convênio + Scope -->
<div class="flex flex-wrap gap-1 mt-2 xl:justify-center">
<Tag v-if="statusPaciente" :value="statusPaciente" severity="success" class="!text-[0.68rem]" />
<Tag v-if="convenio" :value="convenio" severity="info" class="!text-[0.68rem]" />
<Tag v-if="patientScope" :value="patientScope" severity="secondary" class="!text-[0.68rem]" />
</div>
<!-- Tags coloridas -->
<div v-if="tags?.length" class="flex flex-wrap gap-1 mt-1.5 xl:justify-center">
<span v-for="t in tags" :key="t.id"
class="inline-flex items-center px-2 py-0.5 rounded-full text-[0.68rem] font-medium"
:style="tagStyle(t)">
{{ t.name }}
</span>
</div>
</div>
</div>
<!-- Métricas 2×2 -->
<div v-if="totalSessoes != null || comparecimentoPct != null || ltvTotal != null || diasUltimaSessao != null"
class="grid grid-cols-2 gap-3 text-center pb-4 mb-4 border-b border-[var(--surface-border)]">
<div v-if="totalSessoes != null">
<p class="text-xl font-bold text-[var(--text-color)]">{{ totalSessoes }}</p>
<p class="text-[0.65rem] text-[var(--text-color-secondary)] mt-0.5">Sessões</p>
</div>
<div v-if="comparecimentoPct != null">
<p class="text-xl font-bold text-emerald-600">{{ comparecimentoPct }}%</p>
<p class="text-[0.65rem] text-[var(--text-color-secondary)] mt-0.5">Comparec.</p>
</div>
<div v-if="ltvTotal != null">
<p class="text-base font-bold text-[var(--text-color)]">{{ fmtCurrency(ltvTotal) }}</p>
<p class="text-[0.65rem] text-[var(--text-color-secondary)] mt-0.5">LTV total</p>
</div>
<div v-if="diasUltimaSessao != null">
<p class="text-xl font-bold text-amber-500">{{ diasUltimaSessao }}d</p>
<p class="text-[0.65rem] text-[var(--text-color-secondary)] mt-0.5">Últ. sessão</p>
</div>
</div>
<!-- Grupo e Tags -->
<div class="flex flex-col gap-3 text-sm text-[var(--text-color-secondary)] pb-4 mb-4 border-b border-[var(--surface-border)]">
<div>
<p class="text-[0.62rem] font-bold uppercase tracking-widest text-[var(--text-color-secondary)] opacity-50 mb-1">
{{ groupCountLabel }}
</p>
<p class="text-xs font-medium text-[var(--text-color)]">{{ groupLabel }}</p>
</div>
<div>
<p class="text-[0.62rem] font-bold uppercase tracking-widest text-[var(--text-color-secondary)] opacity-50 mb-1.5">
Tags
</p>
<div class="flex flex-wrap gap-1.5">
<Chip v-for="t in tags" :key="t.id"
:label="t.name" :style="tagStyle(t)"
class="!border !rounded-full !text-xs" />
<span v-if="!tags?.length" class="text-xs text-[var(--text-color-secondary)]">—</span>
</div>
</div>
</div>
<!-- Nav tabs principais -->
<div class="flex flex-col gap-0.5">
<p class="text-[0.62rem] font-bold uppercase tracking-widest text-[var(--text-color-secondary)] opacity-50 px-3 mb-1">
Navegação
</p>
<button
v-for="(tab, i) in mainTabs" :key="i"
type="button"
class="flex items-center gap-2.5 rounded-lg px-3 py-2 text-left
text-[0.82rem] border transition-colors duration-100"
:class="activeTab === i
? 'bg-indigo-50 border-indigo-200 text-indigo-700 font-semibold'
: 'border-transparent text-[var(--text-color)] hover:bg-[var(--surface-ground)] font-medium'"
@click="activeTab = i; if(i === 0) activeAccValues = ['0']"
>
<i :class="tab.icon" class="text-sm opacity-60 shrink-0" />
<span>{{ tab.label }}</span>
</button>
</div>
<!-- Submenu Perfil (desktop, aba 0) -->
<div v-if="activeTab === 0 && !isCompact"
class="mt-3 pt-3 border-t border-[var(--surface-border)] flex flex-col gap-0.5">
<p class="text-[0.62rem] font-bold uppercase tracking-widest text-[var(--text-color-secondary)] opacity-50 px-3 mb-1">
Seções do perfil
</p>
<button
v-for="item in profileNavItems" :key="item.value"
type="button"
class="flex items-center gap-2.5 rounded-lg px-3 py-1.5 text-left
text-[0.79rem] border transition-colors duration-100"
:class="activeAccValue === item.value
? 'bg-indigo-50 border-indigo-200 text-indigo-700 font-semibold'
: 'border-transparent text-[var(--text-color-secondary)] hover:bg-[var(--surface-ground)] font-medium'"
@click="openPanel(Number(item.value))"
>
<i :class="item.icon" class="text-xs opacity-60 shrink-0" />
<span>{{ item.label }}</span>
</button>
</div>
</aside>
<!-- ══ MAIN ═══════════════════════════════════════ -->
<main class="min-w-0 rounded-xl border border-[var(--surface-border)]
bg-[var(--surface-card)] shadow-sm overflow-hidden">
<!-- Tab bar -->
<div class="flex border-b border-[var(--surface-border)] overflow-x-auto scrollbar-none">
<button
v-for="(tab, i) in mainTabs" :key="i"
type="button"
class="flex items-center gap-1.5 px-4 py-3 text-[0.82rem] font-medium
whitespace-nowrap border-b-2 transition-colors duration-150 shrink-0"
:class="activeTab === i
? 'border-[var(--primary-color)] text-[var(--primary-color)]'
: 'border-transparent text-[var(--text-color-secondary)] hover:text-[var(--text-color)]'"
@click="activeTab = i; if(i === 0) activeAccValues = ['0']"
>
<i :class="tab.icon" class="text-sm" />
{{ tab.label }}
</button>
</div>
<!-- ═══════════════════════════════════════════
ABA 0: PERFIL — accordion
═══════════════════════════════════════════ -->
<div v-show="activeTab === 0">
<!-- Banner risco (no topo do conteúdo, abaixo das tabs) -->
<div v-if="riscoElevado"
class="flex items-start gap-3 border-b border-red-100 bg-red-50 px-4 py-3">
<i class="pi pi-exclamation-circle text-red-500 mt-0.5 shrink-0" />
<div>
<p class="text-sm font-semibold text-red-700">
Atenção — paciente com risco elevado sinalizado
</p>
<p class="text-xs text-red-500 mt-0.5">
Verifique as anotações internas e a seção de sessões para mais detalhes.
</p>
</div>
</div>
<!-- Nav compacto (<xl) -->
<div v-if="isCompact"
class="sticky top-0 z-10 border-b border-[var(--surface-border)]
bg-[var(--surface-card)] px-4 py-2.5">
<Button type="button" class="w-full !rounded-full"
icon="pi pi-chevron-down" iconPos="right"
:label="selectedNav ? selectedNav.label : 'Selecionar seção'"
@click="toggleNav($event)" />
<Popover ref="navPopover">
<div class="flex min-w-[240px] flex-col gap-0.5 p-1">
<button
v-for="item in profileNavItems" :key="item.value"
type="button"
class="flex items-center gap-2.5 rounded-lg px-3 py-2
text-left text-sm border cursor-pointer"
:class="activeAccValue === item.value
? 'bg-indigo-50 border-indigo-200 text-indigo-700 font-semibold'
: 'border-transparent text-[var(--text-color)] hover:bg-[var(--surface-ground)] font-medium'"
@click="selectNav(item)"
>
<i :class="item.icon" class="text-sm opacity-60 shrink-0" />
<span>{{ item.label }}</span>
</button>
</div>
</Popover>
</div>
<!-- Accordion -->
<div class="p-4">
<Accordion multiple v-model:value="activeAccValues" class="accordion-clean">
<!-- ── 1. INFORMAÇÕES PESSOAIS ───────────── -->
<AccordionPanel value="0">
<AccordionHeader :ref="(el) => setPanelHeaderRef(el, 0)">
<span class="flex items-center gap-2 text-[0.83rem] font-bold">
<i class="pi pi-pencil text-indigo-500 text-sm" />
1. Informações Pessoais
</span>
</AccordionHeader>
<AccordionContent>
<div class="pt-3 grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Dados de cadastro -->
<div class="rounded-xl border border-[var(--surface-border)] bg-[var(--surface-ground)] p-4">
<p class="text-[0.65rem] font-bold uppercase tracking-widest text-[var(--text-color-secondary)] mb-3 opacity-70">Dados de cadastro</p>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Nome completo</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">{{ dash(nomeCompleto) }}</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Data de nascimento</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">
{{ fmtDateBR(birthValue) }}
<span v-if="calcAge(birthValue) != null" class="text-[var(--text-color-secondary)] font-normal">({{ calcAge(birthValue) }} a)</span>
</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Gênero</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">{{ fmtGender(genero) }}</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Estado civil</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">{{ fmtMarital(estadoCivil) }}</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">CPF</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right font-mono">{{ fmtCPF(patientData.cpf) }}</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">RG</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right font-mono">{{ fmtRG(patientData.rg) }}</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Naturalidade</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">{{ dash(naturalidade) }}</span>
</div>
</div>
<!-- Contato + Origem -->
<div class="space-y-3">
<div class="rounded-xl border border-[var(--surface-border)] bg-[var(--surface-ground)] p-4">
<p class="text-[0.65rem] font-bold uppercase tracking-widest text-[var(--text-color-secondary)] mb-3 opacity-70">Contato</p>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Telefone / Celular</span>
<a v-if="telefone" :href="`tel:${telefone}`" class="text-[0.8rem] font-medium text-right text-[var(--primary-color)] hover:underline">{{ fmtPhoneMobile(telefone) }}</a>
<span v-else class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">—</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Telefone alternativo</span>
<a v-if="telefoneAlternativo" :href="`tel:${telefoneAlternativo}`" class="text-[0.8rem] font-medium text-right text-[var(--primary-color)] hover:underline">{{ fmtPhoneMobile(telefoneAlternativo) }}</a>
<span v-else class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">—</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Email principal</span>
<a v-if="emailPrincipal" :href="`mailto:${emailPrincipal}`" class="text-[0.8rem] font-medium text-right text-[var(--primary-color)] hover:underline truncate max-w-[200px] inline-block">{{ emailPrincipal }}</a>
<span v-else class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">—</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Email alternativo</span>
<a v-if="emailAlternativo" :href="`mailto:${emailAlternativo}`" class="text-[0.8rem] font-medium text-right text-[var(--primary-color)] hover:underline truncate max-w-[200px] inline-block">{{ emailAlternativo }}</a>
<span v-else class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">—</span>
</div>
</div>
<div class="rounded-xl border border-[var(--surface-border)] bg-[var(--surface-ground)] p-4">
<p class="text-[0.65rem] font-bold uppercase tracking-widest text-[var(--text-color-secondary)] mb-3 opacity-70">Origem</p>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">{{ groupCountLabel }}</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">{{ groupLabel }}</span>
</div>
<div class="flex items-start justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Tags</span>
<div class="flex flex-wrap gap-1 justify-end">
<span v-for="t in tags" :key="t.id"
class="inline-flex items-center px-2 py-0.5 rounded-full text-[0.72rem] font-medium"
:style="tagStyle(t)">{{ t.name }}</span>
<span v-if="!tags?.length" class="text-[0.8rem] text-[var(--text-color)] font-medium">—</span>
</div>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Onde nos conheceu?</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">{{ dash(ondeNosConheceu) }}</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Encaminhado por</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">{{ dash(encaminhadoPor) }}</span>
</div>
</div>
</div>
<!-- Observações full-width -->
<div v-if="observacoes"
class="md:col-span-2 rounded-xl border border-[var(--surface-border)] bg-[var(--surface-ground)] p-4">
<p class="text-[0.65rem] font-bold uppercase tracking-widest text-[var(--text-color-secondary)] mb-3 opacity-70">Observações</p>
<p class="text-[0.83rem] text-[var(--text-color)] leading-relaxed whitespace-pre-wrap">{{ observacoes }}</p>
</div>
</div>
</AccordionContent>
</AccordionPanel>
<!-- ── 2. ENDEREÇO ───────────────────────── -->
<AccordionPanel value="1">
<AccordionHeader :ref="(el) => setPanelHeaderRef(el, 1)">
<span class="flex items-center gap-2 text-[0.83rem] font-bold">
<i class="pi pi-map-marker text-indigo-500 text-sm" />
2. Endereço
</span>
</AccordionHeader>
<AccordionContent>
<div class="pt-3">
<div class="rounded-xl border border-[var(--surface-border)] bg-[var(--surface-ground)] p-4">
<p class="text-[0.65rem] font-bold uppercase tracking-widest text-[var(--text-color-secondary)] mb-3 opacity-70">Localização</p>
<div class="grid grid-cols-1 sm:grid-cols-2">
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">CEP</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right font-mono">{{ dash(cep) }}</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">País</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">{{ dash(pais) }}</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Cidade</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">{{ dash(cidade) }}</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Estado</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">{{ dash(estado) }}</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Endereço</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">{{ dash(endereco) }}</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Número</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">{{ dash(numero) }}</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Bairro</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">{{ dash(bairro) }}</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Complemento</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">{{ dash(complemento) }}</span>
</div>
</div>
</div>
</div>
</AccordionContent>
</AccordionPanel>
<!-- ── 3. DADOS ADICIONAIS ───────────────── -->
<AccordionPanel value="2">
<AccordionHeader :ref="(el) => setPanelHeaderRef(el, 2)">
<span class="flex items-center gap-2 text-[0.83rem] font-bold">
<i class="pi pi-tags text-indigo-500 text-sm" />
3. Dados Adicionais
</span>
</AccordionHeader>
<AccordionContent>
<div class="pt-3">
<div class="rounded-xl border border-[var(--surface-border)] bg-[var(--surface-ground)] p-4">
<p class="text-[0.65rem] font-bold uppercase tracking-widest text-[var(--text-color-secondary)] mb-3 opacity-70">Formação & família</p>
<div class="grid grid-cols-1 sm:grid-cols-2">
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Escolaridade</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">{{ dash(escolaridade) }}</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Profissão</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">{{ dash(profissao) }}</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Nome de um parente</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">{{ dash(nomeParente) }}</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Grau de parentesco</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">{{ dash(grauParentesco) }}</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Telefone do parente</span>
<a v-if="telefoneParente" :href="`tel:${telefoneParente}`" class="text-[0.8rem] font-medium text-right text-[var(--primary-color)] hover:underline">{{ fmtPhoneMobile(telefoneParente) }}</a>
<span v-else class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">—</span>
</div>
</div>
</div>
</div>
</AccordionContent>
</AccordionPanel>
<!-- ── 4. RESPONSÁVEL ────────────────────── -->
<AccordionPanel value="3">
<AccordionHeader :ref="(el) => setPanelHeaderRef(el, 3)">
<span class="flex items-center gap-2 text-[0.83rem] font-bold">
<i class="pi pi-users text-indigo-500 text-sm" />
4. Responsável
</span>
</AccordionHeader>
<AccordionContent>
<div class="pt-3">
<div class="rounded-xl border border-[var(--surface-border)] bg-[var(--surface-ground)] p-4">
<p class="text-[0.65rem] font-bold uppercase tracking-widest text-[var(--text-color-secondary)] mb-3 opacity-70">Dados do responsável</p>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Nome</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">{{ dash(nomeResponsavel) }}</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">CPF</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right font-mono">{{ fmtCPF(cpfResponsavel) }}</span>
</div>
<div class="flex items-baseline justify-between gap-4 py-1.5 border-b border-[var(--surface-border)]">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Telefone</span>
<a v-if="telefoneResponsavel" :href="`tel:${telefoneResponsavel}`" class="text-[0.8rem] font-medium text-right text-[var(--primary-color)] hover:underline">{{ fmtPhoneMobile(telefoneResponsavel) }}</a>
<span v-else class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">—</span>
</div>
<div v-if="observacaoResponsavel" class="pt-2">
<p class="text-[0.75rem] text-[var(--text-color-secondary)] mb-1">Observação</p>
<p class="text-[0.83rem] text-[var(--text-color)] leading-relaxed whitespace-pre-wrap">{{ observacaoResponsavel }}</p>
</div>
<div v-else class="flex items-baseline justify-between gap-4 py-1.5">
<span class="text-[0.8rem] text-[var(--text-color-secondary)] shrink-0">Observação</span>
<span class="text-[0.8rem] text-[var(--text-color)] font-medium text-right">—</span>
</div>
</div>
</div>
</AccordionContent>
</AccordionPanel>
<!-- ── 5. ANOTAÇÕES INTERNAS ─────────────── -->
<AccordionPanel value="4">
<AccordionHeader :ref="(el) => setPanelHeaderRef(el, 4)">
<span class="flex items-center gap-2 text-[0.83rem] font-bold">
<i class="pi pi-file-edit text-indigo-500 text-sm" />
5. Anotações Internas
</span>
</AccordionHeader>
<AccordionContent>
<div class="pt-3">
<div class="rounded-xl border border-[var(--surface-border)] bg-[var(--surface-ground)] p-4">
<div class="flex items-center gap-1.5 text-xs text-[var(--text-color-secondary)] opacity-70 mb-3">
<i class="pi pi-lock text-[0.7rem]" />
Campo interno: não aparece no cadastro externo.
</div>
<p class="text-[0.83rem] text-[var(--text-color)] leading-relaxed whitespace-pre-wrap min-h-[4rem]">
{{ dash(notasInternas) }}
</p>
</div>
</div>
</AccordionContent>
</AccordionPanel>
<!-- ── 6. SESSÕES ────────────────────────── -->
<AccordionPanel value="5">
<AccordionHeader :ref="(el) => setPanelHeaderRef(el, 5)">
<span class="flex items-center gap-2 text-[0.83rem] font-bold">
<i class="pi pi-calendar text-indigo-500 text-sm" />
6. Sessões
</span>
</AccordionHeader>
<AccordionContent>
<div class="pt-3">
<div v-if="sessionsLoading"
class="flex items-center gap-2 text-sm text-[var(--text-color-secondary)] py-4">
<i class="pi pi-spin pi-spinner" /> Carregando sessões…
</div>
<div v-else-if="!sessions.length"
class="text-sm text-[var(--text-color-secondary)] py-4 text-center">
Nenhuma sessão registrada para este paciente.
</div>
<div v-else class="flex flex-col gap-2">
<div
v-for="s in sessions" :key="s.id"
class="rounded-xl border border-[var(--surface-border)]
bg-[var(--surface-ground)] px-4 py-3
flex flex-col sm:flex-row sm:items-center
sm:justify-between gap-2"
>
<div class="flex flex-col gap-1">
<p class="text-[0.85rem] font-semibold text-[var(--text-color)]">
{{ s.titulo_custom || s.titulo || (s.tipo ? s.tipo : 'Sessão') }}
</p>
<div class="text-[0.76rem] text-[var(--text-color-secondary)] flex flex-wrap gap-x-3 gap-y-1">
<span><i class="pi pi-calendar mr-1 opacity-50" />{{ fmtDateTimeBR(s.inicio_em) }}</span>
<span v-if="sessionDuration(s.inicio_em, s.fim_em)">
<i class="pi pi-clock mr-1 opacity-50" />{{ sessionDuration(s.inicio_em, s.fim_em) }}
</span>
<span v-if="s.modalidade">
<i class="pi pi-video mr-1 opacity-50" />{{ s.modalidade === 'online' ? 'Online' : 'Presencial' }}
</span>
</div>
<p v-if="s.observacoes"
class="text-[0.76rem] text-[var(--text-color-secondary)] mt-0.5 line-clamp-2">
{{ s.observacoes }}
</p>
</div>
<Tag :value="STATUS_LABEL[s.status] || s.status || 'Agendado'"
:severity="STATUS_SEVERITY[s.status] || 'info'"
class="shrink-0" />
</div>
</div>
</div>
</AccordionContent>
</AccordionPanel>
</Accordion>
</div>
</div><!-- /aba perfil -->
<!-- ABAS PLACEHOLDER -->
<div v-show="activeTab === 1" class="p-10 text-center text-[var(--text-color-secondary)]">
<i class="pi pi-file-edit text-4xl mb-3 block opacity-30" /><p class="text-sm">Em breve</p>
</div>
<div v-show="activeTab === 2" class="p-10 text-center text-[var(--text-color-secondary)]">
<i class="pi pi-calendar text-4xl mb-3 block opacity-30" /><p class="text-sm">Em breve</p>
</div>
<div v-show="activeTab === 3" class="p-10 text-center text-[var(--text-color-secondary)]">
<i class="pi pi-wallet text-4xl mb-3 block opacity-30" /><p class="text-sm">Em breve</p>
</div>
<div v-show="activeTab === 4" class="py-2">
<DocumentsListPage
:patientId="patient?.id"
:patientName="patient?.nome_completo || patient?.nome_social || ''"
embedded
/>
</div>
<div v-show="activeTab === 5" class="p-2">
<PatientConversationsTab
:patient-id="patient?.id"
:patient-name="patient?.nome_completo || patient?.nome_social || ''"
/>
</div>
</main>
</div>
</div>
<!-- ── FOOTER ──────────────────────────────────────────── -->
<template #footer>
<div class="flex items-center justify-between gap-2 flex-wrap px-1">
<Button
v-if="activeTab === 0"
:label="allOpen ? 'Fechar seções' : 'Abrir seções'"
:icon="allOpen ? 'pi pi-angle-double-up' : 'pi pi-angle-double-down'"
severity="secondary" outlined class="rounded-full"
@click="toggleAllAccordions"
/>
<div v-else />
<div class="flex items-center gap-2">
<Button label="Copiar resumo" icon="pi pi-copy"
severity="secondary" outlined class="rounded-full"
@click="copyResumo" />
<Button label="Editar paciente" icon="pi pi-pencil"
severity="secondary" outlined class="rounded-full"
@click="editPatient" />
<Button label="Fechar" icon="pi pi-times"
severity="secondary" class="rounded-full"
@click="close" />
</div>
</div>
</template>
</Dialog>
</template>
<style scoped>
:deep(.accordion-clean .p-accordionpanel) {
border-radius: 0.75rem;
margin-bottom: 0.5rem;
border: 1px solid var(--surface-border);
overflow: hidden;
}
:deep(.accordion-clean .p-accordionpanel:last-child) { margin-bottom: 0; }
:deep(.accordion-clean .p-accordionheader) {
background: var(--surface-ground);
border: none;
padding: 0.875rem 1rem;
}
:deep(.accordion-clean .p-accordionheader:hover) {
background: var(--surface-hover, #f1f5f9);
}
:deep(.accordion-clean .p-accordioncontent-content) {
padding: 0 1rem 1rem;
border-top: 1px solid var(--surface-border);
}
.scrollbar-none::-webkit-scrollbar { display: none; }
.scrollbar-none { -ms-overflow-style: none; scrollbar-width: none; }
</style>