/* |-------------------------------------------------------------------------- | Agência PSI |-------------------------------------------------------------------------- | Arquivo: src/features/agenda/composables/useAgendaEventPickerBilling.js | Data: 2026-05-04 | | A66 sub-sessão 1C-ii-a — handlers de patient picker + billing items + | 2 watchers (commitment_id auto-fill price, insurance_plan_id limpa | items + reset campos). | | Não inclui (vai pra 1C-ii-b): | - Watcher props.modelValue (init form ao abrir — tem dependências | em loadPatients/ensureServicesLoaded/loadInsurancePlans/ | _loadCommitmentItemsForEvent + supabase pra buscar nome do paciente) | - Watchers de online slots, solicitações pendentes | - Series pills handlers | - Slot selection | - Quick-creates wiring | - onSendManualReminder | | Recebe via argumento: | composer — refs + computeds do composer (1B) | _actions — refs internos do actions (1C-i): _restoringConvenio | commitmentItems — ref | servicePickerSel — ref do select picker (limpado ao trocar convenio) | selectedPlanService — ref do procedure de convênio | services — ref do useServices | loadServices — fn(ownerId) do useServices | getDefaultPrice — fn() do useServices (preço default sugerido) | planServices — computed (de useInsurancePlans + form) | loadActiveDiscount — fn(ownerId, patientId) do usePatientDiscounts | _csLoadItems — fn(eventId) do useCommitmentServices | _csLoadItemsOrTemplate — fn(eventId, ruleId, opts) do useCommitmentServices | isDynamic — computed | props — props do componente parent |-------------------------------------------------------------------------- */ import { ref, watch, nextTick } from 'vue'; import { supabase } from '@/lib/supabase/client'; import { calcFinalPrice } from './agendaEventHelpers'; export function useAgendaEventPickerBilling({ composer, actions, commitmentItems, servicePickerSel, selectedPlanService, services, loadServices, getDefaultPrice, planServices, loadActiveDiscount, _csLoadItems, _csLoadItemsOrTemplate, isDynamic, props }) { // ── Patient picker state ─────────────────────────────────────── const pacientePickerOpen = ref(false); const pacienteSearch = ref(''); const pacientesLoading = ref(false); const pacientesError = ref(''); const patients = ref([]); // ── Cadastro rápido ──────────────────────────────────────────── const cadRapidoOpen = ref(false); // ── Services lazy-load gate ──────────────────────────────────── let _servicesLoaded = false; // ──────────────────────────────────────────────────────────────── // Services pré-carga (lazy: só uma vez por sessão do dialog) // ──────────────────────────────────────────────────────────────── async function ensureServicesLoaded() { if (_servicesLoaded || !props.ownerId) return; _servicesLoaded = true; await loadServices(props.ownerId); } // Reseta o gate (chamado pelo watcher de open na 1C-ii-b) function resetServicesGate() { _servicesLoaded = false; } function applyDefaultPrice() { // Skip particular: preço vem dos commitmentItems if (composer.billingType.value === 'particular') return; // Só auto-preenche em criação (edição preserva o valor salvo) if (!composer.isEdit.value) { const suggested = getDefaultPrice(); if (suggested != null) composer.form.value.price = suggested; } } // ──────────────────────────────────────────────────────────────── // Billing items (commitment_services) // ──────────────────────────────────────────────────────────────── /** * Adiciona um serviço ao billing. Regras: * - Não duplica: se já existe item do mesmo service_id, incrementa quantity * - Aplica desconto ativo do paciente (se houver) * - Recalcula final_price via helper puro */ async function addItem(svc) { if (!svc?.id) return; const existing = commitmentItems.value.find((i) => i.service_id === svc.id); if (existing) { existing.quantity++; existing.final_price = calcFinalPrice(existing.unit_price, existing.quantity, existing.discount_pct, existing.discount_flat); return; } const unit_price = Number(svc.price); const patientId = composer.form.value.patient_id ?? composer.form.value.paciente_id ?? null; let discount_pct = 0; let discount_flat = 0; if (patientId && props.ownerId) { const discount = await loadActiveDiscount(props.ownerId, patientId); if (discount) { discount_pct = Number(discount.discount_pct ?? 0); discount_flat = Number(discount.discount_flat ?? 0); } } commitmentItems.value.push({ service_id: svc.id, service_name: svc.name, quantity: 1, unit_price, discount_pct, discount_flat, final_price: calcFinalPrice(unit_price, 1, discount_pct, discount_flat) }); } function removeItem(index) { commitmentItems.value.splice(index, 1); // Lista vazia em modo dinâmico → restaura duração padrão if (commitmentItems.value.length === 0 && isDynamic.value) { composer.form.value.duracaoMin = props.agendaSettings?.session_duration_min ?? 50; } } function onItemChange(item) { item.final_price = calcFinalPrice(item.unit_price, item.quantity, item.discount_pct, item.discount_flat); } /** * Carrega items do evento (ou template da série) e detecta billingType. * Heurística: se eventRow tem insurance_plan_id → 'convenio'; senão, * presença de items → 'particular', vazio → 'gratuito'. */ async function _loadCommitmentItemsForEvent(eventId) { const ruleId = props.eventRow?.recurrence_id ?? null; const isCustomized = props.eventRow?.services_customized ?? false; const origPlanId = props.eventRow?.insurance_plan_id ?? null; const origGuide = props.eventRow?.insurance_guide_number ?? null; const origInsValue = props.eventRow?.insurance_value != null ? Number(props.eventRow.insurance_value) : null; function applyConvenio() { const origPsId = props.eventRow?.insurance_plan_service_id ?? null; actions._restoringConvenio.value = true; composer.form.value.insurance_plan_id = origPlanId; composer.form.value.insurance_guide_number = origGuide; composer.form.value.insurance_value = origInsValue; composer.form.value.insurance_plan_service_id = origPsId; composer.billingType.value = 'convenio'; nextTick(() => { if (origPsId && planServices.value.find((s) => s.id === origPsId)) { selectedPlanService.value = origPsId; } else { selectedPlanService.value = null; } actions._restoringConvenio.value = false; }); } if (!eventId && !ruleId) { commitmentItems.value = []; if (origPlanId) applyConvenio(); else composer.billingType.value = 'particular'; return; } try { commitmentItems.value = ruleId ? await _csLoadItemsOrTemplate(eventId, ruleId, { allowEmpty: isCustomized }) : await _csLoadItems(eventId); if (origPlanId) applyConvenio(); else composer.billingType.value = commitmentItems.value.length > 0 ? 'particular' : 'gratuito'; } catch (e) { // eslint-disable-next-line no-console console.warn('[useAgendaEventPickerBilling] commitment_services load error:', e?.message); commitmentItems.value = []; if (origPlanId) applyConvenio(); else composer.billingType.value = 'gratuito'; } } /** * Setter do procedimento de convênio (form.insurance_plan_service_id). * Quando muda, atualiza form.insurance_value baseado no value do procedimento. */ function onProcedureSelect(psId) { composer.form.value.insurance_plan_service_id = psId ?? null; if (!psId) { composer.form.value.insurance_value = null; return; } const ps = planServices.value.find((s) => s.id === psId); composer.form.value.insurance_value = ps?.value != null ? Number(ps.value) : null; } // ──────────────────────────────────────────────────────────────── // Patient picker // ──────────────────────────────────────────────────────────────── function selectCommitment(c) { if (!c?.id) return; composer.form.value.commitment_id = c.id; composer.form.value.extra_fields = {}; if (Array.isArray(c.fields)) { for (const f of c.fields) composer.form.value.extra_fields[f.key] = ''; } composer.step.value = 2; if (composer.requiresPatient.value) loadPatients(true); } function goBack() { if (composer.isEdit.value || !composer.allowBack.value) return; composer.step.value = 1; composer.form.value.commitment_id = null; composer.form.value.paciente_id = null; composer.form.value.paciente_nome = ''; } function openPacientePicker() { if (!composer.requiresPatient.value) return; pacientePickerOpen.value = true; loadPatients(false); } function clearPatientsCache() { patients.value = []; pacientesError.value = ''; pacienteSearch.value = ''; } async function loadPatients(force = false) { try { if (pacientesLoading.value) return; if (!force && patients.value?.length) return; pacientesError.value = ''; pacientesLoading.value = true; let q = supabase .from('patients') .select('id,nome_completo,email_principal,telefone,status,avatar_url,tenant_id,responsible_member_id,created_at') .order('created_at', { ascending: false }) .limit(500); if (props.tenantId) q = q.eq('tenant_id', props.tenantId); if (props.restrictPatientsToOwner && props.patientScopeOwnerId) { q = q.eq('responsible_member_id', props.patientScopeOwnerId); } const { data, error } = await q; if (error) throw error; patients.value = (data || []).map((r) => ({ id: r.id, nome: r.nome_completo ?? '', email: r.email_principal ?? '', telefone: r.telefone ?? '', status: r.status ?? '', avatar_url: r.avatar_url ?? '' })); } catch (e) { pacientesError.value = e?.message || 'Falha ao carregar pacientes.'; patients.value = []; } finally { pacientesLoading.value = false; } } function selectPaciente(p) { if (!p?.id) return; // Bloqueia clique em paciente arquivado/inativo — defesa em camadas: // o template do picker ja marca esses items como disabled, mas se // alguem chamar selectPaciente programaticamente (cache stale, etc), // a regra precisa valer. if (p.status && p.status !== 'Ativo') return; composer.form.value.paciente_id = p.id; composer.form.value.paciente_nome = p.nome || ''; composer.form.value.paciente_avatar = p.avatar_url || ''; // Sem isso, form.paciente_status fica '' e canSave nao consegue // aplicar getPatientAgendaPermissions — qualquer falha do filtro // acima vira sessao criavel com paciente fora do escopo. composer.form.value.paciente_status = p.status || ''; pacientePickerOpen.value = false; } function clearPaciente() { composer.form.value.paciente_id = null; composer.form.value.paciente_nome = ''; composer.form.value.paciente_avatar = ''; composer.form.value.paciente_status = ''; actions.samePatientConflict.value = null; } function openCadastroRapido() { cadRapidoOpen.value = true; } function abrirCadastroCompleto() { if (!props.newPatientRoute) return; // Abre em nova aba pra o user voltar pro dialog depois window.open(props.newPatientRoute, '_blank', 'noopener'); } // ──────────────────────────────────────────────────────────────── // Watchers // ──────────────────────────────────────────────────────────────── /** * Auto-fill de price quando o user troca commitment em criação. * Ignorado em edição pra preservar o valor salvo. */ watch( () => composer.form.value.commitment_id, async (newId) => { if (!newId || composer.isEdit.value || !composer.visible.value) return; await ensureServicesLoaded(); applyDefaultPrice(); } ); /** * Limpa procedure + campos de convênio quando muda plano. * Quando seleciona convênio: zera items (exclusividade convênio vs serviços). * `_restoringConvenio` ignora o watch durante restauração de eventRow editado. */ watch( () => composer.form.value.insurance_plan_id, (planId) => { if (actions._restoringConvenio.value) return; selectedPlanService.value = null; composer.form.value.insurance_plan_service_id = null; if (!planId) { composer.form.value.insurance_value = null; composer.form.value.insurance_guide_number = null; return; } commitmentItems.value = []; servicePickerSel.value = null; } ); return { // refs do picker pacientePickerOpen, pacienteSearch, pacientesLoading, pacientesError, patients, cadRapidoOpen, // billing/services ensureServicesLoaded, resetServicesGate, applyDefaultPrice, addItem, removeItem, onItemChange, _loadCommitmentItemsForEvent, onProcedureSelect, // picker selectCommitment, goBack, openPacientePicker, clearPatientsCache, loadPatients, selectPaciente, clearPaciente, openCadastroRapido, abrirCadastroCompleto }; }