MelissaPaciente: fix openWhatsapp + dialog inline novo lancamento financeiro
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)
- <Dialog> 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
<!-- Dialog: novo lancamento manual (chamado pelo botao "Lancamento"
|
||||
da sidebar Acoes Rapidas e CTA do empty state da Tab Financeiro). -->
|
||||
<Dialog
|
||||
v-model:visible="novoLancOpen"
|
||||
modal
|
||||
dismissable-mask
|
||||
:style="{ width: '420px', maxWidth: '92vw' }"
|
||||
header="Novo lançamento"
|
||||
>
|
||||
<div class="mpa-novo-lanc">
|
||||
<div class="mpa-novo-lanc__field">
|
||||
<label class="mpa-novo-lanc__label">Descrição</label>
|
||||
<InputText
|
||||
v-model="novoLancForm.description"
|
||||
placeholder="Ex: Sessão semanal, Avaliação inicial…"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="mpa-novo-lanc__row">
|
||||
<div class="mpa-novo-lanc__field">
|
||||
<label class="mpa-novo-lanc__label">Valor (R$) *</label>
|
||||
<InputNumber
|
||||
v-model="novoLancForm.amount"
|
||||
mode="currency"
|
||||
currency="BRL"
|
||||
locale="pt-BR"
|
||||
:min="0"
|
||||
class="w-full"
|
||||
input-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="mpa-novo-lanc__field">
|
||||
<label class="mpa-novo-lanc__label">Vencimento *</label>
|
||||
<InputText
|
||||
v-model="novoLancForm.due_date"
|
||||
type="date"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mpa-novo-lanc__field">
|
||||
<label class="mpa-novo-lanc__label">Forma de pagamento</label>
|
||||
<Select
|
||||
v-model="novoLancForm.payment_method"
|
||||
:options="PAYMENT_METHODS"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
placeholder="Opcional"
|
||||
class="w-full"
|
||||
show-clear
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<button class="mpa-quick-btn" @click="novoLancOpen = false">
|
||||
<i class="pi pi-times" />
|
||||
<span>Cancelar</span>
|
||||
</button>
|
||||
<button
|
||||
class="mpa-quick-btn mpa-quick-btn--cta"
|
||||
:disabled="financialHook.busy.value"
|
||||
@click="salvarLancamento"
|
||||
>
|
||||
<i :class="financialHook.busy.value ? 'pi pi-spin pi-spinner' : 'pi pi-check'" :style="{ color: '#10b981' }" />
|
||||
<span>Salvar lançamento</span>
|
||||
</button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -2886,6 +3010,35 @@ onBeforeUnmount(() => {
|
||||
font-size: 0.66rem !important;
|
||||
}
|
||||
|
||||
/* ═══════ Dialog: novo lancamento (form simples) ═══════ */
|
||||
.mpa-novo-lanc {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
.mpa-novo-lanc__row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
@media (max-width: 540px) {
|
||||
.mpa-novo-lanc__row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
.mpa-novo-lanc__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
.mpa-novo-lanc__label {
|
||||
font-size: 0.74rem;
|
||||
font-weight: 700;
|
||||
color: var(--m-text);
|
||||
}
|
||||
|
||||
/* ═══════ Tab Conversas (Fase 7) — CTA pra drawer ═══════ */
|
||||
.mpa-conv-cta {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user