MelissaPaciente Fase 7: Tabs Documentos + Conversas (KPIs + embed componentes existentes)
Duas tabs entregues numa sessao — sao mais leves porque reusam
DocumentsListPage e PatientConversationsTab existentes (testados em
producao no PatientProntuario legacy) com KPIs Melissa por cima.
EXTENSAO src/features/patients/utils/patientFormatters.js
- fmtSize(bytes): legivel B/KB/MB/GB
- DOC_TYPE_LABEL map: atestado/receita/laudo/encaminhamento/termo/etc
- chConvLabel(c): whatsapp -> WhatsApp / sms -> SMS / email -> E-mail
EXTENSAO src/features/patients/composables/usePatientDocuments.js
- topType computed: { tipo, count, label } do tipo mais comum
- pendentes computed: count status_revisao === 'pendente'
- sizeTotalFormatted computed: fmtSize(totalBytes)
EXTENSAO src/features/patients/composables/usePatientMessages.js
- primeiraMensagem computed (mais antiga)
- canais computed: Set de m.channel unicos
MELISSAPACIENTE.VUE — Tab Documentos
- 4 KPIs adaptativos (so renderizam com dados):
Total + sizeTotalFormatted / Mais comum / Ultimo / Revisao pendente
- DocumentsListPage embedded no card Melissa (mpa-embed wrapper).
Reusa upload/preview/listagem testados.
MELISSAPACIENTE.VUE — Tab Conversas
- 4 KPIs: Mensagens com canais / Recebidas % / Enviadas % / Ultima
- CTA "Abrir conversa no drawer" estilo WhatsApp pill verde #25d366
que emite open-whatsapp pro parent (Fase 8 integra com
conversationDrawerStore.openForPatient)
- PatientConversationsTab embedded — thread completa com filter/media
CSS: ~50L novos (mpa-conv-cta + mpa-embed wrapper).
Removido kpiDocumentos nao usado (substituido por documentsHook.total
direto).
ESLint: 0 errors da minha mudanca.
PROXIMA: Fase 8 wire-up final (Dialog -> router.push em MelissaPacientes/
MelissaAgenda; decisao sobre TherapistDashboard + PatientsListPage).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,66 @@ Touched: none
|
|||||||
## [2026-05-08 00:00] session | Melissa cfg-* nativas + temas + cronometro DB
|
## [2026-05-08 00:00] session | Melissa cfg-* nativas + temas + cronometro DB
|
||||||
Touched: none
|
Touched: none
|
||||||
|
|
||||||
|
## [2026-05-08 18:30] session | MelissaPaciente Fase 7 — Tabs Documentos + Conversas
|
||||||
|
Touched: none
|
||||||
|
Detalhes: Duas tabs entregues numa sessao (sao mais leves: KPIs + embed
|
||||||
|
de componentes existentes ja testados).
|
||||||
|
|
||||||
|
EXTENSAO patientFormatters.js:
|
||||||
|
- fmtSize(bytes): B/KB/MB/GB legivel
|
||||||
|
- DOC_TYPE_LABEL: atestado/receita/laudo/encaminhamento/termo/etc
|
||||||
|
- chConvLabel: whatsapp/sms/email -> WhatsApp/SMS/E-mail
|
||||||
|
|
||||||
|
EXTENSAO usePatientDocuments.js:
|
||||||
|
- topType computed: { tipo, count, label } do mais comum (DOC_TYPE_LABEL)
|
||||||
|
- pendentes computed: count de status_revisao === 'pendente'
|
||||||
|
- sizeTotalFormatted computed: fmtSize(totalBytes)
|
||||||
|
- Import patientFormatters dentro do composable.
|
||||||
|
|
||||||
|
EXTENSAO usePatientMessages.js:
|
||||||
|
- primeiraMensagem computed (mais antiga, [length-1])
|
||||||
|
- canais computed: Set de m.channel unicos
|
||||||
|
|
||||||
|
MELISSAPACIENTE.VUE — script
|
||||||
|
- Imports: DocumentsListPage, PatientConversationsTab, chConvLabel
|
||||||
|
- Removido kpiDocumentos (nao usado mais — substituido por
|
||||||
|
documentsHook.total.value direto)
|
||||||
|
|
||||||
|
MELISSAPACIENTE.VUE — Tab Documentos (Fase 7)
|
||||||
|
- Loading state.
|
||||||
|
- 4 KPIs adaptativos (so renderizam quando ha dados):
|
||||||
|
- 01 Total + sizeTotalFormatted
|
||||||
|
- 02 Mais comum (label do tipo + count) — opcional
|
||||||
|
- 03 Ultimo + relative + dateBR — opcional
|
||||||
|
- 04 Revisao pendente (laranja) — opcional, so quando > 0
|
||||||
|
- DocumentsListPage embedded no card Melissa (mpa-embed) — reusa o
|
||||||
|
componente existente que ja faz upload/preview/listagem completa.
|
||||||
|
Wrapper ze-ra padding pra ele preencher tudo.
|
||||||
|
|
||||||
|
MELISSAPACIENTE.VUE — Tab Conversas (Fase 7)
|
||||||
|
- Loading state.
|
||||||
|
- 4 KPIs (so renderizam quando ha mensagens):
|
||||||
|
- 01 Mensagens total + canais ("via WhatsApp, SMS")
|
||||||
|
- 02 Recebidas + % do total
|
||||||
|
- 03 Enviadas + % do total
|
||||||
|
- 04 Ultima relative + direction + 1ª contato dim
|
||||||
|
- CTA "Abrir conversa no drawer" estilo WhatsApp (verde #25d366) que
|
||||||
|
emite open-whatsapp pro parent (futuro: integra com
|
||||||
|
conversationDrawerStore.openForPatient na Fase 8).
|
||||||
|
- PatientConversationsTab embedded no mesmo wrapper mpa-embed —
|
||||||
|
thread completa com filter/scroll/media.
|
||||||
|
|
||||||
|
CSS: ~50L novos pros componentes (mpa-conv-cta + mpa-embed wrapper).
|
||||||
|
Padrao Melissa: CTA WhatsApp circular pill, embed wrapper transparente.
|
||||||
|
|
||||||
|
ESLint: 0 errors da minha mudanca.
|
||||||
|
|
||||||
|
PROXIMA: Fase 8 (wire-up final) — substituir Dialog do PatientProntuario
|
||||||
|
por router.push('/melissa/paciente?id=X') nos 4 callsites Melissa
|
||||||
|
(MelissaPacientes, MelissaAgenda); decidir se TherapistDashboard e
|
||||||
|
PatientsListPage tambem migram. PatientProntuario.vue pode ficar (legacy
|
||||||
|
fallback) ou deletar.
|
||||||
|
|
||||||
## [2026-05-08 17:30] session | MelissaPaciente Fase 6 — Tab Financeiro completa + mark paid mutation
|
## [2026-05-08 17:30] session | MelissaPaciente Fase 6 — Tab Financeiro completa + mark paid mutation
|
||||||
Touched: none
|
Touched: none
|
||||||
Detalhes: Tab Financeiro espelha o legacy + adiciona mutation que o
|
Detalhes: Tab Financeiro espelha o legacy + adiciona mutation que o
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { supabase } from '@/lib/supabase/client';
|
import { supabase } from '@/lib/supabase/client';
|
||||||
|
import { fmtSize, DOC_TYPE_LABEL } from '@/features/patients/utils/patientFormatters';
|
||||||
|
|
||||||
export function usePatientDocuments() {
|
export function usePatientDocuments() {
|
||||||
const documents = ref([]);
|
const documents = ref([]);
|
||||||
@@ -57,6 +58,34 @@ export function usePatientDocuments() {
|
|||||||
});
|
});
|
||||||
const ultimo = computed(() => documents.value[0] || null);
|
const ultimo = computed(() => documents.value[0] || null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tipo de documento mais comum (alimenta KPI "Mais comum").
|
||||||
|
* Retorna { tipo, count, label } ou null se vazio.
|
||||||
|
*/
|
||||||
|
const topType = computed(() => {
|
||||||
|
const por = {};
|
||||||
|
for (const d of documents.value) {
|
||||||
|
const t = d.tipo_documento || 'outro';
|
||||||
|
por[t] = (por[t] || 0) + 1;
|
||||||
|
}
|
||||||
|
const entries = Object.entries(por).sort((a, b) => b[1] - a[1]);
|
||||||
|
if (!entries.length) return null;
|
||||||
|
const [tipo, count] = entries[0];
|
||||||
|
return { tipo, count, label: DOC_TYPE_LABEL[tipo] || tipo };
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count de documentos com status_revisao === 'pendente'.
|
||||||
|
*/
|
||||||
|
const pendentes = computed(() =>
|
||||||
|
documents.value.filter((d) => d.status_revisao === 'pendente').length
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tamanho total formatado em string legivel (B/KB/MB/GB).
|
||||||
|
*/
|
||||||
|
const sizeTotalFormatted = computed(() => fmtSize(totalBytes.value));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
documents,
|
documents,
|
||||||
loading,
|
loading,
|
||||||
@@ -65,6 +94,9 @@ export function usePatientDocuments() {
|
|||||||
total,
|
total,
|
||||||
totalBytes,
|
totalBytes,
|
||||||
tiposCount,
|
tiposCount,
|
||||||
ultimo
|
ultimo,
|
||||||
|
topType,
|
||||||
|
pendentes,
|
||||||
|
sizeTotalFormatted
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,16 @@ export function usePatientMessages() {
|
|||||||
messages.value.filter((m) => m.direction === 'out' || m.direction === 'outbound').length
|
messages.value.filter((m) => m.direction === 'out' || m.direction === 'outbound').length
|
||||||
);
|
);
|
||||||
const ultimaMensagem = computed(() => messages.value[0] || null);
|
const ultimaMensagem = computed(() => messages.value[0] || null);
|
||||||
|
const primeiraMensagem = computed(() => messages.value[messages.value.length - 1] || null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canais unicos usados nas mensagens (whatsapp, sms, email).
|
||||||
|
*/
|
||||||
|
const canais = computed(() => {
|
||||||
|
const set = new Set();
|
||||||
|
for (const m of messages.value) if (m.channel) set.add(m.channel);
|
||||||
|
return [...set];
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
messages,
|
messages,
|
||||||
@@ -59,6 +69,8 @@ export function usePatientMessages() {
|
|||||||
recentes,
|
recentes,
|
||||||
totalIn,
|
totalIn,
|
||||||
totalOut,
|
totalOut,
|
||||||
ultimaMensagem
|
ultimaMensagem,
|
||||||
|
primeiraMensagem,
|
||||||
|
canais
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,6 +153,43 @@ export function fmtDateTimeBR(iso) {
|
|||||||
return `${dd}/${mm}/${d.getFullYear()} ${hh}:${mi}`;
|
return `${dd}/${mm}/${d.getFullYear()} ${hh}:${mi}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bytes -> string legivel (B/KB/MB/GB).
|
||||||
|
*/
|
||||||
|
export function fmtSize(bytes) {
|
||||||
|
const b = Number(bytes) || 0;
|
||||||
|
if (b < 1024) return `${b} B`;
|
||||||
|
if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)} KB`;
|
||||||
|
if (b < 1024 * 1024 * 1024) return `${(b / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
|
return `${(b / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map de tipos de documento clinico pra label pt-br.
|
||||||
|
*/
|
||||||
|
export const DOC_TYPE_LABEL = {
|
||||||
|
atestado: 'Atestado',
|
||||||
|
receita: 'Receita',
|
||||||
|
laudo: 'Laudo',
|
||||||
|
encaminhamento: 'Encaminhamento',
|
||||||
|
termo: 'Termo',
|
||||||
|
termo_assinado: 'Termo assinado',
|
||||||
|
relatorio: 'Relatório',
|
||||||
|
declaracao: 'Declaração',
|
||||||
|
outro: 'Outro'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Channel label pra conversa: whatsapp -> WhatsApp, sms -> SMS, email -> E-mail.
|
||||||
|
*/
|
||||||
|
export function chConvLabel(c) {
|
||||||
|
const k = String(c || '').toLowerCase();
|
||||||
|
if (k === 'whatsapp') return 'WhatsApp';
|
||||||
|
if (k === 'sms') return 'SMS';
|
||||||
|
if (k === 'email') return 'E-mail';
|
||||||
|
return c || '';
|
||||||
|
}
|
||||||
|
|
||||||
export function fmtCurrency(v) {
|
export function fmtCurrency(v) {
|
||||||
if (v === null || v === undefined || v === '') return '—';
|
if (v === null || v === undefined || v === '') return '—';
|
||||||
return `R$ ${Number(v).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
return `R$ ${Number(v).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue';
|
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import MelissaConfigList from './MelissaConfigList.vue';
|
import MelissaConfigList from './MelissaConfigList.vue';
|
||||||
|
import DocumentsListPage from '@/features/documents/DocumentsListPage.vue';
|
||||||
|
import PatientConversationsTab from '@/features/patients/prontuario/PatientConversationsTab.vue';
|
||||||
import { usePatientDetail } from '@/features/patients/composables/usePatientDetail';
|
import { usePatientDetail } from '@/features/patients/composables/usePatientDetail';
|
||||||
import { usePatientSessions } from '@/features/patients/composables/usePatientSessions';
|
import { usePatientSessions } from '@/features/patients/composables/usePatientSessions';
|
||||||
import { usePatientFinancial } from '@/features/patients/composables/usePatientFinancial';
|
import { usePatientFinancial } from '@/features/patients/composables/usePatientFinancial';
|
||||||
@@ -41,6 +43,7 @@ import {
|
|||||||
fmtMarital,
|
fmtMarital,
|
||||||
fmtPhoneMobile,
|
fmtPhoneMobile,
|
||||||
sessionDuration,
|
sessionDuration,
|
||||||
|
chConvLabel,
|
||||||
recordStatus,
|
recordStatus,
|
||||||
RECORD_STATUS_LABEL,
|
RECORD_STATUS_LABEL,
|
||||||
fmtPaymentMethod,
|
fmtPaymentMethod,
|
||||||
@@ -318,7 +321,6 @@ const sessoesComEvolucao = computed(() =>
|
|||||||
const kpiSessoes = computed(() => sessionsHook.totalSessoes.value);
|
const kpiSessoes = computed(() => sessionsHook.totalSessoes.value);
|
||||||
const kpiRealizadas = computed(() => sessionsHook.totalRealizadas.value);
|
const kpiRealizadas = computed(() => sessionsHook.totalRealizadas.value);
|
||||||
const kpiMensagens = computed(() => messagesHook.messages.value.length);
|
const kpiMensagens = computed(() => messagesHook.messages.value.length);
|
||||||
const kpiDocumentos = computed(() => documentsHook.total.value);
|
|
||||||
|
|
||||||
// ── Acoes ──────────────────────────────────────────────────
|
// ── Acoes ──────────────────────────────────────────────────
|
||||||
function close() { emit('close'); }
|
function close() { emit('close'); }
|
||||||
@@ -1638,41 +1640,179 @@ onBeforeUnmount(() => {
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ABA: Documentos -->
|
<!-- ABA: Documentos (Fase 7 — KPIs + DocumentsListPage embedded) -->
|
||||||
<div v-else-if="activeTab === 'doc'" class="mpa-tab">
|
<div v-else-if="activeTab === 'doc'" class="mpa-tab">
|
||||||
<div class="mpa-w">
|
<!-- Loading -->
|
||||||
<div class="mpa-w__head">
|
<div v-if="documentsHook.loading.value" class="mpa-empty">
|
||||||
<div class="mpa-w__icon mpa-w__icon--orange"><i class="pi pi-folder" /></div>
|
<i class="pi pi-spin pi-spinner mr-2" /> Carregando…
|
||||||
<div class="mpa-w__title">
|
|
||||||
<div class="mpa-w__title-text">Documentos — Fase 7</div>
|
|
||||||
<div class="mpa-w__sub">Listagem completa + upload + preview</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mpa-w__body">
|
|
||||||
<p class="mpa-placeholder">
|
|
||||||
Em desenvolvimento — <strong>Fase 7</strong>. Total: <strong>{{ kpiDocumentos }}</strong> documentos.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ABA: Conversas -->
|
<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">
|
<div v-else-if="activeTab === 'conv'" class="mpa-tab">
|
||||||
<div class="mpa-w">
|
<!-- Loading -->
|
||||||
<div class="mpa-w__head">
|
<div v-if="messagesHook.loading.value" class="mpa-empty">
|
||||||
<div class="mpa-w__icon" style="background:#22c55e22;color:#22c55e"><i class="pi pi-whatsapp" /></div>
|
<i class="pi pi-spin pi-spinner mr-2" /> Carregando…
|
||||||
<div class="mpa-w__title">
|
|
||||||
<div class="mpa-w__title-text">Conversas — Fase 7</div>
|
|
||||||
<div class="mpa-w__sub">Thread completa + envio</div>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<div class="mpa-w__body">
|
</article>
|
||||||
<p class="mpa-placeholder">
|
|
||||||
Em desenvolvimento — <strong>Fase 7</strong>. {{ messagesHook.totalIn.value }} recebidas
|
<article class="mpa-kpi" style="--c:#4ade80">
|
||||||
/ {{ messagesHook.totalOut.value }} enviadas.
|
<span class="mpa-kpi__num">02</span>
|
||||||
</p>
|
<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>
|
</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>
|
</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">
|
||||||
|
· 1ª {{ 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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -2662,6 +2802,57 @@ onBeforeUnmount(() => {
|
|||||||
font-size: 0.66rem !important;
|
font-size: 0.66rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ═══════ 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) ═══════ */
|
/* ═══════ Tab Financeiro (Fase 6) ═══════ */
|
||||||
.mpa-fin__head-actions {
|
.mpa-fin__head-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user