diff --git a/Obsidian/Brain/log.md b/Obsidian/Brain/log.md index 04f3859..b318e87 100644 --- a/Obsidian/Brain/log.md +++ b/Obsidian/Brain/log.md @@ -50,6 +50,66 @@ Touched: none ## [2026-05-08 00:00] session | Melissa cfg-* nativas + temas + cronometro DB 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 Touched: none Detalhes: Tab Financeiro espelha o legacy + adiciona mutation que o diff --git a/src/features/patients/composables/usePatientDocuments.js b/src/features/patients/composables/usePatientDocuments.js index 873aeac..795de83 100644 --- a/src/features/patients/composables/usePatientDocuments.js +++ b/src/features/patients/composables/usePatientDocuments.js @@ -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 }; } diff --git a/src/features/patients/composables/usePatientMessages.js b/src/features/patients/composables/usePatientMessages.js index 543e87c..88fb189 100644 --- a/src/features/patients/composables/usePatientMessages.js +++ b/src/features/patients/composables/usePatientMessages.js @@ -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 }; } diff --git a/src/features/patients/utils/patientFormatters.js b/src/features/patients/utils/patientFormatters.js index 548e870..c82ce8e 100644 --- a/src/features/patients/utils/patientFormatters.js +++ b/src/features/patients/utils/patientFormatters.js @@ -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 })}`; diff --git a/src/layout/melissa/MelissaPaciente.vue b/src/layout/melissa/MelissaPaciente.vue index f8d329f..dc76552 100644 --- a/src/layout/melissa/MelissaPaciente.vue +++ b/src/layout/melissa/MelissaPaciente.vue @@ -21,6 +21,8 @@ import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'; import { useToast } from 'primevue/usetoast'; 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 { usePatientSessions } from '@/features/patients/composables/usePatientSessions'; import { usePatientFinancial } from '@/features/patients/composables/usePatientFinancial'; @@ -41,6 +43,7 @@ import { fmtMarital, fmtPhoneMobile, sessionDuration, + chConvLabel, recordStatus, RECORD_STATUS_LABEL, fmtPaymentMethod, @@ -318,7 +321,6 @@ const sessoesComEvolucao = computed(() => const kpiSessoes = computed(() => sessionsHook.totalSessoes.value); const kpiRealizadas = computed(() => sessionsHook.totalRealizadas.value); const kpiMensagens = computed(() => messagesHook.messages.value.length); -const kpiDocumentos = computed(() => documentsHook.total.value); // ── Acoes ────────────────────────────────────────────────── function close() { emit('close'); } @@ -1638,41 +1640,179 @@ onBeforeUnmount(() => { - +
-
-
-
-
-
Documentos — Fase 7
-
Listagem completa + upload + preview
-
-
-
-

- Em desenvolvimento — Fase 7. Total: {{ kpiDocumentos }} documentos. -

-
+ +
+ Carregando…
+ +
- +
-
-
-
-
-
Conversas — Fase 7
-
Thread completa + envio
-
-
-
-

- Em desenvolvimento — Fase 7. {{ messagesHook.totalIn.value }} recebidas - / {{ messagesHook.totalOut.value }} enviadas. -

-
+ +
+ Carregando…
+ +
@@ -2662,6 +2802,57 @@ onBeforeUnmount(() => { 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) ═══════ */ .mpa-fin__head-actions { display: flex;