F3 schema-per-tenant: frontend usa tenantDb() pra tabelas tenant

- useTenantDb composable + lib/supabase/tenantClient (tenantDb/tenantSchemaName)
- tenantStore: getters activeTenantSlug/activeTenantSchema; my_tenants() RPC
  passa a devolver slug+nome (migration 07)
- codemod scripts/codemod-tenant-db.py: supabase.from('<84 tabelas + 6 views
  tenant>') -> tenantDb().from(...) em 139 arquivos (777 chamadas), remove
  .eq('tenant_id') das cadeias tenant (173)
- passada manual (4 agentes): remove tenant_id de payloads insert/upsert/update,
  selects, .or/.is de defaults; onConflict ajustado pros uniques sem tenant_id
  (singletons usam 'singleton'); realtime de tabelas tenant aponta pro schema
  do tenant ativo; repos dropam tenant_id defensivamente de payloads externos
- agendaSelects: tenant_id fora do AGENDA_EVENT_SELECT (quebraria PostgREST)
- zero embeds cross-schema (todos FK embeds sao tenant->tenant ou global->global)
- build de producao passa; 67 .js checados

Pendente (fora do escopo F3, sao cross-tenant/anon -> F4/F6):
- AgendadorPublicoPage (anon, resolve tenant por link_slug)
- Saas{Feriados,NotificationTemplates,DocumentTemplates,Whatsapp}Page
  (gerenciam defaults do sistema / views cross-tenant)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-06-13 04:44:59 -03:00
parent 05c6746e33
commit a7f6bcbe66
142 changed files with 1404 additions and 1472 deletions
@@ -32,6 +32,7 @@
import { ref } from 'vue';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { buildBloqueioBackgroundEvents } from '@/features/agenda/services/agendaMappers';
export function useAgendaBloqueios() {
@@ -55,14 +56,12 @@ export function useAgendaBloqueios() {
// Query: recorrentes (qualquer data) OU não-recorrentes com
// data_inicio <= isoEnd e (data_fim ?? data_inicio) >= isoStart.
// 2 queries simples + merge pra evitar string-building frágil.
const baseNonRec = supabase
.from('agenda_bloqueios')
const baseNonRec = tenantDb().from('agenda_bloqueios')
.select('*')
.eq('recorrente', false)
.lte('data_inicio', isoEnd)
.or(`data_fim.gte.${isoStart},and(data_fim.is.null,data_inicio.gte.${isoStart})`);
const baseRec = supabase
.from('agenda_bloqueios')
const baseRec = tenantDb().from('agenda_bloqueios')
.select('*')
.eq('recorrente', true);
@@ -38,6 +38,7 @@ import { ref, watch } from 'vue';
import { useToast } from 'primevue/usetoast';
import { useConfirm } from 'primevue/useconfirm';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { labelStatusSessao } from './agendaEventHelpers';
const EVENTO_TIPO_SESSAO = 'sessao';
@@ -157,7 +158,7 @@ export function useAgendaEventActions({
toast.add({ severity: 'success', summary: 'Status atualizado', detail: `Sessão marcada como ${labelStatusSessao(newVal)}.`, life: 3000 });
return;
}
const { data, error } = await supabase.from('agenda_eventos').update({ status: newVal }).eq('id', formId).select().single();
const { data, error } = await tenantDb().from('agenda_eventos').update({ status: newVal }).eq('id', formId).select().single();
if (error) throw error;
toast.add({ severity: 'success', summary: 'Status atualizado', detail: `Sessão marcada como ${labelStatusSessao(newVal)}.`, life: 3000 });
emit('updated', data);
@@ -213,8 +214,7 @@ export function useAgendaEventActions({
const dayStart = new Date(d.getFullYear(), d.getMonth(), d.getDate()).toISOString();
const dayEnd = new Date(d.getFullYear(), d.getMonth(), d.getDate() + 1).toISOString();
let q = supabase
.from('agenda_eventos')
let q = tenantDb().from('agenda_eventos')
.select('id, inicio_em, fim_em, titulo')
.eq('patient_id', pid)
.gte('inicio_em', dayStart)
@@ -41,6 +41,7 @@
import { ref, computed, watch, nextTick } from 'vue';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
export function generateRuleDates(rule) {
const { type, interval = 1, weekdays = [], start_date, end_date, max_occurrences } = rule || {};
if (!start_date || !weekdays?.length) return [];
@@ -150,14 +151,13 @@ export function useAgendaEventLifecycle({
}
serieLoading.value = true;
try {
const { data: rule, error: ruleErr } = await supabase.from('recurrence_rules').select('*').eq('id', rid).maybeSingle();
const { data: rule, error: ruleErr } = await tenantDb().from('recurrence_rules').select('*').eq('id', rid).maybeSingle();
if (ruleErr) throw ruleErr;
const { data: excData } = await supabase.from('recurrence_exceptions').select('original_date, type, reason').eq('recurrence_id', rid);
const { data: excData } = await tenantDb().from('recurrence_exceptions').select('original_date, type, reason').eq('recurrence_id', rid);
const exMap = new Map((excData || []).map((e) => [e.original_date, e]));
const { data: realData } = await supabase
.from('agenda_eventos')
const { data: realData } = await tenantDb().from('agenda_eventos')
.select('id, inicio_em, fim_em, status, recurrence_date')
.eq('recurrence_id', rid)
.is('mirror_of_event_id', null)
@@ -236,8 +236,7 @@ export function useAgendaEventLifecycle({
// 1) Record direto (materializada que tem agenda_evento_id real)
const isVirtualId = typeof evId === 'string' && evId.startsWith('rec::');
if (evId && !isVirtualId) {
const { data, error } = await supabase
.from('financial_records')
const { data, error } = await tenantDb().from('financial_records')
.select('id, amount, final_amount, status, due_date, paid_at, payment_method')
.eq('agenda_evento_id', evId)
.in('status', ['pending', 'paid', 'overdue'])
@@ -255,8 +254,7 @@ export function useAgendaEventLifecycle({
// materializadas sem cobrança individual) herdam status do
// contrato pra UI mostrar "Cobrança paga" coerentemente.
if (ruleId && patientId) {
const { data: contracts } = await supabase
.from('billing_contracts')
const { data: contracts } = await tenantDb().from('billing_contracts')
.select('id, package_price, charging_style, status')
.eq('patient_id', patientId)
.eq('type', 'package')
@@ -266,8 +264,7 @@ export function useAgendaEventLifecycle({
if (upfront) {
// Confere se há record PAGO ligado a qualquer evento do
// mesmo recurrence_id (ou seja, contrato foi quitado).
const { data: siblingEvents } = await supabase
.from('agenda_eventos')
const { data: siblingEvents } = await tenantDb().from('agenda_eventos')
.select('id')
.eq('recurrence_id', ruleId);
const ids = (siblingEvents || []).map((e) => e.id);
@@ -276,8 +273,7 @@ export function useAgendaEventLifecycle({
// pending OU overdue). Pacote upfront tem 1 record
// unico cobrindo toda a serie — qualquer status dele
// trava as siblings (cobranca ja emitida, imutavel).
const { data: anyRec } = await supabase
.from('financial_records')
const { data: anyRec } = await tenantDb().from('financial_records')
.select('id, amount, final_amount, status, due_date, paid_at, payment_method')
.in('agenda_evento_id', ids)
.in('status', ['paid', 'pending', 'overdue'])
@@ -315,8 +311,7 @@ export function useAgendaEventLifecycle({
const evId = props.eventRow?.id;
if (!evId) return;
try {
const { data, error } = await supabase
.from('financial_records')
const { data, error } = await tenantDb().from('financial_records')
.select('id, amount, final_amount, status, due_date, paid_at, payment_method')
.eq('agenda_evento_id', evId)
.in('status', ['pending', 'paid', 'overdue'])
@@ -341,8 +336,7 @@ export function useAgendaEventLifecycle({
// Só faz sentido pra sessão de série
if (!patientId || !ruleId) return;
try {
const { data, error } = await supabase
.from('billing_contracts')
const { data, error } = await tenantDb().from('billing_contracts')
.select('id, type, total_sessions, sessions_used, package_price, charging_style, status, active_from')
.eq('patient_id', patientId)
.eq('type', 'package')
@@ -522,8 +516,7 @@ export function useAgendaEventLifecycle({
if (serieValorMode) serieValorMode.value = 'multiplicar';
if (composer.isEdit.value && composer.form.value.paciente_id && !composer.form.value.paciente_nome) {
supabase
.from('patients')
tenantDb().from('patients')
.select('id, nome_completo')
.eq('id', composer.form.value.paciente_id)
.maybeSingle()
@@ -602,8 +595,7 @@ export function useAgendaEventLifecycle({
const d = new Date(composer.form.value.dia);
const isoDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
const { data } = await supabase
.from('agendador_solicitacoes')
const { data } = await tenantDb().from('agendador_solicitacoes')
.select('id, paciente_nome, paciente_sobrenome, paciente_email')
.eq('owner_id', props.ownerId)
.eq('status', 'pendente')
@@ -625,8 +617,7 @@ export function useAgendaEventLifecycle({
const dow = new Date(dia).getDay();
loadingOnlineSlots.value = true;
try {
const { data } = await supabase
.from('agenda_online_slots')
const { data } = await tenantDb().from('agenda_online_slots')
.select('time')
.eq('owner_id', props.ownerId)
.eq('weekday', dow)
@@ -38,6 +38,7 @@
*/
import { ref, watch, nextTick } from 'vue';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { calcFinalPrice } from './agendaEventHelpers';
export function useAgendaEventPickerBilling({
@@ -254,13 +255,11 @@ export function useAgendaEventPickerBilling({
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')
let q = tenantDb().from('patients')
.select('id,nome_completo,email_principal,telefone,status,avatar_url,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);
}
@@ -27,6 +27,7 @@
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
// Shape interno de CommitmentItem:
// {
// service_id: uuid,
@@ -56,7 +57,7 @@ export function useCommitmentServices() {
async function loadItems(eventId) {
if (!eventId) return [];
const { data, error } = await supabase.from('commitment_services').select('service_id, quantity, unit_price, discount_pct, discount_flat, final_price, services(name)').eq('commitment_id', eventId).order('created_at', { ascending: true });
const { data, error } = await tenantDb().from('commitment_services').select('service_id, quantity, unit_price, discount_pct, discount_flat, final_price, services(name)').eq('commitment_id', eventId).order('created_at', { ascending: true });
if (error) throw error;
return (data || []).map(_mapRow);
@@ -73,7 +74,7 @@ export function useCommitmentServices() {
if (!eventId) throw new Error('eventId é obrigatório para salvar commitment_services.');
// 1. Remove itens existentes deste evento
const { error: deleteError } = await supabase.from('commitment_services').delete().eq('commitment_id', eventId);
const { error: deleteError } = await tenantDb().from('commitment_services').delete().eq('commitment_id', eventId);
if (deleteError) throw deleteError;
@@ -89,14 +90,14 @@ export function useCommitmentServices() {
final_price: item.final_price
}));
const { error: insertError } = await supabase.from('commitment_services').insert(rows);
const { error: insertError } = await tenantDb().from('commitment_services').insert(rows);
if (insertError) throw insertError;
}
// 3. Marca a ocorrência como customizada (impede sobrescrita por edições do raiz)
if (markCustomized) {
const { error: updateError } = await supabase.from('agenda_eventos').update({ services_customized: true }).eq('id', eventId);
const { error: updateError } = await tenantDb().from('agenda_eventos').update({ services_customized: true }).eq('id', eventId);
if (updateError) throw updateError;
}
@@ -107,7 +108,7 @@ export function useCommitmentServices() {
async function loadRuleItems(ruleId) {
if (!ruleId) return [];
const { data, error } = await supabase.from('recurrence_rule_services').select('service_id, quantity, unit_price, discount_pct, discount_flat, final_price, services(name)').eq('rule_id', ruleId).order('created_at', { ascending: true });
const { data, error } = await tenantDb().from('recurrence_rule_services').select('service_id, quantity, unit_price, discount_pct, discount_flat, final_price, services(name)').eq('rule_id', ruleId).order('created_at', { ascending: true });
if (error) throw error;
return (data || []).map(_mapRow);
@@ -120,7 +121,7 @@ export function useCommitmentServices() {
async function saveRuleItems(ruleId, items) {
if (!ruleId) throw new Error('ruleId é obrigatório para salvar recurrence_rule_services.');
const { error: deleteError } = await supabase.from('recurrence_rule_services').delete().eq('rule_id', ruleId);
const { error: deleteError } = await tenantDb().from('recurrence_rule_services').delete().eq('rule_id', ruleId);
if (deleteError) throw deleteError;
@@ -136,7 +137,7 @@ export function useCommitmentServices() {
final_price: item.final_price
}));
const { error: insertError } = await supabase.from('recurrence_rule_services').insert(rows);
const { error: insertError } = await tenantDb().from('recurrence_rule_services').insert(rows);
if (insertError) throw insertError;
}
@@ -171,7 +172,7 @@ export function useCommitmentServices() {
if (!ruleId) return;
// Busca IDs das ocorrências materializadas elegíveis
let q = supabase.from('agenda_eventos').select('id').eq('recurrence_id', ruleId);
let q = tenantDb().from('agenda_eventos').select('id').eq('recurrence_id', ruleId);
if (!ignoreCustomized) {
q = q.eq('services_customized', false);
@@ -189,8 +190,7 @@ export function useCommitmentServices() {
// em batch evita N round-trips. Status considerados imutáveis: pending,
// paid, overdue. cancelled é ok propagar (record foi descartado).
const eventIds = events.map((e) => e.id);
const { data: lockedEvents, error: frErr } = await supabase
.from('financial_records')
const { data: lockedEvents, error: frErr } = await tenantDb().from('financial_records')
.select('agenda_evento_id')
.in('agenda_evento_id', eventIds)
.in('status', ['pending', 'paid', 'overdue']);
@@ -202,7 +202,7 @@ export function useCommitmentServices() {
// Para cada evento elegível: delete + insert (padrão idempotente)
for (const ev of eligibleEvents) {
const { error: delErr } = await supabase.from('commitment_services').delete().eq('commitment_id', ev.id);
const { error: delErr } = await tenantDb().from('commitment_services').delete().eq('commitment_id', ev.id);
if (delErr) throw delErr;
if (items?.length) {
@@ -215,7 +215,7 @@ export function useCommitmentServices() {
discount_flat: item.discount_flat ?? 0,
final_price: item.final_price
}));
const { error: insErr } = await supabase.from('commitment_services').insert(rows);
const { error: insErr } = await tenantDb().from('commitment_services').insert(rows);
if (insErr) throw insErr;
}
}
@@ -17,6 +17,7 @@
import { computed, ref } from 'vue';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
export function useDeterminedCommitments(tenantIdRef) {
const loading = ref(false);
const error = ref('');
@@ -39,10 +40,9 @@ export function useDeterminedCommitments(tenantIdRef) {
loading.value = true;
error.value = '';
const { data, error: err } = await supabase
.from('determined_commitments')
.select('id,tenant_id,created_by,is_native,native_key,is_locked,active,name,description,bg_color,text_color,created_at,determined_commitment_fields(id,key,label,field_type,required,sort_order)')
.eq('tenant_id', tenantId.value) // ✅ SOMENTE tenant corrente
const { data, error: err } = await tenantDb().from('determined_commitments')
.select('id,created_by,is_native,native_key,is_locked,active,name,description,bg_color,text_color,created_at,determined_commitment_fields(id,key,label,field_type,required,sort_order)')
// ✅ SOMENTE tenant corrente
.eq('active', true)
.order('is_native', { ascending: false })
.order('name', { ascending: true });
@@ -28,6 +28,7 @@
import { ref } from 'vue';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
export function useFinancialExceptions() {
const exceptions = ref([]);
const loading = ref(false);
@@ -39,7 +40,7 @@ export function useFinancialExceptions() {
loading.value = true;
error.value = '';
try {
const { data, error: err } = await supabase.from('financial_exceptions').select('*').or(`owner_id.eq.${ownerId},owner_id.is.null`).order('exception_type', { ascending: true }).order('created_at', { ascending: true });
const { data, error: err } = await tenantDb().from('financial_exceptions').select('*').or(`owner_id.eq.${ownerId},owner_id.is.null`).order('exception_type', { ascending: true }).order('created_at', { ascending: true });
if (err) throw err;
exceptions.value = data || [];
@@ -60,8 +61,7 @@ export function useFinancialExceptions() {
error.value = '';
try {
if (payload.id) {
const { error: err } = await supabase
.from('financial_exceptions')
const { error: err } = await tenantDb().from('financial_exceptions')
.update({
charge_mode: payload.charge_mode,
charge_value: payload.charge_value ?? null,
@@ -72,9 +72,8 @@ export function useFinancialExceptions() {
.eq('id', payload.id);
if (err) throw err;
} else {
const { error: err } = await supabase.from('financial_exceptions').insert({
const { error: err } = await tenantDb().from('financial_exceptions').insert({
owner_id: payload.owner_id,
tenant_id: payload.tenant_id ?? null,
exception_type: payload.exception_type,
charge_mode: payload.charge_mode,
charge_value: payload.charge_value ?? null,
@@ -96,7 +95,7 @@ export function useFinancialExceptions() {
async function remove(id) {
error.value = '';
try {
const { error: err } = await supabase.from('financial_exceptions').delete().eq('id', id);
const { error: err } = await tenantDb().from('financial_exceptions').delete().eq('id', id);
if (err) throw err;
exceptions.value = exceptions.value.filter((e) => e.id !== id);
} catch (e) {
@@ -30,6 +30,7 @@
import { ref } from 'vue';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
export function useInsurancePlans() {
const plans = ref([]);
const loading = ref(false);
@@ -40,8 +41,7 @@ export function useInsurancePlans() {
loading.value = true;
error.value = null;
try {
const { data, error: err } = await supabase
.from('insurance_plans')
const { data, error: err } = await tenantDb().from('insurance_plans')
.select(
`
*,
@@ -66,8 +66,7 @@ export function useInsurancePlans() {
error.value = null;
try {
if (payload.id) {
const { error: err } = await supabase
.from('insurance_plans')
const { error: err } = await tenantDb().from('insurance_plans')
.update({
name: payload.name,
notes: payload.notes || null,
@@ -76,9 +75,8 @@ export function useInsurancePlans() {
.eq('id', payload.id);
if (err) throw err;
} else {
const { error: err } = await supabase.from('insurance_plans').insert({
const { error: err } = await tenantDb().from('insurance_plans').insert({
owner_id: payload.owner_id,
tenant_id: payload.tenant_id,
name: payload.name,
notes: payload.notes || null
});
@@ -93,7 +91,7 @@ export function useInsurancePlans() {
async function toggle(id, active) {
error.value = null;
try {
const { error: err } = await supabase.from('insurance_plans').update({ active }).eq('id', id);
const { error: err } = await tenantDb().from('insurance_plans').update({ active }).eq('id', id);
if (err) throw err;
const plan = plans.value.find((p) => p.id === id);
if (plan) plan.active = active;
@@ -106,7 +104,7 @@ export function useInsurancePlans() {
async function remove(id) {
error.value = null;
try {
const { error: err } = await supabase.from('insurance_plans').update({ active: false }).eq('id', id);
const { error: err } = await tenantDb().from('insurance_plans').update({ active: false }).eq('id', id);
if (err) throw err;
const plan = plans.value.find((p) => p.id === id);
if (plan) plan.active = false;
@@ -120,8 +118,7 @@ export function useInsurancePlans() {
error.value = null;
try {
if (payload.id) {
const { error: err } = await supabase
.from('insurance_plan_services')
const { error: err } = await tenantDb().from('insurance_plan_services')
.update({
name: payload.name,
value: payload.value
@@ -129,7 +126,7 @@ export function useInsurancePlans() {
.eq('id', payload.id);
if (err) throw err;
} else {
const { error: err } = await supabase.from('insurance_plan_services').insert({
const { error: err } = await tenantDb().from('insurance_plan_services').insert({
insurance_plan_id: payload.insurance_plan_id,
name: payload.name,
value: payload.value
@@ -145,7 +142,7 @@ export function useInsurancePlans() {
async function togglePlanService(id, active) {
error.value = null;
try {
const { error: err } = await supabase.from('insurance_plan_services').update({ active }).eq('id', id);
const { error: err } = await tenantDb().from('insurance_plan_services').update({ active }).eq('id', id);
if (err) throw err;
} catch (e) {
error.value = e?.message || 'Erro ao atualizar procedimento';
@@ -156,7 +153,7 @@ export function useInsurancePlans() {
async function removeDefinitivo(id) {
error.value = null;
try {
const { error: err } = await supabase.from('insurance_plans').delete().eq('id', id);
const { error: err } = await tenantDb().from('insurance_plans').delete().eq('id', id);
if (err) throw err;
plans.value = plans.value.filter((p) => p.id !== id);
} catch (e) {
@@ -168,7 +165,7 @@ export function useInsurancePlans() {
async function removePlanService(id) {
error.value = null;
try {
const { error: err } = await supabase.from('insurance_plan_services').delete().eq('id', id);
const { error: err } = await tenantDb().from('insurance_plan_services').delete().eq('id', id);
if (err) throw err;
} catch (e) {
error.value = e?.message || 'Erro ao remover procedimento';
@@ -29,6 +29,7 @@
import { ref } from 'vue';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
export function usePatientDiscounts() {
const discounts = ref([]);
const loading = ref(false);
@@ -40,7 +41,7 @@ export function usePatientDiscounts() {
loading.value = true;
error.value = '';
try {
const { data, error: err } = await supabase.from('patient_discounts').select('*').eq('owner_id', ownerId).order('created_at', { ascending: false });
const { data, error: err } = await tenantDb().from('patient_discounts').select('*').eq('owner_id', ownerId).order('created_at', { ascending: false });
if (err) throw err;
discounts.value = data || [];
@@ -53,17 +54,19 @@ export function usePatientDiscounts() {
}
// ── Criar ou atualizar um desconto ───────────────────────────────────
// payload deve conter: { owner_id, tenant_id, patient_id, discount_pct, discount_flat, ... }
// payload deve conter: { owner_id, patient_id, discount_pct, discount_flat, ... }
// Se payload.id estiver presente, faz UPDATE; caso contrário, INSERT.
async function save(payload) {
error.value = '';
try {
if (payload.id) {
const { id, owner_id, tenant_id, ...fields } = payload;
const { error: err } = await supabase.from('patient_discounts').update(fields).eq('id', id).eq('owner_id', owner_id);
const { error: err } = await tenantDb().from('patient_discounts').update(fields).eq('id', id).eq('owner_id', owner_id);
if (err) throw err;
} else {
const { error: err } = await supabase.from('patient_discounts').insert(payload);
// eslint-disable-next-line no-unused-vars
const { tenant_id: _dropTenantId, ...insertFields } = payload;
const { error: err } = await tenantDb().from('patient_discounts').insert(insertFields);
if (err) throw err;
}
} catch (e) {
@@ -76,7 +79,7 @@ export function usePatientDiscounts() {
async function remove(id) {
error.value = '';
try {
const { error: err } = await supabase.from('patient_discounts').update({ active: false }).eq('id', id);
const { error: err } = await tenantDb().from('patient_discounts').update({ active: false }).eq('id', id);
if (err) throw err;
discounts.value = discounts.value.filter((d) => d.id !== id);
} catch (e) {
@@ -95,8 +98,7 @@ export function usePatientDiscounts() {
if (!ownerId || !patientId) return null;
try {
const now = new Date().toISOString();
const { data, error: err } = await supabase
.from('patient_discounts')
const { data, error: err } = await tenantDb().from('patient_discounts')
.select('*')
.eq('owner_id', ownerId)
.eq('patient_id', patientId)
@@ -23,6 +23,7 @@
import { ref } from 'vue';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
export function useProfessionalPricing() {
const rows = ref([]); // professional_pricing rows
const loading = ref(false);
@@ -34,7 +35,7 @@ export function useProfessionalPricing() {
loading.value = true;
error.value = '';
try {
const { data, error: err } = await supabase.from('professional_pricing').select('id, determined_commitment_id, price, notes').eq('owner_id', ownerId);
const { data, error: err } = await tenantDb().from('professional_pricing').select('id, determined_commitment_id, price, notes').eq('owner_id', ownerId);
if (err) throw err;
rows.value = data || [];
@@ -31,6 +31,7 @@
import { ref } from 'vue';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
import { assertTenantId } from '@/features/agenda/services/_tenantGuards';
import { logRecurrence, logError, logPerf } from '@/support/supportLogger';
@@ -326,7 +327,6 @@ function buildOccurrence(rule, date, originalIso, exception) {
owner_id: rule.owner_id,
therapist_id: rule.therapist_id,
terapeuta_id: rule.therapist_id,
tenant_id: rule.tenant_id,
// nome do paciente — injetado pelo loadAndExpand via _patient
paciente_nome: rule._patient?.nome_completo ?? null,
@@ -452,12 +452,7 @@ export function useRecurrence() {
// Busca regras sem end_date (abertas) + regras com end_date >= rangeStart
// Dois selects separados evitam problemas com .or() + .is.null no Supabase JS
const baseQuery = () => {
let q = supabase.from('recurrence_rules').select('*').eq('owner_id', ownerId).eq('status', 'ativo').lte('start_date', endISO).order('start_date', { ascending: true });
// Filtra por tenant quando disponível — defesa em profundidade
if (tenantId && tenantId !== 'null' && tenantId !== 'undefined') {
q = q.eq('tenant_id', tenantId);
}
return q;
return tenantDb().from('recurrence_rules').select('*').eq('owner_id', ownerId).eq('status', 'ativo').lte('start_date', endISO).order('start_date', { ascending: true });
};
const [resOpen, resWithEnd] = await Promise.all([baseQuery().is('end_date', null), baseQuery().gte('end_date', startISO).not('end_date', 'is', null)]);
@@ -504,11 +499,11 @@ export function useRecurrence() {
const endISO = toISO(rangeEnd);
// Query 1 — comportamento original: exceções cujo original_date está no range
const q1 = supabase.from('recurrence_exceptions').select('*').in('recurrence_id', ids).gte('original_date', startISO).lte('original_date', endISO);
const q1 = tenantDb().from('recurrence_exceptions').select('*').in('recurrence_id', ids).gte('original_date', startISO).lte('original_date', endISO);
// Query 2 — bug fix: remarcações cujo new_date cai neste range
// (original_date pode estar antes ou depois do range)
const q2 = supabase.from('recurrence_exceptions').select('*').in('recurrence_id', ids).eq('type', 'reschedule_session').not('new_date', 'is', null).gte('new_date', startISO).lte('new_date', endISO);
const q2 = tenantDb().from('recurrence_exceptions').select('*').in('recurrence_id', ids).eq('type', 'reschedule_session').not('new_date', 'is', null).gte('new_date', startISO).lte('new_date', endISO);
const [res1, res2] = await Promise.all([q1, q2]);
@@ -550,7 +545,7 @@ export function useRecurrence() {
// Busca nomes dos pacientes das regras carregadas
const patientIds = [...new Set(rules.value.map((r) => r.patient_id).filter(Boolean))];
if (patientIds.length) {
const { data: patients } = await supabase.from('patients').select('id, nome_completo, avatar_url').in('id', patientIds);
const { data: patients } = await tenantDb().from('patients').select('id, nome_completo, avatar_url').in('id', patientIds);
// injeta nome diretamente na regra para o buildOccurrence usar
const pMap = new Map((patients || []).map((p) => [p.id, p]));
for (const rule of rules.value) {
@@ -579,15 +574,14 @@ export function useRecurrence() {
/**
* Cria uma nova regra de recorrência.
* tenant_id é injetado do tenantStore se não vier no payload (defesa em profundidade).
* tenant_id é dropado defensivamente — schema-per-tenant não tem essa coluna.
* @param {Object} rule - campos da tabela recurrence_rules
* @returns {Object} regra criada
*/
async function createRule(rule) {
const tenantId = currentTenantId();
logRecurrence('createRule →', { patient_id: rule?.patient_id, type: rule?.type });
const safeRule = { ...rule, tenant_id: rule?.tenant_id || tenantId };
const { data, error: err } = await supabase.from('recurrence_rules').insert([safeRule]).select('*').single();
const { tenant_id: _dropTenantId, ...safeRule } = rule || {};
const { data, error: err } = await tenantDb().from('recurrence_rules').insert([safeRule]).select('*').single();
if (err) {
logError('useRecurrence', 'createRule ERRO', err);
throw err;
@@ -598,15 +592,14 @@ export function useRecurrence() {
/**
* Atualiza a regra toda (editar todos).
* Filtro adicional por tenant_id — defesa em profundidade (RLS cobre, mas reforçamos).
* Isolamento multi-tenant garantido pelo schema do tenant (tenantDb).
*/
async function updateRule(id, patch) {
const tenantId = currentTenantId();
const { data, error: err } = await supabase
.from('recurrence_rules')
const { data, error: err } = await tenantDb().from('recurrence_rules')
.update({ ...patch, updated_at: new Date().toISOString() })
.eq('id', id)
.eq('tenant_id', tenantId)
.select('*')
.single();
if (err) throw err;
@@ -614,15 +607,14 @@ export function useRecurrence() {
}
/**
* Cancela a série inteira (filtro por tenant_id — defesa em profundidade).
* Cancela a série inteira.
*/
async function cancelRule(id) {
const tenantId = currentTenantId();
const { error: err } = await supabase
.from('recurrence_rules')
const { error: err } = await tenantDb().from('recurrence_rules')
.update({ status: 'cancelado', updated_at: new Date().toISOString() })
.eq('id', id)
.eq('tenant_id', tenantId);
;
if (err) throw err;
}
@@ -654,13 +646,11 @@ export function useRecurrence() {
/**
* Cria ou atualiza uma exceção para uma ocorrência específica.
* tenant_id é injetado do tenantStore se não vier no payload.
* tenant_id é dropado defensivamente — schema-per-tenant não tem essa coluna.
*/
async function upsertException(ex) {
const tenantId = currentTenantId();
const safeEx = { ...ex, tenant_id: ex?.tenant_id || tenantId };
const { data, error: err } = await supabase
.from('recurrence_exceptions')
const { tenant_id: _dropTenantId, ...safeEx } = ex || {};
const { data, error: err } = await tenantDb().from('recurrence_exceptions')
.upsert([safeEx], { onConflict: 'recurrence_id,original_date' })
.select('*')
.single();
@@ -670,16 +660,14 @@ export function useRecurrence() {
/**
* Remove uma exceção (restaura a ocorrência ao normal).
* Filtro por tenant_id — defesa em profundidade.
*/
async function deleteException(recurrenceId, originalDate) {
const tenantId = currentTenantId();
const { error: err } = await supabase
.from('recurrence_exceptions')
const { error: err } = await tenantDb().from('recurrence_exceptions')
.delete()
.eq('recurrence_id', recurrenceId)
.eq('original_date', originalDate)
.eq('tenant_id', tenantId);
;
if (err) throw err;
}
@@ -29,6 +29,7 @@
import { ref } from 'vue';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
export function useServices() {
const services = ref([]);
const loading = ref(false);
@@ -39,7 +40,7 @@ export function useServices() {
loading.value = true;
error.value = '';
try {
const { data, error: err } = await supabase.from('services').select('id, name, description, price, duration_min, active').eq('owner_id', ownerId).order('created_at', { ascending: true });
const { data, error: err } = await tenantDb().from('services').select('id, name, description, price, duration_min, active').eq('owner_id', ownerId).order('created_at', { ascending: true });
if (err) throw err;
services.value = data || [];
@@ -61,7 +62,7 @@ export function useServices() {
// Nome unico por owner (case-insensitive). No update,
// ignora o proprio id pra nao conflitar consigo mesmo
// quando o usuario salva sem mudar o nome.
let dupQuery = supabase.from('services').select('id').eq('owner_id', payload.owner_id).ilike('name', name).limit(1);
let dupQuery = tenantDb().from('services').select('id').eq('owner_id', payload.owner_id).ilike('name', name).limit(1);
if (payload.id) dupQuery = dupQuery.neq('id', payload.id);
const { data: dups, error: dupErr } = await dupQuery;
if (dupErr) throw dupErr;
@@ -71,10 +72,12 @@ export function useServices() {
if (payload.id) {
const { id, owner_id, tenant_id, ...fields } = payload;
const { error: err } = await supabase.from('services').update(fields).eq('id', id).eq('owner_id', owner_id);
const { error: err } = await tenantDb().from('services').update(fields).eq('id', id).eq('owner_id', owner_id);
if (err) throw err;
} else {
const { error: err } = await supabase.from('services').insert(payload);
// eslint-disable-next-line no-unused-vars
const { tenant_id: _dropTenantId, ...insertFields } = payload;
const { error: err } = await tenantDb().from('services').insert(insertFields);
if (err) throw err;
}
} catch (e) {
@@ -86,7 +89,7 @@ export function useServices() {
async function toggle(id, active) {
error.value = '';
try {
const { error: err } = await supabase.from('services').update({ active }).eq('id', id);
const { error: err } = await tenantDb().from('services').update({ active }).eq('id', id);
if (err) throw err;
const svc = services.value.find((s) => s.id === id);
if (svc) svc.active = active;
@@ -99,7 +102,7 @@ export function useServices() {
async function remove(id) {
error.value = '';
try {
const { error: err } = await supabase.from('services').delete().eq('id', id);
const { error: err } = await tenantDb().from('services').delete().eq('id', id);
if (err) throw err;
services.value = services.value.filter((s) => s.id !== id);
} catch (e) {