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