MelissaPaciente Fase 2: Tab Visao Geral completa (4 KPIs + timeline + msgs + notas)
Reescreveu o placeholder da aba Visao Geral por uma versao 1:1 do
PatientProntuario.vue legado, com estilo Melissa nativo e dados
alimentados pelos composables criados na Fase 1.
NOVO: src/features/patients/utils/patientFormatters.js (~165L)
- Helpers compartilhaveis extraidos do PatientProntuario:
parseDateLoose, fmtDateBR, fmtDateTimeBR, fmtCurrency, fmtRelative
(pt-br: "agora"/"ha 5 min"/"em 2 dias"/"ha 3 sem"), sessionDuration,
calcAge.
- STATUS_LABEL e STATUS_SEVERITY pra mapear status de sessao (cobre
variantes: realizado/realizada, falta/faltou, cancelado/cancelada).
- tagStyle com contraste auto (luminance WCAG-ish: bg colorido +
texto preto/branco baseado em luminance < 0.45).
- Sera reutilizado pelas Fases 3-7 e na Fase 8 substitui as funcoes
duplicadas do PatientProntuario.
EXTENSAO de composables (Fase 1):
- usePatientSessions: novo computed `ultimasAtendidas` (top 6 sessoes
com status realiz/falt/cancel/remarc pra Timeline). totalRealizadas/
Faltas/Canceladas refinados pra usar regex (cobre variantes pt-br).
- usePatientFinancial: novo computed `statusFinanceiro` que retorna
{ emDia: bool, proxVenc: record, totalPendente, totalPago, vencidos }
pra alimentar KPI 02 com info detalhada de status financeiro.
MELISSAPACIENTE.VUE — Visao Geral reescrita:
- 4 KPI cards ricos (substituem os simples da Fase 1):
- 01 Sessoes: realizadas / total + faltas + canceladas
- 02 Pagamento: status (Em dia/atraso) + prox venc + cor adaptativa
(vermelho atrasado / primary ok)
- 03 Proxima sessao: relative + datetime + modalidade
- 04 Mensagens: ultima relative + direction + count
- Grid 2-col abaixo (1.4fr / 1fr em >=900px):
- Timeline coluna esquerda: dots coloridos por status, tags severity,
chips modalidade + duracao, nota observacoes inline.
- Coluna direita: Mensagens recentes (4) com border-left in/out +
meta direction/relative + body 3-line clamp; Notas e observacoes
em card papel com label uppercase e icone lock.
- Removeu kpiEmAberto/Atrasado nao usados (statusFinanceiro encapsula).
CSS: ~280L novos pros componentes (KPIs ricos, panel base, empty rich,
timeline, mensagens, notas). Mantem o pattern visual Melissa.
ESLint: 0 errors da minha mudanca.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,47 @@ Touched: none
|
||||
## [2026-05-08 00:00] session | Melissa cfg-* nativas + temas + cronometro DB
|
||||
Touched: none
|
||||
|
||||
## [2026-05-08 13:00] session | MelissaPaciente Fase 2 — Tab Visao Geral completa
|
||||
Touched: none
|
||||
Detalhes: Reescreveu a aba Visao Geral do MelissaPaciente substituindo o
|
||||
placeholder por uma versao 1:1 do PatientProntuario.vue legado, mas com
|
||||
estilo Melissa nativo.
|
||||
|
||||
NOVO: src/features/patients/utils/patientFormatters.js (~165L)
|
||||
- Helpers compartilhaveis extraidos do PatientProntuario: parseDateLoose,
|
||||
fmtDateBR, fmtDateTimeBR, fmtCurrency, fmtRelative, sessionDuration,
|
||||
calcAge. STATUS_LABEL/SEVERITY pra sessoes. tagStyle com luminance auto
|
||||
(texto preto/branco baseado em contraste WCAG-ish). Sera usado pelas
|
||||
Fases 3-7 e finalmente pelo PatientProntuario tambem (Fase 8).
|
||||
|
||||
EXTENSAO de composables:
|
||||
- usePatientSessions ganha computed `ultimasAtendidas` (top 6 sessoes
|
||||
realizadas/faltadas/canceladas pra Timeline). Refinou totalRealizadas/
|
||||
Faltas/Canceladas pra usar regex (cobre variantes pt-br).
|
||||
- usePatientFinancial ganha computed `statusFinanceiro` ({ emDia, proxVenc,
|
||||
totalPendente, totalPago, vencidos }) pra alimentar o KPI 02 com info
|
||||
detalhada.
|
||||
|
||||
MELISSAPACIENTE.VUE — Visao Geral (Fase 2 done):
|
||||
- 4 KPI cards ricos (era 4 simples na Fase 1):
|
||||
- 01 Sessoes: realizadas + total + faltas + cancel.
|
||||
- 02 Pagamento: emDia/atraso + proxVenc + pendente, com cor adaptativa
|
||||
(vermelho quando atrasado, primary quando ok).
|
||||
- 03 Proxima sessao: relative + datetime + modalidade.
|
||||
- 04 Mensagens: ultimaMensagem relative + direction + count.
|
||||
- Grid 2-col abaixo: Timeline (1.4fr) + coluna direita (1fr) com
|
||||
Mensagens recentes + Notas/observacoes.
|
||||
- Timeline com dot colorido por status (verde/vermelho/amarelo) +
|
||||
STATUS_LABEL/SEVERITY do utils + chips modalidade/duracao + nota
|
||||
observacoes inline.
|
||||
- Mensagens recentes com border-left colorida (verde=in / azul=out) +
|
||||
meta direction + relative + body 3-line clamp.
|
||||
- Notas e observacoes com card papel + label uppercase + icone lock
|
||||
pras internas.
|
||||
- Removeu kpiEmAberto/Atrasado nao usados (statusFinanceiro encapsula).
|
||||
|
||||
ESLint: 0 errors. Working tree limpa antes do commit.
|
||||
|
||||
## [2026-05-08 11:30] session | MelissaPaciente Fase 1 (foundation: composables + skeleton + slug)
|
||||
Touched: none (sem mudanca de wiki)
|
||||
Detalhes: User escolheu "Full rewrite Melissa nativo" pra portar
|
||||
|
||||
@@ -69,6 +69,38 @@ export function usePatientFinancial() {
|
||||
return [...pagos].sort((a, b) => new Date(b.paid_at) - new Date(a.paid_at))[0];
|
||||
});
|
||||
|
||||
/**
|
||||
* Status financeiro detalhado pra KPI da Visao Geral.
|
||||
* - emDia: nenhum pendente vencido (paid_at NULL && due_date < hoje)
|
||||
* - proxVenc: proximo pendente com due_date no futuro
|
||||
* - totalPendente / totalPago: somatorio
|
||||
* - vencidos: count de pendentes vencidos
|
||||
*/
|
||||
const statusFinanceiro = computed(() => {
|
||||
const recs = records.value;
|
||||
if (!recs?.length) {
|
||||
return { emDia: null, proxVenc: null, totalPendente: 0, totalPago: 0, vencidos: 0 };
|
||||
}
|
||||
const now = Date.now();
|
||||
const pendentes = recs.filter((r) => !r.paid_at);
|
||||
const pagos = recs.filter((r) => !!r.paid_at);
|
||||
const vencidos = pendentes.filter(
|
||||
(r) => r.due_date && new Date(r.due_date + 'T23:59:59').getTime() < now
|
||||
);
|
||||
const proxVenc = pendentes
|
||||
.filter((r) => r.due_date && new Date(r.due_date + 'T00:00:00').getTime() >= now)
|
||||
.sort((a, b) => new Date(a.due_date) - new Date(b.due_date))[0] || null;
|
||||
const totalPendente = pendentes.reduce((acc, r) => acc + (Number(r.amount) || 0), 0);
|
||||
const totalPago = pagos.reduce((acc, r) => acc + (Number(r.amount) || 0), 0);
|
||||
return {
|
||||
emDia: vencidos.length === 0,
|
||||
proxVenc,
|
||||
totalPendente,
|
||||
totalPago,
|
||||
vencidos: vencidos.length
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
records,
|
||||
loading,
|
||||
@@ -77,6 +109,7 @@ export function usePatientFinancial() {
|
||||
totalRecebido,
|
||||
totalEmAberto,
|
||||
totalAtrasado,
|
||||
ultimoPago
|
||||
ultimoPago,
|
||||
statusFinanceiro
|
||||
};
|
||||
}
|
||||
|
||||
@@ -58,14 +58,27 @@ export function usePatientSessions() {
|
||||
});
|
||||
|
||||
const totalSessoes = computed(() => sessions.value.length);
|
||||
|
||||
// Conta status com regex pra cobrir variantes pt-br
|
||||
// (realizada/realizado/presente; falta/faltou; cancelada/cancelado/remarcada).
|
||||
const totalRealizadas = computed(() =>
|
||||
sessions.value.filter((s) => s.status === 'realizada' || s.status === 'presente').length
|
||||
sessions.value.filter((s) => /realiz|present/i.test(String(s.status || ''))).length
|
||||
);
|
||||
const totalFaltas = computed(() =>
|
||||
sessions.value.filter((s) => s.status === 'falta').length
|
||||
sessions.value.filter((s) => /falt/i.test(String(s.status || ''))).length
|
||||
);
|
||||
const totalCanceladas = computed(() =>
|
||||
sessions.value.filter((s) => s.status === 'cancelada' || s.status === 'cancelado').length
|
||||
sessions.value.filter((s) => /cancel|remarca/i.test(String(s.status || ''))).length
|
||||
);
|
||||
|
||||
/**
|
||||
* Top 6 sessoes "atendidas" (qualquer status que indica encontro: realizado,
|
||||
* faltou, cancelado, remarcado) — alimenta a Timeline da Visao Geral.
|
||||
*/
|
||||
const ultimasAtendidas = computed(() =>
|
||||
sessions.value
|
||||
.filter((s) => /realiz|present|falt|cancel|remarca/i.test(String(s.status || '')))
|
||||
.slice(0, 6)
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -78,6 +91,7 @@ export function usePatientSessions() {
|
||||
totalSessoes,
|
||||
totalRealizadas,
|
||||
totalFaltas,
|
||||
totalCanceladas
|
||||
totalCanceladas,
|
||||
ultimasAtendidas
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Arquivo: src/features/patients/utils/patientFormatters.js
|
||||
|
|
||||
| Helpers de formatacao compartilhaveis entre PatientProntuario.vue (legacy)
|
||||
| e MelissaPaciente.vue (Melissa nativo). Extraidos do PatientProntuario
|
||||
| pra eliminar duplicacao quando MelissaPaciente substituir o legacy
|
||||
| na Fase 8.
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tenta varios formatos de data: ISO, DD/MM/YYYY, YYYY-MM-DD, etc.
|
||||
*/
|
||||
export function parseDateLoose(v) {
|
||||
if (!v) return null;
|
||||
if (v instanceof Date) return Number.isNaN(v.getTime()) ? null : v;
|
||||
const s = String(v).trim();
|
||||
if (!s) return null;
|
||||
// DD/MM/YYYY
|
||||
let m = s.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
|
||||
if (m) {
|
||||
const d = new Date(Number(m[3]), Number(m[2]) - 1, Number(m[1]));
|
||||
return Number.isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
// YYYY-MM-DD
|
||||
m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (m) {
|
||||
const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
||||
return Number.isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
const d = new Date(s);
|
||||
return Number.isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
|
||||
export function dash(v) {
|
||||
const s = String(v ?? '').trim();
|
||||
return s || '—';
|
||||
}
|
||||
|
||||
export function fmtDateBR(v) {
|
||||
const d = parseDateLoose(v);
|
||||
if (!d) return v ? dash(v) : '—';
|
||||
return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}`;
|
||||
}
|
||||
|
||||
export function fmtDateTimeBR(iso) {
|
||||
if (!iso) return '—';
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return iso;
|
||||
const dd = String(d.getDate()).padStart(2, '0');
|
||||
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const hh = String(d.getHours()).padStart(2, '0');
|
||||
const mi = String(d.getMinutes()).padStart(2, '0');
|
||||
return `${dd}/${mm}/${d.getFullYear()} ${hh}:${mi}`;
|
||||
}
|
||||
|
||||
export function fmtCurrency(v) {
|
||||
if (v === null || v === undefined || v === '') return '—';
|
||||
return `R$ ${Number(v).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
}
|
||||
|
||||
export function sessionDuration(inicio, fim) {
|
||||
if (!inicio || !fim) return null;
|
||||
const diff = new Date(fim) - new Date(inicio);
|
||||
if (diff <= 0) return null;
|
||||
const min = Math.round(diff / 60000);
|
||||
if (min < 60) return `${min} min`;
|
||||
const h = Math.floor(min / 60);
|
||||
const m = min % 60;
|
||||
return m ? `${h}h ${m}min` : `${h}h`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data relativa em pt-BR.
|
||||
* Retorna: "agora", "ha 5 min", "ha 2 h", "ontem", "ha 3 dias",
|
||||
* "em 2 dias", "em 3 sem", ou data absoluta para >= 5 semanas.
|
||||
*/
|
||||
export function fmtRelative(iso) {
|
||||
if (!iso) return '—';
|
||||
const target = new Date(iso).getTime();
|
||||
if (Number.isNaN(target)) return '—';
|
||||
const diff = target - Date.now();
|
||||
const abs = Math.abs(diff);
|
||||
const past = diff < 0;
|
||||
const min = Math.round(abs / 60000);
|
||||
if (min < 1) return 'agora';
|
||||
if (min < 60) return past ? `há ${min} min` : `em ${min} min`;
|
||||
const h = Math.round(min / 60);
|
||||
if (h < 24) return past ? `há ${h} h` : `em ${h} h`;
|
||||
const d = Math.round(h / 24);
|
||||
if (d === 1) return past ? 'ontem' : 'amanhã';
|
||||
if (d < 7) return past ? `há ${d} dias` : `em ${d} dias`;
|
||||
const w = Math.round(d / 7);
|
||||
if (w < 5) return past ? `há ${w} sem` : `em ${w} sem`;
|
||||
return fmtDateBR(iso);
|
||||
}
|
||||
|
||||
/**
|
||||
* Idade em anos com base em data de nascimento.
|
||||
*/
|
||||
export function calcAge(v) {
|
||||
const d = parseDateLoose(v);
|
||||
if (!d) return null;
|
||||
const now = new Date();
|
||||
let age = now.getFullYear() - d.getFullYear();
|
||||
const m = now.getMonth() - d.getMonth();
|
||||
if (m < 0 || (m === 0 && now.getDate() < d.getDate())) age--;
|
||||
return age;
|
||||
}
|
||||
|
||||
/**
|
||||
* Status visual de uma sessao da agenda.
|
||||
*/
|
||||
export const STATUS_LABEL = {
|
||||
agendado: 'Agendado',
|
||||
realizado: 'Realizado',
|
||||
realizada: 'Realizado',
|
||||
faltou: 'Faltou',
|
||||
falta: 'Faltou',
|
||||
cancelado: 'Cancelado',
|
||||
cancelada: 'Cancelado',
|
||||
remarcado: 'Remarcado',
|
||||
bloqueado: 'Bloqueado'
|
||||
};
|
||||
|
||||
export const STATUS_SEVERITY = {
|
||||
agendado: 'info',
|
||||
realizado: 'success',
|
||||
realizada: 'success',
|
||||
faltou: 'danger',
|
||||
falta: 'danger',
|
||||
cancelado: 'warn',
|
||||
cancelada: 'warn',
|
||||
remarcado: 'secondary',
|
||||
bloqueado: 'secondary'
|
||||
};
|
||||
|
||||
/**
|
||||
* Tag styling com contraste auto (texto preto/branco baseado em luminancia).
|
||||
*/
|
||||
function normalizeHexColor(c) {
|
||||
const s = String(c ?? '').trim();
|
||||
if (!s) return '';
|
||||
if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(s)) return s;
|
||||
if (/^([0-9a-f]{3}|[0-9a-f]{6})$/i.test(s)) return `#${s}`;
|
||||
return s;
|
||||
}
|
||||
|
||||
function hexToRgb(hex) {
|
||||
const h = String(hex || '').replace('#', '').trim();
|
||||
if (!/^([0-9a-f]{3}|[0-9a-f]{6})$/i.test(h)) return null;
|
||||
const full = h.length === 3 ? h.split('').map((ch) => ch + ch).join('') : h;
|
||||
const r = parseInt(full.slice(0, 2), 16);
|
||||
const g = parseInt(full.slice(2, 4), 16);
|
||||
const b = parseInt(full.slice(4, 6), 16);
|
||||
if ([r, g, b].some((n) => Number.isNaN(n))) return null;
|
||||
return { r, g, b };
|
||||
}
|
||||
|
||||
function relativeLuminance({ r, g, b }) {
|
||||
const srgb = [r, g, b].map((v) => v / 255).map((c) => (c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)));
|
||||
return 0.2126 * srgb[0] + 0.7152 * srgb[1] + 0.0722 * srgb[2];
|
||||
}
|
||||
|
||||
export function bestTextColor(bg) {
|
||||
const rgb = hexToRgb(normalizeHexColor(bg));
|
||||
if (!rgb) return '#0f172a';
|
||||
return relativeLuminance(rgb) < 0.45 ? '#ffffff' : '#0f172a';
|
||||
}
|
||||
|
||||
export function tagStyle(t) {
|
||||
const bg = normalizeHexColor(t?.color || t?.cor);
|
||||
if (!bg) return {};
|
||||
return { background: bg, color: bestTextColor(bg), borderColor: 'transparent' };
|
||||
}
|
||||
@@ -26,6 +26,17 @@ import { usePatientSessions } from '@/features/patients/composables/usePatientSe
|
||||
import { usePatientFinancial } from '@/features/patients/composables/usePatientFinancial';
|
||||
import { usePatientMessages } from '@/features/patients/composables/usePatientMessages';
|
||||
import { usePatientDocuments } from '@/features/patients/composables/usePatientDocuments';
|
||||
import {
|
||||
calcAge,
|
||||
fmtRelative,
|
||||
fmtDateBR,
|
||||
fmtDateTimeBR,
|
||||
fmtCurrency,
|
||||
sessionDuration,
|
||||
STATUS_LABEL,
|
||||
STATUS_SEVERITY,
|
||||
tagStyle as tagStyleHelper
|
||||
} from '@/features/patients/utils/patientFormatters';
|
||||
// Tag/Skeleton: auto via PrimeVueResolver
|
||||
|
||||
const props = defineProps({
|
||||
@@ -104,15 +115,8 @@ const avatarInitials = computed(() => {
|
||||
});
|
||||
|
||||
const ageLabel = computed(() => {
|
||||
const dob = patientData.value?.data_nascimento;
|
||||
if (!dob) return '';
|
||||
const birth = new Date(dob);
|
||||
if (Number.isNaN(birth.getTime())) return '';
|
||||
const now = new Date();
|
||||
let age = now.getFullYear() - birth.getFullYear();
|
||||
const m = now.getMonth() - birth.getMonth();
|
||||
if (m < 0 || (m === 0 && now.getDate() < birth.getDate())) age--;
|
||||
return `${age} anos`;
|
||||
const age = calcAge(patientData.value?.data_nascimento);
|
||||
return age == null ? '' : `${age} anos`;
|
||||
});
|
||||
|
||||
const convenio = computed(() => patientData.value?.convenio || patientData.value?.plano_saude || '');
|
||||
@@ -120,19 +124,23 @@ const statusPaciente = computed(() => patientData.value?.status || '');
|
||||
const riscoElevado = computed(() => !!patientData.value?.risco_elevado);
|
||||
|
||||
function tagStyle(t) {
|
||||
if (!t?.color) return {};
|
||||
return {
|
||||
background: `${t.color}22`,
|
||||
color: t.color,
|
||||
border: `1px solid ${t.color}44`
|
||||
};
|
||||
return tagStyleHelper(t);
|
||||
}
|
||||
|
||||
// ── KPIs basicos pro Visao Geral (placeholder Fase 1) ──────
|
||||
// ── Notas + observacoes (campos opcionais do paciente) ─────
|
||||
function pickField(obj, keys = []) {
|
||||
for (const k of keys) {
|
||||
const v = obj?.[k];
|
||||
if (v !== null && v !== undefined && String(v).trim()) return v;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const observacoes = computed(() => pickField(patientData.value, ['observacoes', 'notes_short']));
|
||||
const notasInternas = computed(() => pickField(patientData.value, ['notas_internas', 'notes']));
|
||||
|
||||
// ── KPIs Visao Geral (Fase 2) ──────────────────────────────
|
||||
const kpiSessoes = computed(() => sessionsHook.totalSessoes.value);
|
||||
const kpiRealizadas = computed(() => sessionsHook.totalRealizadas.value);
|
||||
const kpiEmAberto = computed(() => financialHook.totalEmAberto.value);
|
||||
const kpiAtrasado = computed(() => financialHook.totalAtrasado.value);
|
||||
const kpiMensagens = computed(() => messagesHook.messages.value.length);
|
||||
const kpiDocumentos = computed(() => documentsHook.total.value);
|
||||
|
||||
@@ -397,48 +405,222 @@ void toast;
|
||||
|
||||
<!-- Conteudo de cada aba -->
|
||||
<template v-else>
|
||||
<!-- ABA: Visao Geral (KPIs simples ja funcionam Fase 1) -->
|
||||
<!-- ABA: Visao Geral (Fase 2 — KPIs + Timeline + Mensagens + Notas) -->
|
||||
<div v-if="activeTab === 'overview'" class="mpa-tab">
|
||||
<!-- ── 4 KPI cards ──────────────────────── -->
|
||||
<div class="mpa-kpis">
|
||||
<article class="mpa-kpi" style="--c:#10b981">
|
||||
<div class="mpa-kpi__icon"><i class="pi pi-calendar" /></div>
|
||||
<div class="mpa-kpi__value">{{ kpiSessoes }}</div>
|
||||
<div class="mpa-kpi__label">Sessões totais</div>
|
||||
<div class="mpa-kpi__sub">{{ kpiRealizadas }} realizadas</div>
|
||||
</article>
|
||||
<article class="mpa-kpi" style="--c:#f59e0b">
|
||||
<div class="mpa-kpi__icon"><i class="pi pi-wallet" /></div>
|
||||
<div class="mpa-kpi__value">R$ {{ kpiEmAberto.toFixed(2) }}</div>
|
||||
<div class="mpa-kpi__label">Em aberto</div>
|
||||
<div v-if="kpiAtrasado > 0" class="mpa-kpi__sub mpa-kpi__sub--alert">
|
||||
R$ {{ kpiAtrasado.toFixed(2) }} atrasado
|
||||
<!-- 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>
|
||||
<article class="mpa-kpi" style="--c:#22c55e">
|
||||
<div class="mpa-kpi__icon"><i class="pi pi-whatsapp" /></div>
|
||||
<div class="mpa-kpi__value">{{ kpiMensagens }}</div>
|
||||
<div class="mpa-kpi__label">Mensagens trocadas</div>
|
||||
|
||||
<!-- 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>
|
||||
<article class="mpa-kpi" style="--c:#f97316">
|
||||
<div class="mpa-kpi__icon"><i class="pi pi-folder" /></div>
|
||||
<div class="mpa-kpi__value">{{ kpiDocumentos }}</div>
|
||||
<div class="mpa-kpi__label">Documentos</div>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<div class="mpa-w">
|
||||
<div class="mpa-w__head">
|
||||
<div class="mpa-w__icon"><i class="pi pi-info-circle" /></div>
|
||||
<div class="mpa-w__title">
|
||||
<div class="mpa-w__title-text">Visão Geral — Fase 1</div>
|
||||
<div class="mpa-w__sub">KPIs básicos já funcionam. Timeline + cards ricos: Fase 2.</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>
|
||||
</div>
|
||||
<div class="mpa-w__body">
|
||||
<p class="mpa-placeholder">
|
||||
Esta aba será expandida na <strong>Fase 2</strong> com timeline de últimas
|
||||
interações, próxima sessão, alertas e cards de últimas mensagens/lançamentos.
|
||||
</p>
|
||||
</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>
|
||||
@@ -1019,62 +1201,360 @@ void toast;
|
||||
border-color: color-mix(in srgb, var(--p-primary-color) 28%, transparent);
|
||||
}
|
||||
|
||||
/* ═══════ KPIs (Visao Geral Fase 1) ═══════ */
|
||||
/* ═══════ KPIs (Visao Geral Fase 2 — 4 cards ricos) ═══════ */
|
||||
.mpa-kpis {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.mpa-kpi {
|
||||
position: relative;
|
||||
padding: 14px;
|
||||
padding: 14px 14px 12px;
|
||||
border-radius: 12px;
|
||||
background: var(--m-bg-soft);
|
||||
border: 1px solid var(--m-border);
|
||||
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 60%);
|
||||
opacity: 0.08;
|
||||
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) 15%, transparent);
|
||||
background: color-mix(in srgb, var(--c, #6366f1) 16%, transparent);
|
||||
color: var(--c, #6366f1);
|
||||
margin-bottom: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mpa-kpi__icon > i { font-size: 0.85rem; }
|
||||
.mpa-kpi__value {
|
||||
font-size: 1.4rem;
|
||||
.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.1;
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.02em;
|
||||
margin-top: auto;
|
||||
}
|
||||
.mpa-kpi__label {
|
||||
font-size: 0.78rem;
|
||||
.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__sub {
|
||||
.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);
|
||||
opacity: 0.8;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.mpa-kpi__sub--alert { color: rgb(220, 38, 38); font-weight: 600; }
|
||||
.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;
|
||||
}
|
||||
|
||||
/* ═══════ 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 {
|
||||
|
||||
Reference in New Issue
Block a user