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" /> + + + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ +