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:
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
import { ref, computed } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { fmtSize, DOC_TYPE_LABEL } from '@/features/patients/utils/patientFormatters';
|
||||
|
||||
export function usePatientDocuments() {
|
||||
const documents = ref([]);
|
||||
@@ -57,6 +58,34 @@ export function usePatientDocuments() {
|
||||
});
|
||||
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 {
|
||||
documents,
|
||||
loading,
|
||||
@@ -65,6 +94,9 @@ export function usePatientDocuments() {
|
||||
total,
|
||||
totalBytes,
|
||||
tiposCount,
|
||||
ultimo
|
||||
ultimo,
|
||||
topType,
|
||||
pendentes,
|
||||
sizeTotalFormatted
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,6 +50,16 @@ export function usePatientMessages() {
|
||||
messages.value.filter((m) => m.direction === 'out' || m.direction === 'outbound').length
|
||||
);
|
||||
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 {
|
||||
messages,
|
||||
@@ -59,6 +69,8 @@ export function usePatientMessages() {
|
||||
recentes,
|
||||
totalIn,
|
||||
totalOut,
|
||||
ultimaMensagem
|
||||
ultimaMensagem,
|
||||
primeiraMensagem,
|
||||
canais
|
||||
};
|
||||
}
|
||||
|
||||
@@ -153,6 +153,43 @@ export function fmtDateTimeBR(iso) {
|
||||
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) {
|
||||
if (v === null || v === undefined || v === '') return '—';
|
||||
return `R$ ${Number(v).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
|
||||
Reference in New Issue
Block a user