From 64005a5b079dd01f45725862714fda9bcc9839da Mon Sep 17 00:00:00 2001 From: Leonardo Date: Fri, 8 May 2026 11:12:22 -0300 Subject: [PATCH] MelissaPaciente: fix openWhatsapp + dialog inline novo lancamento financeiro MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DOIS BUGS DE COMPORTAMENTO: 1. openWhatsapp nao abria o drawer conversationDrawerStore.openForPatient(patientId) espera STRING id, nao objeto. Eu passava { id, name, phone, avatar_url } — store ignorava e drawer nunca abria. FIX: passar String(props.patientId) (mesmo pattern que MelissaPacientes). BONUS: a store seta this.error sem dar throw quando paciente nao tem telefone cadastrado. Detectamos com `if (err && !isOpen)` e mostramos toast warn com a mensagem da store ("Paciente sem telefone cadastrado"). Funcao virou async pra aguardar o openForPatient. 2. addFinancial era placeholder "Em breve" User correto: o sistema ja tem suporte (composables/useFinancialRecords tem createManualRecord). Implementado dialog inline simples no prontuario. NOVO em src/features/patients/composables/usePatientFinancial.js - createRecord(patientId, payload) — INSERT financial_records com type='receita', resolve owner_id (auth.getUser) e tenant_id (lazy import tenantStore pra evitar circular). Auto-reload via _lastPatientId. Retorna {ok, data?, error?}. NOVO em MelissaPaciente.vue - Refs novoLancOpen + novoLancForm (description/amount/due_date/payment_method) - PAYMENT_METHODS array (Pix/Cartao/Dinheiro/Transferencia/Boleto/Convenio) - addFinancial() agora abre o dialog (era toast "em breve") - salvarLancamento() handler com validacao (valor > 0, due_date obrigatorio) - v-model:visible 420px com form: descricao + grid 2-col (valor InputNumber BRL + vencimento date input) + select forma - CSS .mpa-novo-lanc + responsive (1-col em <540px) ESLint: 0 errors. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../composables/usePatientFinancial.js | 55 ++++- src/layout/melissa/MelissaPaciente.vue | 191 ++++++++++++++++-- 2 files changed, 226 insertions(+), 20 deletions(-) diff --git a/src/features/patients/composables/usePatientFinancial.js b/src/features/patients/composables/usePatientFinancial.js index bf1fac2..f29ceb5 100644 --- a/src/features/patients/composables/usePatientFinancial.js +++ b/src/features/patients/composables/usePatientFinancial.js @@ -138,6 +138,58 @@ export function usePatientFinancial() { } } + /** + * Cria um novo lancamento manual (type=receita) pro paciente. + * Insere com tenant_id + owner_id resolvidos via auth/tenant store. + * Auto-reload ao final pra refletir nos KPIs e tabela. + * + * payload: { description, amount, due_date, payment_method? } + * Retorna {ok, data?, error?}. + */ + async function createRecord(patientId, payload = {}) { + if (!patientId || busy.value) return { ok: false, error: 'busy' }; + if (!payload?.amount || Number.isNaN(Number(payload.amount))) { + return { ok: false, error: 'Valor invalido' }; + } + busy.value = true; + try { + const { data: userData } = await supabase.auth.getUser(); + const ownerId = userData?.user?.id; + // tenant_id: tenta tenantStore lazy import, fallback null (RLS + // via owner_id ainda permite insert). + let tenantId = null; + try { + const { useTenantStore } = await import('@/stores/tenantStore'); + tenantId = useTenantStore().activeTenantId || null; + } catch { /* sem tenant store — segue */ } + + const row = { + patient_id: patientId, + owner_id: ownerId, + tenant_id: tenantId, + type: 'receita', + amount: Number(payload.amount), + due_date: payload.due_date || null, + description: String(payload.description || '').trim() || null, + payment_method: payload.payment_method || null, + paid_at: null + }; + + const { data, error: err } = await supabase + .from('financial_records') + .insert([row]) + .select() + .single(); + if (err) throw err; + if (_lastPatientId) await load(_lastPatientId); + return { ok: true, data }; + } catch (e) { + return { ok: false, error: e?.message || 'Erro ao criar lançamento' }; + } finally { + busy.value = false; + } + } + /** * Reverte: remove paid_at (volta pra pendente). Auto-reload. */ @@ -172,6 +224,7 @@ export function usePatientFinancial() { statusFinanceiro, recordsOrdenados, markPaid, - markUnpaid + markUnpaid, + createRecord }; } diff --git a/src/layout/melissa/MelissaPaciente.vue b/src/layout/melissa/MelissaPaciente.vue index 4a5d3b0..7857fa4 100644 --- a/src/layout/melissa/MelissaPaciente.vue +++ b/src/layout/melissa/MelissaPaciente.vue @@ -357,19 +357,27 @@ async function onPatientSaved() { } } -// Open WhatsApp: usa o conversationDrawerStore global (mesmo padrao -// que MelissaPacientes usa). O drawer desce sobre o Melissa sem fechar. -function openWhatsapp() { +// 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) return; - const data = patientData.value || {}; + if (!props.patientId) { + toast.add({ severity: 'warn', summary: 'Paciente sem ID', life: 2500 }); + return; + } try { - if (typeof conversationDrawerStore.openForPatient === 'function') { - conversationDrawerStore.openForPatient({ - id: props.patientId, - name: data.nome_completo || data.nome || '', - phone: data.telefone || data.phone || '', - avatar_url: data.avatar_url || data.avatar || '' + 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) { @@ -382,17 +390,64 @@ function openWhatsapp() { } } -// Add financial: por enquanto so emite. Futuro: dialog inline de -// novo lancamento (Fase 9). +// 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); - toast.add({ - severity: 'info', - summary: 'Em breve', - detail: 'Dialog de novo lançamento será adicionado numa próxima sessão.', - life: 3000 - }); + if (!props.patientId) { + toast.add({ severity: 'warn', summary: 'Paciente sem ID', life: 2500 }); + return; + } + novoLancForm.value = { + description: '', + amount: null, + due_date: new Date().toISOString().slice(0, 10), + payment_method: '' + }; + novoLancOpen.value = true; } +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) { @@ -1891,6 +1946,75 @@ onBeforeUnmount(() => { :patient-id="cadastroPatientId" @created="onPatientSaved" /> + + + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ +