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
@@ -27,6 +27,7 @@ import Message from 'primevue/message';
import { useConfirm } from 'primevue/useconfirm';
import { useToast } from 'primevue/usetoast';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import ComponentCadastroRapido from '@/components/ComponentCadastroRapido.vue';
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue';
import AgendaEventoFinanceiroPanel from '@/components/agenda/AgendaEventoFinanceiroPanel.vue';
@@ -803,8 +804,7 @@ async function openSessionRecordsDialog() {
sessionRecordsDialogOpen.value = true;
sessionRecordsLoading.value = true;
try {
const { data, error } = await supabase
.from('financial_records')
const { data, error } = await tenantDb().from('financial_records')
.select('id, description, amount, final_amount, status, due_date, paid_at, payment_method, created_at')
.eq('agenda_evento_id', eid)
.is('deleted_at', null)
@@ -17,6 +17,7 @@
<script setup>
import { ref, computed, watch } from 'vue';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useFeriados } from '@/composables/useFeriados';
import { useToast } from 'primevue/usetoast';
import DatePicker from 'primevue/datepicker';
@@ -168,7 +169,6 @@ async function confirmar() {
try {
const base = {
owner_id: props.ownerId,
tenant_id: props.tenantId,
tipo: 'bloqueio',
recorrente: false
};
@@ -204,7 +204,7 @@ async function confirmar() {
return;
}
const { error } = await supabase.from('agenda_bloqueios').insert(rows);
const { error } = await tenantDb().from('agenda_bloqueios').insert(rows);
if (error) throw error;
// Marcar sessões existentes como "remarcado"
@@ -229,7 +229,7 @@ async function marcarSessoesParaRemarcar(bloqueios) {
// Para cada bloqueio, tenta marcar sessões existentes como 'remarcado'
for (const b of bloqueios) {
try {
let query = supabase.from('agenda_eventos').update({ status: 'remarcado' }).eq('owner_id', props.ownerId).eq('tipo', 'sessao').gte('inicio_em', `${b.data_inicio}T00:00:00`).lte('inicio_em', `${b.data_fim}T23:59:59`);
let query = tenantDb().from('agenda_eventos').update({ status: 'remarcado' }).eq('owner_id', props.ownerId).eq('tipo', 'sessao').gte('inicio_em', `${b.data_inicio}T00:00:00`).lte('inicio_em', `${b.data_fim}T23:59:59`);
if (b.hora_inicio && b.hora_fim) {
// filtra pela hora aproximada — comparação UTC simplificada
@@ -250,7 +250,6 @@ async function salvarFeriadoMunicipal() {
const iso = toISO(fform.value.data);
try {
await criarFeriado({
tenant_id: props.tenantId,
owner_id: props.ownerId,
tipo: 'municipal',
nome: fform.value.nome.trim(),
@@ -20,6 +20,7 @@ import { ref, watch } from 'vue';
import { useToast } from 'primevue/usetoast';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
const props = defineProps({
modelValue: { type: Boolean, default: false },
insurancePlanId: { type: String, default: '' },
@@ -61,7 +62,7 @@ async function onSave() {
value: Number(form.value.value),
active: true
};
const { data, error } = await supabase.from('insurance_plan_services').insert(payload).select().single();
const { data, error } = await tenantDb().from('insurance_plan_services').insert(payload).select().single();
if (error) throw error;
toast.add({ severity: 'success', summary: 'Procedimento cadastrado', life: 2200 });
emit('created', data);
@@ -18,6 +18,7 @@
import { ref, computed, onMounted, watch } from 'vue';
import { useRouter } from 'vue-router';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
import { useToast } from 'primevue/usetoast';
import { useFeriados } from '@/composables/useFeriados';
@@ -109,7 +110,7 @@ async function loadBloqueiosMes() {
const end = `${ano}-${String(mesAtual).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
loadingBloqueios.value = true;
try {
const { data } = await supabase.from('agenda_bloqueios').select('data_inicio').eq('owner_id', _ownerId.value).in('origem', ['agenda_feriado', 'agenda_dia']).gte('data_inicio', start).lte('data_inicio', end);
const { data } = await tenantDb().from('agenda_bloqueios').select('data_inicio').eq('owner_id', _ownerId.value).in('origem', ['agenda_feriado', 'agenda_dia']).gte('data_inicio', start).lte('data_inicio', end);
bloqueiosDatas.value = new Set((data || []).map((r) => r.data_inicio));
} catch {
/* silencioso */
@@ -152,7 +153,6 @@ async function confirmarBloqueio(feriado) {
try {
const row = {
owner_id: _ownerId.value,
tenant_id: _tenantId.value,
tipo: 'bloqueio',
recorrente: false,
titulo: `Feriado: ${feriado.nome}`,
@@ -163,11 +163,11 @@ async function confirmarBloqueio(feriado) {
origem: 'agenda_feriado'
};
const { error } = await supabase.from('agenda_bloqueios').insert([row]);
const { error } = await tenantDb().from('agenda_bloqueios').insert([row]);
if (error) throw error;
// Marcar sessões existentes no dia como 'remarcado'
await supabase.from('agenda_eventos').update({ status: 'remarcado' }).eq('owner_id', _ownerId.value).eq('tipo', 'sessao').gte('inicio_em', `${feriado.data}T00:00:00`).lte('inicio_em', `${feriado.data}T23:59:59`);
await tenantDb().from('agenda_eventos').update({ status: 'remarcado' }).eq('owner_id', _ownerId.value).eq('tipo', 'sessao').gte('inicio_em', `${feriado.data}T00:00:00`).lte('inicio_em', `${feriado.data}T23:59:59`);
bloqueiosDatas.value = new Set([...bloqueiosDatas.value, feriado.data]);
toast.add({
@@ -212,7 +212,6 @@ async function salvar() {
saving.value = true;
try {
await criar({
tenant_id: _tenantId.value,
owner_id: _ownerId.value,
tipo: 'municipal',
nome: form.value.nome.trim(),
@@ -11,7 +11,7 @@
| o id pra que o parent pré-selecione no select de serviços.
|
| Campos mínimos (obrigatórios no schema):
| name, price, owner_id, tenant_id
| name, price, owner_id
| Opcionais úteis:
| duration_min, description
|--------------------------------------------------------------------------
@@ -20,6 +20,7 @@
import { ref, watch } from 'vue';
import { useToast } from 'primevue/usetoast';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
const props = defineProps({
@@ -72,7 +73,7 @@ async function onSave() {
// Nome unico por owner (case-insensitive) — espelha a validacao
// do useServices.save() pra impedir duplicata tambem quando o
// cadastro vem do quick-create dentro do AgendaEventDialog.
const { data: dups, error: dupErr } = await supabase.from('services').select('id').eq('owner_id', ownerId).ilike('name', name).limit(1);
const { data: dups, error: dupErr } = await tenantDb().from('services').select('id').eq('owner_id', ownerId).ilike('name', name).limit(1);
if (dupErr) throw dupErr;
if (dups && dups.length > 0) {
toast.add({ severity: 'warn', summary: 'Nome em uso', detail: 'Já existe um serviço com este nome.', life: 3500 });
@@ -82,14 +83,13 @@ async function onSave() {
const payload = {
owner_id: ownerId,
tenant_id: tid,
name,
price: Number(form.value.price),
duration_min: form.value.duration_min ? Number(form.value.duration_min) : null,
description: form.value.description?.trim().slice(0, 500) || null,
active: true
};
const { data, error } = await supabase.from('services').insert(payload).select().single();
const { data, error } = await tenantDb().from('services').insert(payload).select().single();
if (error) throw error;
toast.add({ severity: 'success', summary: 'Serviço criado', life: 2200 });
emit('created', data);
@@ -18,6 +18,7 @@
Acessível via SupportDebugBanner botão "Docs". -->
<script setup>
import { ref } from 'vue';
import { tenantDb } from '@/lib/supabase/tenantClient';
const props = defineProps({
visible: { type: Boolean, default: false }
@@ -141,7 +142,7 @@ const activeTab = ref(0);
<!-- Tab 1: Tabelas -->
<TabPanel header="Tabelas">
<div class="dd-section">
<p class="dd-p">Todas as tabelas usam <strong>Row Level Security (RLS)</strong> habilitada.</p>
<p class="dd-p">Todas as tabelas usam <strong>Row Level Security (RLS)</strong> habilitada. As tabelas da agenda vivem no schema do tenant (<code>tenant_&lt;slug&gt;</code>, sem coluna <code>tenant_id</code>) e são acessadas via <code>tenantDb().from(...)</code>.</p>
<h3 class="dd-h3">Core</h3>
<table class="dd-table">
@@ -156,12 +157,12 @@ const activeTab = ref(0);
<tr>
<td><code>agenda_configuracoes</code></td>
<td>Configurações da agenda por owner (terapeuta ou clínica)</td>
<td>owner_id, tenant_id, slot_duration_minutes, start_time, end_time, days_of_week</td>
<td>owner_id, slot_duration_minutes, start_time, end_time, days_of_week</td>
</tr>
<tr>
<td><code>agenda_eventos</code></td>
<td>Eventos individuais (sessões, bloqueios avulsos)</td>
<td>id, owner_id, tenant_id, patient_id, starts_at, ends_at, status, recurrence_rule_id, tipo</td>
<td>id, owner_id, patient_id, starts_at, ends_at, status, recurrence_rule_id, tipo</td>
</tr>
<tr>
<td><code>agenda_bloqueios</code></td>
@@ -217,7 +218,7 @@ const activeTab = ref(0);
<tr>
<td><code>determined_commitments</code></td>
<td>Tipos de compromisso determinístico (ex: Avaliação, Supervisão)</td>
<td>id, owner_id, tenant_id, name, color, duration_minutes</td>
<td>id, owner_id, name, color, duration_minutes</td>
</tr>
<tr>
<td><code>determined_commitment_fields</code></td>
@@ -232,7 +233,7 @@ const activeTab = ref(0);
<tr>
<td><code>services</code></td>
<td>Catálogo de serviços do terapeuta/clínica</td>
<td>id, owner_id, tenant_id, name, default_price, active</td>
<td>id, owner_id, name, default_price, active</td>
</tr>
<tr>
<td><code>professional_pricing</code></td>
@@ -636,8 +637,7 @@ async function loadEvents (ownerId, range) &#123;
logAPI('useAgendaEvents', 'loadEvents start', &#123; ownerId, range &#125;)
try &#123;
const &#123; data, error &#125; = await supabase
.from('agenda_eventos')
const &#123; data, error &#125; = await tenantDb().from('agenda_eventos')
.select('*')
.eq('owner_id', ownerId)
@@ -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) {
+19 -26
View File
@@ -47,6 +47,7 @@ import { useAgendaSettings } from '@/features/agenda/composables/useAgendaSettin
import { useTenantStore } from '@/stores/tenantStore';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
const router = useRouter();
const route = useRoute();
const toast = useToast();
@@ -677,12 +678,11 @@ async function loadMonthSearchRows() {
const end = new Date(d.getFullYear(), d.getMonth() + 1, 1).toISOString();
monthSearchLoading.value = true;
try {
const { data, error } = await supabase
.from('agenda_eventos')
const { data, error } = await tenantDb().from('agenda_eventos')
.select(
'id, owner_id, tenant_id, tipo, status, titulo, inicio_em, fim_em, observacoes, modalidade, determined_commitment_id, insurance_plan_id, insurance_guide_number, insurance_value, insurance_plan_service_id, patients!agenda_eventos_patient_id_fkey(nome_completo)'
'id, owner_id, tipo, status, titulo, inicio_em, fim_em, observacoes, modalidade, determined_commitment_id, insurance_plan_id, insurance_guide_number, insurance_value, insurance_plan_service_id, patients!agenda_eventos_patient_id_fkey(nome_completo)'
)
.eq('tenant_id', tid)
.in('owner_id', ids)
.is('mirror_of_event_id', null)
.gte('inicio_em', start)
@@ -915,7 +915,7 @@ async function debugPatientsForColumn(staffUserId) {
console.log('tenant_member_id (mapeado):', memberId);
try {
const { count, error } = await supabase.from('patients').select('id', { count: 'exact', head: true }).eq('tenant_id', tid);
const { count, error } = await tenantDb().from('patients').select('id', { count: 'exact', head: true });
if (error) throw error;
console.log('patients total no tenant:', count);
} catch (e) {
@@ -924,7 +924,7 @@ async function debugPatientsForColumn(staffUserId) {
if (memberId && isUuid(memberId)) {
try {
const { count, error } = await supabase.from('patients').select('id', { count: 'exact', head: true }).eq('tenant_id', tid).eq('responsible_member_id', memberId);
const { count, error } = await tenantDb().from('patients').select('id', { count: 'exact', head: true }).eq('responsible_member_id', memberId);
if (error) throw error;
console.log('patients por responsible_member_id:', count);
} catch (e) {
@@ -932,10 +932,9 @@ async function debugPatientsForColumn(staffUserId) {
}
try {
const { data, error } = await supabase
.from('patients')
const { data, error } = await tenantDb().from('patients')
.select('id,nome_completo,email_principal,telefone,responsible_member_id,created_at')
.eq('tenant_id', tid)
.eq('responsible_member_id', memberId)
.order('created_at', { ascending: false })
.limit(5);
@@ -1212,8 +1211,7 @@ async function onUpdateSeriesEvent({ id, status, recurrence_date, inicio_em, fim
if (!is_virtual || !inicio_em) return;
const rid = row.recurrence_id ?? row.serie_id ?? null;
const rDate = recurrence_date || inicio_em?.slice(0, 10);
const { data: existing } = await supabase
.from('agenda_eventos')
const { data: existing } = await tenantDb().from('agenda_eventos')
.select('id')
.eq('recurrence_id', rid)
.eq('recurrence_date', rDate)
@@ -1281,9 +1279,8 @@ async function _offerBillingContract(basePayload, recorrencia, tenantId) {
rejectLabel: 'Agora não',
accept: async () => {
try {
const { error } = await supabase.from('billing_contracts').insert({
const { error } = await tenantDb().from('billing_contracts').insert({
owner_id: basePayload.owner_id,
tenant_id: tenantId,
patient_id: basePayload.paciente_id,
type: 'package',
total_sessions: n,
@@ -1454,7 +1451,7 @@ async function onDialogSave(arg) {
extra_fields: basePayload.extra_fields ?? null
});
if (arg.onSaved) {
const { data: existing } = await supabase.from('agenda_eventos').select('id').eq('recurrence_id', recurrenceId).eq('recurrence_date', originalDate).maybeSingle();
const { data: existing } = await tenantDb().from('agenda_eventos').select('id').eq('recurrence_id', recurrenceId).eq('recurrence_date', originalDate).maybeSingle();
if (existing?.id) {
eventId = existing.id;
} else {
@@ -1550,8 +1547,7 @@ async function onDialogSave(arg) {
});
// Propaga campos não-serviço para sessões já materializadas da série
await supabase
.from('agenda_eventos')
await tenantDb().from('agenda_eventos')
.update({
modalidade: basePayload.modalidade ?? 'presencial',
titulo_custom: basePayload.titulo_custom ?? null,
@@ -1599,8 +1595,7 @@ async function onDialogSave(arg) {
});
// Propaga TODOS os campos para TODAS as sessões materializadas (sem exceção)
await supabase
.from('agenda_eventos')
await tenantDb().from('agenda_eventos')
.update({
modalidade: basePayload.modalidade ?? 'presencial',
titulo_custom: basePayload.titulo_custom ?? null,
@@ -1708,7 +1703,7 @@ async function onDialogDelete(arg) {
if (isVirtual) {
const rDate = row.original_date || row.inicio_em?.slice(0, 10);
const existing = await supabase.from('agenda_eventos').select('id').eq('recurrence_id', recurrenceId).eq('recurrence_date', rDate).maybeSingle();
const existing = await tenantDb().from('agenda_eventos').select('id').eq('recurrence_id', recurrenceId).eq('recurrence_date', rDate).maybeSingle();
if (existing.data?.id) {
await updateClinic(existing.data.id, { recurrence_id: null, recurrence_date: null }, { tenantId: tid });
@@ -1926,8 +1921,7 @@ async function loadMiniMonthEvents(refDate) {
try {
const tid = tenantId.value;
// 1. Eventos normais (bolinhas)
let evQ = supabase.from('agenda_eventos').select('inicio_em').gte('inicio_em', start.toISOString()).lte('inicio_em', end.toISOString());
if (tid) evQ = evQ.eq('tenant_id', tid);
let evQ = tenantDb().from('agenda_eventos').select('inicio_em').gte('inicio_em', start.toISOString()).lte('inicio_em', end.toISOString());
const { data: evData } = await evQ;
const evSet = new Set();
@@ -1961,7 +1955,7 @@ async function loadMiniMonthEvents(refDate) {
const isoStart = `${year}-${pad(month + 1)}-01`;
const lastDay = new Date(year, month + 1, 0).getDate();
const isoEnd = `${year}-${pad(month + 1)}-${pad(lastDay)}`;
let blkQ = supabase.from('agenda_bloqueios').select('data_inicio').is('hora_inicio', null).gte('data_inicio', isoStart).lte('data_inicio', isoEnd);
let blkQ = tenantDb().from('agenda_bloqueios').select('data_inicio').is('hora_inicio', null).gte('data_inicio', isoStart).lte('data_inicio', isoEnd);
if (clinicOwnerId.value) blkQ = blkQ.eq('owner_id', clinicOwnerId.value);
const { data: blkData } = await blkQ;
miniBlockedDaySet.value = new Set((blkData || []).map((r) => r.data_inicio));
@@ -2050,10 +2044,9 @@ async function bloquearFeriadoDoAlerta(feriado) {
if (!clinicOwnerId.value || !tenantId.value) return;
feriadosAlertaSalvando.value = feriado.data;
try {
const { error } = await supabase.from('agenda_bloqueios').insert([
const { error } = await tenantDb().from('agenda_bloqueios').insert([
{
owner_id: clinicOwnerId.value,
tenant_id: tenantId.value,
tipo: 'bloqueio',
recorrente: false,
titulo: `Feriado: ${feriado.nome}`,
@@ -2065,7 +2058,7 @@ async function bloquearFeriadoDoAlerta(feriado) {
}
]);
if (error) throw error;
await supabase.from('agenda_eventos').update({ status: 'remarcado' }).eq('owner_id', clinicOwnerId.value).eq('tipo', 'sessao').gte('inicio_em', `${feriado.data}T00:00:00`).lte('inicio_em', `${feriado.data}T23:59:59`);
await tenantDb().from('agenda_eventos').update({ status: 'remarcado' }).eq('owner_id', clinicOwnerId.value).eq('tipo', 'sessao').gte('inicio_em', `${feriado.data}T00:00:00`).lte('inicio_em', `${feriado.data}T23:59:59`);
feriadosAlertaBloqueados.value = new Set([...feriadosAlertaBloqueados.value, feriado.data]);
miniBlockedDaySet.value = new Set([...miniBlockedDaySet.value, feriado.data]);
@@ -2088,7 +2081,7 @@ async function desbloquearFeriadoDoAlerta(feriado) {
if (!clinicOwnerId.value) return;
feriadosAlertaSalvando.value = `unblock_${feriado.data}`;
try {
const { error } = await supabase.from('agenda_bloqueios').delete().eq('owner_id', clinicOwnerId.value).eq('data_inicio', feriado.data).in('origem', ['agenda_feriado', 'agenda_dia']);
const { error } = await tenantDb().from('agenda_bloqueios').delete().eq('owner_id', clinicOwnerId.value).eq('data_inicio', feriado.data).in('origem', ['agenda_feriado', 'agenda_dia']);
if (error) throw error;
@@ -18,6 +18,7 @@
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
import { useAgendaClinicStaff } from '@/features/agenda/composables/useAgendaClinicStaff';
import { useToast } from 'primevue/usetoast';
@@ -78,11 +79,10 @@ async function load() {
if (!userId.value) return;
loading.value = true;
try {
let q = supabase.from('recurrence_rules').select('*').order('start_date', { ascending: false });
let q = tenantDb().from('recurrence_rules').select('*').order('start_date', { ascending: false });
if (isClinic.value) {
if (!tenantId.value) return;
q = q.eq('tenant_id', tenantId.value);
if (filterOwner.value) q = q.eq('owner_id', filterOwner.value);
} else {
q = q.eq('owner_id', userId.value);
@@ -97,7 +97,7 @@ async function load() {
const patientIds = [...new Set(rawRules.map((r) => r.patient_id).filter(Boolean))];
const patientMap = {};
if (patientIds.length) {
const { data: pts } = await supabase.from('patients').select('id, nome_completo, avatar_url').in('id', patientIds);
const { data: pts } = await tenantDb().from('patients').select('id, nome_completo, avatar_url').in('id', patientIds);
for (const p of pts || []) patientMap[p.id] = p;
}
for (const r of rawRules) r._patient = patientMap[r.patient_id] || null;
@@ -115,8 +115,8 @@ async function load() {
async function reloadSessions(ruleIds) {
const [exRes, sessRes] = await Promise.all([
supabase.from('recurrence_exceptions').select('*').in('recurrence_id', ruleIds).order('original_date'),
supabase.from('agenda_eventos').select('id, recurrence_id, recurrence_date, status, inicio_em, fim_em').in('recurrence_id', ruleIds).order('inicio_em')
tenantDb().from('recurrence_exceptions').select('*').in('recurrence_id', ruleIds).order('original_date'),
tenantDb().from('agenda_eventos').select('id, recurrence_id, recurrence_date, status, inicio_em, fim_em').in('recurrence_id', ruleIds).order('inicio_em')
]);
const exm = {};
for (const ex of exRes.data || []) {
@@ -254,17 +254,16 @@ const PILL_CLASS = {
async function onPillStatusChange(rule, s, newStatus) {
try {
if (s.real_id) {
await supabase.from('agenda_eventos').update({ status: newStatus }).eq('id', s.real_id);
await tenantDb().from('agenda_eventos').update({ status: newStatus }).eq('id', s.real_id);
} else {
const { data: ex } = await supabase.from('agenda_eventos').select('id').eq('recurrence_id', rule.id).eq('recurrence_date', s.date).maybeSingle();
const { data: ex } = await tenantDb().from('agenda_eventos').select('id').eq('recurrence_id', rule.id).eq('recurrence_date', s.date).maybeSingle();
if (ex?.id) {
await supabase.from('agenda_eventos').update({ status: newStatus }).eq('id', ex.id);
await tenantDb().from('agenda_eventos').update({ status: newStatus }).eq('id', ex.id);
} else {
await supabase.from('agenda_eventos').insert({
await tenantDb().from('agenda_eventos').insert({
recurrence_id: rule.id,
recurrence_date: s.date,
owner_id: rule.owner_id,
tenant_id: rule.tenant_id,
tipo: 'sessao',
status: newStatus,
inicio_em: s.date + 'T' + (rule.start_time || '00:00') + ':00',
@@ -287,7 +286,7 @@ async function onCancelRule(rule) {
const name = rule._patient?.nome_completo || 'paciente';
if (!confirm(`Encerrar a série de "${name}"?\n\nSessões futuras deixarão de ser geradas. Sessões passadas já registradas são mantidas.`)) return;
try {
await supabase.from('recurrence_rules').update({ status: 'cancelado', updated_at: new Date().toISOString() }).eq('id', rule.id);
await tenantDb().from('recurrence_rules').update({ status: 'cancelado', updated_at: new Date().toISOString() }).eq('id', rule.id);
toast.add({ severity: 'success', summary: 'Série encerrada', life: 2000 });
await load();
} catch (e) {
@@ -297,7 +296,7 @@ async function onCancelRule(rule) {
async function onReactivateRule(rule) {
try {
await supabase.from('recurrence_rules').update({ status: 'ativo', updated_at: new Date().toISOString() }).eq('id', rule.id);
await tenantDb().from('recurrence_rules').update({ status: 'ativo', updated_at: new Date().toISOString() }).eq('id', rule.id);
toast.add({ severity: 'success', summary: 'Série reativada', life: 2000 });
await load();
} catch (e) {
@@ -20,6 +20,7 @@ import { useRouter, useRoute } from 'vue-router';
import { useTenantStore } from '@/stores/tenantStore';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useToast } from 'primevue/usetoast';
import { useConfirm } from 'primevue/useconfirm';
@@ -606,8 +607,7 @@ async function loadMonthSearchRows() {
try {
// 1. Eventos reais do banco — inclui recurrence_id/recurrence_date para
// mergeWithStoredSessions deduplicar sessões materializadas de séries.
const { data, error } = await supabase
.from('agenda_eventos')
const { data, error } = await tenantDb().from('agenda_eventos')
.select(
'id, owner_id, tipo, status, titulo, inicio_em, fim_em, observacoes, modalidade, determined_commitment_id, insurance_plan_id, insurance_guide_number, insurance_value, insurance_plan_service_id, recurrence_id, recurrence_date, patients!agenda_eventos_patient_id_fkey(nome_completo, status)'
)
@@ -981,7 +981,7 @@ async function loadAgendadorSlug() {
const uid = ownerId.value;
if (!uid) return;
try {
const { data } = await supabase.from('agendador_configuracoes').select('link_slug').eq('owner_id', uid).eq('ativo', true).maybeSingle();
const { data } = await tenantDb().from('agendador_configuracoes').select('link_slug').eq('owner_id', uid).eq('ativo', true).maybeSingle();
agendadorSlug.value = data?.link_slug || '';
} catch {
agendadorSlug.value = '';
@@ -993,7 +993,7 @@ async function loadCadastroToken() {
const { data: authData } = await supabase.auth.getUser();
const uid = authData?.user?.id;
if (!uid) return;
const { data } = await supabase.from('patient_invites').select('token').eq('owner_id', uid).eq('active', true).order('created_at', { ascending: false }).limit(1);
const { data } = await tenantDb().from('patient_invites').select('token').eq('owner_id', uid).eq('active', true).order('created_at', { ascending: false }).limit(1);
cadastroToken.value = data?.[0]?.token || '';
} catch {
cadastroToken.value = '';
@@ -1031,7 +1031,7 @@ const desativadoFcRef = ref(null);
async function loadDesativados() {
if (!ownerId.value) return;
try {
const { data: pats, error: pErr } = await supabase.from('patients').select('id, nome_completo, status').eq('owner_id', ownerId.value).in('status', ['Inativo', 'Arquivado']);
const { data: pats, error: pErr } = await tenantDb().from('patients').select('id, nome_completo, status').eq('owner_id', ownerId.value).in('status', ['Inativo', 'Arquivado']);
if (pErr) {
console.warn('[loadDesativados] patients error:', pErr);
@@ -1044,9 +1044,8 @@ async function loadDesativados() {
}
const patIds = pats.map((p) => p.id);
const sessQ = supabase.from('agenda_eventos').select('id, patient_id, inicio_em, fim_em, status, titulo, modalidade, determined_commitment_id').in('patient_id', patIds).order('inicio_em', { ascending: true });
const sessQ = tenantDb().from('agenda_eventos').select('id, patient_id, inicio_em, fim_em, status, titulo, modalidade, determined_commitment_id').in('patient_id', patIds).order('inicio_em', { ascending: true });
if (ownerId.value) sessQ.eq('owner_id', ownerId.value);
if (clinicTenantId.value) sessQ.eq('tenant_id', clinicTenantId.value);
const { data: sessions, error: sErr } = await sessQ;
if (sErr) {
@@ -1278,7 +1277,7 @@ async function loadMiniMonthEvents(refDate) {
try {
// 1. Eventos reais (agenda_eventos)
const { data: evData } = await supabase.from('agenda_eventos').select('inicio_em').eq('owner_id', ownerId.value).gte('inicio_em', start.toISOString()).lte('inicio_em', end.toISOString());
const { data: evData } = await tenantDb().from('agenda_eventos').select('inicio_em').eq('owner_id', ownerId.value).gte('inicio_em', start.toISOString()).lte('inicio_em', end.toISOString());
const evSet = new Set();
for (const r of evData || []) {
@@ -1303,8 +1302,7 @@ async function loadMiniMonthEvents(refDate) {
const isoStart = `${year}-${pad(month + 1)}-01`;
const lastDay = new Date(year, month + 1, 0).getDate();
const isoEnd = `${year}-${pad(month + 1)}-${pad(lastDay)}`;
const { data: blkData } = await supabase
.from('agenda_bloqueios')
const { data: blkData } = await tenantDb().from('agenda_bloqueios')
.select('data_inicio')
.eq('owner_id', ownerId.value || '')
.is('hora_inicio', null)
@@ -1398,10 +1396,9 @@ async function bloquearFeriadoDoAlerta(feriado) {
if (!ownerId.value || !clinicTenantId.value) return;
feriadosAlertaSalvando.value = feriado.data;
try {
const { error } = await supabase.from('agenda_bloqueios').insert([
const { error } = await tenantDb().from('agenda_bloqueios').insert([
{
owner_id: ownerId.value,
tenant_id: clinicTenantId.value,
tipo: 'bloqueio',
recorrente: false,
titulo: `Feriado: ${feriado.nome}`,
@@ -1413,7 +1410,7 @@ async function bloquearFeriadoDoAlerta(feriado) {
}
]);
if (error) throw error;
await supabase.from('agenda_eventos').update({ status: 'remarcado' }).eq('owner_id', ownerId.value).eq('tipo', 'sessao').gte('inicio_em', `${feriado.data}T00:00:00`).lte('inicio_em', `${feriado.data}T23:59:59`);
await tenantDb().from('agenda_eventos').update({ status: 'remarcado' }).eq('owner_id', ownerId.value).eq('tipo', 'sessao').gte('inicio_em', `${feriado.data}T00:00:00`).lte('inicio_em', `${feriado.data}T23:59:59`);
feriadosAlertaBloqueados.value = new Set([...feriadosAlertaBloqueados.value, feriado.data]);
miniBlockedDaySet.value = new Set([...miniBlockedDaySet.value, feriado.data]);
@@ -1438,7 +1435,7 @@ async function desbloquearFeriadoDoAlerta(feriado) {
if (!ownerId.value) return;
feriadosAlertaSalvando.value = `unblock_${feriado.data}`;
try {
const { error } = await supabase.from('agenda_bloqueios').delete().eq('owner_id', ownerId.value).eq('data_inicio', feriado.data).in('origem', ['agenda_feriado', 'agenda_dia']);
const { error } = await tenantDb().from('agenda_bloqueios').delete().eq('owner_id', ownerId.value).eq('data_inicio', feriado.data).in('origem', ['agenda_feriado', 'agenda_dia']);
if (error) throw error;
@@ -1736,8 +1733,7 @@ async function onUpdateSeriesEvent({ id, status, recurrence_date, inicio_em, fim
if (!is_virtual || !inicio_em) return;
const rid = row.recurrence_id ?? row.serie_id ?? null;
const rDate = recurrence_date || inicio_em?.slice(0, 10);
const { data: existing } = await supabase
.from('agenda_eventos')
const { data: existing } = await tenantDb().from('agenda_eventos')
.select('id')
.eq('recurrence_id', rid)
.eq('recurrence_date', rDate)
@@ -1807,9 +1803,8 @@ async function _offerBillingContract(normalized, recorrencia, tenantId) {
rejectLabel: 'Agora não',
accept: async () => {
try {
const { error } = await supabase.from('billing_contracts').insert({
const { error } = await tenantDb().from('billing_contracts').insert({
owner_id: normalized.owner_id,
tenant_id: tenantId,
patient_id: normalized.paciente_id,
type: 'package',
total_sessions: n,
@@ -1933,12 +1928,11 @@ async function onDialogSave(arg) {
if (recorrencia?.conflitos?.length && createdRule?.id) {
const exceptions = recorrencia.conflitos.map((c) => ({
recurrence_id: createdRule.id,
tenant_id: clinicId,
original_date: c.date,
type: c.conflict.type === 'feriado' ? 'holiday_block' : c.conflict.type === 'bloqueado' ? 'cancel_session' : c.conflict.type === 'folga' ? 'cancel_session' : 'cancel_session',
reason: c.conflict.label
}));
const { error: exErr } = await supabase.from('recurrence_exceptions').insert(exceptions);
const { error: exErr } = await tenantDb().from('recurrence_exceptions').insert(exceptions);
if (exErr) logError('AgendaTerapeutaPage', 'onDialogSave: erro ao inserir exceptions', exErr);
}
@@ -1998,7 +1992,7 @@ async function onDialogSave(arg) {
extra_fields: normalized.extra_fields ?? null
});
if (arg.onSaved) {
const { data: existing } = await supabase.from('agenda_eventos').select('id').eq('recurrence_id', recurrenceId).eq('recurrence_date', originalDate).maybeSingle();
const { data: existing } = await tenantDb().from('agenda_eventos').select('id').eq('recurrence_id', recurrenceId).eq('recurrence_date', originalDate).maybeSingle();
if (existing?.id) {
eventId = existing.id;
} else {
@@ -2091,8 +2085,7 @@ async function onDialogSave(arg) {
});
// Propaga campos não-serviço para sessões já materializadas da série
await supabase
.from('agenda_eventos')
await tenantDb().from('agenda_eventos')
.update({
modalidade: normalized.modalidade ?? 'presencial',
titulo_custom: normalized.titulo_custom ?? null,
@@ -2140,8 +2133,7 @@ async function onDialogSave(arg) {
});
// Propaga TODOS os campos para TODAS as sessões materializadas (sem exceção)
await supabase
.from('agenda_eventos')
await tenantDb().from('agenda_eventos')
.update({
modalidade: normalized.modalidade ?? 'presencial',
titulo_custom: normalized.titulo_custom ?? null,
@@ -2205,8 +2197,7 @@ async function onDialogSave(arg) {
let detail = 'Já existe um compromisso nesse horário. Verifique a agenda e escolha outro horário.';
try {
if (normalized?.inicio_em && normalized?.fim_em && normalized?.owner_id) {
const { data: conflicting } = await supabase
.from('agenda_eventos')
const { data: conflicting } = await tenantDb().from('agenda_eventos')
.select('titulo, inicio_em, fim_em')
.eq('owner_id', normalized.owner_id)
.lt('inicio_em', normalized.fim_em)
@@ -2276,7 +2267,7 @@ async function onDialogDelete(arg) {
if (isVirtual) {
// Ocorrência virtual: materializa como evento avulso (sem recurrence_id)
const rDate = row.original_date || row.inicio_em?.slice(0, 10);
const existing = await supabase.from('agenda_eventos').select('id').eq('recurrence_id', recurrenceId).eq('recurrence_date', rDate).maybeSingle();
const existing = await tenantDb().from('agenda_eventos').select('id').eq('recurrence_id', recurrenceId).eq('recurrence_date', rDate).maybeSingle();
if (existing.data?.id) {
await update(existing.data.id, { recurrence_id: null, recurrence_date: null });
@@ -19,6 +19,7 @@ import { ref, computed, onMounted, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useTenantStore } from '@/stores/tenantStore';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useToast } from 'primevue/usetoast';
import AgendaEventDialog from '@/features/agenda/components/AgendaEventDialog.vue';
@@ -63,14 +64,12 @@ async function load() {
if (!ownerId.value) return;
loading.value = true;
try {
let q = supabase
.from('agendador_solicitacoes')
.select('id, owner_id, tenant_id, paciente_nome, paciente_sobrenome, paciente_email, paciente_celular, paciente_cpf, tipo, modalidade, data_solicitada, hora_solicitada, reservado_ate, motivo, como_conheceu, status, created_at')
let q = tenantDb().from('agendador_solicitacoes')
.select('id, owner_id, paciente_nome, paciente_sobrenome, paciente_email, paciente_celular, paciente_cpf, tipo, modalidade, data_solicitada, hora_solicitada, reservado_ate, motivo, como_conheceu, status, created_at')
.order('data_solicitada', { ascending: false })
.order('hora_solicitada', { ascending: true });
if (isClinic.value) q = q.eq('tenant_id', tenantId.value);
else q = q.eq('owner_id', ownerId.value);
if (!isClinic.value) q = q.eq('owner_id', ownerId.value);
if (filtroStatus.value) q = q.eq('status', filtroStatus.value);
@@ -79,9 +78,8 @@ async function load() {
solicitacoes.value = data || [];
if (filtroStatus.value !== 'pendente') {
let qp = supabase.from('agendador_solicitacoes').select('id', { count: 'exact', head: true }).eq('status', 'pendente');
if (isClinic.value) qp = qp.eq('tenant_id', tenantId.value);
else qp = qp.eq('owner_id', ownerId.value);
let qp = tenantDb().from('agendador_solicitacoes').select('id', { count: 'exact', head: true }).eq('status', 'pendente');
if (!isClinic.value) qp = qp.eq('owner_id', ownerId.value);
const { count } = await qp;
totalPendentes.value = count || 0;
} else {
@@ -90,9 +88,8 @@ async function load() {
// Conta autorizados (sempre, independente do filtro ativo)
if (filtroStatus.value !== 'autorizado') {
let qa = supabase.from('agendador_solicitacoes').select('id', { count: 'exact', head: true }).eq('status', 'autorizado');
if (isClinic.value) qa = qa.eq('tenant_id', tenantId.value);
else qa = qa.eq('owner_id', ownerId.value);
let qa = tenantDb().from('agendador_solicitacoes').select('id', { count: 'exact', head: true }).eq('status', 'autorizado');
if (!isClinic.value) qa = qa.eq('owner_id', ownerId.value);
const { count: ca } = await qa;
totalAutorizados.value = ca || 0;
} else {
@@ -158,7 +155,7 @@ const aprovando = ref(null);
async function aprovar(s) {
aprovando.value = s.id;
try {
const { error } = await supabase.from('agendador_solicitacoes').update({ status: 'autorizado', autorizado_em: new Date().toISOString() }).eq('id', s.id);
const { error } = await tenantDb().from('agendador_solicitacoes').update({ status: 'autorizado', autorizado_em: new Date().toISOString() }).eq('id', s.id);
if (error) throw error;
toast.add({ severity: 'success', summary: 'Autorizado', detail: `Solicitação de ${nomeCompleto(s)} autorizada.`, life: 3000 });
await load();
@@ -187,8 +184,7 @@ async function confirmarRecusa() {
if (!s) return;
recusandoId.value = s.id;
try {
const { error } = await supabase
.from('agendador_solicitacoes')
const { error } = await tenantDb().from('agendador_solicitacoes')
.update({ status: 'recusado', recusado_motivo: recusaMotivo.value || null })
.eq('id', s.id);
if (error) throw error;
@@ -241,17 +237,15 @@ async function converterEmSessao(s) {
async function encontrarOuCriarPaciente(s) {
const email = s.paciente_email?.toLowerCase().trim();
if (email) {
const { data: found } = await supabase.from('patients').select('id').eq('tenant_id', tenantId.value).ilike('email_principal', email).maybeSingle();
const { data: found } = await tenantDb().from('patients').select('id').ilike('email_principal', email).maybeSingle();
if (found?.id) return found.id;
}
const { data: memberData, error: memberErr } = await supabase.from('tenant_members').select('id').eq('tenant_id', tenantId.value).eq('user_id', ownerId.value).eq('status', 'active').maybeSingle();
if (memberErr || !memberData?.id) throw new Error('Membro ativo não encontrado para criação do paciente.');
const scope = isClinic.value ? 'clinic' : 'therapist';
const nomeCompleto_ = [s.paciente_nome, s.paciente_sobrenome].filter(Boolean).join(' ');
const { data: novo, error: criErr } = await supabase
.from('patients')
const { data: novo, error: criErr } = await tenantDb().from('patients')
.insert({
tenant_id: tenantId.value,
responsible_member_id: memberData.id,
owner_id: ownerId.value,
nome_completo: nomeCompleto_,
@@ -299,7 +293,7 @@ async function onEventSaved(arg) {
if (normalized[k] !== undefined) dbPayload[k] = normalized[k];
}
await createEvento(dbPayload);
const { error } = await supabase.from('agendador_solicitacoes').update({ status: 'convertido' }).eq('id', target.id);
const { error } = await tenantDb().from('agendador_solicitacoes').update({ status: 'convertido' }).eq('id', target.id);
if (error) throw error;
toast.add({ severity: 'success', summary: 'Convertido!', detail: `Sessão criada para ${nomeCompleto(target)}.`, life: 4000 });
await load();
@@ -26,6 +26,7 @@ import Menu from 'primevue/menu';
import DeterminedCommitmentDialog from '@/features/agenda/components/DeterminedCommitmentDialog.vue';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
const toast = useToast();
@@ -161,10 +162,9 @@ async function fetchAll() {
}
loading.value = true;
try {
const { data: cData, error: cErr } = await supabase
.from('determined_commitments')
.select('id, tenant_id, is_native, native_key, is_locked, active, name, description, bg_color, text_color, created_at, updated_at')
.eq('tenant_id', tenantId)
const { data: cData, error: cErr } = await tenantDb().from('determined_commitments')
.select('id, is_native, native_key, is_locked, active, name, description, bg_color, text_color, created_at, updated_at')
.order('is_native', { ascending: false })
.order('created_at', { ascending: false });
if (cErr) throw cErr;
@@ -172,10 +172,9 @@ async function fetchAll() {
const ids = (cData || []).map((x) => x.id);
let fieldsByCommitmentId = {};
if (ids.length > 0) {
const { data: fData, error: fErr } = await supabase
.from('determined_commitment_fields')
.select('id, tenant_id, commitment_id, key, label, field_type, required, sort_order')
.eq('tenant_id', tenantId)
const { data: fData, error: fErr } = await tenantDb().from('determined_commitment_fields')
.select('id, commitment_id, key, label, field_type, required, sort_order')
.in('commitment_id', ids)
.order('sort_order', { ascending: true });
if (fErr) throw fErr;
@@ -193,7 +192,7 @@ async function fetchAll() {
}, {});
}
const { data: lData, error: lErr } = await supabase.from('commitment_time_logs').select('commitment_id, minutes').eq('tenant_id', tenantId);
const { data: lData, error: lErr } = await tenantDb().from('commitment_time_logs').select('commitment_id, minutes');
if (lErr) throw lErr;
const totals = {};
for (const row of lData || []) {
@@ -253,7 +252,7 @@ async function onToggleActive(c) {
if (!tenantId) return;
saving.value = true;
try {
const { error } = await supabase.from('determined_commitments').update({ active: !!c.active }).eq('tenant_id', tenantId).eq('id', c.id);
const { error } = await tenantDb().from('determined_commitments').update({ active: !!c.active }).eq('id', c.id);
if (error) throw error;
toast.add({ severity: 'success', summary: 'Atualizado', detail: `"${c.name}" ${c.active ? 'ativo' : 'inativo'}.`, life: 2500 });
} catch (e) {
@@ -271,10 +270,8 @@ async function onSave(payload) {
try {
await supabase.auth.getUser();
if (dlgMode.value === 'create') {
const { data: newC, error: cErr } = await supabase
.from('determined_commitments')
const { data: newC, error: cErr } = await tenantDb().from('determined_commitments')
.insert({
tenant_id: tenantId,
is_native: false,
native_key: null,
is_locked: false,
@@ -284,14 +281,13 @@ async function onSave(payload) {
bg_color: payload.bg_color || null,
text_color: payload.text_color || null
})
.select('id, tenant_id, is_native, native_key, is_locked, active, name, description, bg_color, text_color, created_at, updated_at')
.select('id, is_native, native_key, is_locked, active, name, description, bg_color, text_color, created_at, updated_at')
.single();
if (cErr) throw cErr;
const fields = Array.isArray(payload.fields) ? payload.fields : [];
if (fields.length > 0) {
const { error: fErr } = await supabase.from('determined_commitment_fields').insert(
const { error: fErr } = await tenantDb().from('determined_commitment_fields').insert(
fields.map((f, idx) => ({
tenant_id: tenantId,
commitment_id: newC.id,
key: f.key,
label: f.label,
@@ -304,8 +300,7 @@ async function onSave(payload) {
}
toast.add({ severity: 'success', summary: 'Criado', detail: 'Compromisso criado.', life: 2500 });
} else {
const { error: upErr } = await supabase
.from('determined_commitments')
const { error: upErr } = await tenantDb().from('determined_commitments')
.update({
name: payload.name,
description: payload.description,
@@ -313,16 +308,15 @@ async function onSave(payload) {
bg_color: payload.bg_color || null,
text_color: payload.text_color || null
})
.eq('tenant_id', tenantId)
.eq('id', payload.id);
if (upErr) throw upErr;
const { error: delErr } = await supabase.from('determined_commitment_fields').delete().eq('tenant_id', tenantId).eq('commitment_id', payload.id);
const { error: delErr } = await tenantDb().from('determined_commitment_fields').delete().eq('commitment_id', payload.id);
if (delErr) throw delErr;
const fields = Array.isArray(payload.fields) ? payload.fields : [];
if (fields.length > 0) {
const { error: insErr } = await supabase.from('determined_commitment_fields').insert(
const { error: insErr } = await tenantDb().from('determined_commitment_fields').insert(
fields.map((f, idx) => ({
tenant_id: tenantId,
commitment_id: payload.id,
key: f.key,
label: f.label,
@@ -358,11 +352,11 @@ async function onDelete(c) {
if (!tenantId) return;
saving.value = true;
try {
const { error: fErr } = await supabase.from('determined_commitment_fields').delete().eq('tenant_id', tenantId).eq('commitment_id', c.id);
const { error: fErr } = await tenantDb().from('determined_commitment_fields').delete().eq('commitment_id', c.id);
if (fErr) throw fErr;
const { error: lErr } = await supabase.from('commitment_time_logs').delete().eq('tenant_id', tenantId).eq('commitment_id', c.id);
const { error: lErr } = await tenantDb().from('commitment_time_logs').delete().eq('commitment_id', c.id);
if (lErr) throw lErr;
const { data: delRows, error: dErr } = await supabase.from('determined_commitments').delete().eq('tenant_id', tenantId).eq('id', c.id).eq('is_native', false).select('id');
const { data: delRows, error: dErr } = await tenantDb().from('determined_commitments').delete().eq('id', c.id).eq('is_native', false).select('id');
if (dErr) throw dErr;
if (!delRows?.length) throw new Error('DELETE bloqueado por RLS.');
toast.add({ severity: 'success', summary: 'Excluído', detail: 'Compromisso removido.', life: 2500 });
@@ -21,6 +21,7 @@
*/
import { dateToISO } from '@/features/agenda/utils/timeHelpers';
import { tenantDb } from '@/lib/supabase/tenantClient';
// ── Helpers puros ─────────────────────────────────────────────────────────
@@ -155,10 +156,9 @@ export async function loadStatusChangeContext({ supabase, row, eventoId, status,
const excType = exceptionTypeMap[status];
if (excType && tenantId) {
try {
const { data } = await supabase
.from('financial_exceptions')
const { data } = await tenantDb().from('financial_exceptions')
.select('*')
.eq('tenant_id', tenantId)
.eq('exception_type', excType)
.or(`owner_id.eq.${ownerId},owner_id.is.null`)
.order('owner_id', { ascending: false, nullsLast: true })
@@ -177,8 +177,7 @@ export async function loadStatusChangeContext({ supabase, row, eventoId, status,
const contractId = row?.billing_contract_id ?? null;
if (contractId) {
try {
const { data } = await supabase
.from('billing_contracts')
const { data } = await tenantDb().from('billing_contracts')
.select('*')
.eq('id', contractId)
.maybeSingle();
@@ -189,14 +188,12 @@ export async function loadStatusChangeContext({ supabase, row, eventoId, status,
}
if (!ctx.billingContract && eventoId) {
try {
const { data: ev } = await supabase
.from('agenda_eventos')
const { data: ev } = await tenantDb().from('agenda_eventos')
.select('billing_contract_id')
.eq('id', eventoId)
.maybeSingle();
if (ev?.billing_contract_id) {
const { data: c } = await supabase
.from('billing_contracts')
const { data: c } = await tenantDb().from('billing_contracts')
.select('*')
.eq('id', ev.billing_contract_id)
.maybeSingle();
@@ -208,10 +205,9 @@ export async function loadStatusChangeContext({ supabase, row, eventoId, status,
}
if (!ctx.billingContract && patientId && tenantId) {
try {
const { data: c } = await supabase
.from('billing_contracts')
const { data: c } = await tenantDb().from('billing_contracts')
.select('*')
.eq('tenant_id', tenantId)
.eq('patient_id', patientId)
.eq('status', 'active')
.eq('type', 'package')
@@ -227,8 +223,7 @@ export async function loadStatusChangeContext({ supabase, row, eventoId, status,
// 3) Pending record
if (eventoId) {
try {
const { data } = await supabase
.from('financial_records')
const { data } = await tenantDb().from('financial_records')
.select('*')
.eq('agenda_evento_id', eventoId)
.in('status', ['pending', 'overdue'])
@@ -244,8 +239,7 @@ export async function loadStatusChangeContext({ supabase, row, eventoId, status,
// 3b) Paid record pré-existente (caso C12: antecipar pagamento).
if (eventoId) {
try {
const { data } = await supabase
.from('financial_records')
const { data } = await tenantDb().from('financial_records')
.select('id, status, amount, final_amount, paid_at, payment_method')
.eq('agenda_evento_id', eventoId)
.eq('status', 'paid')
@@ -266,16 +260,14 @@ export async function loadStatusChangeContext({ supabase, row, eventoId, status,
saldoConsumed: false
};
try {
const { data: evRow } = await supabase
.from('agenda_eventos')
const { data: evRow } = await tenantDb().from('agenda_eventos')
.select('status, billing_contract_id')
.eq('id', eventoId)
.maybeSingle();
if (evRow) {
ctx.reverseArtifacts.previousStatus = evRow.status;
}
const { data: recs } = await supabase
.from('financial_records')
const { data: recs } = await tenantDb().from('financial_records')
.select('id, status, amount, final_amount, description, paid_at, payment_method')
.eq('agenda_evento_id', eventoId)
.neq('status', 'cancelled')
@@ -336,8 +328,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
const today = new Date().toISOString().slice(0, 10);
const reason = `Cancelada via reversão de status (${r.previousStatus} → agendado) em ${today}`;
for (const id of pendingIds) {
const { error: cErr } = await supabase
.from('financial_records')
const { error: cErr } = await tenantDb().from('financial_records')
.update({
status: 'cancelled',
notes: `[${today}] ${reason}`,
@@ -356,8 +347,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
// 2) Devolver saldo
if (decision.reverseRestoreSaldo && r.saldoConsumed && ctx.billingContract?.id) {
try {
const { data: freshContract, error: fetchErr } = await supabase
.from('billing_contracts')
const { data: freshContract, error: fetchErr } = await tenantDb().from('billing_contracts')
.select('sessions_used, total_sessions, status')
.eq('id', ctx.billingContract.id)
.maybeSingle();
@@ -369,7 +359,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
if (currentUsed >= totalSessions) {
patch.status = 'active';
}
const { error: dErr } = await supabase.from('billing_contracts').update(patch).eq('id', ctx.billingContract.id);
const { error: dErr } = await tenantDb().from('billing_contracts').update(patch).eq('id', ctx.billingContract.id);
if (dErr) throw dErr;
} catch (e) {
console.error('[agendaBilling/reverse] erro decrementando saldo:', e?.message);
@@ -380,7 +370,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
// 3) Desamarrar billing_contract_id (só se devolveu saldo)
if (decision.reverseRestoreSaldo && r.saldoConsumed) {
try {
await supabase.from('agenda_eventos').update({ billing_contract_id: null, updated_at: new Date().toISOString() }).eq('id', eventoId);
await tenantDb().from('agenda_eventos').update({ billing_contract_id: null, updated_at: new Date().toISOString() }).eq('id', eventoId);
} catch (e) {
console.warn('[agendaBilling/reverse] erro desamarrando billing_contract_id:', e?.message);
}
@@ -393,8 +383,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
// 1) Consumir saldo
if (decision.consumeSaldo && ctx.billingContract?.id) {
tasks.push(
supabase
.from('billing_contracts')
tenantDb().from('billing_contracts')
.update({ sessions_used: (ctx.billingContract.sessions_used ?? 0) + 1 })
.eq('id', ctx.billingContract.id)
);
@@ -404,8 +393,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
const isForwardStatus = novoStatus === 'realizado' || novoStatus === 'faltou' || novoStatus === 'cancelado';
if (isForwardStatus && ctx.billingContract?.id && eventoId) {
tasks.push(
supabase
.from('agenda_eventos')
tenantDb().from('agenda_eventos')
.update({ billing_contract_id: ctx.billingContract.id, updated_at: new Date().toISOString() })
.eq('id', eventoId)
);
@@ -418,7 +406,6 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
const fineDesc = novoStatus === 'faltou' ? `Multa por falta · sessão ${sessaoLabel}` : `Taxa de cancelamento tardio · sessão ${sessaoLabel}`;
const finePayload = {
owner_id: uid,
tenant_id: tenantId,
patient_id: patientId,
agenda_evento_id: eventoId,
amount: decision.fineAmount,
@@ -429,8 +416,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
type: 'receita'
};
tasks.push(
supabase
.from('financial_records')
tenantDb().from('financial_records')
.insert(finePayload)
.then(({ error }) => {
if (error) {
@@ -455,8 +441,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
const noteEntry = `[${today}] ${reasonText}`;
const noteText = ctx.pendingRecord.notes ? `${ctx.pendingRecord.notes}\n${noteEntry}` : noteEntry;
tasks.push(
supabase
.from('financial_records')
tenantDb().from('financial_records')
.update({
status: 'cancelled',
notes: noteText,
@@ -469,8 +454,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
// 3) Realizado avulsa pendente: marcar pendingRecord como pago (ou só status)
if (decision.markPaid && ctx.pendingRecord?.id) {
tasks.push(
supabase
.from('financial_records')
tenantDb().from('financial_records')
.update({
status: 'paid',
paid_at: new Date().toISOString(),
@@ -492,8 +476,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
}
}
try {
const { data: freshContract, error: fetchErr } = await supabase
.from('billing_contracts')
const { data: freshContract, error: fetchErr } = await tenantDb().from('billing_contracts')
.select('sessions_used, total_sessions, status')
.eq('id', ctx.billingContract.id)
.maybeSingle();
@@ -504,7 +487,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
if (newUsed >= (freshContract?.total_sessions ?? 0)) {
patch.status = 'completed';
}
const { error: incErr } = await supabase.from('billing_contracts').update(patch).eq('id', ctx.billingContract.id);
const { error: incErr } = await tenantDb().from('billing_contracts').update(patch).eq('id', ctx.billingContract.id);
if (incErr) throw incErr;
tx({ severity: 'success', summary: 'Sessão consumida', detail: `Saldo: ${newUsed}/${freshContract?.total_sessions ?? '?'}. Pagamento já estava registrado.`, life: 4000 });
} catch (e) {
@@ -520,7 +503,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
const dueIso = row.inicio_em ? new Date(row.inicio_em).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10);
try {
const { error: linkErr } = await supabase.from('agenda_eventos').update({ billing_contract_id: ctx.billingContract.id, updated_at: new Date().toISOString() }).eq('id', eventoId);
const { error: linkErr } = await tenantDb().from('agenda_eventos').update({ billing_contract_id: ctx.billingContract.id, updated_at: new Date().toISOString() }).eq('id', eventoId);
if (linkErr) throw linkErr;
} catch (e) {
console.error('[agendaBilling] erro amarrando billing_contract_id:', e?.message);
@@ -548,7 +531,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
if (newUsed >= (ctx.billingContract.total_sessions ?? 0)) {
patchContract.status = 'completed';
}
const { error: incErr } = await supabase.from('billing_contracts').update(patchContract).eq('id', ctx.billingContract.id);
const { error: incErr } = await tenantDb().from('billing_contracts').update(patchContract).eq('id', ctx.billingContract.id);
if (incErr) throw incErr;
} catch (e) {
console.error('[agendaBilling] erro incrementando sessions_used:', e?.message);
@@ -570,8 +553,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
// Pós-processamento do record gerado pelo pacote saldo
if (decision.generatePackageCharge && eventoId) {
try {
const { data: newRec } = await supabase
.from('financial_records')
const { data: newRec } = await tenantDb().from('financial_records')
.select('id')
.eq('agenda_evento_id', eventoId)
.order('created_at', { ascending: false })
@@ -579,8 +561,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
.single();
if (newRec?.id) {
if (decision.markPaid) {
await supabase
.from('financial_records')
await tenantDb().from('financial_records')
.update({
status: 'paid',
paid_at: new Date().toISOString(),
@@ -589,8 +570,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
})
.eq('id', newRec.id);
} else if (decision.paymentMethod === 'link') {
await supabase
.from('financial_records')
await tenantDb().from('financial_records')
.update({ payment_method: 'asaas', updated_at: new Date().toISOString() })
.eq('id', newRec.id);
}
@@ -609,11 +589,9 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
export async function createPackageContract({ supabase, rule, normalized, recorrencia, tenantId, packageStyle = 'upfront', paymentMethod = 'link', markPaidNow = false }) {
const { n, packagePrice } = computeSeriePrice(recorrencia);
try {
const { data: createdContract, error: contractErr } = await supabase
.from('billing_contracts')
const { data: createdContract, error: contractErr } = await tenantDb().from('billing_contracts')
.insert({
owner_id: normalized.owner_id,
tenant_id: tenantId,
patient_id: normalized.paciente_id,
type: 'package',
total_sessions: n,
@@ -645,11 +623,9 @@ export async function createPackageContract({ supabase, rule, normalized, recorr
startDt.setHours(hh, mm, 0, 0);
const endDt = new Date(startDt.getTime() + durMin * 60 * 1000);
const { data: createdEvent, error: evErr } = await supabase
.from('agenda_eventos')
const { data: createdEvent, error: evErr } = await tenantDb().from('agenda_eventos')
.insert({
owner_id: rule.owner_id,
tenant_id: tenantId,
terapeuta_id: rule.therapist_id ?? null,
recurrence_id: rule.id,
recurrence_date: firstISO,
@@ -680,8 +656,7 @@ export async function createPackageContract({ supabase, rule, normalized, recorr
if (cobErr) throw cobErr;
const paidNow = markPaidNow === true && paymentMethod !== 'link';
const { data: recRow } = await supabase
.from('financial_records')
const { data: recRow } = await tenantDb().from('financial_records')
.select('id')
.eq('agenda_evento_id', createdEvent.id)
.order('created_at', { ascending: false })
@@ -696,7 +671,7 @@ export async function createPackageContract({ supabase, rule, normalized, recorr
patch.status = 'paid';
patch.paid_at = new Date().toISOString();
}
await supabase.from('financial_records').update(patch).eq('id', recRow.id);
await tenantDb().from('financial_records').update(patch).eq('id', recRow.id);
}
const methodLabel = {
@@ -745,7 +720,6 @@ export async function materializeAndChargePerSession({ supabase, rule, normalize
const endDt = new Date(startDt.getTime() + durMin * 60 * 1000);
return {
owner_id: rule.owner_id,
tenant_id: tenantId,
terapeuta_id: rule.therapist_id ?? null,
recurrence_id: rule.id,
recurrence_date: iso,
@@ -762,7 +736,7 @@ export async function materializeAndChargePerSession({ supabase, rule, normalize
};
});
const { data: createdEvents, error: evErr } = await supabase.from('agenda_eventos').insert(rows).select('id, inicio_em');
const { data: createdEvents, error: evErr } = await tenantDb().from('agenda_eventos').insert(rows).select('id, inicio_em');
if (evErr) throw evErr;
let okCount = 0;
@@ -15,6 +15,7 @@
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import {
assertTenantId as assertValidTenantId,
assertIsoRange as assertValidIsoRange,
@@ -24,7 +25,7 @@ import { AGENDA_EVENT_SELECT, flattenAgendaRow } from './agendaSelects';
/**
* Lista eventos para mosaico da clínica (admin/secretaria) dentro de um tenant específico.
* IMPORTANTE: SEM tenant_id aqui vira vazamento multi-tenant.
* Isolamento multi-tenant garantido pelo schema do tenant (tenantDb).
*/
export async function listClinicEvents({ tenantId, ownerIds, startISO, endISO } = {}) {
assertValidTenantId(tenantId);
@@ -34,10 +35,9 @@ export async function listClinicEvents({ tenantId, ownerIds, startISO, endISO }
const safeOwnerIds = sanitizeOwnerIds(ownerIds);
if (!safeOwnerIds.length) return [];
const { data, error } = await supabase
.from('agenda_eventos')
const { data, error } = await tenantDb().from('agenda_eventos')
.select(AGENDA_EVENT_SELECT)
.eq('tenant_id', tenantId)
.in('owner_id', safeOwnerIds)
.gte('inicio_em', startISO)
.lt('inicio_em', endISO)
@@ -78,13 +78,11 @@ export async function createClinicAgendaEvento(payload, { tenantId } = {}) {
throw new Error('owner_id é obrigatório para criação pela clínica.');
}
const insertPayload = {
...payload,
tenant_id: tenantId
};
// dropa tenant_id se vier no payload (schema-per-tenant não tem a coluna)
// eslint-disable-next-line no-unused-vars
const { tenant_id: _dropTenantId, ...insertPayload } = payload;
const { data, error } = await supabase
.from('agenda_eventos')
const { data, error } = await tenantDb().from('agenda_eventos')
.insert(insertPayload)
.select(AGENDA_EVENT_SELECT)
.single();
@@ -95,7 +93,7 @@ export async function createClinicAgendaEvento(payload, { tenantId } = {}) {
/**
* Atualização segura para clínica:
* - filtra por id + tenant_id (evita update cruzado)
* - filtra por id (isolamento via schema do tenant)
* - permite editar owner_id (caso você mova evento para outro profissional)
*/
export async function updateClinicAgendaEvento(id, patch, { tenantId } = {}) {
@@ -103,11 +101,13 @@ export async function updateClinicAgendaEvento(id, patch, { tenantId } = {}) {
if (!patch) throw new Error('Patch vazio.');
assertValidTenantId(tenantId);
const { data, error } = await supabase
.from('agenda_eventos')
.update(patch)
// eslint-disable-next-line no-unused-vars
const { tenant_id: _dropTenantId, ...safePatch } = patch;
const { data, error } = await tenantDb().from('agenda_eventos')
.update(safePatch)
.eq('id', id)
.eq('tenant_id', tenantId)
.select(AGENDA_EVENT_SELECT)
.single();
@@ -117,13 +117,13 @@ export async function updateClinicAgendaEvento(id, patch, { tenantId } = {}) {
/**
* Delete seguro para clínica:
* - filtra por id + tenant_id
* - filtra por id (isolamento via schema do tenant)
*/
export async function deleteClinicAgendaEvento(id, { tenantId } = {}) {
if (!id) throw new Error('ID inválido.');
assertValidTenantId(tenantId);
const { error } = await supabase.from('agenda_eventos').delete().eq('id', id).eq('tenant_id', tenantId);
const { error } = await tenantDb().from('agenda_eventos').delete().eq('id', id);
if (error) throw error;
return true;
@@ -143,8 +143,7 @@ function _mapRow(r) {
// timestamps
inicio_em: r.inicio_em,
fim_em: r.fim_em,
tenant_id: r.tenant_id ?? null
fim_em: r.fim_em
}
};
}
@@ -15,6 +15,7 @@
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
import { assertTenantId as assertValidTenantId, assertIsoRange, getUid } from './_tenantGuards';
import { AGENDA_EVENT_SELECT, flattenAgendaRow } from './agendaSelects';
@@ -23,8 +24,7 @@ import { AGENDA_EVENT_SELECT, flattenAgendaRow } from './agendaSelects';
export async function getMyAgendaSettings() {
const uid = await getUid();
const { data, error } = await supabase
.from('agenda_configuracoes')
const { data, error } = await tenantDb().from('agenda_configuracoes')
.select('*')
.eq('owner_id', uid)
.order('created_at', { ascending: false })
@@ -36,8 +36,7 @@ export async function getMyAgendaSettings() {
export async function getMyWorkSchedule() {
const uid = await getUid();
const { data, error } = await supabase
.from('agenda_regras_semanais')
const { data, error } = await tenantDb().from('agenda_regras_semanais')
.select('dia_semana, hora_inicio, hora_fim, ativo')
.eq('owner_id', uid)
.eq('ativo', true)
@@ -78,10 +77,9 @@ export async function listMyAgendaEvents({ startISO, endISO, ownerId, tenantId,
const uid = ownerId || (await getUid());
const tid = resolveTenantId(tenantId);
let q = supabase
.from('agenda_eventos')
let q = tenantDb().from('agenda_eventos')
.select(AGENDA_EVENT_SELECT)
.eq('tenant_id', tid)
.eq('owner_id', uid)
.gte('inicio_em', startISO)
.lt('inicio_em', endISO)
@@ -96,9 +94,8 @@ export async function listMyAgendaEvents({ startISO, endISO, ownerId, tenantId,
/**
* Criação segura:
* - injeta tenant_id do tenantStore
* - injeta owner_id do usuário logado (ignora owner_id vindo de fora)
* - dropa paciente_id (campo legado) se vier no payload
* - dropa paciente_id (campo legado) e tenant_id (schema-per-tenant não tem a coluna) se vierem no payload
*/
export async function createAgendaEvento(payload) {
if (!payload) throw new Error('Payload vazio.');
@@ -106,11 +103,10 @@ export async function createAgendaEvento(payload) {
const tid = resolveTenantId();
// eslint-disable-next-line no-unused-vars
const { paciente_id: _dropped, ...rest } = payload;
const insertPayload = { ...rest, tenant_id: tid, owner_id: uid };
const { paciente_id: _dropped, tenant_id: _dropTenantId, ...rest } = payload;
const insertPayload = { ...rest, owner_id: uid };
const { data, error } = await supabase
.from('agenda_eventos')
const { data, error } = await tenantDb().from('agenda_eventos')
.insert([insertPayload])
.select(AGENDA_EVENT_SELECT)
.single();
@@ -120,7 +116,7 @@ export async function createAgendaEvento(payload) {
}
/**
* Atualização segura: filtra por id + tenant_id (RLS reforça no banco).
* Atualização segura: filtra por id (isolamento via schema do tenant; RLS reforça no banco).
*/
export async function updateAgendaEvento(id, patch, { tenantId } = {}) {
if (!id) throw new Error('ID inválido.');
@@ -128,13 +124,12 @@ export async function updateAgendaEvento(id, patch, { tenantId } = {}) {
const tid = resolveTenantId(tenantId);
// eslint-disable-next-line no-unused-vars
const { paciente_id: _dropped, ...safePatch } = patch;
const { paciente_id: _dropped, tenant_id: _dropTenantId, ...safePatch } = patch;
const { data, error } = await supabase
.from('agenda_eventos')
const { data, error } = await tenantDb().from('agenda_eventos')
.update(safePatch)
.eq('id', id)
.eq('tenant_id', tid)
.select(AGENDA_EVENT_SELECT)
.single();
@@ -143,13 +138,13 @@ export async function updateAgendaEvento(id, patch, { tenantId } = {}) {
}
/**
* Delete seguro: filtra por id + tenant_id.
* Delete seguro: filtra por id (isolamento via schema do tenant).
*/
export async function deleteAgendaEvento(id, { tenantId } = {}) {
if (!id) throw new Error('ID inválido.');
const tid = resolveTenantId(tenantId);
const { error } = await supabase.from('agenda_eventos').delete().eq('id', id).eq('tenant_id', tid);
const { error } = await tenantDb().from('agenda_eventos').delete().eq('id', id);
if (error) throw error;
return true;
}
@@ -21,7 +21,7 @@
export const AGENDA_EVENT_SELECT = `
id, owner_id, patient_id, tipo, status,
titulo, titulo_custom, observacoes, inicio_em, fim_em,
terapeuta_id, tenant_id, visibility_scope,
terapeuta_id, visibility_scope,
determined_commitment_id, link_online, extra_fields, modalidade,
recurrence_id, recurrence_date,
mirror_of_event_id, price,
+1 -1
View File
@@ -13,7 +13,7 @@
*/
const ALLOWED_FIELDS = [
'tenant_id', 'owner_id', 'terapeuta_id', 'patient_id',
'owner_id', 'terapeuta_id', 'patient_id',
'tipo', 'status', 'titulo', 'observacoes', 'modalidade',
'inicio_em', 'fim_em', 'visibility_scope',
'mirror_of_event_id', 'mirror_source',
@@ -17,6 +17,7 @@
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
import { assertTenantId } from './_tenantGuards';
import {
@@ -41,7 +42,7 @@ function resolveTenantId(tenantIdArg) {
export async function listThreads({ tenantId, limit = 500 } = {}) {
const tid = resolveTenantId(tenantId);
const { data, error } = await supabase.from('conversation_threads').select(CONVERSATION_THREAD_SELECT).eq('tenant_id', tid).order('last_message_at', { ascending: false }).limit(limit);
const { data, error } = await tenantDb().from('conversation_threads').select(CONVERSATION_THREAD_SELECT).order('last_message_at', { ascending: false }).limit(limit);
if (error) throw error;
return data || [];
@@ -54,7 +55,7 @@ export async function getThreadById(threadId, { tenantId } = {}) {
if (!threadId) throw new Error('threadId obrigatório.');
const tid = resolveTenantId(tenantId);
const { data, error } = await supabase.from('conversation_threads').select(CONVERSATION_THREAD_SELECT).eq('id', threadId).eq('tenant_id', tid).maybeSingle();
const { data, error } = await tenantDb().from('conversation_threads').select(CONVERSATION_THREAD_SELECT).eq('id', threadId).maybeSingle();
if (error) throw error;
return data || null;
@@ -67,7 +68,7 @@ export async function updateThread(threadId, patch, { tenantId } = {}) {
if (!threadId) throw new Error('threadId obrigatório.');
const tid = resolveTenantId(tenantId);
const { data, error } = await supabase.from('conversation_threads').update({ ...patch, updated_at: new Date().toISOString() }).eq('id', threadId).eq('tenant_id', tid).select(CONVERSATION_THREAD_SELECT).single();
const { data, error } = await tenantDb().from('conversation_threads').update({ ...patch, updated_at: new Date().toISOString() }).eq('id', threadId).select(CONVERSATION_THREAD_SELECT).single();
if (error) throw error;
return data;
@@ -82,7 +83,7 @@ export async function listMessagesByThread(threadId, { tenantId, limit = 500 } =
if (!threadId) return [];
const tid = resolveTenantId(tenantId);
const { data, error } = await supabase.from('conversation_messages').select(CONVERSATION_MESSAGE_SELECT).eq('tenant_id', tid).eq('thread_id', threadId).order('created_at', { ascending: true }).limit(limit);
const { data, error } = await tenantDb().from('conversation_messages').select(CONVERSATION_MESSAGE_SELECT).eq('thread_id', threadId).order('created_at', { ascending: true }).limit(limit);
if (error) throw error;
return data || [];
@@ -96,7 +97,7 @@ export async function listMessagesByPatient(patientId, { tenantId, limit = 200 }
if (!patientId) return [];
const tid = resolveTenantId(tenantId);
const { data, error } = await supabase.from('conversation_messages').select(CONVERSATION_MESSAGE_SELECT_BRIEF).eq('tenant_id', tid).eq('patient_id', patientId).order('created_at', { ascending: false }).limit(limit);
const { data, error } = await tenantDb().from('conversation_messages').select(CONVERSATION_MESSAGE_SELECT_BRIEF).eq('patient_id', patientId).order('created_at', { ascending: false }).limit(limit);
if (error) throw error;
return data || [];
@@ -109,7 +110,7 @@ export async function updateMessageKanban(messageId, kanbanStatus, { tenantId }
if (!messageId) throw new Error('messageId obrigatório.');
const tid = resolveTenantId(tenantId);
const { error } = await supabase.from('conversation_messages').update({ kanban_status: kanbanStatus, updated_at: new Date().toISOString() }).eq('id', messageId).eq('tenant_id', tid);
const { error } = await tenantDb().from('conversation_messages').update({ kanban_status: kanbanStatus, updated_at: new Date().toISOString() }).eq('id', messageId);
if (error) throw error;
}
@@ -12,6 +12,7 @@
import { ref, reactive, watch, computed } from 'vue'
import { useToast } from 'primevue/usetoast'
import { supabase } from '@/lib/supabase/client'
import { tenantDb } from '@/lib/supabase/tenantClient';
import {
createSignatureRequests,
listSignatures,
@@ -61,8 +62,7 @@ function removeSignatario(idx) {
async function fetchPatientEmails(patientId) {
if (!patientId) { patientEmails.value = []; return }
try {
const { data } = await supabase
.from('patients')
const { data } = await tenantDb().from('patients')
.select('email_principal, email_alternativo')
.eq('id', patientId)
.single()
@@ -20,6 +20,7 @@ import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
// helpers
const router = useRouter();
@@ -75,7 +76,7 @@ async function loadSummary(uid) {
totalDespesas.value = Number(s?.total_despesas ?? 0);
// Pending e overdue separados (sem filtro de mês)
const { data: pendRows } = await supabase.from('financial_records').select('status, final_amount').eq('owner_id', uid).is('deleted_at', null).in('status', ['pending', 'overdue']);
const { data: pendRows } = await tenantDb().from('financial_records').select('status, final_amount').eq('owner_id', uid).is('deleted_at', null).in('status', ['pending', 'overdue']);
let pen = 0,
ove = 0;
@@ -141,7 +142,7 @@ async function loadCashflow() {
cashflowLoading.value = true;
cashflowError.value = false;
try {
const { data, error } = await supabase.from('v_cashflow_projection').select('mes_label, receitas_projetadas, despesas_projetadas, saldo_projetado, count_registros').order('mes', { ascending: true });
const { data, error } = await tenantDb().from('v_cashflow_projection').select('mes_label, receitas_projetadas, despesas_projetadas, saldo_projetado, count_registros').order('mes', { ascending: true });
if (error) throw error;
cashflowRows.value = data ?? [];
} catch {
@@ -20,6 +20,7 @@ import { useToast } from 'primevue/usetoast';
import { useConfirm } from 'primevue/useconfirm';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
import { useFinancialRecords } from '@/composables/useFinancialRecords';
@@ -47,7 +48,7 @@ async function loadPatients() {
const tenantId = tenantStore.activeTenantId;
if (!tenantId) return;
const { data } = await supabase.from('patients').select('id, nome_completo, identification_color').eq('tenant_id', tenantId).order('nome_completo');
const { data } = await tenantDb().from('patients').select('id, nome_completo, identification_color').order('nome_completo');
patients.value = data ?? [];
}
@@ -21,6 +21,7 @@
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
// ─── Status mapping Asaas → financial_records.status ────────────────────────
@@ -128,10 +129,9 @@ export async function getPaymentForRecord(financialRecordId) {
if (!financialRecordId) return null;
const tenantId = resolveTenantId();
const { data, error } = await supabase
.from('asaas_payments')
const { data, error } = await tenantDb().from('asaas_payments')
.select('id, asaas_payment_id, billing_type, status, value, due_date, payment_date, invoice_url, payment_url, bank_slip_url, pix_qr_code, pix_copy_paste, cancelled_at')
.eq('tenant_id', tenantId)
.eq('financial_record_id', financialRecordId)
.is('cancelled_at', null)
.order('created_at', { ascending: false })
@@ -167,7 +167,7 @@ export async function syncPayment(asaasPaymentId) {
*/
export async function isGatewayEnabled() {
const tenantId = resolveTenantId();
const { data, error } = await supabase.from('payment_settings').select('asaas_enabled, asaas_environment').eq('tenant_id', tenantId).maybeSingle();
const { data, error } = await tenantDb().from('payment_settings').select('asaas_enabled, asaas_environment').maybeSingle();
if (error) return false;
return !!data?.asaas_enabled;
}
@@ -13,6 +13,7 @@
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
import { assertTenantId, getUid } from './_tenantGuards';
import { BILLING_CONTRACT_SELECT } from './financialSelects';
@@ -31,7 +32,7 @@ export async function listForPatient(patientId, { tenantId, includeDeleted = fal
if (!patientId) return [];
const tid = resolveTenantId(tenantId);
let q = supabase.from('billing_contracts').select(BILLING_CONTRACT_SELECT).eq('tenant_id', tid).eq('patient_id', patientId).order('created_at', { ascending: false });
let q = tenantDb().from('billing_contracts').select(BILLING_CONTRACT_SELECT).eq('patient_id', patientId).order('created_at', { ascending: false });
if (!includeDeleted) q = q.is('deleted_at', null);
@@ -48,7 +49,7 @@ export async function getById(contractId, { tenantId } = {}) {
if (!contractId) throw new Error('contractId obrigatório.');
const tid = resolveTenantId(tenantId);
const { data, error } = await supabase.from('billing_contracts').select(BILLING_CONTRACT_SELECT).eq('id', contractId).eq('tenant_id', tid).maybeSingle();
const { data, error } = await tenantDb().from('billing_contracts').select(BILLING_CONTRACT_SELECT).eq('id', contractId).maybeSingle();
if (error) throw error;
return data || null;
@@ -65,7 +66,6 @@ export async function create(payload) {
const tid = resolveTenantId(payload.tenantId);
const row = {
tenant_id: tid,
owner_id: uid,
patient_id: payload.patient_id,
charging_style: payload.charging_style,
@@ -77,7 +77,7 @@ export async function create(payload) {
end_date: payload.end_date || null
};
const { data, error } = await supabase.from('billing_contracts').insert([row]).select(BILLING_CONTRACT_SELECT).single();
const { data, error } = await tenantDb().from('billing_contracts').insert([row]).select(BILLING_CONTRACT_SELECT).single();
if (error) throw error;
return data;
@@ -93,7 +93,7 @@ export async function update(contractId, patch, { tenantId } = {}) {
// eslint-disable-next-line no-unused-vars
const { updated_at: _dropped, ...safePatch } = patch || {};
const { data, error } = await supabase.from('billing_contracts').update(safePatch).eq('id', contractId).eq('tenant_id', tid).select(BILLING_CONTRACT_SELECT).single();
const { data, error } = await tenantDb().from('billing_contracts').update(safePatch).eq('id', contractId).select(BILLING_CONTRACT_SELECT).single();
if (error) throw error;
return data;
@@ -128,7 +128,7 @@ export async function findRecordsByRecurrence(recurrenceId, { tenantId } = {}) {
if (!recurrenceId) return [];
const tid = resolveTenantId(tenantId);
const { data, error } = await supabase.from('financial_records').select('id, status, agenda_evento_id, billing_contract_id').eq('tenant_id', tid).is('deleted_at', null).not('agenda_evento_id', 'is', null);
const { data, error } = await tenantDb().from('financial_records').select('id, status, agenda_evento_id, billing_contract_id').is('deleted_at', null).not('agenda_evento_id', 'is', null);
// NOTE: filter por recurrence_id requer join — fica como TODO no orchestrator
// (memória project_cross_week_propagation: query records cross-week por recurrence_id).
@@ -11,6 +11,7 @@
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
import { assertTenantId, getUid } from './_tenantGuards';
import { FINANCIAL_EXCEPTION_SELECT } from './financialSelects';
@@ -35,10 +36,9 @@ export async function getRule(exceptionType, { tenantId } = {}) {
const tid = resolveTenantId(tenantId);
const uid = await getUid();
const { data, error } = await supabase
.from('financial_exceptions')
const { data, error } = await tenantDb().from('financial_exceptions')
.select(FINANCIAL_EXCEPTION_SELECT)
.eq('tenant_id', tid)
.eq('exception_type', exceptionType)
.or(`owner_id.eq.${uid},owner_id.is.null`)
.order('owner_id', { ascending: false, nullsLast: true })
@@ -54,7 +54,7 @@ export async function getRule(exceptionType, { tenantId } = {}) {
*/
export async function listAll({ tenantId } = {}) {
const tid = resolveTenantId(tenantId);
const { data, error } = await supabase.from('financial_exceptions').select(FINANCIAL_EXCEPTION_SELECT).eq('tenant_id', tid).order('exception_type', { ascending: true });
const { data, error } = await tenantDb().from('financial_exceptions').select(FINANCIAL_EXCEPTION_SELECT).order('exception_type', { ascending: true });
if (error) throw error;
return data || [];
}
@@ -72,7 +72,6 @@ export async function upsertRule(payload) {
const tid = resolveTenantId(payload.tenantId);
const row = {
tenant_id: tid,
owner_id: payload.ownerScoped ? uid : null,
exception_type: payload.exception_type,
charge_mode: payload.charge_mode || 'none',
@@ -83,7 +82,8 @@ export async function upsertRule(payload) {
updated_at: new Date().toISOString()
};
const { data, error } = await supabase.from('financial_exceptions').upsert(row, { onConflict: 'tenant_id,owner_id,exception_type' }).select(FINANCIAL_EXCEPTION_SELECT).single();
// TODO(schema-per-tenant): conferir unique (PK é só id; antes era tenant_id,owner_id,exception_type)
const { data, error } = await tenantDb().from('financial_exceptions').upsert(row, { onConflict: 'owner_id,exception_type' }).select(FINANCIAL_EXCEPTION_SELECT).single();
if (error) throw error;
return data;
@@ -16,6 +16,7 @@
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
import { assertTenantId, getUid } from './_tenantGuards';
import { FINANCIAL_RECORD_SELECT, flattenFinancialRecord } from './financialSelects';
@@ -46,10 +47,9 @@ export async function list(filters = {}) {
const limit = filters.limit ?? 50;
const offset = filters.offset ?? 0;
let q = supabase
.from('financial_records')
let q = tenantDb().from('financial_records')
.select(FINANCIAL_RECORD_SELECT, { count: 'exact' })
.eq('tenant_id', tid)
.is('deleted_at', null)
.order('due_date', { ascending: false })
.range(offset, offset + limit - 1);
@@ -75,7 +75,7 @@ export async function getById(recordId, { tenantId } = {}) {
if (!recordId) throw new Error('recordId obrigatório.');
const tid = resolveTenantId(tenantId);
const { data, error } = await supabase.from('financial_records').select(FINANCIAL_RECORD_SELECT).eq('id', recordId).eq('tenant_id', tid).maybeSingle();
const { data, error } = await tenantDb().from('financial_records').select(FINANCIAL_RECORD_SELECT).eq('id', recordId).maybeSingle();
if (error) throw error;
return data ? flattenFinancialRecord(data) : null;
@@ -89,7 +89,7 @@ export async function listByEvent(eventId, { tenantId } = {}) {
if (!eventId) return [];
const tid = resolveTenantId(tenantId);
const { data, error } = await supabase.from('financial_records').select(FINANCIAL_RECORD_SELECT).eq('tenant_id', tid).eq('agenda_evento_id', eventId).is('deleted_at', null);
const { data, error } = await tenantDb().from('financial_records').select(FINANCIAL_RECORD_SELECT).eq('agenda_evento_id', eventId).is('deleted_at', null);
if (error) throw error;
return (data || []).map(flattenFinancialRecord);
@@ -140,7 +140,6 @@ export async function createManual(payload) {
const amount = Number(payload.amount);
const row = {
tenant_id: tid,
owner_id: uid,
patient_id: payload.patient_id ?? null,
agenda_evento_id: null,
@@ -155,7 +154,7 @@ export async function createManual(payload) {
notes: payload.notes ? String(payload.notes).trim() || null : null
};
const { data, error } = await supabase.from('financial_records').insert([row]).select(FINANCIAL_RECORD_SELECT).single();
const { data, error } = await tenantDb().from('financial_records').insert([row]).select(FINANCIAL_RECORD_SELECT).single();
if (error) throw error;
return flattenFinancialRecord(data);
@@ -184,8 +183,7 @@ export async function markAsUnpaid(recordId, { tenantId } = {}) {
if (!recordId) throw new Error('recordId obrigatório.');
const tid = resolveTenantId(tenantId);
const { error } = await supabase
.from('financial_records')
const { error } = await tenantDb().from('financial_records')
.update({
status: 'pending',
paid_at: null,
@@ -193,13 +191,13 @@ export async function markAsUnpaid(recordId, { tenantId } = {}) {
updated_at: new Date().toISOString()
})
.eq('id', recordId)
.eq('tenant_id', tid);
;
if (error) throw error;
}
/**
* Cancela record (soft status='cancelled'). Defesa em profundidade: .eq('tenant_id').
* Cancela record (soft status='cancelled').
*/
export async function cancel(recordId, { tenantId, reason } = {}) {
if (!recordId) throw new Error('recordId obrigatório.');
@@ -208,7 +206,7 @@ export async function cancel(recordId, { tenantId, reason } = {}) {
const patch = { status: 'cancelled', updated_at: new Date().toISOString() };
if (reason) patch.notes = String(reason).trim() || null;
const { error } = await supabase.from('financial_records').update(patch).eq('id', recordId).eq('tenant_id', tid);
const { error } = await tenantDb().from('financial_records').update(patch).eq('id', recordId);
if (error) throw error;
}
@@ -223,7 +221,7 @@ export async function update(recordId, patch, { tenantId } = {}) {
const safePatch = { ...patch, updated_at: new Date().toISOString() };
const { data, error } = await supabase.from('financial_records').update(safePatch).eq('id', recordId).eq('tenant_id', tid).select(FINANCIAL_RECORD_SELECT).single();
const { data, error } = await tenantDb().from('financial_records').update(safePatch).eq('id', recordId).select(FINANCIAL_RECORD_SELECT).single();
if (error) throw error;
return flattenFinancialRecord(data);
@@ -8,12 +8,13 @@
| Pure functions seguindo blueprints/repository-blueprint.md.
|
| Schema (servicos_prontuarios.sql):
| id, owner_id, tenant_id,
| id, owner_id,
| name text, notes text, default_value numeric(10,2),
| active boolean DEFAULT true, created_at, updated_at
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
import { assertTenantId, getUid } from './_tenantGuards';
import { INSURANCE_PLAN_SELECT } from './insurancePlansSelects';
@@ -37,7 +38,7 @@ export async function listForOwner({ ownerId, tenantId, includeInactive = false
const tid = resolveTenantId(tenantId);
const uid = ownerId || (await getUid());
let q = supabase.from('insurance_plans').select(INSURANCE_PLAN_SELECT).eq('tenant_id', tid).eq('owner_id', uid).order('name', { ascending: true });
let q = tenantDb().from('insurance_plans').select(INSURANCE_PLAN_SELECT).eq('owner_id', uid).order('name', { ascending: true });
if (!includeInactive) q = q.eq('active', true);
@@ -47,14 +48,14 @@ export async function listForOwner({ ownerId, tenantId, includeInactive = false
}
/**
* convênio por id. Filtra owner_id + tenant_id por segurança.
* convênio por id. Filtra owner_id por segurança.
*/
export async function getById(id, { tenantId } = {}) {
if (!id) throw new Error('ID inválido.');
const tid = resolveTenantId(tenantId);
const uid = await getUid();
const { data, error } = await supabase.from('insurance_plans').select(INSURANCE_PLAN_SELECT).eq('id', id).eq('tenant_id', tid).eq('owner_id', uid).maybeSingle();
const { data, error } = await tenantDb().from('insurance_plans').select(INSURANCE_PLAN_SELECT).eq('id', id).eq('owner_id', uid).maybeSingle();
if (error) throw error;
return data || null;
@@ -76,7 +77,7 @@ export async function findByName({ name, ownerId, tenantId } = {}) {
const safeName = String(name).trim();
if (!safeName) return null;
const { data, error } = await supabase.from('insurance_plans').select(INSURANCE_PLAN_SELECT).eq('tenant_id', tid).eq('owner_id', uid).eq('active', true).ilike('name', safeName).limit(1).maybeSingle();
const { data, error } = await tenantDb().from('insurance_plans').select(INSURANCE_PLAN_SELECT).eq('owner_id', uid).eq('active', true).ilike('name', safeName).limit(1).maybeSingle();
if (error) throw error;
return data || null;
@@ -84,7 +85,7 @@ export async function findByName({ name, ownerId, tenantId } = {}) {
/**
* Cria convênio. Pré-checa duplicidade por nome (case-insensitive) se
* existe ativo, lança erro PT-BR. Repository injeta owner_id + tenant_id.
* existe ativo, lança erro PT-BR. Repository injeta owner_id.
*/
export async function create(payload) {
if (!payload) throw new Error('Payload vazio.');
@@ -103,21 +104,20 @@ export async function create(payload) {
const insertPayload = {
owner_id: uid,
tenant_id: tid,
name: name.slice(0, 120),
notes: payload.notes ? String(payload.notes).trim().slice(0, 500) || null : null,
default_value: payload.default_value != null && payload.default_value !== '' ? Number(payload.default_value) : null,
active: payload.active !== false
};
const { data, error } = await supabase.from('insurance_plans').insert([insertPayload]).select(INSURANCE_PLAN_SELECT).single();
const { data, error } = await tenantDb().from('insurance_plans').insert([insertPayload]).select(INSURANCE_PLAN_SELECT).single();
if (error) throw error;
return data;
}
/**
* Atualiza convênio. Filtra por id + tenant_id.
* Atualiza convênio. Filtra por id.
*/
export async function update(id, patch, { tenantId } = {}) {
if (!id) throw new Error('ID inválido.');
@@ -127,7 +127,7 @@ export async function update(id, patch, { tenantId } = {}) {
const safePatch = sanitize(patch);
safePatch.updated_at = new Date().toISOString();
const { data, error } = await supabase.from('insurance_plans').update(safePatch).eq('id', id).eq('tenant_id', tid).select(INSURANCE_PLAN_SELECT).single();
const { data, error } = await tenantDb().from('insurance_plans').update(safePatch).eq('id', id).select(INSURANCE_PLAN_SELECT).single();
if (error) throw error;
return data;
@@ -140,7 +140,7 @@ export async function softDelete(id, { tenantId } = {}) {
if (!id) throw new Error('ID inválido.');
const tid = resolveTenantId(tenantId);
const { error } = await supabase.from('insurance_plans').update({ active: false, updated_at: new Date().toISOString() }).eq('id', id).eq('tenant_id', tid);
const { error } = await tenantDb().from('insurance_plans').update({ active: false, updated_at: new Date().toISOString() }).eq('id', id);
if (error) throw error;
return true;
@@ -149,7 +149,9 @@ export async function softDelete(id, { tenantId } = {}) {
// ─── helpers internos ────────────────────────────────────────────────────────
function sanitize(payload) {
const out = { ...payload };
// Dropa tenant_id defensivamente (schema-per-tenant: coluna não existe mais)
const { tenant_id: _drop, ...rest } = payload;
const out = { ...rest };
if ('name' in out && typeof out.name === 'string') {
const t = out.name.trim();
out.name = t === '' ? null : t.slice(0, 120);
@@ -8,12 +8,13 @@
| blueprints/repository-blueprint.md.
|
| Schema (servicos_prontuarios.sql):
| id, owner_id, tenant_id, nome, crm, especialidade,
| id, owner_id, nome, crm, especialidade,
| telefone_profissional, telefone_pessoal, email, clinica,
| cidade, estado='SP', observacoes, ativo=true, created_at, updated_at
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
import { assertTenantId, getUid } from './_tenantGuards';
import { MEDICO_LIST_SELECT, MEDICO_FULL_SELECT } from './medicosSelects';
@@ -38,7 +39,7 @@ export async function listForOwner({ ownerId, tenantId, includeInactive = false
const tid = resolveTenantId(tenantId);
const uid = ownerId || (await getUid());
let q = supabase.from('medicos').select(MEDICO_LIST_SELECT).eq('tenant_id', tid).eq('owner_id', uid).order('nome', { ascending: true });
let q = tenantDb().from('medicos').select(MEDICO_LIST_SELECT).eq('owner_id', uid).order('nome', { ascending: true });
if (!includeInactive) q = q.eq('ativo', true);
@@ -48,7 +49,7 @@ export async function listForOwner({ ownerId, tenantId, includeInactive = false
}
/**
* um médico completo (pra edit). Filtra owner_id + tenant_id por segurança.
* um médico completo (pra edit). Filtra owner_id por segurança.
*
* @param {string} id
* @param {Object} [opts]
@@ -59,14 +60,14 @@ export async function getById(id, { tenantId } = {}) {
const tid = resolveTenantId(tenantId);
const uid = await getUid();
const { data, error } = await supabase.from('medicos').select(MEDICO_FULL_SELECT).eq('id', id).eq('tenant_id', tid).eq('owner_id', uid).maybeSingle();
const { data, error } = await tenantDb().from('medicos').select(MEDICO_FULL_SELECT).eq('id', id).eq('owner_id', uid).maybeSingle();
if (error) throw error;
return data || null;
}
/**
* Cria médico. Injeta owner_id (uid logado) + tenant_id (store).
* Cria médico. Injeta owner_id (uid logado).
* Payload aceita os campos canônicos da tabela; o repository sanitiza
* trims e nullif vazio.
*
@@ -83,18 +84,17 @@ export async function create(payload) {
const insertPayload = {
...sanitize(payload),
owner_id: uid,
tenant_id: tid,
ativo: payload.ativo !== false
};
const { data, error } = await supabase.from('medicos').insert([insertPayload]).select(MEDICO_FULL_SELECT).single();
const { data, error } = await tenantDb().from('medicos').insert([insertPayload]).select(MEDICO_FULL_SELECT).single();
if (error) throw error;
return data;
}
/**
* Atualiza médico. Filtra por id + tenant_id (defesa em profundidade RLS reforça).
* Atualiza médico. Filtra por id (defesa em profundidade RLS reforça).
* updated_at é atualizado server-side ou aqui se não houver trigger.
*
* @param {string} id
@@ -112,7 +112,7 @@ export async function update(id, patch, { tenantId } = {}) {
updated_at: new Date().toISOString()
};
const { data, error } = await supabase.from('medicos').update(safePatch).eq('id', id).eq('tenant_id', tid).select(MEDICO_FULL_SELECT).single();
const { data, error } = await tenantDb().from('medicos').update(safePatch).eq('id', id).select(MEDICO_FULL_SELECT).single();
if (error) throw error;
return data;
@@ -130,7 +130,7 @@ export async function softDelete(id, { tenantId } = {}) {
if (!id) throw new Error('ID inválido.');
const tid = resolveTenantId(tenantId);
const { error } = await supabase.from('medicos').update({ ativo: false, updated_at: new Date().toISOString() }).eq('id', id).eq('tenant_id', tid);
const { error } = await tenantDb().from('medicos').update({ ativo: false, updated_at: new Date().toISOString() }).eq('id', id);
if (error) throw error;
return true;
@@ -141,12 +141,14 @@ export async function softDelete(id, { tenantId } = {}) {
/**
* Sanitiza payload: trim em strings, nullif vazio.
* Não sanitiza telefones ( chegam digits-only do componente)
* nem owner_id/tenant_id/ativo (controlados pelo repository).
* nem owner_id/ativo (controlados pelo repository).
* Dropa tenant_id defensivamente (schema-per-tenant: coluna não existe mais).
*/
function sanitize(payload) {
const stringFields = ['nome', 'crm', 'especialidade', 'telefone_profissional', 'telefone_pessoal', 'email', 'clinica', 'cidade', 'estado', 'observacoes'];
const out = { ...payload };
const { tenant_id: _drop, ...rest } = payload;
const out = { ...rest };
for (const f of stringFields) {
if (f in out) {
const v = out[f];
+10 -12
View File
@@ -16,6 +16,7 @@
-->
<script setup>
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import {
listGroupsByPatient,
listTagsByPatient,
@@ -76,13 +77,12 @@ async function abrirSessoes(pat) {
recorrencias.value = [];
try {
const [evts, recs] = await Promise.all([
supabase
.from('agenda_eventos')
tenantDb().from('agenda_eventos')
.select('id, titulo, tipo, status, inicio_em, fim_em, modalidade, insurance_guide_number, insurance_value, insurance_plans(name)')
.eq('patient_id', pat.id)
.order('inicio_em', { ascending: false })
.limit(100),
supabase.from('recurrence_rules').select('id, type, interval, weekdays, start_date, end_date, start_time, duration_min, status').eq('patient_id', pat.id).order('start_date', { ascending: false })
tenantDb().from('recurrence_rules').select('id, type, interval, weekdays, start_date, end_date, start_time, duration_min, status').eq('patient_id', pat.id).order('start_date', { ascending: false })
]);
sessoesLista.value = evts.data || [];
recorrencias.value = recs.data || [];
@@ -487,11 +487,9 @@ function withOwnerFilter(q) {
return uid.value ? q.eq('owner_id', uid.value) : q;
}
// Defesa em profundidade: filtra por tenant_id do tenantStore em todas as queries.
// RLS cobre no backend, mas blindamos no cliente (padrão do projeto).
// Schema-per-tenant: isolamento via schema tenant_<slug>; tabela não tem mais coluna tenant_id.
function withTenantFilter(q) {
const tid = tenantStore.activeTenantId;
return tid ? q.eq('tenant_id', tid) : q;
return q;
}
// Filtered rows
@@ -547,13 +545,13 @@ async function fetchAll() {
discountMap.value = {};
if (uid.value) {
const now = new Date().toISOString();
const { data: discRows } = await supabase.from('patient_discounts').select('patient_id, discount_pct, discount_flat').eq('owner_id', uid.value).eq('active', true).or(`active_to.is.null,active_to.gte.${now}`);
const { data: discRows } = await tenantDb().from('patient_discounts').select('patient_id, discount_pct, discount_flat').eq('owner_id', uid.value).eq('active', true).or(`active_to.is.null,active_to.gte.${now}`);
if (discRows) discountMap.value = Object.fromEntries(discRows.map((d) => [d.patient_id, d]));
}
insuranceMap.value = {};
if (uid.value) {
const { data: insRows } = await supabase.from('agenda_eventos').select('patient_id, insurance_plan_id, insurance_plans(name)').eq('owner_id', uid.value).not('insurance_plan_id', 'is', null).order('inicio_em', { ascending: false });
const { data: insRows } = await tenantDb().from('agenda_eventos').select('patient_id, insurance_plan_id, insurance_plans(name)').eq('owner_id', uid.value).not('insurance_plan_id', 'is', null).order('inicio_em', { ascending: false });
if (insRows) {
for (const row of insRows) {
if (!insuranceMap.value[row.patient_id]) insuranceMap.value[row.patient_id] = row.insurance_plans?.name ?? null;
@@ -574,7 +572,7 @@ async function fetchAll() {
}
async function listPatients() {
let q = supabase.from('patients').select('id, owner_id, nome_completo, email_principal, telefone, avatar_url, status, last_attended_at, created_at, updated_at').order('created_at', { ascending: false });
let q = tenantDb().from('patients').select('id, owner_id, nome_completo, email_principal, telefone, avatar_url, status, last_attended_at, created_at, updated_at').order('created_at', { ascending: false });
q = withTenantFilter(withOwnerFilter(q));
const { data, error } = await q;
if (error) throw error;
@@ -590,7 +588,7 @@ async function listPatients() {
}
async function listGroups() {
let q = supabase.from('patient_groups').select('id, owner_id, nome, cor, is_system, is_active').eq('is_active', true).order('nome', { ascending: true });
let q = tenantDb().from('patient_groups').select('id, owner_id, nome, cor, is_system, is_active').eq('is_active', true).order('nome', { ascending: true });
q = withTenantFilter(q);
if (uid.value) q = q.or(`is_system.eq.true,owner_id.eq.${uid.value}`);
else q = q.eq('is_system', true);
@@ -600,7 +598,7 @@ async function listGroups() {
}
async function listTags() {
let q = supabase.from('patient_tags').select('id, owner_id, nome, cor').order('nome', { ascending: true });
let q = tenantDb().from('patient_tags').select('id, owner_id, nome, cor').order('nome', { ascending: true });
q = withTenantFilter(q);
if (uid.value) q = q.eq('owner_id', uid.value);
const { data, error } = await q;
@@ -64,6 +64,7 @@ import { useRoleGuard } from '@/composables/useRoleGuard'
import { useToast } from 'primevue/usetoast'
import { useConfirm } from 'primevue/useconfirm'
import { supabase } from '@/lib/supabase/client'
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore'
import { logError } from '@/support/supportLogger'
import { digitsOnly, fmtCPF, fmtRG, fmtPhone, toISODate, generateCPF } from '@/utils/validators'
@@ -647,7 +648,7 @@ async function onSubmit () {
await openPanel(0); return
}
const payload = sanitizePayload(form.value, ownerId)
payload.tenant_id = tenantId; payload.responsible_member_id = memberId
payload.responsible_member_id = memberId
if (isEdit.value) {
await updatePatient(patientId.value, payload)
await maybeUploadAvatar(ownerId, patientId.value)
@@ -706,7 +707,7 @@ async function doDelete () {
['patients', 'id'],
]
for (const [tbl, col] of tables) {
const { error } = await supabase.from(tbl).delete().eq(col, pid); if (error) throw error
const { error } = await tenantDb().from(tbl).delete().eq(col, pid); if (error) throw error
}
toast.add({ severity:'success', summary:'Excluído', detail:'Paciente removido.', life:2500 })
if (props.dialogMode) { emit('created', null); return }
@@ -766,7 +767,7 @@ async function createGroupPersist () {
createGroupSaving.value=true
try {
const ownerId=await getOwnerId(); const { tenantId }=await resolveTenantContextOrFail()
const { data, error }=await supabase.from('patient_groups').insert({ owner_id:ownerId, tenant_id:tenantId, nome:name, cor:color, is_system:false, is_active:true }).select('id').single()
const { data, error }=await tenantDb().from('patient_groups').insert({ owner_id:ownerId, nome:name, cor:color, is_system:false, is_active:true }).select('id').single()
if (error) throw error
groups.value=await listGroups(); if (data?.id) grupoIdSelecionado.value=data.id
toast.add({ severity:'success', summary:'Grupo criado.', life:2500 }); createGroupDialog.value=false
@@ -782,7 +783,7 @@ async function createTagPersist () {
createTagSaving.value=true
try {
const ownerId=await getOwnerId(); const { tenantId }=await resolveTenantContextOrFail()
const { data, error }=await supabase.from('patient_tags').insert({ owner_id:ownerId, tenant_id:tenantId, nome:name, cor:color }).select('id').single()
const { data, error }=await tenantDb().from('patient_tags').insert({ owner_id:ownerId, nome:name, cor:color }).select('id').single()
if (error) throw error
tags.value=await listTags()
if (data?.id) { const s=new Set([...(tagIdsSelecionadas.value||[]),data.id]); tagIdsSelecionadas.value=Array.from(s) }
@@ -16,6 +16,7 @@
-->
<script setup>
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
// Fase 2 (Graphify hotspot): convertToPatient duplicado em 2 pages INSERT/UPDATE
// extraídos pro repository pra remover duplicação.
@@ -275,7 +276,7 @@ const intakeSections = computed(() => {
async function fetchIntakes() {
loading.value = true;
try {
const { data, error } = await supabase.from('patient_intake_requests').select('*').order('created_at', { ascending: false });
const { data, error } = await tenantDb().from('patient_intake_requests').select('*').order('created_at', { ascending: false });
if (error) throw error;
const weight = (s) => (s === 'new' ? 0 : s === 'converted' ? 1 : s === 'rejected' ? 2 : 9);
rows.value = (data || []).slice().sort((a, b) => {
@@ -322,7 +323,7 @@ async function markRejected() {
dlg.value.saving = true;
try {
const reason = String(dlg.value.reject_note || '').trim() || null;
const { error } = await supabase.from('patient_intake_requests').update({ status: 'rejected', rejected_reason: reason, updated_at: new Date().toISOString() }).eq('id', item.id);
const { error } = await tenantDb().from('patient_intake_requests').update({ status: 'rejected', rejected_reason: reason, updated_at: new Date().toISOString() }).eq('id', item.id);
if (error) throw error;
toast.add({ severity: 'success', summary: 'Rejeitado', detail: 'Solicitação rejeitada.', life: 2500 });
await fetchIntakes();
@@ -371,7 +372,6 @@ async function convertToPatient() {
const intakeAvatar = cleanStr(item.avatar_url) || cleanStr(item.foto_url) || cleanStr(item.photo_url) || null;
const patientPayload = {
tenant_id: tenantId,
responsible_member_id: responsibleMemberId,
owner_id: ownerId,
nome_completo: cleanStr(fNome(item)),
@@ -21,6 +21,7 @@ import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { useRouter } from 'vue-router';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import Checkbox from 'primevue/checkbox';
import Menu from 'primevue/menu';
@@ -201,7 +202,7 @@ function applyRealCountsToGroups(groupsArr, countMap) {
async function fetchRealGroupCountsForOwner() {
const ownerId = (await supabase.auth.getUser())?.data?.user?.id;
if (!ownerId) throw new Error('Sessão inválida.');
const { data, error } = await supabase.from('patient_group_patient').select('patient_group_id, patient:patients!inner(id, owner_id)').eq('patient.owner_id', ownerId);
const { data, error } = await tenantDb().from('patient_group_patient').select('patient_group_id, patient:patients!inner(id, owner_id)').eq('patient.owner_id', ownerId);
if (error) throw error;
const map = Object.create(null);
for (const row of data || []) {
@@ -362,7 +363,7 @@ async function openGroupPatientsModal(groupRow) {
patientsDialog.items = [];
patientsDialog.search = '';
try {
const { data, error } = await supabase.from('patient_group_patient').select('patient_id, patient:patients(id, nome_completo, email_principal, telefone, avatar_url)').eq('patient_group_id', groupRow.id);
const { data, error } = await tenantDb().from('patient_group_patient').select('patient_id, patient:patients(id, nome_completo, email_principal, telefone, avatar_url)').eq('patient_group_id', groupRow.id);
if (error) throw error;
patientsDialog.items = (data || [])
.map((r) => r.patient)
@@ -6,6 +6,7 @@
<script setup>
import { ref, computed, onMounted, watch } from 'vue';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useConversationDrawerStore } from '@/stores/conversationDrawerStore';
import { formatDistanceToNow, format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
@@ -31,8 +32,7 @@ async function load() {
if (!props.patientId) return;
loading.value = true;
try {
const { data, error } = await supabase
.from('conversation_messages')
const { data, error } = await tenantDb().from('conversation_messages')
.select('id, channel, direction, from_number, to_number, body, media_url, media_mime, provider, kanban_status, received_at, created_at, responded_at, delivery_status')
.eq('patient_id', props.patientId)
.order('created_at', { ascending: true })
@@ -27,6 +27,7 @@ import Popover from 'primevue/popover';
import { useToast } from 'primevue/usetoast';
import { useConfirm } from 'primevue/useconfirm';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
import { useConversationDrawerStore } from '@/stores/conversationDrawerStore';
@@ -379,8 +380,7 @@ async function updateSessionStatus(ev, novoStatus, msg) {
if (!ev?.id || sessionBusy.value) return;
sessionBusy.value = true;
try {
const { error } = await supabase
.from('agenda_eventos')
const { error } = await tenantDb().from('agenda_eventos')
.update({ status: novoStatus })
.eq('id', ev.id);
if (error) throw error;
@@ -432,8 +432,7 @@ function openAddPhone() {
// bloqueamos save por falha no check.
async function findPhoneOwner(digits, excludeId) {
try {
const { data: byPat } = await supabase
.from('patients')
const { data: byPat } = await tenantDb().from('patients')
.select('id, nome_completo')
.eq('telefone', digits)
.neq('id', excludeId)
@@ -441,8 +440,7 @@ async function findPhoneOwner(digits, excludeId) {
.maybeSingle();
if (byPat?.id) return byPat;
const { data: byCp } = await supabase
.from('contact_phones')
const { data: byCp } = await tenantDb().from('contact_phones')
.select('entity_id')
.eq('entity_type', 'patient')
.eq('number', digits)
@@ -450,8 +448,7 @@ async function findPhoneOwner(digits, excludeId) {
.limit(1)
.maybeSingle();
if (byCp?.entity_id) {
const { data: p } = await supabase
.from('patients')
const { data: p } = await tenantDb().from('patients')
.select('id, nome_completo')
.eq('id', byCp.entity_id)
.maybeSingle();
@@ -508,8 +505,7 @@ async function _persistPhone(id, digits, tenantId) {
// 1) Garante o contact_type "whatsapp" (system, slug fixo via
// seed_014_global_data).
const { data: ctype, error: errType } = await supabase
.from('contact_types')
const { data: ctype, error: errType } = await tenantDb().from('contact_types')
.select('id')
.eq('slug', 'whatsapp')
.order('is_system', { ascending: false })
@@ -519,8 +515,7 @@ async function _persistPhone(id, digits, tenantId) {
if (!ctype?.id) throw new Error('Tipo de contato "WhatsApp" não encontrado.');
// 2) Insere ou atualiza em contact_phones (entity_type=patient).
const { data: existing } = await supabase
.from('contact_phones')
const { data: existing } = await tenantDb().from('contact_phones')
.select('id, is_primary')
.eq('entity_type', 'patient')
.eq('entity_id', id)
@@ -529,22 +524,18 @@ async function _persistPhone(id, digits, tenantId) {
.maybeSingle();
if (existing?.id) {
const { error: errUpd } = await supabase
.from('contact_phones')
const { error: errUpd } = await tenantDb().from('contact_phones')
.update({ number: digits, whatsapp_linked_at: new Date().toISOString() })
.eq('id', existing.id);
if (errUpd) throw errUpd;
} else {
const { count } = await supabase
.from('contact_phones')
const { count } = await tenantDb().from('contact_phones')
.select('id', { count: 'exact', head: true })
.eq('entity_type', 'patient')
.eq('entity_id', id);
const isPrimary = (count || 0) === 0;
const { error: errIns } = await supabase
.from('contact_phones')
const { error: errIns } = await tenantDb().from('contact_phones')
.insert({
tenant_id: tenantId,
entity_type: 'patient',
entity_id: id,
contact_type_id: ctype.id,
@@ -820,8 +811,7 @@ async function loadSessions(patientId) {
sessionsLoading.value = true;
sessions.value = [];
try {
const { data, error } = await supabase
.from('agenda_eventos')
const { data, error } = await tenantDb().from('agenda_eventos')
.select('id, inicio_em, fim_em, status, modalidade, tipo, titulo, titulo_custom, observacoes')
.eq('patient_id', patientId)
.order('inicio_em', { ascending: false })
@@ -840,8 +830,7 @@ async function loadRecentMessages(patientId) {
messagesLoading.value = true;
recentMessages.value = [];
try {
const { data, error } = await supabase
.from('conversation_messages')
const { data, error } = await tenantDb().from('conversation_messages')
.select('id, body, direction, created_at, channel, kanban_status')
.eq('patient_id', patientId)
.order('created_at', { ascending: false })
@@ -858,8 +847,7 @@ async function loadDocumentsList(patientId) {
documentsLoading.value = true;
documentsList.value = [];
try {
const { data, error } = await supabase
.from('documents')
const { data, error } = await tenantDb().from('documents')
.select('id, tipo_documento, created_at, status_revisao, tamanho_bytes')
.eq('patient_id', patientId)
.is('deleted_at', null)
@@ -878,8 +866,7 @@ async function loadFinancialRecent(patientId) {
financialLoading.value = true;
financialRecords.value = [];
try {
const { data, error } = await supabase
.from('financial_records')
const { data, error } = await tenantDb().from('financial_records')
.select('id, type, amount, due_date, paid_at, description, payment_method, category, created_at')
.eq('patient_id', patientId)
.eq('type', 'receita')
@@ -892,15 +879,15 @@ async function loadFinancialRecent(patientId) {
}
async function getPatientById(id) {
const { data, error } = await supabase.from('patients').select('*').eq('id', id).maybeSingle();
const { data, error } = await tenantDb().from('patients').select('*').eq('id', id).maybeSingle();
if (error) throw error;
return data;
}
async function getPatientRelations(id) {
const { data: g, error: ge } = await supabase.from('patient_group_patient').select('patient_group_id').eq('patient_id', id);
const { data: g, error: ge } = await tenantDb().from('patient_group_patient').select('patient_group_id').eq('patient_id', id);
if (ge) throw ge;
const { data: t, error: te } = await supabase.from('patient_patient_tag').select('tag_id').eq('patient_id', id);
const { data: t, error: te } = await tenantDb().from('patient_patient_tag').select('tag_id').eq('patient_id', id);
if (te) throw te;
return {
groupIds: (g || []).map(x => x.patient_group_id).filter(Boolean),
@@ -910,14 +897,14 @@ async function getPatientRelations(id) {
async function getGroupsByIds(ids) {
if (!ids?.length) return [];
const { data, error } = await supabase.from('patient_groups').select('id, nome').in('id', ids).order('nome', { ascending: true });
const { data, error } = await tenantDb().from('patient_groups').select('id, nome').in('id', ids).order('nome', { ascending: true });
if (error) throw error;
return (data || []).map(g => ({ id: g.id, name: g.nome }));
}
async function getTagsByIds(ids) {
if (!ids?.length) return [];
const { data, error } = await supabase.from('patient_tags').select('id, nome, cor').in('id', ids).order('nome', { ascending: true });
const { data, error } = await tenantDb().from('patient_tags').select('id, nome, cor').in('id', ids).order('nome', { ascending: true });
if (error) throw error;
return (data || []).map(t => ({ id: t.id, name: t.nome, color: t.cor }));
}
@@ -989,8 +976,7 @@ async function setProximaSessaoStatus(novoStatus, msgSucesso) {
if (!ev?.id || sessionBusy.value) return;
sessionBusy.value = true;
try {
const { error } = await supabase
.from('agenda_eventos')
const { error } = await tenantDb().from('agenda_eventos')
.update({ status: novoStatus })
.eq('id', ev.id);
if (error) throw error;
@@ -5,9 +5,9 @@
| Arquivo: src/features/patients/prontuario/services/clinicalNoteTemplatesRepository.js
|
| Repository de clinical_note_templates. Escopo escalonado:
| - Sistema (is_system=true, tenant_id NULL) todos authenticated leem
| - Tenant-wide (tenant_id, owner_id NULL) membros do tenant
| - Owner (tenant_id + owner_id) o owner
| - Sistema (is_system=true) todos authenticated leem
| - Tenant-wide (owner_id NULL) membros do tenant (schema do tenant)
| - Owner (owner_id) o owner
|
| RLS bloqueia INSERT/UPDATE/DELETE de templates is_system via seed.
| Templates do tenant podem ser criados/editados pelo tenant_admin.
@@ -16,6 +16,7 @@
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
import { assertTenantId, getUid } from './_tenantGuards';
import { CLINICAL_NOTE_TEMPLATE_SELECT } from './clinicalNotesSelects';
@@ -41,7 +42,7 @@ function resolveTenantId(tenantIdArg) {
export async function listAvailable({ noteType, tenantId, includeInactive = false } = {}) {
resolveTenantId(tenantId); // garante tenant ativo (RLS depende)
let q = supabase.from('clinical_note_templates').select(CLINICAL_NOTE_TEMPLATE_SELECT).order('is_system', { ascending: false }).order('name', { ascending: true });
let q = tenantDb().from('clinical_note_templates').select(CLINICAL_NOTE_TEMPLATE_SELECT).order('is_system', { ascending: false }).order('name', { ascending: true });
if (!includeInactive) q = q.eq('active', true);
if (noteType) q = q.eq('note_type', noteType);
@@ -57,7 +58,7 @@ export async function listAvailable({ noteType, tenantId, includeInactive = fals
export async function getById(templateId) {
if (!templateId) throw new Error('ID inválido.');
const { data, error } = await supabase.from('clinical_note_templates').select(CLINICAL_NOTE_TEMPLATE_SELECT).eq('id', templateId).maybeSingle();
const { data, error } = await tenantDb().from('clinical_note_templates').select(CLINICAL_NOTE_TEMPLATE_SELECT).eq('id', templateId).maybeSingle();
if (error) throw error;
return data || null;
@@ -70,7 +71,7 @@ export async function getById(templateId) {
export async function getByKey(key, { noteType } = {}) {
if (!key) throw new Error('Key inválida.');
let q = supabase.from('clinical_note_templates').select(CLINICAL_NOTE_TEMPLATE_SELECT).eq('key', key).eq('active', true);
let q = tenantDb().from('clinical_note_templates').select(CLINICAL_NOTE_TEMPLATE_SELECT).eq('key', key).eq('active', true);
if (noteType) q = q.eq('note_type', noteType);
const { data, error } = await q.order('is_system', { ascending: false }).limit(1).maybeSingle();
@@ -94,7 +95,6 @@ export async function create(payload) {
const tid = resolveTenantId();
const row = {
tenant_id: tid,
owner_id: payload.ownerScoped ? uid : null,
key: String(payload.key).trim(),
name: String(payload.name).trim(),
@@ -106,7 +106,7 @@ export async function create(payload) {
active: payload.active !== false
};
const { data, error } = await supabase.from('clinical_note_templates').insert([row]).select(CLINICAL_NOTE_TEMPLATE_SELECT).single();
const { data, error } = await tenantDb().from('clinical_note_templates').insert([row]).select(CLINICAL_NOTE_TEMPLATE_SELECT).single();
if (error) throw error;
return data;
@@ -120,8 +120,9 @@ export async function update(templateId, patch) {
const safePatch = { ...patch, updated_at: new Date().toISOString() };
if ('is_system' in safePatch) delete safePatch.is_system; // RLS bloqueia mas defesa em profundidade
if ('tenant_id' in safePatch) delete safePatch.tenant_id; // schema-per-tenant: coluna não existe mais
const { data, error } = await supabase.from('clinical_note_templates').update(safePatch).eq('id', templateId).select(CLINICAL_NOTE_TEMPLATE_SELECT).single();
const { data, error } = await tenantDb().from('clinical_note_templates').update(safePatch).eq('id', templateId).select(CLINICAL_NOTE_TEMPLATE_SELECT).single();
if (error) throw error;
return data;
@@ -133,7 +134,7 @@ export async function update(templateId, patch) {
export async function softDelete(templateId) {
if (!templateId) throw new Error('ID inválido.');
const { error } = await supabase.from('clinical_note_templates').update({ active: false, updated_at: new Date().toISOString() }).eq('id', templateId);
const { error } = await tenantDb().from('clinical_note_templates').update({ active: false, updated_at: new Date().toISOString() }).eq('id', templateId);
if (error) throw error;
return true;
@@ -15,6 +15,7 @@
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
import { assertTenantId, getUid } from './_tenantGuards';
import {
@@ -55,10 +56,9 @@ export async function listForPatient(patientId, { tenantId, noteType = null, inc
const tid = resolveTenantId(tenantId);
const select = brief ? CLINICAL_NOTE_SELECT_BRIEF : CLINICAL_NOTE_SELECT;
let q = supabase
.from('clinical_notes')
let q = tenantDb().from('clinical_notes')
.select(select)
.eq('tenant_id', tid)
.eq('patient_id', patientId)
.order('pinned', { ascending: false })
.order('created_at', { ascending: false });
@@ -80,7 +80,7 @@ export async function listForSession(sessionEventId, { tenantId, brief = false }
const tid = resolveTenantId(tenantId);
const select = brief ? CLINICAL_NOTE_SELECT_BRIEF : CLINICAL_NOTE_SELECT;
const { data, error } = await supabase.from('clinical_notes').select(select).eq('tenant_id', tid).eq('session_event_id', sessionEventId).is('deleted_at', null).order('created_at', { ascending: false });
const { data, error } = await tenantDb().from('clinical_notes').select(select).eq('session_event_id', sessionEventId).is('deleted_at', null).order('created_at', { ascending: false });
if (error) throw error;
return (data || []).map(flattenNoteRow);
@@ -93,7 +93,7 @@ export async function getById(noteId, { tenantId } = {}) {
if (!noteId) throw new Error('ID inválido.');
const tid = resolveTenantId(tenantId);
const { data, error } = await supabase.from('clinical_notes').select(CLINICAL_NOTE_SELECT).eq('id', noteId).eq('tenant_id', tid).maybeSingle();
const { data, error } = await tenantDb().from('clinical_notes').select(CLINICAL_NOTE_SELECT).eq('id', noteId).maybeSingle();
if (error) throw error;
return data ? flattenNoteRow(data) : null;
@@ -140,7 +140,7 @@ export async function create(payload) {
created_by: uid
};
const { data, error } = await supabase.from('clinical_notes').insert([row]).select(CLINICAL_NOTE_SELECT).single();
const { data, error } = await tenantDb().from('clinical_notes').insert([row]).select(CLINICAL_NOTE_SELECT).single();
if (error) throw error;
return flattenNoteRow(data);
@@ -164,7 +164,7 @@ export async function update(noteId, patch, { tenantId } = {}) {
const safePatch = { ...sanitize(patch), updated_by: uid };
const { data, error } = await supabase.from('clinical_notes').update(safePatch).eq('id', noteId).eq('tenant_id', tid).select(CLINICAL_NOTE_SELECT).single();
const { data, error } = await tenantDb().from('clinical_notes').update(safePatch).eq('id', noteId).select(CLINICAL_NOTE_SELECT).single();
if (error) throw error;
return flattenNoteRow(data);
@@ -179,11 +179,10 @@ export async function softDelete(noteId, { tenantId } = {}) {
const tid = resolveTenantId(tenantId);
const uid = await getUid();
const { error } = await supabase
.from('clinical_notes')
const { error } = await tenantDb().from('clinical_notes')
.update({ deleted_at: new Date().toISOString(), deleted_by: uid, updated_by: uid })
.eq('id', noteId)
.eq('tenant_id', tid);
;
if (error) throw error;
return true;
@@ -197,7 +196,7 @@ export async function restore(noteId, { tenantId } = {}) {
const tid = resolveTenantId(tenantId);
const uid = await getUid();
const { error } = await supabase.from('clinical_notes').update({ deleted_at: null, deleted_by: null, updated_by: uid }).eq('id', noteId).eq('tenant_id', tid);
const { error } = await tenantDb().from('clinical_notes').update({ deleted_at: null, deleted_by: null, updated_by: uid }).eq('id', noteId);
if (error) throw error;
return true;
@@ -216,7 +215,7 @@ export async function setPinned(noteId, pinned, { tenantId } = {}) {
export async function listVersions(noteId) {
if (!noteId) return [];
const { data, error } = await supabase.from('clinical_note_versions').select(CLINICAL_NOTE_VERSION_SELECT).eq('note_id', noteId).order('version_number', { ascending: false });
const { data, error } = await tenantDb().from('clinical_note_versions').select(CLINICAL_NOTE_VERSION_SELECT).eq('note_id', noteId).order('version_number', { ascending: false });
if (error) throw error;
return data || [];
@@ -229,7 +228,7 @@ export async function getVersion(noteId, versionNumber) {
if (!noteId) throw new Error('noteId obrigatório.');
if (!versionNumber) throw new Error('versionNumber obrigatório.');
const { data, error } = await supabase.from('clinical_note_versions').select(CLINICAL_NOTE_VERSION_SELECT).eq('note_id', noteId).eq('version_number', versionNumber).maybeSingle();
const { data, error } = await tenantDb().from('clinical_note_versions').select(CLINICAL_NOTE_VERSION_SELECT).eq('note_id', noteId).eq('version_number', versionNumber).maybeSingle();
if (error) throw error;
return data || null;
@@ -6,7 +6,7 @@
| V#3 fundação: queries de patients centralizadas.
|
| Pages e composables devem chamar este repo em vez de fazer
| supabase.from('patients') direto.
| tenantDb().from('patients') direto.
|
| Inclui também reads cross-feature em escopo de paciente (agenda_eventos,
| financial_records, documents, recurrence_rules, conversation_messages,
@@ -16,6 +16,7 @@
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
import { assertTenantId, getUid } from '@/features/agenda/services/_tenantGuards';
import {
@@ -51,7 +52,7 @@ function resolveTenantId(tenantIdArg) {
export async function listPatients({ tenantId, ownerId = null, includeInactive = true, limit = null } = {}) {
const tid = resolveTenantId(tenantId);
let q = supabase.from('patients').select(PATIENTS_SELECT_BASE).eq('tenant_id', tid);
let q = tenantDb().from('patients').select(PATIENTS_SELECT_BASE);
if (ownerId) q = q.eq('owner_id', ownerId);
if (!includeInactive) q = q.neq('status', 'Inativo');
if (limit) q = q.limit(limit);
@@ -65,7 +66,7 @@ export async function listPatients({ tenantId, ownerId = null, includeInactive =
export async function getPatientById(id, { tenantId } = {}) {
if (!id) throw new Error('id obrigatório');
const tid = resolveTenantId(tenantId);
const { data, error } = await supabase.from('patients').select(PATIENTS_SELECT_BASE).eq('id', id).eq('tenant_id', tid).maybeSingle();
const { data, error } = await tenantDb().from('patients').select(PATIENTS_SELECT_BASE).eq('id', id).maybeSingle();
if (error) throw error;
return data;
}
@@ -77,9 +78,9 @@ export async function createPatient(payload) {
// criar pacientes "de outro terapeuta". Repository é defesa em profundidade.
const ownerId = await getUid();
// eslint-disable-next-line no-unused-vars
const { owner_id: _dropped, ...rest } = payload || {};
const row = { ...rest, tenant_id: tid, owner_id: ownerId };
const { data, error } = await supabase.from('patients').insert(row).select(PATIENTS_SELECT_BASE).single();
const { owner_id: _dropped, tenant_id: _tenantDropped, ...rest } = payload || {};
const row = { ...rest, owner_id: ownerId };
const { data, error } = await tenantDb().from('patients').insert(row).select(PATIENTS_SELECT_BASE).single();
if (error) throw error;
return data;
}
@@ -87,7 +88,7 @@ export async function createPatient(payload) {
export async function updatePatient(id, patch, { tenantId } = {}) {
if (!id) throw new Error('id obrigatório');
const tid = resolveTenantId(tenantId);
const { data, error } = await supabase.from('patients').update(patch).eq('id', id).eq('tenant_id', tid).select(PATIENTS_SELECT_BASE).single();
const { data, error } = await tenantDb().from('patients').update(patch).eq('id', id).select(PATIENTS_SELECT_BASE).single();
if (error) throw error;
return data;
}
@@ -95,7 +96,7 @@ export async function updatePatient(id, patch, { tenantId } = {}) {
export async function softDeletePatient(id, { tenantId } = {}) {
if (!id) throw new Error('id obrigatório');
const tid = resolveTenantId(tenantId);
const { error } = await supabase.from('patients').update({ status: 'Arquivado' }).eq('id', id).eq('tenant_id', tid);
const { error } = await tenantDb().from('patients').update({ status: 'Arquivado' }).eq('id', id);
if (error) throw error;
}
@@ -111,8 +112,8 @@ export async function softDeletePatient(id, { tenantId } = {}) {
export async function getPatientRelations(patientId) {
if (!patientId) return { groupIds: [], tagIds: [] };
const [{ data: g, error: ge }, { data: t, error: te }] = await Promise.all([
supabase.from('patient_group_patient').select('patient_group_id').eq('patient_id', patientId),
supabase.from('patient_patient_tag').select('tag_id').eq('patient_id', patientId)
tenantDb().from('patient_group_patient').select('patient_group_id').eq('patient_id', patientId),
tenantDb().from('patient_patient_tag').select('tag_id').eq('patient_id', patientId)
]);
if (ge) throw ge;
if (te) throw te;
@@ -126,7 +127,7 @@ export async function getPatientRelations(patientId) {
export async function listGroups({ tenantId, ownerId = null } = {}) {
const tid = resolveTenantId(tenantId);
let q = supabase.from('patient_groups').select(PATIENT_GROUPS_SELECT).eq('tenant_id', tid).eq('is_active', true);
let q = tenantDb().from('patient_groups').select(PATIENT_GROUPS_SELECT).eq('is_active', true);
if (ownerId) q = q.or(`is_system.eq.true,owner_id.eq.${ownerId}`);
q = q.order('nome', { ascending: true });
const { data, error } = await q;
@@ -137,7 +138,7 @@ export async function listGroups({ tenantId, ownerId = null } = {}) {
export async function listGroupsByPatient(patientIds, { tenantId } = {}) {
if (!patientIds?.length) return [];
const tid = resolveTenantId(tenantId);
const { data, error } = await supabase.from('patient_group_patient').select('patient_id, patient_group_id').eq('tenant_id', tid).in('patient_id', patientIds);
const { data, error } = await tenantDb().from('patient_group_patient').select('patient_id, patient_group_id').in('patient_id', patientIds);
if (error) throw error;
return data || [];
}
@@ -147,7 +148,7 @@ export async function listGroupsByPatient(patientIds, { tenantId } = {}) {
*/
export async function getGroupsByIds(ids) {
if (!ids?.length) return [];
const { data, error } = await supabase.from('patient_groups').select(PATIENT_GROUPS_SELECT_BRIEF).in('id', ids).order('nome', { ascending: true });
const { data, error } = await tenantDb().from('patient_groups').select(PATIENT_GROUPS_SELECT_BRIEF).in('id', ids).order('nome', { ascending: true });
if (error) throw error;
return (data || []).map((g) => ({ id: g.id, name: g.nome }));
}
@@ -158,10 +159,10 @@ export async function getGroupsByIds(ids) {
export async function replacePatientGroup(patientId, groupId, { tenantId } = {}) {
if (!patientId) throw new Error('patientId obrigatório');
const tid = resolveTenantId(tenantId);
const { error: del } = await supabase.from('patient_group_patient').delete().eq('patient_id', patientId).eq('tenant_id', tid);
const { error: del } = await tenantDb().from('patient_group_patient').delete().eq('patient_id', patientId);
if (del) throw del;
if (!groupId) return;
const { error: ins } = await supabase.from('patient_group_patient').insert({ patient_id: patientId, patient_group_id: groupId, tenant_id: tid });
const { error: ins } = await tenantDb().from('patient_group_patient').insert({ patient_id: patientId, patient_group_id: groupId });
if (ins) throw ins;
}
@@ -169,7 +170,7 @@ export async function replacePatientGroup(patientId, groupId, { tenantId } = {})
export async function listTags({ tenantId, ownerId = null } = {}) {
const tid = resolveTenantId(tenantId);
let q = supabase.from('patient_tags').select(PATIENT_TAGS_SELECT).eq('tenant_id', tid);
let q = tenantDb().from('patient_tags').select(PATIENT_TAGS_SELECT);
if (ownerId) q = q.eq('owner_id', ownerId);
const { data, error } = await q;
if (error) throw error;
@@ -179,7 +180,7 @@ export async function listTags({ tenantId, ownerId = null } = {}) {
export async function listTagsByPatient(patientIds, { tenantId } = {}) {
if (!patientIds?.length) return [];
const tid = resolveTenantId(tenantId);
const { data, error } = await supabase.from('patient_patient_tag').select('patient_id, tag_id').eq('tenant_id', tid).in('patient_id', patientIds);
const { data, error } = await tenantDb().from('patient_patient_tag').select('patient_id, tag_id').in('patient_id', patientIds);
if (error) throw error;
return data || [];
}
@@ -189,7 +190,7 @@ export async function listTagsByPatient(patientIds, { tenantId } = {}) {
*/
export async function getTagsByIds(ids) {
if (!ids?.length) return [];
const { data, error } = await supabase.from('patient_tags').select(PATIENT_TAGS_SELECT_BRIEF).in('id', ids).order('nome', { ascending: true });
const { data, error } = await tenantDb().from('patient_tags').select(PATIENT_TAGS_SELECT_BRIEF).in('id', ids).order('nome', { ascending: true });
if (error) throw error;
return (data || []).map((t) => ({ id: t.id, name: t.nome, color: t.cor }));
}
@@ -202,12 +203,12 @@ export async function replacePatientTags(patientId, tagIds, { tenantId, ownerId
if (!ownerId) throw new Error('ownerId obrigatório');
const tid = resolveTenantId(tenantId);
const { error: del } = await supabase.from('patient_patient_tag').delete().eq('patient_id', patientId).eq('owner_id', ownerId).eq('tenant_id', tid);
const { error: del } = await tenantDb().from('patient_patient_tag').delete().eq('patient_id', patientId).eq('owner_id', ownerId);
if (del) throw del;
const clean = Array.from(new Set((tagIds || []).filter(Boolean)));
if (!clean.length) return;
const { error: ins } = await supabase.from('patient_patient_tag').insert(clean.map((tag_id) => ({ owner_id: ownerId, patient_id: patientId, tag_id, tenant_id: tid })));
const { error: ins } = await tenantDb().from('patient_patient_tag').insert(clean.map((tag_id) => ({ owner_id: ownerId, patient_id: patientId, tag_id })));
if (ins) throw ins;
}
@@ -224,10 +225,9 @@ export async function replacePatientTags(patientId, tagIds, { tenantId, ownerId
export async function listSessionsByPatient(patientId, { tenantId } = {}) {
if (!patientId) return [];
const tid = resolveTenantId(tenantId);
const { data, error } = await supabase
.from('agenda_eventos')
const { data, error } = await tenantDb().from('agenda_eventos')
.select(PATIENT_SESSIONS_SELECT)
.eq('tenant_id', tid)
.eq('patient_id', patientId)
.order('inicio_em', { ascending: false })
.limit(100);
@@ -251,7 +251,6 @@ export async function createPatientSession(patientId, payload) {
const row = {
patient_id: patientId,
owner_id: uid,
tenant_id: tid,
inicio_em: payload.inicio_em,
fim_em: payload.fim_em,
status: payload.status || 'agendado',
@@ -266,7 +265,7 @@ export async function createPatientSession(patientId, payload) {
price: payload.price ?? null
};
const { data, error } = await supabase.from('agenda_eventos').insert([row]).select().single();
const { data, error } = await tenantDb().from('agenda_eventos').insert([row]).select().single();
if (error) throw error;
return data;
}
@@ -277,7 +276,7 @@ export async function createPatientSession(patientId, payload) {
export async function updatePatientSessionStatus(sessionId, status, { tenantId } = {}) {
if (!sessionId) throw new Error('sessionId obrigatório');
const tid = resolveTenantId(tenantId);
const { error } = await supabase.from('agenda_eventos').update({ status }).eq('id', sessionId).eq('tenant_id', tid);
const { error } = await tenantDb().from('agenda_eventos').update({ status }).eq('id', sessionId);
if (error) throw error;
}
@@ -287,7 +286,7 @@ export async function updatePatientSessionStatus(sessionId, status, { tenantId }
*/
export async function findSessionByRecurrence(recurrenceId, recurrenceDate) {
if (!recurrenceId || !recurrenceDate) return null;
const { data, error } = await supabase.from('agenda_eventos').select('id').eq('recurrence_id', recurrenceId).eq('recurrence_date', recurrenceDate).maybeSingle();
const { data, error } = await tenantDb().from('agenda_eventos').select('id').eq('recurrence_id', recurrenceId).eq('recurrence_date', recurrenceDate).maybeSingle();
if (error) throw error;
return data || null;
}
@@ -303,10 +302,9 @@ export async function findSessionByRecurrence(recurrenceId, recurrenceDate) {
export async function listFinancialRecordsByPatient(patientId, { tenantId } = {}) {
if (!patientId) return [];
const tid = resolveTenantId(tenantId);
const { data, error } = await supabase
.from('financial_records')
const { data, error } = await tenantDb().from('financial_records')
.select(PATIENT_FINANCIAL_RECORDS_SELECT)
.eq('tenant_id', tid)
.eq('patient_id', patientId)
.eq('type', 'receita')
.order('created_at', { ascending: false })
@@ -329,7 +327,6 @@ export async function createFinancialRecord(patientId, payload) {
const row = {
patient_id: patientId,
owner_id: uid,
tenant_id: tid,
type: 'receita',
amount: Number(payload.amount),
due_date: payload.due_date || null,
@@ -338,7 +335,7 @@ export async function createFinancialRecord(patientId, payload) {
paid_at: null
};
const { data, error } = await supabase.from('financial_records').insert([row]).select().single();
const { data, error } = await tenantDb().from('financial_records').insert([row]).select().single();
if (error) throw error;
return data;
}
@@ -349,7 +346,7 @@ export async function createFinancialRecord(patientId, payload) {
export async function markFinancialRecordPaid(recordId, { tenantId } = {}) {
if (!recordId) throw new Error('recordId obrigatório');
const tid = resolveTenantId(tenantId);
const { error } = await supabase.from('financial_records').update({ paid_at: new Date().toISOString() }).eq('id', recordId).eq('tenant_id', tid);
const { error } = await tenantDb().from('financial_records').update({ paid_at: new Date().toISOString() }).eq('id', recordId);
if (error) throw error;
}
@@ -359,7 +356,7 @@ export async function markFinancialRecordPaid(recordId, { tenantId } = {}) {
export async function markFinancialRecordUnpaid(recordId, { tenantId } = {}) {
if (!recordId) throw new Error('recordId obrigatório');
const tid = resolveTenantId(tenantId);
const { error } = await supabase.from('financial_records').update({ paid_at: null }).eq('id', recordId).eq('tenant_id', tid);
const { error } = await tenantDb().from('financial_records').update({ paid_at: null }).eq('id', recordId);
if (error) throw error;
}
@@ -370,10 +367,9 @@ export async function markFinancialRecordUnpaid(recordId, { tenantId } = {}) {
export async function listDocumentsByPatient(patientId, { tenantId } = {}) {
if (!patientId) return [];
const tid = resolveTenantId(tenantId);
const { data, error } = await supabase
.from('documents')
const { data, error } = await tenantDb().from('documents')
.select(PATIENT_DOCUMENTS_SELECT)
.eq('tenant_id', tid)
.eq('patient_id', patientId)
.is('deleted_at', null)
.order('created_at', { ascending: false })
@@ -390,10 +386,9 @@ export async function listDocumentsByPatient(patientId, { tenantId } = {}) {
export async function listMessagesByPatient(patientId, { tenantId } = {}) {
if (!patientId) return [];
const tid = resolveTenantId(tenantId);
const { data, error } = await supabase
.from('conversation_messages')
const { data, error } = await tenantDb().from('conversation_messages')
.select(PATIENT_MESSAGES_SELECT)
.eq('tenant_id', tid)
.eq('patient_id', patientId)
.order('created_at', { ascending: false })
.limit(200);
@@ -409,10 +404,9 @@ export async function listMessagesByPatient(patientId, { tenantId } = {}) {
export async function listRecurrencesByPatient(patientId, { tenantId } = {}) {
if (!patientId) return [];
const tid = resolveTenantId(tenantId);
const { data, error } = await supabase
.from('recurrence_rules')
const { data, error } = await tenantDb().from('recurrence_rules')
.select(PATIENT_RECURRENCE_RULES_SELECT)
.eq('tenant_id', tid)
.eq('patient_id', patientId)
.order('start_date', { ascending: false });
if (error) throw error;
@@ -425,7 +419,7 @@ export async function listRecurrencesByPatient(patientId, { tenantId } = {}) {
export async function updateRecurrenceStatus(ruleId, status, { tenantId } = {}) {
if (!ruleId) throw new Error('ruleId obrigatório');
const tid = resolveTenantId(tenantId);
const { error } = await supabase.from('recurrence_rules').update({ status, updated_at: new Date().toISOString() }).eq('id', ruleId).eq('tenant_id', tid);
const { error } = await tenantDb().from('recurrence_rules').update({ status, updated_at: new Date().toISOString() }).eq('id', ruleId);
if (error) throw error;
}
@@ -436,7 +430,7 @@ export async function updateRecurrenceStatus(ruleId, status, { tenantId } = {})
export async function listSupportContactsByPatient(patientId, { tenantId } = {}) {
if (!patientId) return [];
const tid = resolveTenantId(tenantId);
const { data, error } = await supabase.from('patient_support_contacts').select(PATIENT_SUPPORT_CONTACTS_SELECT).eq('tenant_id', tid).eq('patient_id', patientId).order('is_primario', { ascending: false });
const { data, error } = await tenantDb().from('patient_support_contacts').select(PATIENT_SUPPORT_CONTACTS_SELECT).eq('patient_id', patientId).order('is_primario', { ascending: false });
if (error) throw error;
return data || [];
}
@@ -452,7 +446,7 @@ export async function replacePatientSupportContacts(patientId, contacts, { tenan
if (!ownerId) throw new Error('ownerId obrigatório');
const tid = resolveTenantId(tenantId);
const { error: del } = await supabase.from('patient_support_contacts').delete().eq('patient_id', patientId).eq('owner_id', ownerId).eq('tenant_id', tid);
const { error: del } = await tenantDb().from('patient_support_contacts').delete().eq('patient_id', patientId).eq('owner_id', ownerId);
if (del) throw del;
if (!contacts?.length) return;
@@ -460,11 +454,10 @@ export async function replacePatientSupportContacts(patientId, contacts, { tenan
const rows = contacts.map((c) => ({
...c,
patient_id: patientId,
owner_id: ownerId,
tenant_id: tid
owner_id: ownerId
}));
const { error: ins } = await supabase.from('patient_support_contacts').insert(rows);
const { error: ins } = await tenantDb().from('patient_support_contacts').insert(rows);
if (ins) throw ins;
}
@@ -490,8 +483,7 @@ export async function markIntakeConverted(intakeId, patientId, { tenantId } = {}
// tenant_id no patient_intake_requests pode ser nullable (intake público sem tenant)
// — só filtramos se passado explícito.
let q = supabase
.from('patient_intake_requests')
let q = tenantDb().from('patient_intake_requests')
.update({
status: 'converted',
converted_patient_id: patientId,
@@ -499,11 +491,6 @@ export async function markIntakeConverted(intakeId, patientId, { tenantId } = {}
})
.eq('id', intakeId);
if (tenantId) {
const tid = resolveTenantId(tenantId);
q = q.eq('tenant_id', tid);
}
const { error } = await q;
if (error) throw error;
}
+10 -9
View File
@@ -25,6 +25,7 @@ import { useRouter } from 'vue-router';
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { logError } from '@/support/supportLogger';
const router = useRouter();
@@ -204,7 +205,7 @@ async function buscarEtiquetas() {
const ownerId = await getOwnerId();
// 1) tenta view com contagem
const v = await supabase.from('v_tag_patient_counts').select('*').eq('owner_id', ownerId).order('nome', { ascending: true });
const v = await tenantDb().from('v_tag_patient_counts').select('*').eq('owner_id', ownerId).order('nome', { ascending: true });
if (!v.error) {
etiquetas.value = (v.data || []).map(normalizarEtiquetaRow);
@@ -212,10 +213,10 @@ async function buscarEtiquetas() {
}
// 2) fallback tabela direta
const t = await supabase.from('patient_tags').select('id, owner_id, nome, cor, is_padrao, name, color, is_native, created_at, updated_at').eq('owner_id', ownerId).order('nome', { ascending: true });
const t = await tenantDb().from('patient_tags').select('id, owner_id, nome, cor, is_padrao, name, color, is_native, created_at, updated_at').eq('owner_id', ownerId).order('nome', { ascending: true });
if (t.error && /column .*nome/i.test(String(t.error.message || ''))) {
const t2 = await supabase.from('patient_tags').select('id, owner_id, name, color, is_native, created_at, updated_at').eq('owner_id', ownerId).order('name', { ascending: true });
const t2 = await tenantDb().from('patient_tags').select('id, owner_id, name, color, is_native, created_at, updated_at').eq('owner_id', ownerId).order('name', { ascending: true });
if (t2.error) throw t2.error;
etiquetas.value = (t2.data || []).map((r) => normalizarEtiquetaRow({ ...r, patient_count: 0 }));
return;
@@ -262,14 +263,14 @@ async function salvarDlg() {
if (dlg.mode === 'create') {
const tenantId = await getActiveTenantId(ownerId);
const res = await supabase.from('patient_tags').insert({ owner_id: ownerId, tenant_id: tenantId, nome, cor });
const res = await tenantDb().from('patient_tags').insert({ owner_id: ownerId, nome, cor });
if (res.error) throw res.error;
toast.add({ severity: 'success', summary: 'Tag criada', detail: nome, life: 2500 });
} else {
let res = await supabase.from('patient_tags').update({ nome, cor, updated_at: new Date().toISOString() }).eq('id', dlg.id).eq('owner_id', ownerId);
let res = await tenantDb().from('patient_tags').update({ nome, cor, updated_at: new Date().toISOString() }).eq('id', dlg.id).eq('owner_id', ownerId);
// fallback legado
if (res.error && /column .*nome/i.test(String(res.error.message || ''))) {
res = await supabase.from('patient_tags').update({ name: nome, color: cor, updated_at: new Date().toISOString() }).eq('id', dlg.id).eq('owner_id', ownerId);
res = await tenantDb().from('patient_tags').update({ name: nome, color: cor, updated_at: new Date().toISOString() }).eq('id', dlg.id).eq('owner_id', ownerId);
}
if (res.error) throw res.error;
toast.add({ severity: 'success', summary: 'Tag atualizada', detail: nome, life: 2500 });
@@ -326,9 +327,9 @@ async function excluirTags(rows) {
toast.add({ severity: 'warn', summary: 'Nada para excluir', detail: 'Tags padrão não podem ser removidas.', life: 4000 });
return;
}
const pivotDel = await supabase.from('patient_patient_tag').delete().eq('owner_id', ownerId).in('tag_id', ids);
const pivotDel = await tenantDb().from('patient_patient_tag').delete().eq('owner_id', ownerId).in('tag_id', ids);
if (pivotDel.error) throw pivotDel.error;
const tagDel = await supabase.from('patient_tags').delete().eq('owner_id', ownerId).in('id', ids);
const tagDel = await tenantDb().from('patient_tags').delete().eq('owner_id', ownerId).in('id', ids);
if (tagDel.error) throw tagDel.error;
etiquetasSelecionadas.value = [];
toast.add({ severity: 'success', summary: 'Excluído', detail: `${ids.length} tag(s) removida(s).`, life: 3000 });
@@ -359,7 +360,7 @@ async function carregarPacientesDaTag(tag) {
modalPacientes.error = '';
try {
const ownerId = await getOwnerId();
const { data, error } = await supabase.from('patient_patient_tag').select('patient_id, patients:patients(id, nome_completo, email_principal, telefone, avatar_url)').eq('owner_id', ownerId).eq('tag_id', tag.id);
const { data, error } = await tenantDb().from('patient_patient_tag').select('patient_id, patients:patients(id, nome_completo, email_principal, telefone, avatar_url)').eq('owner_id', ownerId).eq('tag_id', tag.id);
if (error) throw error;
modalPacientes.items = (data || [])
.map((r) => r.patients)
@@ -19,6 +19,7 @@ import { useInsurancePlans } from '@/features/agenda/composables/useInsurancePla
import { useServices } from '@/features/agenda/composables/useServices';
import { useLayout } from '@/layout/composables/layout';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
import { applyThemeEngine } from '@/theme/theme.options';
import InputMask from 'primevue/inputmask';
@@ -412,7 +413,7 @@ onMounted(async () => {
if (!uid.value) return;
userEmail.value = user.email || '';
const { data: cfg } = await supabase.from('agenda_configuracoes').select('setup_concluido, setup_clinica_concluido, atendimento_mode').eq('owner_id', uid.value).maybeSingle();
const { data: cfg } = await tenantDb().from('agenda_configuracoes').select('setup_concluido, setup_clinica_concluido, atendimento_mode').eq('owner_id', uid.value).maybeSingle();
if (cfg && (cfg.setup_concluido || cfg.setup_clinica_concluido || !!cfg.atendimento_mode)) {
router.replace('/pages/notfound');
return;
@@ -439,7 +440,7 @@ async function loadNegocio() {
if (!tenantId.value) return;
// Fonte única: company_profiles
const { data } = await supabase.from('company_profiles').select('nome_fantasia,tipo_empresa,logo_url,cep,logradouro,numero,complemento,bairro,cidade,estado,telefone,email,site,redes_sociais').eq('tenant_id', tenantId.value).maybeSingle();
const { data } = await tenantDb().from('company_profiles').select('nome_fantasia,tipo_empresa,logo_url,cep,logradouro,numero,complemento,bairro,cidade,estado,telefone,email,site,redes_sociais').maybeSingle();
if (!data) return;
@@ -477,7 +478,7 @@ async function loadNegocio() {
}
async function loadAtendimento() {
if (!uid.value) return;
const { data } = await supabase.from('agenda_configuracoes').select('atendimento_mode').eq('owner_id', uid.value).maybeSingle();
const { data } = await tenantDb().from('agenda_configuracoes').select('atendimento_mode').eq('owner_id', uid.value).maybeSingle();
if (data?.atendimento_mode) {
atendimento.value.mode = data.atendimento_mode;
markSaved('atendimento');
@@ -570,9 +571,8 @@ async function saveNegocio(silent = false) {
}
// Fonte única: company_profiles (upsert garante insert ou update)
const { error } = await supabase.from('company_profiles').upsert(
const { error } = await tenantDb().from('company_profiles').upsert(
{
tenant_id: tenantId.value,
nome_fantasia: negocio.value.name.trim() || null,
tipo_empresa: negocio.value.type || null,
logo_url: logoUrl || null,
@@ -588,7 +588,7 @@ async function saveNegocio(silent = false) {
site: negocio.value.siteUrl?.trim() || null,
redes_sociais: redes
},
{ onConflict: 'tenant_id' }
{ onConflict: 'singleton' }
);
if (error) throw error;
@@ -614,10 +614,9 @@ async function saveAtendimento(silent = false) {
await saveService({ name: 'Atendimento padrão', price: 0, duration_min: 50, owner_id: uid.value, tenant_id: tenantId.value });
await loadServices(uid.value);
}
const { error } = await supabase.from('agenda_configuracoes').upsert(
const { error } = await tenantDb().from('agenda_configuracoes').upsert(
{
owner_id: uid.value,
tenant_id: tenantId.value,
atendimento_mode: atendimento.value.mode,
updated_at: new Date().toISOString()
},
@@ -691,7 +690,7 @@ async function onFinish() {
finishing.value = true;
try {
const now = new Date().toISOString();
const { error: finErr } = await supabase.from('agenda_configuracoes').upsert({ owner_id: uid.value, tenant_id: tenantId.value, setup_concluido: true, setup_concluido_em: now }, { onConflict: 'owner_id' });
const { error: finErr } = await tenantDb().from('agenda_configuracoes').upsert({ owner_id: uid.value, setup_concluido: true, setup_concluido_em: now }, { onConflict: 'owner_id' });
if (finErr) throw finErr;
done.value = true;
} catch (e) {
+9 -10
View File
@@ -19,6 +19,7 @@ import { useInsurancePlans } from '@/features/agenda/composables/useInsurancePla
import { useServices } from '@/features/agenda/composables/useServices';
import { useLayout } from '@/layout/composables/layout';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
import { applyThemeEngine } from '@/theme/theme.options';
import InputMask from 'primevue/inputmask';
@@ -416,7 +417,7 @@ onMounted(async () => {
if (!uid.value) return;
userEmail.value = user.email || '';
const { data: cfg } = await supabase.from('agenda_configuracoes').select('setup_concluido, setup_clinica_concluido, atendimento_mode').eq('owner_id', uid.value).maybeSingle();
const { data: cfg } = await tenantDb().from('agenda_configuracoes').select('setup_concluido, setup_clinica_concluido, atendimento_mode').eq('owner_id', uid.value).maybeSingle();
if (cfg && (cfg.setup_concluido || cfg.setup_clinica_concluido || !!cfg.atendimento_mode)) {
router.replace('/pages/notfound');
return;
@@ -443,7 +444,7 @@ async function loadNegocio() {
if (!tenantId.value) return;
// Fonte única: company_profiles
const { data } = await supabase.from('company_profiles').select('nome_fantasia,tipo_empresa,logo_url,cep,logradouro,numero,complemento,bairro,cidade,estado,telefone,email,site,redes_sociais').eq('tenant_id', tenantId.value).maybeSingle();
const { data } = await tenantDb().from('company_profiles').select('nome_fantasia,tipo_empresa,logo_url,cep,logradouro,numero,complemento,bairro,cidade,estado,telefone,email,site,redes_sociais').maybeSingle();
if (!data) return;
@@ -481,7 +482,7 @@ async function loadNegocio() {
}
async function loadAtendimento() {
if (!uid.value) return;
const { data } = await supabase.from('agenda_configuracoes').select('atendimento_mode').eq('owner_id', uid.value).maybeSingle();
const { data } = await tenantDb().from('agenda_configuracoes').select('atendimento_mode').eq('owner_id', uid.value).maybeSingle();
if (data?.atendimento_mode) {
atendimento.value.mode = data.atendimento_mode;
markSaved('atendimento');
@@ -574,7 +575,6 @@ async function saveNegocio(silent = false) {
}
const payload = {
tenant_id: tenantId.value,
nome_fantasia: negocio.value.name.trim() || null,
tipo_empresa: negocio.value.type || null,
logo_url: logoUrl || null,
@@ -592,15 +592,15 @@ async function saveNegocio(silent = false) {
};
// Verificar se já existe registro para este tenant
const { data: existing } = await supabase.from('company_profiles').select('id').eq('tenant_id', tenantId.value).maybeSingle();
const { data: existing } = await tenantDb().from('company_profiles').select('id').maybeSingle();
let error;
if (existing?.id) {
// Já existe usar UPDATE direto pelo id
({ error } = await supabase.from('company_profiles').update(payload).eq('id', existing.id));
({ error } = await tenantDb().from('company_profiles').update(payload).eq('id', existing.id));
} else {
// Não existe INSERT
({ error } = await supabase.from('company_profiles').insert(payload));
({ error } = await tenantDb().from('company_profiles').insert(payload));
}
if (error) throw error;
@@ -626,10 +626,9 @@ async function saveAtendimento(silent = false) {
await saveService({ name: 'Atendimento padrão', price: 0, duration_min: 50, owner_id: uid.value, tenant_id: tenantId.value });
await loadServices(uid.value);
}
const { error } = await supabase.from('agenda_configuracoes').upsert(
const { error } = await tenantDb().from('agenda_configuracoes').upsert(
{
owner_id: uid.value,
tenant_id: tenantId.value,
atendimento_mode: atendimento.value.mode,
updated_at: new Date().toISOString()
},
@@ -703,7 +702,7 @@ async function onFinish() {
finishing.value = true;
try {
const now = new Date().toISOString();
const { error: finErr } = await supabase.from('agenda_configuracoes').upsert({ owner_id: uid.value, tenant_id: tenantId.value, setup_concluido: true, setup_concluido_em: now }, { onConflict: 'owner_id' });
const { error: finErr } = await tenantDb().from('agenda_configuracoes').upsert({ owner_id: uid.value, setup_concluido: true, setup_concluido_em: now }, { onConflict: 'owner_id' });
if (finErr) throw finErr;
// Fecha o dialog ANTES de mostrar a tela de parabéns
dialogVisible.value = false;