Files
agenciapsilmno/src/layout/melissa/MelissaPaciente.vue
T
Leonardo 2dae4a11ae roadmap #11: recently viewed (ultimos 5 pacientes acessados)
ROADMAP item #1.3 #11. localStorage por user_id pra isolar sessoes
diferentes no mesmo browser. ROADMAP sugeria localStorage OU tabela
user_recent_access — escolhi localStorage por simplicidade (sem
migration adicional + zero round-trip por visita).

composables/useRecentPatients.js:
- useRecentPatients() — composable reativo Tipo A: items + hasItems
  + addVisit + remove + clear + refresh
- registerPatientVisit(patient) — helper stateless pra usar fora
  de setup (ex: navigation guards, action handlers)
- Sincroniza entre instancias na mesma aba via CustomEvent + 'storage'
- Max 5 items. Dedup por id, novo no topo.

Wire-up de visita (registra ao carregar prontuario):
- MelissaPaciente.vue: registerPatientVisit no loadAll apos detail.load
- PatientProntuario.vue: registerPatientVisit em loadDetail apos p resolved

Wire-up de visualizacao (mostra quando query vazia):
- GlobalSearch.vue: grupo "Acessados recentemente" antes dos Atalhos.
  goTo("recent") navega pra /therapist/patients/:id.
- MelissaBusca.vue: grupo "Acessados recentemente". emit('paciente')
  reusando a logica do MelissaLayout que ja navega pra
  /melissa/paciente?id=X.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 05:17:51 -03:00

4411 lines
185 KiB
Vue

<script setup>
/*
* MelissaPaciente — Pagina nativa Melissa pra Prontuario do Paciente.
*
* FASE 1 (foundation): chrome glass + drawer mobile + sidebar (avatar +
* acoes + nav 7 tabs) + main com 7 abas placeholders. Os composables
* de dados (usePatientDetail/Sessions/Financial/Messages/Documents) ja
* carregam o paciente quando :patientId muda.
*
* Fase 1 NAO substitui ainda o PatientProntuario.vue legado (3593L) —
* os 4 callsites continuam funcionando. Vai sendo migrado nas fases
* 2-8 (cada tab uma sessao dedicada).
*
* Layout:
* - sidebar (320px, ESQUERDA, drawer no mobile) — header avatar +
* acoes rapidas + nav 7 tabs + sub-nav perfil
* - main (flex 1) — tab content
*
* Prefixo CSS: .mpa-* (Melissa PAciente).
*/
import { ref, computed, watch, nextTick, inject, onMounted, onBeforeUnmount } from 'vue';
import { useRouter } from 'vue-router';
import { useToast } from 'primevue/usetoast';
import { useConversationDrawerStore } from '@/stores/conversationDrawerStore';
import { MELISSA_AGENDA_KEY } from './composables/useMelissaAgenda';
import AgendaEventDialog from '@/features/agenda/components/AgendaEventDialog.vue';
import DocumentsListPage from '@/features/documents/DocumentsListPage.vue';
import PatientConversationsTab from '@/features/patients/prontuario/PatientConversationsTab.vue';
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue';
import { usePatientDetail } from '@/features/patients/composables/usePatientDetail';
import { registerPatientVisit } from '@/composables/useRecentPatients';
import { usePatientSessions } from '@/features/patients/composables/usePatientSessions';
import { usePatientFinancial } from '@/features/patients/composables/usePatientFinancial';
import { usePatientMessages } from '@/features/patients/composables/usePatientMessages';
import { usePatientDocuments } from '@/features/patients/composables/usePatientDocuments';
import { usePatientRecurrences } from '@/features/patients/composables/usePatientRecurrences';
import {
pickField,
calcAge,
fmtRelative,
fmtDateBR,
fmtDateTimeBR,
fmtCurrency,
fmtHourShort,
fmtDayShort,
fmtRecurrenceLabel,
fmtRecurrenceFim,
fmtCPF,
fmtRG,
fmtGender,
fmtMarital,
fmtPhoneMobile,
sessionDuration,
chConvLabel,
recordStatus,
RECORD_STATUS_LABEL,
fmtPaymentMethod,
STATUS_LABEL,
STATUS_SEVERITY,
tagStyle as tagStyleHelper
} from '@/features/patients/utils/patientFormatters';
// Tag/Skeleton: auto via PrimeVueResolver
const props = defineProps({
patientId: { type: String, default: '' }
});
const emit = defineEmits(['close', 'edit', 'add-financial', 'open-whatsapp']);
const router = useRouter();
const toast = useToast();
const conversationDrawerStore = useConversationDrawerStore();
// Inject do MelissaLayout — da acesso ao AgendaEventDialog global,
// suas mutations (onCreateEventoForPatient) e o flag dialogOpen pra
// detectar fechamento e refetchar sessions/recorrencias.
const melissaAgenda = inject(MELISSA_AGENDA_KEY, null);
// ── Composables de dados ───────────────────────────────────
const detail = usePatientDetail();
const sessionsHook = usePatientSessions();
const financialHook = usePatientFinancial();
const messagesHook = usePatientMessages();
const documentsHook = usePatientDocuments();
const recorrenciasHook = usePatientRecurrences();
// ── Breakpoints + drawer ───────────────────────────────────
const drawerOpen = ref(false);
const isMobile = ref(false);
let _mqMobile = null;
function _onMqMobileChange(e) {
isMobile.value = e.matches;
if (!e.matches) drawerOpen.value = false;
}
function toggleDrawer() { drawerOpen.value = !drawerOpen.value; }
function fecharDrawer() { drawerOpen.value = false; }
// ── Tabs ───────────────────────────────────────────────────
const TABS = [
{ key: 'overview', label: 'Visão Geral', icon: 'pi pi-chart-line', color: '#a855f7' },
{ key: 'perfil', label: 'Perfil', icon: 'pi pi-user', color: '#3b82f6' },
{ key: 'pron', label: 'Prontuário', icon: 'pi pi-file-edit', color: '#06b6d4' },
{ key: 'agenda', label: 'Agenda', icon: 'pi pi-calendar', color: '#10b981' },
{ key: 'financ', label: 'Financeiro', icon: 'pi pi-wallet', color: '#f59e0b' },
{ key: 'doc', label: 'Documentos', icon: 'pi pi-folder', color: '#f97316' },
{ key: 'conv', label: 'Conversas', icon: 'pi pi-whatsapp', color: '#22c55e' }
];
const activeTab = ref('overview');
function selectTab(key) {
activeTab.value = key;
if (isMobile.value) drawerOpen.value = false;
}
// Filtros da aba Prontuario (MVP — usa session.observacoes como evolucao)
const pronFilter = ref('com-evolucao'); // com-evolucao | todas | realiz | falt | cancel
const PRON_FILTERS = [
{ value: 'com-evolucao', label: 'Com evolução', icon: 'pi pi-file-edit' },
{ value: 'todas', label: 'Todas', icon: 'pi pi-list' },
{ value: 'realiz', label: 'Realizadas', icon: 'pi pi-check-circle' },
{ value: 'falt', label: 'Faltas', icon: 'pi pi-user-minus' },
{ value: 'cancel', label: 'Cancelamentos', icon: 'pi pi-ban' }
];
// Filtros da aba Agenda (lista cronologica + agrupamento por mes)
const agendaFilter = ref('all'); // all | future | past | realiz | falt | cancel
const AGENDA_FILTERS = [
{ value: 'all', label: 'Todas', icon: 'pi pi-list' },
{ value: 'future', label: 'Próximas', icon: 'pi pi-calendar-plus' },
{ value: 'past', label: 'Passadas', icon: 'pi pi-history' },
{ value: 'realiz', label: 'Realizadas', icon: 'pi pi-check-circle' },
{ value: 'falt', label: 'Faltas', icon: 'pi pi-user-minus' },
{ value: 'cancel', label: 'Canceladas', icon: 'pi pi-ban' }
];
// Sub-nav da aba Perfil
const PROFILE_SECTIONS = [
{ key: 'pessoais', label: 'Informações Pessoais', icon: 'pi pi-pencil' },
{ key: 'endereco', label: 'Endereço', icon: 'pi pi-map-marker' },
{ key: 'adicional', label: 'Dados Adicionais', icon: 'pi pi-tags' },
{ key: 'resp', label: 'Responsável', icon: 'pi pi-users' },
{ key: 'anot', label: 'Anotações', icon: 'pi pi-file-edit' },
{ 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) {
const s = String(v ?? '').trim();
return s || '—';
}
const patientData = computed(() => detail.patient.value || {});
const nomeCompleto = computed(() => patientData.value?.nome_completo || patientData.value?.nome || 'Paciente');
const avatarUrl = computed(() =>
patientData.value?.avatar_url || patientData.value?.avatar || null
);
const avatarInitials = computed(() => {
const parts = String(nomeCompleto.value || '').trim().split(/\s+/).filter(Boolean);
if (!parts.length) return '·';
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
});
const ageLabel = computed(() => {
const age = calcAge(patientData.value?.data_nascimento);
return age == null ? '' : `${age} anos`;
});
const convenio = computed(() => patientData.value?.convenio || patientData.value?.plano_saude || '');
const statusPaciente = computed(() => patientData.value?.status || '');
const riscoElevado = computed(() => !!patientData.value?.risco_elevado);
function tagStyle(t) {
return tagStyleHelper(t);
}
// ── 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');
// ── Tab Agenda: bloco recorrencias do paciente ─────────────
const recorrenciasShowCanc = ref(false);
const recorrenciasVisiveis = computed(() =>
recorrenciasShowCanc.value ? recorrenciasHook.rules.value : recorrenciasHook.ativas.value
);
async function onCancelRecurrence(rule) {
const result = await recorrenciasHook.cancel(rule.id);
if (result.ok) {
toast.add({ severity: 'success', summary: 'Recorrência cancelada', life: 2200 });
} else {
toast.add({
severity: 'error',
summary: 'Falha ao cancelar',
detail: result.error || 'Erro inesperado',
life: 4000
});
}
}
async function onReactivateRecurrence(rule) {
const result = await recorrenciasHook.reactivate(rule.id);
if (result.ok) {
toast.add({ severity: 'success', summary: 'Recorrência reativada', life: 2200 });
} else {
toast.add({
severity: 'error',
summary: 'Falha ao reativar',
detail: result.error || 'Erro inesperado',
life: 4000
});
}
}
// ── Tab Agenda: filtros + agrupamento por mes ──────────────
const agendaSessoesFiltradas = computed(() => {
const list = sessionsHook.sessions.value;
const now = Date.now();
switch (agendaFilter.value) {
case 'future': return list.filter((s) => s.inicio_em && new Date(s.inicio_em).getTime() > now);
case 'past': return list.filter((s) => s.inicio_em && new Date(s.inicio_em).getTime() <= now);
case 'realiz': return list.filter((s) => /realiz|present/i.test(String(s.status || '')));
case 'falt': return list.filter((s) => /falt/i.test(String(s.status || ''))) ;
case 'cancel': return list.filter((s) => /cancel|remarca/i.test(String(s.status || '')));
default: return list;
}
});
// Agrupa por "Mes de YYYY" mantendo ordem DESC (mais recente primeiro)
const agendaAgrupadas = computed(() => {
const groups = [];
let key = null;
for (const ev of agendaSessoesFiltradas.value) {
if (!ev.inicio_em) continue;
const d = new Date(ev.inicio_em);
const k = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
if (k !== key) {
key = k;
groups.push({
key: k,
label: d.toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' })
.replace(/(^|\s)\S/g, (l) => l.toUpperCase()),
items: []
});
}
groups[groups.length - 1].items.push(ev);
}
return groups;
});
// Handler de mutacao financeira: marcar pago / reverter
async function markRecordPaid(record) {
const result = await financialHook.markPaid(record.id);
if (result.ok) {
toast.add({ severity: 'success', summary: 'Lançamento marcado como pago', life: 2200 });
} else {
toast.add({
severity: 'error',
summary: 'Falha ao marcar pago',
detail: result.error || 'Erro inesperado',
life: 4000
});
}
}
async function revertRecordPaid(record) {
const result = await financialHook.markUnpaid(record.id);
if (result.ok) {
toast.add({ severity: 'success', summary: 'Pagamento revertido', life: 2200 });
} else {
toast.add({
severity: 'error',
summary: 'Falha ao reverter',
detail: result.error || 'Erro inesperado',
life: 4000
});
}
}
// Handler de mutacao de status (Realizada / Falta / Cancelar)
// Passa a row inteira pro composable porque pode ser ocorrência virtual de
// recorrência (id `rec::ruleId::date`) — nesse caso o composable materializa
// uma linha real antes de aplicar o status (UPDATE em id virtual quebra).
async function updateSessionStatus(ev, novoStatus, msg) {
const result = await sessionsHook.updateStatus(ev, novoStatus);
if (result.ok) {
toast.add({ severity: 'success', summary: msg, life: 2200 });
} else {
toast.add({
severity: 'error',
summary: 'Falha ao atualizar',
detail: result.error || 'Erro inesperado',
life: 4000
});
}
}
// ── Tab Prontuario: lista filtrada de sessoes ──────────────
// MVP enquanto anamnese/evolucao_clinica nao existem no schema:
// usa agenda_eventos.observacoes como nota evolutiva.
const pronSessions = computed(() => {
const all = sessionsHook.sessions.value;
if (pronFilter.value === 'todas') return all;
if (pronFilter.value === 'com-evolucao') {
return all.filter((s) => s.observacoes && String(s.observacoes).trim());
}
if (pronFilter.value === 'realiz') {
return all.filter((s) => /realiz|present/i.test(String(s.status || '')));
}
if (pronFilter.value === 'falt') {
return all.filter((s) => /falt/i.test(String(s.status || '')));
}
if (pronFilter.value === 'cancel') {
return all.filter((s) => /cancel|remarca/i.test(String(s.status || '')));
}
return all;
});
const pronSessionsCount = computed(() => pronSessions.value.length);
const sessoesComEvolucao = computed(() =>
sessionsHook.sessions.value.filter((s) => s.observacoes && String(s.observacoes).trim()).length
);
// ── KPIs Visao Geral (Fase 2) ──────────────────────────────
const kpiSessoes = computed(() => sessionsHook.totalSessoes.value);
const kpiRealizadas = computed(() => sessionsHook.totalRealizadas.value);
const kpiMensagens = computed(() => messagesHook.messages.value.length);
// ── Acoes ──────────────────────────────────────────────────
// X (close): volta de onde veio (Agenda OU Pacientes) via history.
// Fallback pra /melissa/pacientes se nao tem history (deep-link direto).
function close() {
emit('close');
if (window.history.length > 1) {
router.back();
} else {
router.push('/melissa/pacientes');
}
}
// Botao dedicado: navega EXPLICITAMENTE pra lista de pacientes.
function goToPacientes() {
router.push('/melissa/pacientes');
}
// Edit: abre o PatientCadastroDialog INLINE (sem sair do prontuario).
// Antes redirecionava pra /melissa/pacientes?edit=X mas isso saia da
// tela do prontuario. Agora o dialog abre por cima e ao salvar
// recarrega os dados do paciente in-place.
const cadastroOpen = ref(false);
const cadastroPatientId = ref(null);
function editPatient() {
emit('edit', props.patientId);
cadastroPatientId.value = String(props.patientId || '');
cadastroOpen.value = true;
}
async function onPatientSaved() {
cadastroOpen.value = false;
if (props.patientId) {
await detail.load(props.patientId);
}
}
// Open WhatsApp: usa o conversationDrawerStore global (mesmo pattern
// que MelissaPacientes — passa STRING id, nao objeto). A store cuida
// de buscar/criar a thread + telefone do paciente. Se nao tem telefone
// cadastrado, store.error eh setado e mostramos toast warn.
async function openWhatsapp() {
emit('open-whatsapp', props.patientId);
if (!props.patientId) {
toast.add({ severity: 'warn', summary: 'Paciente sem ID', life: 2500 });
return;
}
try {
await conversationDrawerStore.openForPatient(String(props.patientId));
// A store seta this.error sem dar throw quando paciente nao tem
// telefone — drawer simplesmente nao abre. Detectamos aqui.
const err = conversationDrawerStore.error;
if (err && !conversationDrawerStore.isOpen) {
toast.add({
severity: 'warn',
summary: 'Não foi possível abrir',
detail: err.message || 'Verifique o telefone do paciente.',
life: 4000
});
}
} catch (e) {
toast.add({
severity: 'error',
summary: 'Falha ao abrir conversa',
detail: e?.message || 'Erro ao abrir o drawer.',
life: 3500
});
}
}
// Add financial: abre dialog inline pra criar lancamento manual.
const novoLancOpen = ref(false);
const novoLancForm = ref({
description: '',
amount: null,
due_date: '',
payment_method: ''
});
function addFinancial() {
emit('add-financial', props.patientId);
if (!props.patientId) {
toast.add({ severity: 'warn', summary: 'Paciente sem ID', life: 2500 });
return;
}
// Navega pra aba Financeiro pra dar contexto antes do dialog abrir.
activeTab.value = 'financ';
if (isMobile.value) drawerOpen.value = false;
novoLancForm.value = {
description: '',
amount: null,
due_date: new Date().toISOString().slice(0, 10),
payment_method: ''
};
novoLancOpen.value = true;
}
// Abre AgendaEventDialog LOCAL (instancia propria) com paciente fixo
// e tipo='sessao' travado. Dados pesados (commitmentOptions, workRules,
// allEvents, agendaSettings, feriados, ownerId, tenantId) vem via inject
// do MelissaLayout. State do dialog (open/eventRow/start/end) e LOCAL
// pra nao colidir com o dialog global da Agenda.
const sessaoDialogOpen = ref(false);
const sessaoDialogEventRow = ref(null);
const sessaoDialogStartISO = ref(null);
const sessaoDialogEndISO = ref(null);
function goAgendar() {
activeTab.value = 'agenda';
if (isMobile.value) drawerOpen.value = false;
if (!props.patientId) {
toast.add({ severity: 'warn', summary: 'Paciente sem ID', life: 2500 });
return;
}
if (!melissaAgenda?.ownerId?.value) {
toast.add({
severity: 'warn',
summary: 'Agenda indisponível',
detail: 'Aguarde a agenda carregar e tente novamente.',
life: 3000
});
return;
}
// Defaults razoaveis: proximo slot 15min + duracao padrao da agenda.
const durMin =
melissaAgenda.settings?.value?.session_duration_min ??
melissaAgenda.settings?.value?.duracao_padrao_minutos ??
50;
const base = new Date();
base.setSeconds(0, 0);
const remainder = base.getMinutes() % 15;
if (remainder !== 0) base.setMinutes(base.getMinutes() + (15 - remainder));
// Acha o commitment "Sessão" (native_key='session') pra pre-popular o
// determined_commitment_id no eventRow — assim o resetForm do composer
// ja inicia com form.commitment_id setado, isSessionEvent=true e os
// blocos de jornada/billing/frequencia aparecem.
const sessaoCommitment = (melissaAgenda.commitmentOptions?.value || [])
.find((c) => c.native_key === 'session');
// Pre-popula tambem nome/avatar/status do paciente — sem isso o resumo
// lateral do dialog mostra "Paciente vinculado" sem nome (ja que o
// composer so faz fetch async de nome quando isEdit=true).
sessaoDialogEventRow.value = {
owner_id: melissaAgenda.ownerId.value,
terapeuta_id: null,
paciente_id: String(props.patientId),
paciente_nome: nomeCompleto.value || '',
paciente_avatar: avatarUrl.value || '',
paciente_status: statusPaciente.value || 'Ativo',
determined_commitment_id: sessaoCommitment?.id || null,
tipo: 'sessao',
status: 'agendado',
titulo: null,
observacoes: null,
visibility_scope: 'public'
};
sessaoDialogStartISO.value = base.toISOString();
sessaoDialogEndISO.value = new Date(base.getTime() + durMin * 60000).toISOString();
sessaoDialogOpen.value = true;
}
// Handlers do dialog local — delegam pros handlers globais do useMelissaAgenda
// (M.onDialogSave/Delete) que ja sabem mexer com agenda_eventos +
// recurrence_rules + exceptions. Apos fechar, refetcha sessions+recorrencias.
async function onSessaoDialogSave(payload) {
if (typeof melissaAgenda?.onDialogSave === 'function') {
await melissaAgenda.onDialogSave(payload);
}
sessaoDialogOpen.value = false;
if (props.patientId) {
await Promise.all([
sessionsHook.load(props.patientId),
recorrenciasHook.load(props.patientId)
]);
}
}
// Mudança de status numa ocorrência (cancelado/remarcado/etc) — delega pro
// handler do composable que SABE materializar ocorrência virtual antes de
// aplicar o status. Sem isso o UPDATE em id virtual quebra ("invalid input
// syntax for type uuid"). Espelha o wire-up de MelissaLayout/AgendaTerapeuta.
async function onSessaoDialogUpdateSeries(payload) {
if (typeof melissaAgenda?.onUpdateSeriesEvent === 'function') {
await melissaAgenda.onUpdateSeriesEvent(payload);
}
if (props.patientId) {
await Promise.all([
sessionsHook.load(props.patientId),
recorrenciasHook.load(props.patientId)
]);
}
}
async function onSessaoDialogDelete(payload) {
if (typeof melissaAgenda?.onDialogDelete === 'function') {
await melissaAgenda.onDialogDelete(payload);
}
sessaoDialogOpen.value = false;
if (props.patientId) {
await Promise.all([
sessionsHook.load(props.patientId),
recorrenciasHook.load(props.patientId)
]);
}
}
async function salvarLancamento() {
const f = novoLancForm.value;
if (!f.amount || Number(f.amount) <= 0) {
toast.add({ severity: 'warn', summary: 'Valor inválido', detail: 'Informe um valor maior que zero.', life: 3000 });
return;
}
if (!f.due_date) {
toast.add({ severity: 'warn', summary: 'Vencimento obrigatório', life: 2500 });
return;
}
const result = await financialHook.createRecord(props.patientId, {
description: f.description,
amount: Number(f.amount),
due_date: f.due_date,
payment_method: f.payment_method || null
});
if (result.ok) {
toast.add({ severity: 'success', summary: 'Lançamento criado', life: 2200 });
novoLancOpen.value = false;
} else {
toast.add({
severity: 'error',
summary: 'Falha ao criar',
detail: result.error || 'Erro inesperado',
life: 4000
});
}
}
const PAYMENT_METHODS = [
{ label: 'Pix', value: 'pix' },
{ label: 'Cartão', value: 'cartao' },
{ label: 'Dinheiro', value: 'dinheiro' },
{ label: 'Transferência', value: 'transferencia' },
{ label: 'Boleto', value: 'boleto' },
{ label: 'Convênio', value: 'convenio' }
];
// ── Load data quando patientId muda ────────────────────────
async function loadAll(id) {
if (!id) return;
await Promise.all([
detail.load(id),
sessionsHook.load(id),
financialHook.load(id),
messagesHook.load(id),
documentsHook.load(id),
recorrenciasHook.load(id)
]);
// Registra visita no histórico "recentemente acessados" (localStorage).
// Fora do Promise.all pra não bloquear renderização.
const p = detail.patient?.value;
if (p?.id) {
try { await registerPatientVisit(p); } catch { /* ignore */ }
}
}
watch(() => props.patientId, async (id) => {
activeTab.value = 'overview';
activeProfileSection.value = 'pessoais';
await loadAll(id);
}, { immediate: true });
// ── Lifecycle (matchMedia) ─────────────────────────────────
onMounted(() => {
if (typeof window !== 'undefined' && window.matchMedia) {
_mqMobile = window.matchMedia('(max-width: 1023px)');
isMobile.value = _mqMobile.matches;
try { _mqMobile.addEventListener('change', _onMqMobileChange); }
catch { _mqMobile.addListener(_onMqMobileChange); }
}
});
onBeforeUnmount(() => {
if (_mqMobile) {
try { _mqMobile.removeEventListener('change', _onMqMobileChange); }
catch { _mqMobile.removeListener(_onMqMobileChange); }
}
});
</script>
<template>
<Transition name="mpa-drawer-fade">
<div
v-show="isMobile && drawerOpen"
class="mpa-mobile-drawer"
:class="{ 'is-open': drawerOpen }"
>
<div id="mpa-mobile-drawer-target" class="mpa-mobile-drawer__scroll" />
</div>
</Transition>
<Transition name="mpa-drawer-fade">
<div
v-show="isMobile && drawerOpen"
class="mpa-mobile-drawer__backdrop"
@click="fecharDrawer"
/>
</Transition>
<section class="mpa-page">
<!-- Header -->
<header class="mpa-page__head">
<button
class="mpa-menu-btn mpa-menu-btn--mobile-only"
v-tooltip.bottom="'Ações & Navegação'"
@click="toggleDrawer"
>
<i class="pi pi-bars" />
<span>Menu</span>
</button>
<div class="mpa-page__title">
<div v-if="avatarUrl" class="mpa-head__avatar">
<img :src="avatarUrl" alt="" />
</div>
<div v-else class="mpa-head__avatar mpa-head__avatar--initials">
<span>{{ avatarInitials }}</span>
</div>
<div class="mpa-head__id">
<div class="mpa-head__name">{{ dash(nomeCompleto) }}</div>
<div class="mpa-head__meta">
<span v-if="ageLabel">{{ ageLabel }}</span>
<span v-if="patientData.pronomes" class="mpa-head__sep">·</span>
<span v-if="patientData.pronomes">{{ patientData.pronomes }}</span>
<Tag v-if="statusPaciente" :value="statusPaciente" severity="success" class="mpa-head__tag" />
<Tag v-if="convenio" :value="convenio" severity="info" class="mpa-head__tag" />
<span v-if="riscoElevado" class="mpa-head__risk">
<i class="pi pi-exclamation-circle" />
risco
</span>
</div>
</div>
</div>
<div class="mpa-page__actions">
<button class="mpa-act-btn" v-tooltip.bottom="'Conversar via WhatsApp'" @click="openWhatsapp">
<i class="pi pi-whatsapp" />
</button>
<button class="mpa-act-btn" v-tooltip.bottom="'Editar paciente'" @click="editPatient">
<i class="pi pi-pencil" />
</button>
<button class="mpa-close" v-tooltip.bottom="'Fechar (Esc)'" @click="close">
<i class="pi pi-times" />
</button>
</div>
</header>
<!-- Banner de risco elevado (subheader) -->
<div v-if="riscoElevado" class="mpa-subheader mpa-subheader--risk">
<i class="pi pi-exclamation-triangle mpa-subheader__icon" />
<span class="mpa-subheader__text">
<strong>Risco elevado sinalizado</strong> mantenha atenção redobrada na próxima sessão.
</span>
</div>
<!-- Body 2-col -->
<div class="mpa-body">
<!-- COL 1: Sidebar esquerda -->
<Teleport to="#mpa-mobile-drawer-target" :disabled="!isMobile">
<aside class="mpa-side">
<!-- Voltar pra Pacientes substitui o botao de Configuracoes
(prontuario pertence a Pacientes, nao a Configuracoes). -->
<button class="mpa-cfg-btn mpa-cfg-btn--back" @click="goToPacientes">
<i class="pi pi-arrow-left" />
<span>Voltar para Pacientes</span>
</button>
<div class="mpa-side__scroll">
<!-- Card: Acoes rapidas -->
<div class="mpa-w mpa-w--side">
<div class="mpa-w__head">
<div class="mpa-w__icon"><i class="pi pi-bolt" /></div>
<div class="mpa-w__title">
<div class="mpa-w__title-text">Ações rápidas</div>
<div class="mpa-w__sub">Atalhos pra sessão atual</div>
</div>
</div>
<div class="mpa-w__body">
<button type="button" class="mpa-quick-btn" @click="openWhatsapp">
<i class="pi pi-whatsapp" :style="{ color: '#22c55e' }" />
<span>Conversar</span>
</button>
<button type="button" class="mpa-quick-btn" @click="editPatient">
<i class="pi pi-pencil" :style="{ color: '#3b82f6' }" />
<span>Editar dados</span>
</button>
<button type="button" class="mpa-quick-btn" @click="addFinancial">
<i class="pi pi-plus" :style="{ color: '#f59e0b' }" />
<span>Lançamento</span>
</button>
<button type="button" class="mpa-quick-btn" @click="goAgendar">
<i class="pi pi-calendar-plus" :style="{ color: '#10b981' }" />
<span>Agendar</span>
</button>
</div>
</div>
<!-- Card: Navegacao (tabs) -->
<div class="mpa-w mpa-w--side">
<div class="mpa-w__head">
<div class="mpa-w__icon"><i class="pi pi-list" /></div>
<div class="mpa-w__title">
<div class="mpa-w__title-text">Navegação</div>
<div class="mpa-w__sub">Click pra trocar de aba</div>
</div>
</div>
<div class="mpa-w__body">
<button
v-for="t in TABS"
:key="t.key"
type="button"
class="mpa-tab-btn"
:class="{ 'is-active': activeTab === t.key }"
@click="selectTab(t.key)"
>
<div
class="mpa-tab-btn__icon"
:style="{
background: t.color + '22',
color: t.color
}"
>
<i :class="t.icon" />
</div>
<span class="mpa-tab-btn__label">{{ t.label }}</span>
<i class="pi pi-chevron-right mpa-tab-btn__chev" />
</button>
</div>
</div>
<!-- Sub-nav Perfil (so quando aba Perfil ativa, desktop) -->
<div v-if="activeTab === 'perfil'" class="mpa-w mpa-w--side">
<div class="mpa-w__head">
<div class="mpa-w__icon"><i class="pi pi-user" /></div>
<div class="mpa-w__title">
<div class="mpa-w__title-text">Seções do perfil</div>
<div class="mpa-w__sub">Acordeão</div>
</div>
</div>
<div class="mpa-w__body">
<button
v-for="s in PROFILE_SECTIONS"
:key="s.key"
type="button"
class="mpa-tab-btn mpa-tab-btn--sub"
:class="{ 'is-active': 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>
</button>
</div>
</div>
<!-- Tags + grupos (preview compacto) -->
<div v-if="detail.groups.value.length || detail.tags.value.length" class="mpa-w mpa-w--side">
<div class="mpa-w__head">
<div class="mpa-w__icon"><i class="pi pi-tag" /></div>
<div class="mpa-w__title">
<div class="mpa-w__title-text">Vínculos</div>
<div class="mpa-w__sub">Grupos e tags</div>
</div>
</div>
<div class="mpa-w__body mpa-w__body--chips">
<span v-for="g in detail.groups.value" :key="`g-${g.id}`" class="mpa-chip mpa-chip--group">
<i class="pi pi-th-large" />
{{ g.name }}
</span>
<span v-for="t in detail.tags.value" :key="`t-${t.id}`" class="mpa-chip" :style="tagStyle(t)">
{{ t.name }}
</span>
</div>
</div>
</div>
</aside>
</Teleport>
<!-- COL 2: Main tab content -->
<div class="mpa-main">
<!-- Loading -->
<template v-if="detail.loading.value">
<div class="mpa-w" v-for="n in 3" :key="`sk-${n}`">
<div class="mpa-w__body">
<Skeleton width="40%" height="20px" class="mb-3" />
<Skeleton v-for="m in 3" :key="`sk-${n}-${m}`" width="100%" height="36px" class="mb-2" />
</div>
</div>
</template>
<!-- Erro -->
<template v-else-if="detail.loadError.value">
<div class="mpa-error">
<i class="pi pi-exclamation-circle" />
<div>
<div class="mpa-error__title">Falha ao carregar</div>
<div class="mpa-error__detail">{{ detail.loadError.value }}</div>
</div>
</div>
</template>
<!-- Conteudo de cada aba -->
<template v-else>
<!-- ABA: Visao Geral (Fase 2 KPIs + Timeline + Mensagens + Notas) -->
<div v-if="activeTab === 'overview'" class="mpa-tab">
<!-- 4 KPI cards -->
<div class="mpa-kpis">
<!-- 01 Sessoes -->
<article class="mpa-kpi" style="--c:#4ade80">
<span class="mpa-kpi__num">01</span>
<header class="mpa-kpi__head">
<div class="mpa-kpi__icon"><i class="pi pi-check-circle" /></div>
<span class="mpa-kpi__tag">Sessões</span>
</header>
<div class="mpa-kpi__big">{{ kpiRealizadas }}</div>
<div class="mpa-kpi__cap">
de {{ kpiSessoes }} totais
<span v-if="sessionsHook.totalFaltas.value" class="mpa-kpi__cap-warn">
· {{ sessionsHook.totalFaltas.value }}
{{ sessionsHook.totalFaltas.value === 1 ? 'falta' : 'faltas' }}
</span>
<span v-if="sessionsHook.totalCanceladas.value" class="mpa-kpi__cap-dim">
· {{ sessionsHook.totalCanceladas.value }} cancel.
</span>
</div>
</article>
<!-- 02 Pagamento -->
<article
class="mpa-kpi"
:style="financialHook.statusFinanceiro.value.emDia === false
? '--c:#f87171'
: '--c:var(--p-primary-color)'"
>
<span class="mpa-kpi__num">02</span>
<header class="mpa-kpi__head">
<div class="mpa-kpi__icon"><i class="pi pi-wallet" /></div>
<span class="mpa-kpi__tag">Pagamento</span>
</header>
<template v-if="financialHook.loading.value">
<div class="mpa-kpi__big mpa-kpi__big--small"></div>
<div class="mpa-kpi__cap">Carregando</div>
</template>
<template v-else-if="!financialHook.records.value.length">
<div class="mpa-kpi__big mpa-kpi__big--small"></div>
<div class="mpa-kpi__cap">Sem lançamentos</div>
</template>
<template v-else>
<div class="mpa-kpi__big mpa-kpi__big--small">
{{ financialHook.statusFinanceiro.value.emDia === false ? 'Em atraso' : 'Em dia' }}
</div>
<div class="mpa-kpi__cap">
<template v-if="financialHook.statusFinanceiro.value.proxVenc">
Próx. venc. {{ fmtDateBR(financialHook.statusFinanceiro.value.proxVenc.due_date) }}
<span class="mpa-kpi__cap-dim">
· {{ fmtRelative(financialHook.statusFinanceiro.value.proxVenc.due_date) }}
</span>
</template>
<template v-else-if="financialHook.statusFinanceiro.value.totalPendente > 0">
{{ fmtCurrency(financialHook.statusFinanceiro.value.totalPendente) }} pendente
</template>
<template v-else>Tudo quitado</template>
</div>
</template>
</article>
<!-- 03 Proxima sessao -->
<article class="mpa-kpi" style="--c:#60a5fa">
<span class="mpa-kpi__num">03</span>
<header class="mpa-kpi__head">
<div class="mpa-kpi__icon"><i class="pi pi-calendar-clock" /></div>
<span class="mpa-kpi__tag">Próxima</span>
</header>
<template v-if="sessionsHook.proximaSessao.value">
<div class="mpa-kpi__big mpa-kpi__big--small">
{{ fmtRelative(sessionsHook.proximaSessao.value.inicio_em) }}
</div>
<div class="mpa-kpi__cap">
{{ fmtDateTimeBR(sessionsHook.proximaSessao.value.inicio_em) }}
<span v-if="sessionsHook.proximaSessao.value.modalidade" class="mpa-kpi__cap-dim">
· {{ sessionsHook.proximaSessao.value.modalidade === 'online' ? 'Online' : 'Presencial' }}
</span>
</div>
</template>
<template v-else>
<div class="mpa-kpi__big mpa-kpi__big--small"></div>
<div class="mpa-kpi__cap">Sem sessão futura agendada</div>
</template>
</article>
<!-- 04 Mensagens -->
<article class="mpa-kpi" style="--c:#34d399">
<span class="mpa-kpi__num">04</span>
<header class="mpa-kpi__head">
<div class="mpa-kpi__icon"><i class="pi pi-comments" /></div>
<span class="mpa-kpi__tag">Mensagens</span>
</header>
<template v-if="messagesHook.loading.value">
<div class="mpa-kpi__big mpa-kpi__big--small"></div>
<div class="mpa-kpi__cap">Carregando</div>
</template>
<template v-else-if="messagesHook.ultimaMensagem.value">
<div class="mpa-kpi__big mpa-kpi__big--small">
{{ fmtRelative(messagesHook.ultimaMensagem.value.created_at) }}
</div>
<div class="mpa-kpi__cap">
Última {{ messagesHook.ultimaMensagem.value.direction === 'inbound' ? 'recebida' : 'enviada' }}
<span class="mpa-kpi__cap-dim">· {{ kpiMensagens }} no histórico</span>
</div>
</template>
<template v-else>
<div class="mpa-kpi__big mpa-kpi__big--small"></div>
<div class="mpa-kpi__cap">Nenhuma conversa registrada</div>
</template>
</article>
</div>
<!-- Grid 2-col: Timeline + Mensagens/Notas -->
<div class="mpa-grid">
<!-- Timeline -->
<section class="mpa-panel">
<header class="mpa-panel__head">
<div class="mpa-panel__title"><i class="pi pi-history" /> Timeline de atendimentos</div>
<span class="mpa-panel__badge">{{ sessionsHook.ultimasAtendidas.value.length }}</span>
</header>
<div class="mpa-panel__body">
<div v-if="sessionsHook.loading.value" class="mpa-empty">Carregando</div>
<div v-else-if="!sessionsHook.ultimasAtendidas.value.length" class="mpa-empty mpa-empty--rich">
<div class="mpa-empty__icon"><i class="pi pi-history" /></div>
<div class="mpa-empty__title">Sem atendimentos registrados</div>
<div class="mpa-empty__sub">As sessões realizadas aparecerão aqui em ordem cronológica.</div>
</div>
<ol v-else class="mpa-timeline">
<li
v-for="s in sessionsHook.ultimasAtendidas.value"
:key="s.id"
class="mpa-tl"
:data-status="String(s.status || 'agendado').toLowerCase()"
>
<span class="mpa-tl__dot" />
<div class="mpa-tl__main">
<div class="mpa-tl__top">
<span class="mpa-tl__when">{{ fmtDateTimeBR(s.inicio_em) }}</span>
<span class="mpa-tl__rel">{{ fmtRelative(s.inicio_em) }}</span>
</div>
<div class="mpa-tl__row">
<Tag
:value="STATUS_LABEL[s.status] || s.status || '—'"
:severity="STATUS_SEVERITY[s.status] || 'info'"
class="mpa-tl__tag"
/>
<span v-if="s.modalidade" class="mpa-tl__chip">
<i :class="s.modalidade === 'online' ? 'pi pi-video' : 'pi pi-map-marker'" />
{{ s.modalidade === 'online' ? 'Online' : 'Presencial' }}
</span>
<span v-if="sessionDuration(s.inicio_em, s.fim_em)" class="mpa-tl__chip mpa-tl__chip--dim">
<i class="pi pi-clock" />
{{ sessionDuration(s.inicio_em, s.fim_em) }}
</span>
</div>
<p v-if="s.observacoes" class="mpa-tl__note">{{ s.observacoes }}</p>
</div>
</li>
</ol>
</div>
</section>
<!-- Coluna direita: Mensagens + Notas -->
<div class="mpa-grid__col">
<!-- Mensagens recentes -->
<section class="mpa-panel">
<header class="mpa-panel__head">
<div class="mpa-panel__title"><i class="pi pi-comments" /> Últimas mensagens</div>
<span class="mpa-panel__badge">{{ kpiMensagens }}</span>
</header>
<div class="mpa-panel__body">
<div v-if="messagesHook.loading.value" class="mpa-empty">Carregando</div>
<div v-else-if="!messagesHook.recentes.value.length" class="mpa-empty mpa-empty--rich">
<div class="mpa-empty__icon"><i class="pi pi-comments" /></div>
<div class="mpa-empty__title">Sem conversa registrada</div>
<div class="mpa-empty__sub">As últimas mensagens aparecerão aqui.</div>
</div>
<ul v-else class="mpa-msgs">
<li
v-for="m in messagesHook.recentes.value"
:key="m.id"
class="mpa-msg"
:class="m.direction === 'inbound' ? 'mpa-msg--in' : 'mpa-msg--out'"
>
<div class="mpa-msg__meta">
<i :class="m.direction === 'inbound' ? 'pi pi-arrow-down-left' : 'pi pi-arrow-up-right'" />
<span>{{ m.direction === 'inbound' ? 'Recebida' : 'Enviada' }}</span>
<span class="mpa-msg__sep">·</span>
<span>{{ fmtRelative(m.created_at) }}</span>
</div>
<p class="mpa-msg__body">{{ m.body || '—' }}</p>
</li>
</ul>
</div>
</section>
<!-- Notas e observacoes -->
<section v-if="notasInternas || observacoes" class="mpa-panel">
<header class="mpa-panel__head">
<div class="mpa-panel__title"><i class="pi pi-file-edit" /> Notas e observações</div>
</header>
<div class="mpa-panel__body mpa-notes">
<div v-if="observacoes" class="mpa-note">
<p class="mpa-note__label">Observações gerais</p>
<p class="mpa-note__text">{{ observacoes }}</p>
</div>
<div v-if="notasInternas" class="mpa-note">
<p class="mpa-note__label">
<i class="pi pi-lock" /> Internas
</p>
<p class="mpa-note__text">{{ notasInternas }}</p>
</div>
</div>
</section>
</div>
</div>
</div>
<!-- ABA: Perfil (Fase 3 6 sections stacked com anchors) -->
<div v-else-if="activeTab === 'perfil'" class="mpa-tab">
<!-- 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-pencil" /></div>
<div class="mpa-w__title">
<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">
<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 (Fase 4 MVP evolucao via session.observacoes) -->
<div v-else-if="activeTab === 'pron'" class="mpa-tab">
<!-- Header explicativo -->
<div class="mpa-pron-hint">
<i class="pi pi-info-circle" />
<div>
<strong>Prontuário em construção.</strong>
Por enquanto mostra as <strong>observações que você anota nas sessões</strong>
como histórico evolutivo. Anamnese estruturada, plano terapêutico e
evolução por temas chegam quando o módulo clínico for liberado.
</div>
</div>
<!-- Mini-stats -->
<div class="mpa-pron-stats">
<article class="mpa-pron-stat" style="--c:#06b6d4">
<i class="pi pi-file-edit" />
<div>
<div class="mpa-pron-stat__value">{{ sessoesComEvolucao }}</div>
<div class="mpa-pron-stat__label">com evolução</div>
</div>
</article>
<article class="mpa-pron-stat" style="--c:#10b981">
<i class="pi pi-check-circle" />
<div>
<div class="mpa-pron-stat__value">{{ sessionsHook.totalRealizadas.value }}</div>
<div class="mpa-pron-stat__label">realizadas</div>
</div>
</article>
<article class="mpa-pron-stat" style="--c:#f87171">
<i class="pi pi-user-minus" />
<div>
<div class="mpa-pron-stat__value">{{ sessionsHook.totalFaltas.value }}</div>
<div class="mpa-pron-stat__label">faltas</div>
</div>
</article>
<article class="mpa-pron-stat" style="--c:#94a3b8">
<i class="pi pi-calendar" />
<div>
<div class="mpa-pron-stat__value">{{ sessionsHook.totalSessoes.value }}</div>
<div class="mpa-pron-stat__label">total</div>
</div>
</article>
</div>
<!-- Filtros -->
<div class="mpa-pron-filters" role="tablist">
<button
v-for="f in PRON_FILTERS"
:key="f.value"
type="button"
role="tab"
:aria-selected="pronFilter === f.value"
class="mpa-pron-filter"
:class="{ 'is-active': pronFilter === f.value }"
@click="pronFilter = f.value"
>
<i :class="f.icon" />
<span>{{ f.label }}</span>
</button>
</div>
<!-- Lista de evolução -->
<div v-if="sessionsHook.loading.value" class="mpa-empty">
<i class="pi pi-spin pi-spinner mr-2" /> Carregando
</div>
<div v-else-if="!pronSessionsCount" class="mpa-empty mpa-empty--rich">
<div class="mpa-empty__icon"><i class="pi pi-file-edit" /></div>
<div class="mpa-empty__title">
<template v-if="!sessionsHook.sessions.value.length">
Sem sessões registradas
</template>
<template v-else-if="pronFilter === 'com-evolucao'">
Nenhuma sessão tem evolução escrita ainda
</template>
<template v-else>
Nenhuma sessão neste filtro
</template>
</div>
<div class="mpa-empty__sub">
<template v-if="pronFilter === 'com-evolucao' && sessionsHook.sessions.value.length">
Use o campo <strong>Observações</strong> ao editar uma sessão pra registrar
como ela transcorreu vai aparecer aqui como nota evolutiva.
</template>
<template v-else-if="!sessionsHook.sessions.value.length">
Quando você atender este paciente, as sessões e evoluções aparecerão aqui.
</template>
<template v-else>
Tente outro filtro acima ou veja "Todas" pra listar o histórico completo.
</template>
</div>
</div>
<div v-else class="mpa-pron-list">
<article
v-for="s in pronSessions"
:key="s.id"
class="mpa-pron-item"
:data-status="String(s.status || 'agendado').toLowerCase()"
>
<div class="mpa-pron-item__head">
<div class="mpa-pron-item__when">
<span class="mpa-pron-item__date">{{ fmtDateTimeBR(s.inicio_em) }}</span>
<span class="mpa-pron-item__rel">{{ fmtRelative(s.inicio_em) }}</span>
</div>
<div class="mpa-pron-item__chips">
<Tag
:value="STATUS_LABEL[s.status] || s.status || 'Agendado'"
:severity="STATUS_SEVERITY[s.status] || 'info'"
class="mpa-pron-item__tag"
/>
<span v-if="s.modalidade" class="mpa-tl__chip">
<i :class="s.modalidade === 'online' ? 'pi pi-video' : 'pi pi-map-marker'" />
{{ s.modalidade === 'online' ? 'Online' : 'Presencial' }}
</span>
<span v-if="sessionDuration(s.inicio_em, s.fim_em)" class="mpa-tl__chip mpa-tl__chip--dim">
<i class="pi pi-clock" /> {{ sessionDuration(s.inicio_em, s.fim_em) }}
</span>
</div>
</div>
<div v-if="s.titulo_custom || s.titulo" class="mpa-pron-item__title">
{{ s.titulo_custom || s.titulo }}
</div>
<div v-if="s.observacoes" class="mpa-pron-item__evol">
<div class="mpa-pron-item__evol-label">
<i class="pi pi-file-edit" />
Evolução
</div>
<p class="mpa-pron-item__evol-text">{{ s.observacoes }}</p>
</div>
<div v-else class="mpa-pron-item__noevol">
<i class="pi pi-circle" />
Sem evolução registrada
</div>
</article>
</div>
<!-- Roadmap card "em breve" -->
<section class="mpa-w mpa-pron-roadmap">
<div class="mpa-w__head">
<div class="mpa-w__icon mpa-w__icon--cyan"><i class="pi pi-sparkles" /></div>
<div class="mpa-w__title">
<div class="mpa-w__title-text">Em breve no prontuário</div>
<div class="mpa-w__sub">Roadmap clínico previsto</div>
</div>
</div>
<div class="mpa-w__body">
<ul class="mpa-pron-roadmap__list">
<li>
<i class="pi pi-clipboard" />
<div>
<strong>Anamnese estruturada</strong>
<span>Modelo configurável por terapeuta com seções (queixa, história, hipótese diagnóstica, objetivos).</span>
</div>
</li>
<li>
<i class="pi pi-bullseye" />
<div>
<strong>Plano terapêutico</strong>
<span>Objetivos com prazo + acompanhamento de progresso ao longo das sessões.</span>
</div>
</li>
<li>
<i class="pi pi-tag" />
<div>
<strong>Evolução por temas</strong>
<span>Tagging das notas pra cruzar evolução com objetivos e gerar relatórios.</span>
</div>
</li>
<li>
<i class="pi pi-shield" />
<div>
<strong>Assinatura digital + LGPD Art. 18</strong>
<span>Notas imutáveis com hash de auditoria, exportação compatível CFP.</span>
</div>
</li>
</ul>
</div>
</section>
</div>
<!-- ABA: Agenda (Fase 5 KPIs + filtros + grupos por mes + acoes) -->
<div v-else-if="activeTab === 'agenda'" class="mpa-tab">
<!-- Loading -->
<div v-if="sessionsHook.loading.value" class="mpa-empty">
<i class="pi pi-spin pi-spinner mr-2" /> Carregando
</div>
<template v-else>
<!-- KPIs (4) -->
<div class="mpa-kpis">
<article class="mpa-kpi" style="--c:var(--p-primary-color)">
<span class="mpa-kpi__num">01</span>
<header class="mpa-kpi__head">
<div class="mpa-kpi__icon"><i class="pi pi-calendar" /></div>
<span class="mpa-kpi__tag">Total</span>
</header>
<div class="mpa-kpi__big">{{ sessionsHook.totalSessoes.value }}</div>
<div class="mpa-kpi__cap">sessões registradas</div>
</article>
<article class="mpa-kpi" style="--c:#4ade80">
<span class="mpa-kpi__num">02</span>
<header class="mpa-kpi__head">
<div class="mpa-kpi__icon"><i class="pi pi-check-circle" /></div>
<span class="mpa-kpi__tag">Realizadas</span>
</header>
<div class="mpa-kpi__big">{{ sessionsHook.totalRealizadas.value }}</div>
<div class="mpa-kpi__cap">
<template v-if="sessionsHook.totalSessoes.value">
{{ Math.round((sessionsHook.totalRealizadas.value / sessionsHook.totalSessoes.value) * 100) }}% do total
</template>
<template v-else></template>
</div>
</article>
<article
class="mpa-kpi"
:style="sessionsHook.totalFaltas.value > 0 ? '--c:#f87171' : '--c:#94a3b8'"
>
<span class="mpa-kpi__num">03</span>
<header class="mpa-kpi__head">
<div class="mpa-kpi__icon"><i class="pi pi-user-minus" /></div>
<span class="mpa-kpi__tag">Faltas</span>
</header>
<div class="mpa-kpi__big">{{ sessionsHook.totalFaltas.value }}</div>
<div class="mpa-kpi__cap">
<template v-if="sessionsHook.totalCanceladas.value">
+ {{ sessionsHook.totalCanceladas.value }} cancel.
</template>
<template v-else>nenhuma falta</template>
</div>
</article>
<article class="mpa-kpi" style="--c:#60a5fa">
<span class="mpa-kpi__num">04</span>
<header class="mpa-kpi__head">
<div class="mpa-kpi__icon"><i class="pi pi-calendar-clock" /></div>
<span class="mpa-kpi__tag">Próxima</span>
</header>
<template v-if="sessionsHook.proximaSessao.value">
<div class="mpa-kpi__big mpa-kpi__big--small">
{{ fmtRelative(sessionsHook.proximaSessao.value.inicio_em) }}
</div>
<div class="mpa-kpi__cap">
{{ fmtDateBR(sessionsHook.proximaSessao.value.inicio_em) }}
· {{ fmtHourShort(sessionsHook.proximaSessao.value.inicio_em) }}
</div>
</template>
<template v-else>
<div class="mpa-kpi__big mpa-kpi__big--small"></div>
<div class="mpa-kpi__cap">Sem futura</div>
</template>
</article>
<article class="mpa-kpi" :style="recorrenciasHook.totalAtivas.value > 0 ? '--c:#a855f7' : '--c:#94a3b8'">
<span class="mpa-kpi__num">05</span>
<header class="mpa-kpi__head">
<div class="mpa-kpi__icon"><i class="pi pi-sync" /></div>
<span class="mpa-kpi__tag">Recorrências</span>
</header>
<div class="mpa-kpi__big">{{ recorrenciasHook.totalAtivas.value }}</div>
<div class="mpa-kpi__cap">
<template v-if="recorrenciasHook.totalAtivas.value === 0">
nenhuma série ativa
</template>
<template v-else>
{{ recorrenciasHook.totalAtivas.value === 1 ? 'série ativa' : 'séries ativas' }}
<span v-if="recorrenciasHook.totalCanceladas.value" class="mpa-kpi__cap-dim">
· {{ recorrenciasHook.totalCanceladas.value }} cancel.
</span>
</template>
</div>
</article>
</div>
<!-- Bloco: recorrencias do paciente (acima dos meses) -->
<section v-if="recorrenciasHook.rules.value.length" class="mpa-panel">
<header class="mpa-panel__head">
<div class="mpa-panel__title"><i class="pi pi-sync" /> Recorrências</div>
<div class="mpa-fin__head-actions">
<span class="mpa-panel__badge">{{ recorrenciasVisiveis.length }}</span>
<label
v-if="recorrenciasHook.totalCanceladas.value > 0"
class="mpa-recur-toggle"
v-tooltip.left="'Mostrar séries canceladas'"
>
<input v-model="recorrenciasShowCanc" type="checkbox" />
<span>Ver canceladas</span>
</label>
</div>
</header>
<ul class="mpa-recur-list">
<li
v-for="rule in recorrenciasVisiveis"
:key="rule.id"
class="mpa-recur-item"
:data-status="rule.status"
>
<div class="mpa-recur-item__icon">
<i class="pi pi-sync" />
</div>
<div class="mpa-recur-item__main">
<div class="mpa-recur-item__top">
<span class="mpa-recur-item__label">{{ fmtRecurrenceLabel(rule) }}</span>
<Tag
v-if="rule.status === 'cancelado'"
value="Cancelada"
severity="warn"
class="mpa-recur-item__tag"
/>
<Tag
v-else
value="Ativa"
severity="success"
class="mpa-recur-item__tag"
/>
</div>
<div class="mpa-recur-item__meta">
<span><i class="pi pi-clock" />{{ rule.duration_min }}min</span>
<span v-if="rule.modalidade">
<i :class="rule.modalidade === 'online' ? 'pi pi-video' : 'pi pi-map-marker'" />
{{ rule.modalidade === 'online' ? 'Online' : 'Presencial' }}
</span>
<span><i class="pi pi-flag-fill" />{{ fmtRecurrenceFim(rule) }}</span>
<span class="mpa-recur-item__since">
desde {{ fmtDateBR(rule.start_date) }}
</span>
</div>
<p v-if="rule.observacoes" class="mpa-recur-item__obs">
{{ rule.observacoes }}
</p>
</div>
<div class="mpa-recur-item__actions">
<button
v-if="rule.status === 'ativo'"
type="button"
v-tooltip.left="'Cancelar série'"
class="mpa-ag__act mpa-ag__act--danger"
:disabled="recorrenciasHook.busy.value"
@click="onCancelRecurrence(rule)"
>
<i class="pi pi-ban" />
</button>
<button
v-else
type="button"
v-tooltip.left="'Reativar série'"
class="mpa-ag__act mpa-ag__act--ok"
:disabled="recorrenciasHook.busy.value"
@click="onReactivateRecurrence(rule)"
>
<i class="pi pi-undo" />
</button>
</div>
</li>
</ul>
</section>
<!-- Filter chips -->
<div class="mpa-pron-filters" role="tablist">
<button
v-for="f in AGENDA_FILTERS"
:key="f.value"
type="button"
role="tab"
:aria-selected="agendaFilter === f.value"
class="mpa-pron-filter"
:class="{ 'is-active': agendaFilter === f.value }"
@click="agendaFilter = f.value"
>
<i :class="f.icon" />
<span>{{ f.label }}</span>
</button>
</div>
<!-- Empty state contextual -->
<div v-if="!agendaAgrupadas.length" class="mpa-empty mpa-empty--rich">
<div class="mpa-empty__icon"><i class="pi pi-calendar-times" /></div>
<div class="mpa-empty__title">
{{ sessionsHook.sessions.value.length ? 'Nenhuma sessão neste filtro' : 'Sem sessões registradas' }}
</div>
<div class="mpa-empty__sub">
<template v-if="sessionsHook.sessions.value.length">
Tente outro filtro acima ou veja "Todas" pra listar o histórico completo.
</template>
<template v-else>
As sessões agendadas e atendidas com este paciente aparecerão aqui.
</template>
</div>
</div>
<!-- Grupos por mes -->
<section
v-for="g in agendaAgrupadas"
:key="g.key"
class="mpa-panel mpa-ag__group"
>
<header class="mpa-panel__head">
<div class="mpa-panel__title"><i class="pi pi-calendar" /> {{ g.label }}</div>
<span class="mpa-panel__badge">{{ g.items.length }}</span>
</header>
<ul class="mpa-ag__list">
<li
v-for="s in g.items"
:key="s.id"
class="mpa-ag__item"
:data-status="String(s.status || 'agendado').toLowerCase()"
>
<!-- Coluna data -->
<div class="mpa-ag__date">
<span class="mpa-ag__date-dow">{{ fmtDayShort(s.inicio_em) }}</span>
<span class="mpa-ag__date-day">{{ new Date(s.inicio_em).getDate() }}</span>
<span class="mpa-ag__date-time">{{ fmtHourShort(s.inicio_em) }}</span>
</div>
<!-- Coluna main -->
<div class="mpa-ag__main">
<div class="mpa-ag__top">
<Tag
:value="STATUS_LABEL[s.status] || s.status || 'Agendado'"
:severity="STATUS_SEVERITY[s.status] || 'info'"
class="mpa-ag__tag"
/>
<span v-if="s.modalidade" class="mpa-tl__chip">
<i :class="s.modalidade === 'online' ? 'pi pi-video' : 'pi pi-map-marker'" />
{{ s.modalidade === 'online' ? 'Online' : 'Presencial' }}
</span>
<span v-if="sessionDuration(s.inicio_em, s.fim_em)" class="mpa-tl__chip mpa-tl__chip--dim">
<i class="pi pi-clock" />
{{ sessionDuration(s.inicio_em, s.fim_em) }}
</span>
<span class="mpa-ag__rel">{{ fmtRelative(s.inicio_em) }}</span>
</div>
<div v-if="s.titulo_custom || s.titulo" class="mpa-ag__title">
{{ s.titulo_custom || s.titulo }}
</div>
<p v-if="s.observacoes" class="mpa-ag__note">{{ s.observacoes }}</p>
</div>
<!-- Coluna actions -->
<div class="mpa-ag__actions">
<button
type="button"
v-tooltip.left="'Marcar como realizada'"
class="mpa-ag__act mpa-ag__act--ok"
:disabled="sessionsHook.busy.value"
@click="updateSessionStatus(s, 'realizado', 'Sessão marcada como realizada')"
>
<i class="pi pi-check-circle" />
</button>
<button
type="button"
v-tooltip.left="'Marcar como falta'"
class="mpa-ag__act mpa-ag__act--warn"
:disabled="sessionsHook.busy.value"
@click="updateSessionStatus(s, 'faltou', 'Marcada como falta')"
>
<i class="pi pi-user-minus" />
</button>
<button
type="button"
v-tooltip.left="'Cancelar'"
class="mpa-ag__act mpa-ag__act--danger"
:disabled="sessionsHook.busy.value"
@click="updateSessionStatus(s, 'cancelado', 'Sessão cancelada')"
>
<i class="pi pi-ban" />
</button>
</div>
</li>
</ul>
</section>
</template>
</div>
<!-- ABA: Financeiro (Fase 6 KPIs + tabela + mark paid) -->
<div v-else-if="activeTab === 'financ'" class="mpa-tab">
<!-- Loading -->
<div v-if="financialHook.loading.value" class="mpa-empty">
<i class="pi pi-spin pi-spinner mr-2" /> Carregando
</div>
<!-- Empty state com CTA -->
<div v-else-if="!financialHook.records.value.length" class="mpa-empty mpa-empty--rich">
<div class="mpa-empty__icon"><i class="pi pi-wallet" /></div>
<div class="mpa-empty__title">Sem lançamentos financeiros</div>
<div class="mpa-empty__sub">
Adicione o primeiro lançamento de cobrança ou recebimento deste paciente.
</div>
<button type="button" class="mpa-quick-btn mpa-quick-btn--cta" @click="addFinancial">
<i class="pi pi-plus" :style="{ color: '#f59e0b' }" />
<span>Novo lançamento</span>
</button>
</div>
<template v-else>
<!-- 3 KPIs financeiros -->
<div class="mpa-kpis">
<article class="mpa-kpi" style="--c:#4ade80">
<span class="mpa-kpi__num">01</span>
<header class="mpa-kpi__head">
<div class="mpa-kpi__icon"><i class="pi pi-check-circle" /></div>
<span class="mpa-kpi__tag">Pago</span>
</header>
<div class="mpa-kpi__big mpa-kpi__big--small">
{{ fmtCurrency(financialHook.statusFinanceiro.value.totalPago) }}
</div>
<div class="mpa-kpi__cap">
{{ financialHook.records.value.filter((r) => !!r.paid_at).length }}
{{ financialHook.records.value.filter((r) => !!r.paid_at).length === 1 ? 'lançamento' : 'lançamentos' }}
</div>
</article>
<article class="mpa-kpi" style="--c:var(--p-primary-color)">
<span class="mpa-kpi__num">02</span>
<header class="mpa-kpi__head">
<div class="mpa-kpi__icon"><i class="pi pi-clock" /></div>
<span class="mpa-kpi__tag">Pendente</span>
</header>
<div class="mpa-kpi__big mpa-kpi__big--small">
{{ fmtCurrency(financialHook.statusFinanceiro.value.totalPendente) }}
</div>
<div class="mpa-kpi__cap">
<template v-if="financialHook.statusFinanceiro.value.proxVenc">
Próx. venc. {{ fmtDateBR(financialHook.statusFinanceiro.value.proxVenc.due_date) }}
</template>
<template v-else>A receber</template>
</div>
</article>
<article
class="mpa-kpi"
:style="financialHook.statusFinanceiro.value.vencidos > 0 ? '--c:#f87171' : '--c:#94a3b8'"
>
<span class="mpa-kpi__num">03</span>
<header class="mpa-kpi__head">
<div class="mpa-kpi__icon"><i class="pi pi-exclamation-triangle" /></div>
<span class="mpa-kpi__tag">Em atraso</span>
</header>
<div class="mpa-kpi__big">{{ financialHook.statusFinanceiro.value.vencidos }}</div>
<div class="mpa-kpi__cap">
{{ financialHook.statusFinanceiro.value.vencidos === 0
? 'Tudo em dia'
: (financialHook.statusFinanceiro.value.vencidos === 1 ? 'lançamento vencido' : 'lançamentos vencidos') }}
</div>
</article>
</div>
<!-- Tabela de lançamentos -->
<section class="mpa-panel">
<header class="mpa-panel__head">
<div class="mpa-panel__title"><i class="pi pi-list" /> Lançamentos</div>
<div class="mpa-fin__head-actions">
<span class="mpa-panel__badge">{{ financialHook.recordsOrdenados.value.length }}</span>
<button type="button" class="mpa-icon-btn-sm" v-tooltip.left="'Novo lançamento'" @click="addFinancial">
<i class="pi pi-plus" />
</button>
</div>
</header>
<div class="mpa-fin__table" role="table">
<div class="mpa-fin__row mpa-fin__row--head" role="row">
<span role="columnheader">Vencimento</span>
<span role="columnheader">Descrição</span>
<span role="columnheader" class="mpa-fin__col-method">Forma</span>
<span role="columnheader" class="mpa-fin__col-amount">Valor</span>
<span role="columnheader" class="mpa-fin__col-status">Status</span>
<span role="columnheader" class="mpa-fin__col-action"></span>
</div>
<div
v-for="r in financialHook.recordsOrdenados.value"
:key="r.id"
class="mpa-fin__row"
:data-status="recordStatus(r)"
role="row"
>
<span class="mpa-fin__date" role="cell">
<span class="mpa-fin__date-main">
{{ r.due_date ? fmtDateBR(r.due_date) : fmtDateBR(r.created_at) }}
</span>
<span v-if="r.due_date" class="mpa-fin__date-rel">
{{ fmtRelative(r.due_date) }}
</span>
</span>
<span class="mpa-fin__desc" role="cell">
{{ r.description || (r.category ? r.category : 'Lançamento') }}
</span>
<span class="mpa-fin__method" role="cell">
{{ fmtPaymentMethod(r.payment_method) || '—' }}
</span>
<span class="mpa-fin__amount" role="cell">
{{ fmtCurrency(Number(r.amount) || 0) }}
</span>
<span class="mpa-fin__status-cell" role="cell">
<span class="mpa-fin__status" :data-status="recordStatus(r)">
<span class="mpa-fin__status-dot" />
{{ RECORD_STATUS_LABEL[recordStatus(r)] }}
</span>
</span>
<span class="mpa-fin__action" role="cell">
<!-- Marca como pago se ainda nao foi -->
<button
v-if="!r.paid_at"
type="button"
v-tooltip.left="'Marcar como pago'"
class="mpa-ag__act mpa-ag__act--ok"
:disabled="financialHook.busy.value"
@click="markRecordPaid(r)"
>
<i class="pi pi-check" />
</button>
<!-- Reverte pagamento -->
<button
v-else
type="button"
v-tooltip.left="'Reverter pagamento'"
class="mpa-ag__act mpa-ag__act--warn"
:disabled="financialHook.busy.value"
@click="revertRecordPaid(r)"
>
<i class="pi pi-undo" />
</button>
</span>
</div>
</div>
</section>
</template>
</div>
<!-- ABA: Documentos (Fase 7 KPIs + DocumentsListPage embedded) -->
<div v-else-if="activeTab === 'doc'" class="mpa-tab">
<!-- Loading -->
<div v-if="documentsHook.loading.value" class="mpa-empty">
<i class="pi pi-spin pi-spinner mr-2" /> Carregando
</div>
<template v-else>
<!-- KPIs (so quando ha algo) -->
<div v-if="documentsHook.total.value" class="mpa-kpis">
<article class="mpa-kpi" style="--c:var(--p-primary-color)">
<span class="mpa-kpi__num">01</span>
<header class="mpa-kpi__head">
<div class="mpa-kpi__icon"><i class="pi pi-folder" /></div>
<span class="mpa-kpi__tag">Total</span>
</header>
<div class="mpa-kpi__big">{{ documentsHook.total.value }}</div>
<div class="mpa-kpi__cap">{{ documentsHook.sizeTotalFormatted.value }} no total</div>
</article>
<article v-if="documentsHook.topType.value" class="mpa-kpi" style="--c:#0ea5e9">
<span class="mpa-kpi__num">02</span>
<header class="mpa-kpi__head">
<div class="mpa-kpi__icon"><i class="pi pi-tag" /></div>
<span class="mpa-kpi__tag">Mais comum</span>
</header>
<div class="mpa-kpi__big mpa-kpi__big--small">
{{ documentsHook.topType.value.label }}
</div>
<div class="mpa-kpi__cap">
{{ documentsHook.topType.value.count }}
{{ documentsHook.topType.value.count === 1 ? 'documento' : 'documentos' }}
</div>
</article>
<article v-if="documentsHook.ultimo.value" class="mpa-kpi" style="--c:#4ade80">
<span class="mpa-kpi__num">03</span>
<header class="mpa-kpi__head">
<div class="mpa-kpi__icon"><i class="pi pi-clock" /></div>
<span class="mpa-kpi__tag">Último</span>
</header>
<div class="mpa-kpi__big mpa-kpi__big--small">
{{ fmtRelative(documentsHook.ultimo.value.created_at) }}
</div>
<div class="mpa-kpi__cap">{{ fmtDateBR(documentsHook.ultimo.value.created_at) }}</div>
</article>
<article v-if="documentsHook.pendentes.value > 0" class="mpa-kpi" style="--c:#f59e0b">
<span class="mpa-kpi__num">04</span>
<header class="mpa-kpi__head">
<div class="mpa-kpi__icon"><i class="pi pi-exclamation-circle" /></div>
<span class="mpa-kpi__tag">Revisão</span>
</header>
<div class="mpa-kpi__big">{{ documentsHook.pendentes.value }}</div>
<div class="mpa-kpi__cap">
{{ documentsHook.pendentes.value === 1 ? 'pendente' : 'pendentes' }}
</div>
</article>
</div>
<!-- DocumentsListPage embedded (ja vem com upload/preview/lista) -->
<section class="mpa-w mpa-embed">
<div class="mpa-w__body mpa-embed__body">
<DocumentsListPage
:patient-id="patientId"
:patient-name="nomeCompleto || ''"
embedded
/>
</div>
</section>
</template>
</div>
<!-- ABA: Conversas (Fase 7 KPIs + CTA + PatientConversationsTab embedded) -->
<div v-else-if="activeTab === 'conv'" class="mpa-tab">
<!-- Loading -->
<div v-if="messagesHook.loading.value" class="mpa-empty">
<i class="pi pi-spin pi-spinner mr-2" /> Carregando
</div>
<template v-else>
<!-- KPIs (so quando ha mensagens) -->
<div v-if="kpiMensagens" class="mpa-kpis">
<article class="mpa-kpi" style="--c:#25d366">
<span class="mpa-kpi__num">01</span>
<header class="mpa-kpi__head">
<div class="mpa-kpi__icon"><i class="pi pi-comments" /></div>
<span class="mpa-kpi__tag">Mensagens</span>
</header>
<div class="mpa-kpi__big">{{ kpiMensagens }}</div>
<div class="mpa-kpi__cap">
<template v-if="messagesHook.canais.value.length">
via {{ messagesHook.canais.value.map(chConvLabel).join(', ') }}
</template>
<template v-else>no histórico</template>
</div>
</article>
<article class="mpa-kpi" style="--c:#4ade80">
<span class="mpa-kpi__num">02</span>
<header class="mpa-kpi__head">
<div class="mpa-kpi__icon"><i class="pi pi-arrow-down-left" /></div>
<span class="mpa-kpi__tag">Recebidas</span>
</header>
<div class="mpa-kpi__big">{{ messagesHook.totalIn.value }}</div>
<div class="mpa-kpi__cap">
<template v-if="kpiMensagens">
{{ Math.round((messagesHook.totalIn.value / kpiMensagens) * 100) }}% do total
</template>
<template v-else></template>
</div>
</article>
<article class="mpa-kpi" style="--c:#60a5fa">
<span class="mpa-kpi__num">03</span>
<header class="mpa-kpi__head">
<div class="mpa-kpi__icon"><i class="pi pi-arrow-up-right" /></div>
<span class="mpa-kpi__tag">Enviadas</span>
</header>
<div class="mpa-kpi__big">{{ messagesHook.totalOut.value }}</div>
<div class="mpa-kpi__cap">
<template v-if="kpiMensagens">
{{ Math.round((messagesHook.totalOut.value / kpiMensagens) * 100) }}% do total
</template>
<template v-else></template>
</div>
</article>
<article class="mpa-kpi" style="--c:var(--p-primary-color)">
<span class="mpa-kpi__num">04</span>
<header class="mpa-kpi__head">
<div class="mpa-kpi__icon"><i class="pi pi-clock" /></div>
<span class="mpa-kpi__tag">Última</span>
</header>
<template v-if="messagesHook.ultimaMensagem.value">
<div class="mpa-kpi__big mpa-kpi__big--small">
{{ fmtRelative(messagesHook.ultimaMensagem.value.created_at) }}
</div>
<div class="mpa-kpi__cap">
{{ messagesHook.ultimaMensagem.value.direction === 'inbound' ? 'Recebida' : 'Enviada' }}
<span v-if="messagesHook.primeiraMensagem.value" class="mpa-kpi__cap-dim">
· {{ fmtRelative(messagesHook.primeiraMensagem.value.created_at) }}
</span>
</div>
</template>
<template v-else>
<div class="mpa-kpi__big mpa-kpi__big--small"></div>
<div class="mpa-kpi__cap">Sem mensagens</div>
</template>
</article>
</div>
<!-- CTA: abrir conversa no drawer global (continuar real-time) -->
<div class="mpa-conv-cta">
<button type="button" class="mpa-conv-cta__btn" @click="openWhatsapp">
<i class="pi pi-whatsapp" />
<span>Abrir conversa no drawer</span>
</button>
<span class="mpa-conv-cta__hint">
Continue a conversa em tempo real sem fechar o prontuário.
</span>
</div>
<!-- PatientConversationsTab embedded (thread completa) -->
<section class="mpa-w mpa-embed">
<div class="mpa-w__body mpa-embed__body">
<PatientConversationsTab
:patient-id="patientId"
:patient-name="nomeCompleto || ''"
/>
</div>
</section>
</template>
</div>
</template>
</div>
</div>
</section>
<!-- Dialog de cadastro/edicao do paciente abre INLINE por cima do
prontuario (z-index PrimeVue ~1100 > .mpa-page z-index 40). Ao
salvar/criar, recarrega os dados do paciente sem sair da tela. -->
<PatientCadastroDialog
v-model="cadastroOpen"
:patient-id="cadastroPatientId"
@created="onPatientSaved"
/>
<!-- Dialog: novo lancamento manual (chamado pelo botao "Lancamento"
da sidebar Acoes Rapidas e CTA do empty state da Tab Financeiro). -->
<Dialog
v-model:visible="novoLancOpen"
modal
dismissable-mask
:style="{ width: '420px', maxWidth: '92vw' }"
header="Novo lançamento"
>
<div class="mpa-novo-lanc">
<div class="mpa-novo-lanc__field">
<label class="mpa-novo-lanc__label">Descrição</label>
<InputText
v-model="novoLancForm.description"
placeholder="Ex: Sessão semanal, Avaliação inicial…"
class="w-full"
/>
</div>
<div class="mpa-novo-lanc__row">
<div class="mpa-novo-lanc__field">
<label class="mpa-novo-lanc__label">Valor (R$) *</label>
<InputNumber
v-model="novoLancForm.amount"
mode="currency"
currency="BRL"
locale="pt-BR"
:min="0"
class="w-full"
input-class="w-full"
/>
</div>
<div class="mpa-novo-lanc__field">
<label class="mpa-novo-lanc__label">Vencimento *</label>
<InputText
v-model="novoLancForm.due_date"
type="date"
class="w-full"
/>
</div>
</div>
<div class="mpa-novo-lanc__field">
<label class="mpa-novo-lanc__label">Forma de pagamento</label>
<Select
v-model="novoLancForm.payment_method"
:options="PAYMENT_METHODS"
option-label="label"
option-value="value"
placeholder="Opcional"
class="w-full"
show-clear
/>
</div>
</div>
<template #footer>
<button class="mpa-quick-btn" @click="novoLancOpen = false">
<i class="pi pi-times" />
<span>Cancelar</span>
</button>
<button
class="mpa-quick-btn mpa-quick-btn--cta"
:disabled="financialHook.busy.value"
@click="salvarLancamento"
>
<i :class="financialHook.busy.value ? 'pi pi-spin pi-spinner' : 'pi pi-check'" :style="{ color: '#10b981' }" />
<span>Salvar lançamento</span>
</button>
</template>
</Dialog>
<!-- AgendaEventDialog LOCAL do prontuario.
- Reusa o componente da Agenda (sem duplicar codigo) com 2 props
novas: lockType (pula step 1) + lockPatient (esconde trocar/limpar).
- Slot #headerLeft sobrescreve o header padrao com icon + "Nova
sessao" + nome do paciente.
- State LOCAL (sessaoDialog*) nao colide com o dialog global do
MelissaLayout que continua na Agenda.
- Dados pesados (commitmentOptions, workRules, allEvents,
agendaSettings, feriados, ownerId, tenantId) vem via inject do
MelissaLayout evita re-fetch.
- Save/Delete delegam pros handlers globais (M.onDialogSave/Delete)
que ja sabem orquestrar agenda_eventos + recurrence_rules. -->
<AgendaEventDialog
v-if="melissaAgenda"
v-model="sessaoDialogOpen"
:event-row="sessaoDialogEventRow"
:initial-start-i-s-o="sessaoDialogStartISO"
:initial-end-i-s-o="sessaoDialogEndISO"
:preset-commitment-id="sessaoDialogEventRow?.determined_commitment_id || null"
:owner-id="melissaAgenda.ownerId?.value || ''"
:tenant-id="melissaAgenda.clinicTenantId?.value || ''"
:commitment-options="melissaAgenda.commitmentOptions?.value || []"
:work-rules="melissaAgenda.workRules?.value || []"
:blocked-dates="[]"
:agenda-settings="melissaAgenda.settings?.value || null"
:all-events="melissaAgenda.allEventsForDialog?.value || []"
:pausas-semanais="melissaAgenda.settings?.value?.pausas_semanais || []"
:feriados="melissaAgenda.feriados?.value || []"
new-patient-route="/therapist/patients/cadastro"
:lock-type="true"
:lock-patient="true"
@save="onSessaoDialogSave"
@delete="onSessaoDialogDelete"
@updateSeriesEvent="onSessaoDialogUpdateSeries"
@editSeriesOccurrence="onSessaoDialogSave"
>
<template #headerLeft>
<div class="mpa-dlg-head">
<div class="mpa-dlg-head__icon">
<i class="pi pi-calendar-plus" />
</div>
<div class="mpa-dlg-head__text">
<div class="mpa-dlg-head__title">Nova sessão</div>
<div class="mpa-dlg-head__sub">{{ dash(nomeCompleto) }}</div>
</div>
</div>
</template>
</AgendaEventDialog>
</template>
<style scoped>
/* ═══════ Page chrome (espelha MelissaAgendador/Negocio) ═══════ */
.mpa-page {
position: absolute;
inset: 6px 6px calc(var(--m-dock-h, 76px) + 6px) var(--m-config-aside-left, 6px);
z-index: 40;
display: flex;
flex-direction: column;
background: var(--m-bg-medium);
backdrop-filter: blur(32px) saturate(160%);
-webkit-backdrop-filter: blur(32px) saturate(160%);
border: 1px solid var(--m-border);
border-radius: 18px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4);
overflow: hidden;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
color: var(--m-text);
animation: mpa-page-enter 240ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
@keyframes mpa-page-enter {
from { opacity: 0; transform: scale(0.985); }
to { opacity: 1; transform: scale(1); }
}
/* Page chrome desktop: largura TOTAL (sem right:max). O prontuario tem
muito conteudo (KPIs grid + tabelas + timeline), full-width facilita
leitura. Diferente das outras Melissa Pages que tem painel flutuante
ou conteudo mais compacto. */
.mpa-page__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid var(--m-border);
flex-shrink: 0;
gap: 10px;
}
.mpa-page__title {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 12px;
}
.mpa-head__avatar {
width: 44px;
height: 44px;
border-radius: 50%;
overflow: hidden;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
flex-shrink: 0;
display: grid;
place-items: center;
}
.mpa-head__avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.mpa-head__avatar--initials {
background: color-mix(in srgb, var(--p-primary-color) 18%, transparent);
color: var(--p-primary-color);
font-weight: 800;
font-size: 0.92rem;
}
.mpa-head__id {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.mpa-head__name {
font-size: 0.98rem;
font-weight: 700;
color: var(--m-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mpa-head__meta {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
font-size: 0.74rem;
color: var(--m-text-muted);
}
.mpa-head__sep { opacity: 0.4; }
.mpa-head__tag {
font-size: 0.66rem !important;
margin-left: 2px;
}
.mpa-head__risk {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 999px;
background: rgba(220, 38, 38, 0.12);
color: rgb(220, 38, 38);
border: 1px solid rgba(220, 38, 38, 0.35);
font-size: 0.66rem;
font-weight: 700;
}
.mpa-head__risk > i { font-size: 0.66rem; }
.mpa-page__actions { display: flex; align-items: center; gap: 6px; }
.mpa-act-btn {
width: 32px;
height: 32px;
display: grid;
place-items: center;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
color: var(--m-text);
border-radius: 8px;
cursor: pointer;
transition: background-color 120ms ease;
}
.mpa-act-btn:hover { background: var(--m-bg-soft-hover); }
.mpa-act-btn > i { font-size: 0.85rem; }
.mpa-close {
width: 32px;
height: 32px;
display: grid;
place-items: center;
background: transparent;
border: 1px solid var(--m-border);
color: var(--m-text-muted);
border-radius: 8px;
cursor: pointer;
transition: background-color 120ms ease, color 120ms ease;
}
.mpa-close:hover { background: var(--m-bg-soft-hover); color: var(--m-text); }
.mpa-menu-btn {
display: none;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
border-radius: 8px;
color: var(--m-text);
cursor: pointer;
font-family: inherit;
font-size: 0.78rem;
font-weight: 600;
transition: background-color 140ms ease;
}
.mpa-menu-btn:hover { background: var(--m-bg-soft-hover); }
.mpa-menu-btn > i { font-size: 0.78rem; color: var(--p-primary-color); }
.mpa-subheader {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px 18px;
border-bottom: 1px solid var(--m-border);
background: var(--m-bg-soft);
font-size: 0.78rem;
color: var(--m-text-muted);
line-height: 1.45;
flex-shrink: 0;
}
.mpa-subheader--risk {
background: rgba(220, 38, 38, 0.10);
border-bottom-color: rgba(220, 38, 38, 0.30);
color: rgb(180, 33, 33);
}
.mpa-subheader__icon { font-size: 0.92rem; flex-shrink: 0; margin-top: 1px; }
.mpa-subheader__text { flex: 1; min-width: 0; }
.mpa-subheader__text strong { font-weight: 700; color: var(--m-text); }
.mpa-subheader--risk .mpa-subheader__text strong { color: rgb(180, 33, 33); }
/* ═══════ Body 2-col ═══════ */
.mpa-body {
flex: 1;
min-height: 0;
display: flex;
overflow: hidden;
}
.mpa-side {
width: 320px;
flex-shrink: 0;
display: flex;
flex-direction: column;
background: var(--m-bg-soft);
border-right: 1px solid var(--m-border);
overflow: hidden;
}
.mpa-side__scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
scrollbar-width: thin;
scrollbar-color: var(--m-border-strong) transparent;
}
.mpa-side__scroll::-webkit-scrollbar { width: 5px; }
.mpa-side__scroll::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
/* Cards da sidebar (.mpa-w) NUNCA podem encolher — caso contrario o flex
shrink default (1) corta conteudo quando ha muitos cards stacked
(Acoes + Nav 7 tabs + Sub-nav Perfil 6 + Vinculos). Combinado com o
overflow:hidden do .mpa-w (necessario pro radius), cards encolhidos
perdem itens da lista interna. flex-shrink: 0 + altura auto garante
que cada card cresce ate o tamanho real do conteudo, e o
.mpa-side__scroll lida com overflow via scroll vertical. */
.mpa-side__scroll > .mpa-w {
flex-shrink: 0;
height: auto;
}
/* Wrapper de cada aba do main: precisa de display:flex + gap senao os
filhos (KPIs grid + cards + panels) ficam colados. .mpa-main ja eh
flex-col com gap, mas como cada aba envolve seus elementos num div
.mpa-tab, ESSE div precisa replicar o spacing. */
.mpa-tab {
display: flex;
flex-direction: column;
gap: 12px;
}
.mpa-cfg-btn {
display: flex;
align-items: center;
gap: 8px;
width: calc(100% - 24px);
margin: 12px 12px 0;
padding: 10px 12px;
background: var(--m-bg-medium);
border: 1px solid var(--m-border);
border-radius: 9px;
color: var(--m-text);
cursor: pointer;
font-family: inherit;
font-size: 0.84rem;
font-weight: 600;
text-align: left;
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
flex-shrink: 0;
}
.mpa-cfg-btn:hover {
background: var(--m-bg-soft-hover);
border-color: var(--m-border-strong);
}
.mpa-cfg-btn--back:hover {
background: color-mix(in srgb, var(--p-primary-color) 8%, var(--m-bg-medium));
border-color: color-mix(in srgb, var(--p-primary-color) 30%, transparent);
}
.mpa-cfg-btn > i:first-child {
color: var(--p-primary-color);
font-size: 0.92rem;
}
.mpa-cfg-btn > span { flex: 1; }
.mpa-main {
flex: 1;
min-width: 0;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
scrollbar-width: thin;
scrollbar-color: var(--m-border-strong) transparent;
}
.mpa-main::-webkit-scrollbar { width: 5px; }
.mpa-main::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
@media (min-width: 1024px) {
.mpa-main {
max-width: 1100px;
margin: 0 auto;
width: 100%;
}
.mpa-main > .mpa-w {
height: auto;
flex-shrink: 0;
}
}
/* ═══════ Card-base ═══════ */
.mpa-w {
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
}
.mpa-w--side {
background: var(--m-bg-medium);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16);
}
.mpa-w__head {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
border-bottom: 1px solid var(--m-border);
}
.mpa-w__icon {
width: 36px;
height: 36px;
display: grid;
place-items: center;
border-radius: 9px;
flex-shrink: 0;
background: color-mix(in srgb, var(--p-primary-color) 15%, transparent);
color: var(--p-primary-color);
}
.mpa-w__icon > i { font-size: 0.95rem; }
.mpa-w__icon--blue { background: color-mix(in srgb, #3b82f6 15%, transparent); color: #3b82f6; }
.mpa-w__icon--cyan { background: color-mix(in srgb, #06b6d4 15%, transparent); color: #06b6d4; }
.mpa-w__icon--green { background: color-mix(in srgb, #10b981 15%, transparent); color: #10b981; }
.mpa-w__icon--orange { background: color-mix(in srgb, #f97316 15%, transparent); color: #f97316; }
.mpa-w__title { flex: 1; min-width: 0; }
.mpa-w__title-text {
font-size: 0.92rem;
font-weight: 700;
color: var(--m-text);
line-height: 1.2;
}
.mpa-w__sub {
font-size: 0.74rem;
color: var(--m-text-muted);
margin-top: 2px;
line-height: 1.3;
}
.mpa-w__body {
padding: 14px;
display: flex;
flex-direction: column;
gap: 10px;
}
.mpa-w__body--chips {
flex-direction: row;
flex-wrap: wrap;
gap: 6px;
}
/* ═══════ Quick-action btn (sidebar) ═══════ */
.mpa-quick-btn {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 12px;
border-radius: 8px;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
color: var(--m-text);
cursor: pointer;
font-family: inherit;
font-size: 0.82rem;
font-weight: 600;
text-align: left;
transition: background-color 120ms ease, border-color 120ms ease;
}
.mpa-quick-btn:hover {
background: var(--m-bg-soft-hover);
border-color: var(--m-border-strong);
}
.mpa-quick-btn > i { font-size: 0.92rem; flex-shrink: 0; }
/* ═══════ Tab btn (sidebar nav) ═══════ */
.mpa-tab-btn {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: 8px;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
color: var(--m-text);
cursor: pointer;
font-family: inherit;
text-align: left;
transition: background-color 120ms ease, border-color 120ms ease;
}
.mpa-tab-btn:hover {
background: var(--m-bg-soft-hover);
border-color: var(--m-border-strong);
}
.mpa-tab-btn.is-active {
background: color-mix(in srgb, var(--p-primary-color) 14%, transparent);
border-color: color-mix(in srgb, var(--p-primary-color) 38%, transparent);
color: var(--p-primary-color);
font-weight: 700;
}
.mpa-tab-btn__icon {
width: 28px;
height: 28px;
display: grid;
place-items: center;
border-radius: 7px;
flex-shrink: 0;
}
.mpa-tab-btn__icon > i { font-size: 0.78rem; }
.mpa-tab-btn__label {
flex: 1;
min-width: 0;
font-size: 0.82rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mpa-tab-btn__chev {
color: var(--m-text-muted);
font-size: 0.7rem;
opacity: 0.5;
flex-shrink: 0;
}
.mpa-tab-btn--sub {
padding: 6px 10px;
background: transparent;
border: 1px solid transparent;
}
.mpa-tab-btn--sub:hover {
background: var(--m-bg-soft);
border-color: var(--m-border);
}
.mpa-tab-btn--sub.is-active {
background: color-mix(in srgb, var(--p-primary-color) 10%, transparent);
}
.mpa-tab-btn__sub-icon {
color: var(--m-text-muted);
font-size: 0.78rem;
flex-shrink: 0;
width: 18px;
text-align: center;
}
/* ═══════ Chips (grupos / tags) ═══════ */
.mpa-chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
border-radius: 999px;
font-size: 0.7rem;
font-weight: 600;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
color: var(--m-text);
}
.mpa-chip > i { font-size: 0.66rem; }
.mpa-chip--group {
background: color-mix(in srgb, var(--p-primary-color) 12%, transparent);
color: var(--p-primary-color);
border-color: color-mix(in srgb, var(--p-primary-color) 28%, transparent);
}
/* ═══════ KPIs (Visao Geral Fase 2 — 4 cards ricos) ═══════ */
.mpa-kpis {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
}
.mpa-kpi {
position: relative;
padding: 14px 14px 12px;
border-radius: 12px;
background: var(--m-bg-soft);
border: 1px solid color-mix(in srgb, var(--c, #6366f1) 28%, var(--m-border));
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
overflow: hidden;
display: flex;
flex-direction: column;
gap: 4px;
min-height: 110px;
}
.mpa-kpi::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, var(--c, #6366f1) 0%, transparent 65%);
opacity: 0.10;
pointer-events: none;
}
.mpa-kpi__num {
position: absolute;
top: 8px;
right: 14px;
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 0.66rem;
color: var(--c, #6366f1);
opacity: 0.45;
font-weight: 800;
pointer-events: none;
}
.mpa-kpi__head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.mpa-kpi__icon {
width: 32px;
height: 32px;
display: grid;
place-items: center;
border-radius: 8px;
background: color-mix(in srgb, var(--c, #6366f1) 16%, transparent);
color: var(--c, #6366f1);
flex-shrink: 0;
}
.mpa-kpi__icon > i { font-size: 0.85rem; }
.mpa-kpi__tag {
font-size: 0.66rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--m-text-muted);
}
.mpa-kpi__big {
font-size: 1.6rem;
font-weight: 800;
color: var(--m-text);
line-height: 1.05;
letter-spacing: -0.02em;
margin-top: auto;
}
.mpa-kpi__big--small { font-size: 1.1rem; }
.mpa-kpi__cap {
font-size: 0.74rem;
color: var(--m-text-muted);
margin-top: 4px;
line-height: 1.4;
}
.mpa-kpi__cap-warn {
color: rgb(245, 158, 11);
font-weight: 600;
}
.mpa-kpi__cap-dim {
opacity: 0.7;
}
/* ═══════ Grid 2-col (Timeline + Mensagens/Notas) ═══════ */
.mpa-grid {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
@media (min-width: 900px) {
.mpa-grid {
grid-template-columns: 1.4fr 1fr;
}
}
.mpa-grid__col {
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
/* ═══════ Painel base ═══════ */
.mpa-panel {
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
border-radius: 12px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.mpa-panel__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 12px 14px;
border-bottom: 1px solid var(--m-border);
}
.mpa-panel__title {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.88rem;
font-weight: 700;
color: var(--m-text);
}
.mpa-panel__title > i {
color: var(--p-primary-color);
font-size: 0.85rem;
}
.mpa-panel__badge {
background: color-mix(in srgb, var(--p-primary-color) 16%, transparent);
color: var(--p-primary-color);
border-radius: 999px;
padding: 2px 10px;
font-size: 0.7rem;
font-weight: 700;
}
.mpa-panel__body {
padding: 12px 14px 14px;
flex: 1;
min-height: 0;
}
/* ═══════ Empty state ═══════ */
.mpa-empty {
text-align: center;
color: var(--m-text-muted);
padding: 16px 0;
font-size: 0.82rem;
}
.mpa-empty--rich {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 24px 12px;
}
.mpa-empty__icon {
width: 44px;
height: 44px;
display: grid;
place-items: center;
border-radius: 50%;
background: var(--m-bg-medium);
color: var(--m-text-muted);
margin-bottom: 4px;
}
.mpa-empty__icon > i { font-size: 1rem; }
.mpa-empty__title {
font-size: 0.88rem;
font-weight: 700;
color: var(--m-text);
}
.mpa-empty__sub {
font-size: 0.74rem;
color: var(--m-text-muted);
line-height: 1.4;
max-width: 280px;
}
/* ═══════ Timeline (sessoes atendidas) ═══════ */
.mpa-timeline {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
position: relative;
}
.mpa-timeline::before {
content: '';
position: absolute;
top: 6px;
bottom: 6px;
left: 6px;
width: 1.5px;
background: var(--m-border);
}
.mpa-tl {
position: relative;
padding-left: 22px;
padding-bottom: 14px;
}
.mpa-tl:last-child { padding-bottom: 0; }
.mpa-tl__dot {
position: absolute;
top: 6px;
left: 1px;
width: 11px;
height: 11px;
border-radius: 50%;
background: var(--m-bg-medium);
border: 2px solid var(--m-text-muted);
}
.mpa-tl[data-status*="realiz"] .mpa-tl__dot,
.mpa-tl[data-status*="present"] .mpa-tl__dot { border-color: rgb(34, 197, 94); }
.mpa-tl[data-status*="falt"] .mpa-tl__dot { border-color: rgb(239, 68, 68); }
.mpa-tl[data-status*="cancel"] .mpa-tl__dot,
.mpa-tl[data-status*="remarc"] .mpa-tl__dot { border-color: rgb(245, 158, 11); }
.mpa-tl__main {
display: flex;
flex-direction: column;
gap: 4px;
}
.mpa-tl__top {
display: flex;
align-items: baseline;
gap: 8px;
flex-wrap: wrap;
}
.mpa-tl__when {
font-size: 0.82rem;
font-weight: 700;
color: var(--m-text);
}
.mpa-tl__rel {
font-size: 0.7rem;
color: var(--m-text-muted);
}
.mpa-tl__row {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.mpa-tl__tag {
font-size: 0.66rem !important;
}
.mpa-tl__chip {
display: inline-flex;
align-items: center;
gap: 3px;
padding: 2px 8px;
border-radius: 999px;
background: var(--m-bg-medium);
color: var(--m-text);
font-size: 0.66rem;
font-weight: 600;
}
.mpa-tl__chip > i { font-size: 0.62rem; opacity: 0.7; }
.mpa-tl__chip--dim {
background: transparent;
color: var(--m-text-muted);
border: 1px solid var(--m-border);
}
.mpa-tl__note {
font-size: 0.78rem;
color: var(--m-text-muted);
line-height: 1.4;
padding: 6px 8px;
border-left: 2px solid var(--m-border);
background: var(--m-bg-medium);
border-radius: 4px;
margin-top: 4px;
}
/* ═══════ Mensagens recentes ═══════ */
.mpa-msgs {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.mpa-msg {
padding: 8px 10px;
border-radius: 8px;
background: var(--m-bg-medium);
border: 1px solid var(--m-border);
border-left-width: 3px;
}
.mpa-msg--in {
border-left-color: rgb(34, 197, 94);
}
.mpa-msg--out {
border-left-color: rgb(96, 165, 250);
}
.mpa-msg__meta {
display: flex;
align-items: center;
gap: 5px;
font-size: 0.66rem;
color: var(--m-text-muted);
font-weight: 600;
margin-bottom: 4px;
}
.mpa-msg__meta > i { font-size: 0.62rem; opacity: 0.7; }
.mpa-msg__sep { opacity: 0.4; }
.mpa-msg__body {
font-size: 0.82rem;
color: var(--m-text);
line-height: 1.45;
word-break: break-word;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
-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;
}
/* ═══════ Dialog: novo lancamento (form simples) ═══════ */
.mpa-novo-lanc {
display: flex;
flex-direction: column;
gap: 12px;
padding-top: 4px;
}
.mpa-novo-lanc__row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
@media (max-width: 540px) {
.mpa-novo-lanc__row {
grid-template-columns: 1fr;
}
}
.mpa-novo-lanc__field {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.mpa-novo-lanc__label {
font-size: 0.74rem;
font-weight: 700;
color: var(--m-text);
}
.mpa-novo-lanc__opt {
font-weight: 400;
color: var(--m-text-muted);
opacity: 0.75;
}
/* Header customizado do Dialog Nova Sessao: icon + titulo + subtitulo */
.mpa-dlg-head {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
min-width: 0;
}
.mpa-dlg-head__icon {
width: 40px;
height: 40px;
display: grid;
place-items: center;
border-radius: 10px;
background: color-mix(in srgb, var(--p-primary-color) 16%, transparent);
color: var(--p-primary-color);
flex-shrink: 0;
}
.mpa-dlg-head__icon > i { font-size: 1.05rem; }
.mpa-dlg-head__text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.mpa-dlg-head__title {
font-size: 1rem;
font-weight: 700;
color: var(--m-text);
line-height: 1.2;
}
.mpa-dlg-head__sub {
font-size: 0.78rem;
color: var(--m-text-muted);
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Bloco de frequencia dentro do dialog Nova Sessao (espelha AgendaEventDialog) */
.mpa-recur {
margin-top: 4px;
padding: 12px;
border-radius: 10px;
background: var(--m-bg-medium);
border: 1px solid var(--m-border);
display: flex;
flex-direction: column;
gap: 10px;
}
/* Chips de frequencia + qtd sessoes */
.mpa-freq-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.mpa-freq-chip {
padding: 5px 12px;
border-radius: 999px;
font-size: 0.78rem;
font-weight: 600;
border: 1px solid var(--m-border);
background: transparent;
color: var(--m-text-muted);
cursor: pointer;
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
white-space: nowrap;
font-family: inherit;
}
.mpa-freq-chip:hover {
border-color: var(--p-primary-color);
color: var(--p-primary-color);
}
.mpa-freq-chip.is-active {
background: var(--p-primary-color);
border-color: var(--p-primary-color);
color: var(--p-primary-contrast-color, #fff);
}
.mpa-freq-chip--sm {
padding: 4px 10px;
font-size: 0.74rem;
}
/* Preview semanal/quinzenal */
.mpa-freq-preview {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 8px;
background: color-mix(in srgb, var(--p-primary-color) 8%, transparent);
border: 1px solid color-mix(in srgb, var(--p-primary-color) 28%, transparent);
font-size: 0.78rem;
font-weight: 500;
color: var(--m-text);
}
.mpa-freq-preview > i {
color: var(--p-primary-color);
font-size: 0.85rem;
}
/* Dias da semana grid (diasEspecificos) */
.mpa-freq-dias {
display: flex;
flex-direction: column;
gap: 6px;
}
.mpa-dias-grid {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.mpa-dia-chip {
min-width: 44px;
padding: 5px 10px;
border-radius: 999px;
font-size: 0.74rem;
font-weight: 700;
border: 1px solid var(--m-border);
background: transparent;
color: var(--m-text-muted);
cursor: pointer;
text-align: center;
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
font-family: inherit;
}
.mpa-dia-chip:hover {
border-color: var(--p-primary-color);
color: var(--p-primary-color);
}
.mpa-dia-chip.is-active {
background: var(--p-primary-color);
border-color: var(--p-primary-color);
color: var(--p-primary-contrast-color, #fff);
}
/* Qtd sessoes */
.mpa-freq-qtd {
display: flex;
flex-direction: column;
gap: 6px;
}
.mpa-freq-qtd-custom {
margin-top: 4px;
padding: 8px;
border-radius: 8px;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
}
/* ═══════ Tab Conversas (Fase 7) — CTA pra drawer ═══════ */
.mpa-conv-cta {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border-radius: 12px;
background: color-mix(in srgb, #25d366 8%, transparent);
border: 1px solid color-mix(in srgb, #25d366 28%, transparent);
flex-wrap: wrap;
}
.mpa-conv-cta__btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 999px;
background: #25d366;
color: #fff;
border: none;
font-family: inherit;
font-size: 0.82rem;
font-weight: 700;
cursor: pointer;
transition: background-color 120ms ease, transform 120ms ease;
}
.mpa-conv-cta__btn:hover {
background: color-mix(in srgb, #25d366 88%, black);
transform: translateY(-1px);
}
.mpa-conv-cta__btn > i { font-size: 0.92rem; }
.mpa-conv-cta__hint {
font-size: 0.74rem;
color: var(--m-text-muted);
flex: 1;
min-width: 0;
}
/* ═══════ Embed wrapper (DocumentsListPage / PatientConversationsTab) ═══════
O componente embedded ja vem com seu proprio padding/scroll. So damos
o card-base do Melissa (border-radius, fundo) e zeramos o body padding
pra ele preencher tudo. */
.mpa-embed {
background: var(--m-bg-soft);
overflow: hidden;
}
.mpa-embed__body {
padding: 0;
gap: 0;
}
/* ═══════ Tab Financeiro (Fase 6) ═══════ */
.mpa-fin__head-actions {
display: flex;
align-items: center;
gap: 6px;
}
.mpa-icon-btn-sm {
width: 26px;
height: 26px;
display: grid;
place-items: center;
border-radius: 6px;
background: var(--m-bg-medium);
border: 1px solid var(--m-border);
color: var(--p-primary-color);
cursor: pointer;
font-family: inherit;
transition: background-color 120ms ease, border-color 120ms ease;
}
.mpa-icon-btn-sm:hover {
background: color-mix(in srgb, var(--p-primary-color) 14%, transparent);
border-color: color-mix(in srgb, var(--p-primary-color) 38%, transparent);
}
.mpa-icon-btn-sm > i { font-size: 0.78rem; }
/* Quick btn variant CTA pra empty state */
.mpa-quick-btn--cta {
margin: 12px auto 0;
padding: 10px 16px;
background: var(--m-bg-medium);
border-color: var(--m-border-strong);
}
/* Tabela financeira */
.mpa-fin__table {
display: flex;
flex-direction: column;
}
.mpa-fin__row {
display: grid;
grid-template-columns: 110px 1fr 90px 100px 100px 36px;
gap: 12px;
align-items: center;
padding: 10px 14px;
border-top: 1px solid var(--m-border);
font-size: 0.82rem;
border-left: 3px solid transparent;
transition: background-color 120ms ease;
}
.mpa-fin__row:first-child { border-top: none; }
.mpa-fin__row:hover:not(.mpa-fin__row--head) { background: var(--m-bg-medium); }
.mpa-fin__row[data-status="pago"] { border-left-color: rgb(34, 197, 94); }
.mpa-fin__row[data-status="vencido"] { border-left-color: rgb(239, 68, 68); }
.mpa-fin__row[data-status="pendente"] { border-left-color: rgb(96, 165, 250); }
.mpa-fin__row--head {
background: var(--m-bg-medium);
font-size: 0.66rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--m-text-muted);
border-left-color: transparent !important;
}
.mpa-fin__date {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.mpa-fin__date-main {
font-weight: 700;
color: var(--m-text);
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 0.78rem;
}
.mpa-fin__date-rel {
font-size: 0.66rem;
color: var(--m-text-muted);
}
.mpa-fin__desc {
color: var(--m-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
.mpa-fin__method {
color: var(--m-text-muted);
font-size: 0.74rem;
}
.mpa-fin__amount {
font-weight: 800;
color: var(--m-text);
text-align: right;
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 0.82rem;
}
.mpa-fin__col-amount { text-align: right; }
.mpa-fin__col-method,
.mpa-fin__col-status,
.mpa-fin__col-action { text-align: center; }
.mpa-fin__status {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px;
border-radius: 999px;
font-size: 0.7rem;
font-weight: 700;
background: var(--m-bg-medium);
border: 1px solid var(--m-border);
}
.mpa-fin__status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.mpa-fin__status[data-status="pago"] {
background: color-mix(in srgb, rgb(34, 197, 94) 12%, transparent);
color: rgb(34, 197, 94);
border-color: color-mix(in srgb, rgb(34, 197, 94) 35%, transparent);
}
.mpa-fin__status[data-status="vencido"] {
background: color-mix(in srgb, rgb(239, 68, 68) 12%, transparent);
color: rgb(239, 68, 68);
border-color: color-mix(in srgb, rgb(239, 68, 68) 35%, transparent);
}
.mpa-fin__status[data-status="pendente"] {
background: color-mix(in srgb, rgb(96, 165, 250) 12%, transparent);
color: rgb(96, 165, 250);
border-color: color-mix(in srgb, rgb(96, 165, 250) 35%, transparent);
}
.mpa-fin__action {
display: flex;
justify-content: center;
}
/* Mobile: stack tabela em cards 2-col */
@media (max-width: 720px) {
.mpa-fin__row--head { display: none; }
.mpa-fin__row {
grid-template-columns: 1fr auto;
grid-template-rows: auto auto auto;
gap: 4px 10px;
padding: 12px 14px;
}
.mpa-fin__date {
grid-row: 1;
grid-column: 1;
}
.mpa-fin__amount {
grid-row: 1;
grid-column: 2;
text-align: right;
}
.mpa-fin__desc {
grid-row: 2;
grid-column: 1 / -1;
font-size: 0.78rem;
}
.mpa-fin__method {
grid-row: 3;
grid-column: 1;
}
.mpa-fin__status-cell {
grid-row: 3;
grid-column: 2;
text-align: right;
}
.mpa-fin__action {
grid-row: 4;
grid-column: 1 / -1;
justify-content: flex-end;
}
}
/* ═══════ Bloco recorrencias do paciente (Tab Agenda topo) ═══════ */
.mpa-recur-toggle {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.7rem;
font-weight: 600;
color: var(--m-text-muted);
cursor: pointer;
padding: 2px 8px;
border-radius: 999px;
border: 1px solid var(--m-border);
background: var(--m-bg-medium);
transition: background-color 120ms ease;
}
.mpa-recur-toggle:hover { background: var(--m-bg-soft-hover); }
.mpa-recur-toggle > input { accent-color: var(--p-primary-color); }
.mpa-recur-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
.mpa-recur-item {
display: grid;
grid-template-columns: 32px 1fr auto;
gap: 12px;
align-items: center;
padding: 10px 14px;
border-top: 1px solid var(--m-border);
border-left: 3px solid #a855f7;
transition: background-color 120ms ease;
}
.mpa-recur-item:first-child { border-top: none; }
.mpa-recur-item:hover { background: var(--m-bg-medium); }
.mpa-recur-item[data-status="cancelado"] {
border-left-color: var(--m-text-muted);
opacity: 0.7;
}
.mpa-recur-item__icon {
width: 32px;
height: 32px;
display: grid;
place-items: center;
border-radius: 8px;
background: color-mix(in srgb, #a855f7 16%, transparent);
color: #a855f7;
flex-shrink: 0;
}
.mpa-recur-item__icon > i { font-size: 0.85rem; }
.mpa-recur-item[data-status="cancelado"] .mpa-recur-item__icon {
background: var(--m-bg-medium);
color: var(--m-text-muted);
}
.mpa-recur-item__main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.mpa-recur-item__top {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.mpa-recur-item__label {
font-size: 0.88rem;
font-weight: 700;
color: var(--m-text);
}
.mpa-recur-item__tag { font-size: 0.66rem !important; }
.mpa-recur-item__meta {
display: flex;
flex-wrap: wrap;
gap: 4px 12px;
font-size: 0.74rem;
color: var(--m-text-muted);
}
.mpa-recur-item__meta > span {
display: inline-flex;
align-items: center;
gap: 4px;
}
.mpa-recur-item__meta > span > i { font-size: 0.66rem; opacity: 0.7; }
.mpa-recur-item__since { opacity: 0.65; }
.mpa-recur-item__obs {
font-size: 0.74rem;
color: var(--m-text-muted);
line-height: 1.4;
padding: 6px 8px;
border-left: 2px solid var(--m-border);
background: var(--m-bg-medium);
border-radius: 4px;
margin-top: 2px;
white-space: pre-wrap;
}
.mpa-recur-item__actions {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
/* Mobile: stack icon + main em coluna; actions vai pra baixo */
@media (max-width: 600px) {
.mpa-recur-item {
grid-template-columns: 32px 1fr;
grid-template-rows: auto auto;
}
.mpa-recur-item__actions {
grid-column: 1 / -1;
justify-content: flex-end;
}
}
/* ═══════ Tab Agenda (Fase 5) ═══════ */
.mpa-ag__group + .mpa-ag__group { margin-top: 10px; }
.mpa-ag__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
.mpa-ag__item {
display: grid;
grid-template-columns: 64px 1fr auto;
gap: 12px;
align-items: center;
padding: 10px 14px;
border-top: 1px solid var(--m-border);
border-left: 3px solid transparent;
transition: background-color 120ms ease;
}
.mpa-ag__item:first-child { border-top: none; }
.mpa-ag__item:hover { background: var(--m-bg-medium); }
.mpa-ag__item[data-status*="realiz"],
.mpa-ag__item[data-status*="present"] { border-left-color: rgb(34, 197, 94); }
.mpa-ag__item[data-status*="falt"] { border-left-color: rgb(239, 68, 68); }
.mpa-ag__item[data-status*="cancel"],
.mpa-ag__item[data-status*="remarc"] { border-left-color: rgb(245, 158, 11); }
.mpa-ag__item[data-status*="agendado"] { border-left-color: rgb(96, 165, 250); }
/* Coluna data (esquerda) */
.mpa-ag__date {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 6px 8px;
border-radius: 8px;
background: var(--m-bg-medium);
border: 1px solid var(--m-border);
min-width: 56px;
}
.mpa-ag__date-dow {
font-size: 0.62rem;
color: var(--m-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
line-height: 1;
}
.mpa-ag__date-day {
font-size: 1.1rem;
font-weight: 800;
color: var(--m-text);
line-height: 1.05;
margin: 2px 0;
letter-spacing: -0.02em;
}
.mpa-ag__date-time {
font-size: 0.66rem;
font-family: 'JetBrains Mono', ui-monospace, monospace;
color: var(--m-text-muted);
font-weight: 600;
line-height: 1;
}
/* Coluna main (centro) */
.mpa-ag__main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.mpa-ag__top {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.mpa-ag__tag { font-size: 0.66rem !important; }
.mpa-ag__rel {
margin-left: auto;
font-size: 0.7rem;
color: var(--m-text-muted);
}
.mpa-ag__title {
font-size: 0.85rem;
font-weight: 600;
color: var(--m-text);
}
.mpa-ag__note {
font-size: 0.74rem;
color: var(--m-text-muted);
line-height: 1.4;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
/* Coluna actions (direita) */
.mpa-ag__actions {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.mpa-ag__act {
width: 30px;
height: 30px;
display: grid;
place-items: center;
border-radius: 7px;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
color: var(--m-text-muted);
cursor: pointer;
font-family: inherit;
transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
}
.mpa-ag__act > i { font-size: 0.85rem; }
.mpa-ag__act:hover:not(:disabled) {
background: var(--m-bg-soft-hover);
color: var(--m-text);
border-color: var(--m-border-strong);
}
.mpa-ag__act:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.mpa-ag__act--ok:hover:not(:disabled) {
background: rgba(34, 197, 94, 0.12);
color: rgb(34, 197, 94);
border-color: rgba(34, 197, 94, 0.40);
}
.mpa-ag__act--warn:hover:not(:disabled) {
background: rgba(245, 158, 11, 0.12);
color: rgb(245, 158, 11);
border-color: rgba(245, 158, 11, 0.40);
}
.mpa-ag__act--danger:hover:not(:disabled) {
background: rgba(239, 68, 68, 0.12);
color: rgb(239, 68, 68);
border-color: rgba(239, 68, 68, 0.40);
}
/* Mobile: stack date + main em coluna; actions vai pra baixo */
@media (max-width: 600px) {
.mpa-ag__item {
grid-template-columns: 56px 1fr;
grid-template-rows: auto auto;
}
.mpa-ag__actions {
grid-column: 1 / -1;
justify-content: flex-end;
}
.mpa-ag__rel {
flex-basis: 100%;
margin-left: 0;
text-align: left;
}
}
/* ═══════ Tab Prontuario (Fase 4 MVP) ═══════ */
.mpa-pron-hint {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px 14px;
border-radius: 10px;
background: color-mix(in srgb, #06b6d4 9%, transparent);
border: 1px solid color-mix(in srgb, #06b6d4 28%, transparent);
color: var(--m-text);
font-size: 0.78rem;
line-height: 1.5;
}
.mpa-pron-hint > i {
color: #06b6d4;
font-size: 1rem;
margin-top: 2px;
flex-shrink: 0;
}
.mpa-pron-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 8px;
}
.mpa-pron-stat {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 10px;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
border-left: 3px solid var(--c, var(--p-primary-color));
}
.mpa-pron-stat > i {
color: var(--c, var(--p-primary-color));
font-size: 1.05rem;
flex-shrink: 0;
}
.mpa-pron-stat__value {
font-size: 1.05rem;
font-weight: 800;
color: var(--m-text);
line-height: 1.1;
}
.mpa-pron-stat__label {
font-size: 0.7rem;
color: var(--m-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
margin-top: 2px;
}
.mpa-pron-filters {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.mpa-pron-filter {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 6px 12px;
border-radius: 999px;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
color: var(--m-text);
cursor: pointer;
font-family: inherit;
font-size: 0.78rem;
font-weight: 600;
transition: all 120ms ease;
}
.mpa-pron-filter > i { font-size: 0.7rem; }
.mpa-pron-filter:hover {
background: var(--m-bg-soft-hover);
border-color: var(--m-border-strong);
}
.mpa-pron-filter.is-active {
background: var(--p-primary-color);
border-color: var(--p-primary-color);
color: var(--p-primary-contrast-color, #fff);
}
.mpa-pron-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.mpa-pron-item {
border-radius: 12px;
border: 1px solid var(--m-border);
background: var(--m-bg-soft);
padding: 12px 14px;
border-left: 3px solid var(--m-text-muted);
transition: border-color 120ms ease;
}
.mpa-pron-item[data-status*="realiz"],
.mpa-pron-item[data-status*="present"] { border-left-color: rgb(34, 197, 94); }
.mpa-pron-item[data-status*="falt"] { border-left-color: rgb(239, 68, 68); }
.mpa-pron-item[data-status*="cancel"],
.mpa-pron-item[data-status*="remarc"] { border-left-color: rgb(245, 158, 11); }
.mpa-pron-item__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 6px;
}
.mpa-pron-item__when {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.mpa-pron-item__date {
font-size: 0.85rem;
font-weight: 700;
color: var(--m-text);
}
.mpa-pron-item__rel {
font-size: 0.7rem;
color: var(--m-text-muted);
}
.mpa-pron-item__chips {
display: flex;
align-items: center;
gap: 5px;
flex-wrap: wrap;
}
.mpa-pron-item__tag { font-size: 0.66rem !important; }
.mpa-pron-item__title {
font-size: 0.82rem;
font-weight: 600;
color: var(--m-text);
margin-bottom: 6px;
}
.mpa-pron-item__evol {
margin-top: 8px;
padding: 10px 12px;
border-radius: 8px;
background: var(--m-bg-medium);
border-left: 2px solid var(--p-primary-color);
}
.mpa-pron-item__evol-label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 0.66rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--p-primary-color);
margin-bottom: 4px;
}
.mpa-pron-item__evol-label > i { font-size: 0.62rem; }
.mpa-pron-item__evol-text {
font-size: 0.85rem;
color: var(--m-text);
line-height: 1.55;
white-space: pre-wrap;
word-break: break-word;
}
.mpa-pron-item__noevol {
margin-top: 6px;
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.7rem;
color: var(--m-text-muted);
opacity: 0.65;
font-style: italic;
}
.mpa-pron-item__noevol > i { font-size: 0.5rem; }
.mpa-pron-roadmap {
margin-top: 6px;
border-style: dashed;
background: transparent;
}
.mpa-pron-roadmap__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
.mpa-pron-roadmap__list > li {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 8px 0;
border-bottom: 1px solid var(--m-border);
}
.mpa-pron-roadmap__list > li:last-child {
border-bottom: none;
padding-bottom: 0;
}
.mpa-pron-roadmap__list > li > i {
color: var(--p-primary-color);
font-size: 0.95rem;
margin-top: 2px;
flex-shrink: 0;
width: 24px;
text-align: center;
}
.mpa-pron-roadmap__list > li > div {
flex: 1;
min-width: 0;
}
.mpa-pron-roadmap__list strong {
display: block;
font-size: 0.85rem;
color: var(--m-text);
margin-bottom: 2px;
}
.mpa-pron-roadmap__list span {
font-size: 0.74rem;
color: var(--m-text-muted);
line-height: 1.4;
}
/* ═══════ Notas e observacoes ═══════ */
.mpa-notes {
display: flex;
flex-direction: column;
gap: 12px;
}
.mpa-note {
padding: 10px 12px;
border-radius: 8px;
background: var(--m-bg-medium);
border: 1px solid var(--m-border);
}
.mpa-note__label {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--m-text-muted);
margin-bottom: 4px;
display: inline-flex;
align-items: center;
gap: 4px;
}
.mpa-note__label > i { font-size: 0.66rem; opacity: 0.7; }
.mpa-note__text {
font-size: 0.82rem;
color: var(--m-text);
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
/* ═══════ Placeholder (em desenvolvimento) ═══════ */
.mpa-placeholder {
font-size: 0.85rem;
color: var(--m-text-muted);
line-height: 1.55;
padding: 6px 0;
}
.mpa-placeholder code {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 0.78rem;
background: var(--m-bg-medium);
padding: 2px 6px;
border-radius: 4px;
color: var(--p-primary-color);
}
/* ═══════ Erro state ═══════ */
.mpa-error {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 14px 16px;
border-radius: 10px;
background: rgba(220, 38, 38, 0.08);
border: 1px solid rgba(220, 38, 38, 0.30);
color: rgb(180, 33, 33);
}
.mpa-error > i { font-size: 1.05rem; margin-top: 2px; flex-shrink: 0; }
.mpa-error__title {
font-size: 0.88rem;
font-weight: 700;
}
.mpa-error__detail {
font-size: 0.78rem;
margin-top: 2px;
opacity: 0.85;
}
/* ═══════ Mobile drawer ═══════ */
.mpa-mobile-drawer {
position: fixed;
top: 0; left: 0;
height: 100vh;
height: 100dvh;
width: min(380px, 90vw);
z-index: 80;
background: var(--m-bg-medium);
backdrop-filter: blur(28px) saturate(160%);
-webkit-backdrop-filter: blur(28px) saturate(160%);
border-right: 1px solid var(--m-border);
transform: translateX(-100%);
transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1);
color: var(--m-text);
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
display: flex;
flex-direction: column;
}
.mpa-mobile-drawer.is-open { transform: translateX(0); }
.mpa-mobile-drawer__scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
scrollbar-width: thin;
scrollbar-color: var(--m-border-strong) transparent;
}
.mpa-mobile-drawer__scroll::-webkit-scrollbar { width: 5px; }
.mpa-mobile-drawer__scroll::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
.mpa-mobile-drawer__scroll .mpa-side {
width: 100%;
border-right: none;
}
.mpa-mobile-drawer__backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
z-index: 79;
}
.mpa-drawer-fade-enter-active,
.mpa-drawer-fade-leave-active { transition: opacity 200ms ease; }
.mpa-drawer-fade-enter-from,
.mpa-drawer-fade-leave-to { opacity: 0; }
/* ═══════ Mobile (<1024px) ═══════ */
@media (max-width: 1023px) {
.mpa-body { flex-direction: column; padding: 0; }
.mpa-body > .mpa-side { display: none; }
.mpa-main { width: 100%; padding: 8px; }
.mpa-main .mpa-w {
height: auto;
flex: 0 0 auto;
align-self: stretch;
}
.mpa-page__title { gap: 8px; }
.mpa-head__avatar { width: 36px; height: 36px; }
.mpa-head__name { font-size: 0.88rem; }
.mpa-head__meta { font-size: 0.7rem; }
.mpa-menu-btn { display: inline-flex; }
.mpa-act-btn:nth-child(2) { display: none; }
.mpa-kpis { grid-template-columns: 1fr 1fr; }
}
</style>