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:
@@ -24,6 +24,7 @@ import Textarea from 'primevue/textarea';
|
||||
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
|
||||
@@ -337,10 +338,9 @@ async function loadNegocio() {
|
||||
tenantId.value = member.tenant_id;
|
||||
|
||||
// Carregar dados da empresa
|
||||
const { data: co, error: coErr } = await supabase
|
||||
.from('company_profiles')
|
||||
const { data: co, error: coErr } = await tenantDb().from('company_profiles')
|
||||
.select('*')
|
||||
.eq('tenant_id', tenantId.value)
|
||||
|
||||
.maybeSingle();
|
||||
|
||||
if (coErr) throw coErr;
|
||||
@@ -401,7 +401,6 @@ async function saveAll() {
|
||||
}
|
||||
|
||||
const payload = {
|
||||
tenant_id: tenantId.value,
|
||||
nome_fantasia: String(form.nome_fantasia || '').trim() || null,
|
||||
razao_social: String(form.razao_social || '').trim() || null,
|
||||
tipo_empresa: String(form.tipo_empresa || '').trim() || null,
|
||||
@@ -423,9 +422,8 @@ async function saveAll() {
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
const { error } = await supabase
|
||||
.from('company_profiles')
|
||||
.upsert(payload, { onConflict: 'tenant_id' });
|
||||
const { error } = await tenantDb().from('company_profiles')
|
||||
.upsert(payload, { onConflict: 'singleton' });
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useRouter } from 'vue-router';
|
||||
import { useLayout } from '@/layout/composables/layout';
|
||||
import Menu from 'primevue/menu';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useClinicKPIs } from '@/composables/useClinicKPIs';
|
||||
import FirstResponseCard from '@/components/dashboard/FirstResponseCard.vue';
|
||||
@@ -419,10 +420,9 @@ async function load() {
|
||||
|
||||
try {
|
||||
const [eventosRes, membrosRes, solRes, cadRes] = await Promise.all([
|
||||
supabase
|
||||
.from('agenda_eventos')
|
||||
tenantDb().from('agenda_eventos')
|
||||
.select('id, inicio_em, fim_em, status, modalidade, tipo, titulo, titulo_custom, patient_id, owner_id, recurrence_id, patients(nome_completo)')
|
||||
.eq('tenant_id', tid)
|
||||
|
||||
.gte('inicio_em', mesInicio)
|
||||
.lte('inicio_em', mesFim)
|
||||
.order('inicio_em', { ascending: true }),
|
||||
@@ -432,17 +432,15 @@ async function load() {
|
||||
.eq('tenant_id', tid)
|
||||
.in('role', ['therapist', 'tenant_admin'])
|
||||
.eq('status', 'active'),
|
||||
supabase
|
||||
.from('agendador_solicitacoes')
|
||||
tenantDb().from('agendador_solicitacoes')
|
||||
.select('id, paciente_nome, paciente_sobrenome, tipo, modalidade, data_solicitada, hora_solicitada')
|
||||
.eq('tenant_id', tid)
|
||||
|
||||
.eq('status', 'pendente')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(10),
|
||||
supabase
|
||||
.from('patient_intake_requests')
|
||||
tenantDb().from('patient_intake_requests')
|
||||
.select('id, nome_completo, status, created_at')
|
||||
.eq('tenant_id', tid)
|
||||
|
||||
.eq('status', 'new')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(10)
|
||||
|
||||
@@ -19,6 +19,7 @@ import { ref, computed, onMounted } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
const toast = useToast();
|
||||
|
||||
// ── Contexto ──────────────────────────────────────────────────
|
||||
@@ -82,8 +83,7 @@ async function init() {
|
||||
}
|
||||
|
||||
async function loadChannel() {
|
||||
const { data } = await supabase
|
||||
.from('notification_channels')
|
||||
const { data } = await tenantDb().from('notification_channels')
|
||||
.select('*')
|
||||
.eq('owner_id', userId.value)
|
||||
.eq('channel', 'sms')
|
||||
@@ -103,8 +103,7 @@ async function loadChannel() {
|
||||
|
||||
async function loadLogs() {
|
||||
logsLoading.value = true;
|
||||
const { data } = await supabase
|
||||
.from('notification_logs')
|
||||
const { data } = await tenantDb().from('notification_logs')
|
||||
.select('id, template_key, recipient_address, status, failure_reason, provider_message_id, sent_at, failed_at, created_at')
|
||||
.eq('owner_id', userId.value)
|
||||
.eq('channel', 'sms')
|
||||
@@ -137,8 +136,7 @@ async function saveCredentials() {
|
||||
const creds = { ...credentials.value };
|
||||
|
||||
if (channel.value?.id) {
|
||||
const { error } = await supabase
|
||||
.from('notification_channels')
|
||||
const { error } = await tenantDb().from('notification_channels')
|
||||
.update({
|
||||
credentials: creds,
|
||||
is_active: true,
|
||||
@@ -147,11 +145,9 @@ async function saveCredentials() {
|
||||
.eq('id', channel.value.id);
|
||||
if (error) throw error;
|
||||
} else {
|
||||
const { data, error } = await supabase
|
||||
.from('notification_channels')
|
||||
const { data, error } = await tenantDb().from('notification_channels')
|
||||
.insert({
|
||||
owner_id: userId.value,
|
||||
tenant_id: tenantId.value,
|
||||
channel: 'sms',
|
||||
provider: 'twilio',
|
||||
display_name: 'SMS via Twilio',
|
||||
@@ -194,8 +190,7 @@ async function testConnection() {
|
||||
|
||||
// Atualiza connection_status para connected
|
||||
if (channel.value?.id) {
|
||||
await supabase
|
||||
.from('notification_channels')
|
||||
await tenantDb().from('notification_channels')
|
||||
.update({ connection_status: 'connected' })
|
||||
.eq('id', channel.value.id);
|
||||
channel.value.connection_status = 'connected';
|
||||
@@ -206,8 +201,7 @@ async function testConnection() {
|
||||
toast.add({ severity: 'warn', summary: 'Falha no teste', detail: data?.message, life: 6000 });
|
||||
|
||||
if (channel.value?.id) {
|
||||
await supabase
|
||||
.from('notification_channels')
|
||||
await tenantDb().from('notification_channels')
|
||||
.update({ connection_status: 'error' })
|
||||
.eq('id', channel.value.id);
|
||||
channel.value.connection_status = 'error';
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useConfirm } from 'primevue/useconfirm';
|
||||
import { formatDistanceToNow, format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useNotificationStore } from '@/stores/notificationStore';
|
||||
import { useConversationDrawerStore } from '@/stores/conversationDrawerStore';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
@@ -92,7 +93,7 @@ async function load() {
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
const { data, error } = await supabase.from('notifications').select('*').eq('owner_id', ownerId.value).order('created_at', { ascending: false }).limit(500);
|
||||
const { data, error } = await tenantDb().from('notifications').select('*').eq('owner_id', ownerId.value).order('created_at', { ascending: false }).limit(500);
|
||||
if (error) throw error;
|
||||
items.value = data || [];
|
||||
} catch (e) {
|
||||
@@ -105,7 +106,7 @@ async function load() {
|
||||
// ─── Actions ─────────────────────────────────────────
|
||||
async function markRead(id) {
|
||||
const now = new Date().toISOString();
|
||||
const { error } = await supabase.from('notifications').update({ read_at: now }).eq('id', id);
|
||||
const { error } = await tenantDb().from('notifications').update({ read_at: now }).eq('id', id);
|
||||
if (error) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: error.message, life: 3500 });
|
||||
return;
|
||||
@@ -118,7 +119,7 @@ async function markRead(id) {
|
||||
}
|
||||
|
||||
async function markUnread(id) {
|
||||
const { error } = await supabase.from('notifications').update({ read_at: null }).eq('id', id);
|
||||
const { error } = await tenantDb().from('notifications').update({ read_at: null }).eq('id', id);
|
||||
if (error) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: error.message, life: 3500 });
|
||||
return;
|
||||
@@ -130,7 +131,7 @@ async function markUnread(id) {
|
||||
}
|
||||
|
||||
async function archive(id) {
|
||||
const { error } = await supabase.from('notifications').update({ archived: true }).eq('id', id);
|
||||
const { error } = await tenantDb().from('notifications').update({ archived: true }).eq('id', id);
|
||||
if (error) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: error.message, life: 3500 });
|
||||
return;
|
||||
@@ -142,7 +143,7 @@ async function archive(id) {
|
||||
}
|
||||
|
||||
async function unarchive(id) {
|
||||
const { error } = await supabase.from('notifications').update({ archived: false }).eq('id', id);
|
||||
const { error } = await tenantDb().from('notifications').update({ archived: false }).eq('id', id);
|
||||
if (error) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: error.message, life: 3500 });
|
||||
return;
|
||||
@@ -168,7 +169,7 @@ function confirmRemove(id) {
|
||||
}
|
||||
|
||||
async function remove(id) {
|
||||
const { error } = await supabase.from('notifications').delete().eq('id', id);
|
||||
const { error } = await tenantDb().from('notifications').delete().eq('id', id);
|
||||
if (error) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: error.message, life: 3500 });
|
||||
return;
|
||||
@@ -182,7 +183,7 @@ async function markAllRead() {
|
||||
const unreadIds = items.value.filter((n) => !n.read_at && !n.archived).map((n) => n.id);
|
||||
if (!unreadIds.length) return;
|
||||
const now = new Date().toISOString();
|
||||
const { error } = await supabase.from('notifications').update({ read_at: now }).in('id', unreadIds);
|
||||
const { error } = await tenantDb().from('notifications').update({ read_at: now }).in('id', unreadIds);
|
||||
if (error) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: error.message, life: 3500 });
|
||||
return;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useLayout } from '@/layout/composables/layout';
|
||||
import { exportSessionsToPDF, exportSessionsToXLSX, exportSessionsToCSV } from '@/services/reportExport.service';
|
||||
@@ -76,10 +77,9 @@ async function loadSessions() {
|
||||
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, patient_id, patients(nome_completo)')
|
||||
.eq('tenant_id', tenantId)
|
||||
|
||||
.eq('owner_id', uid)
|
||||
.gte('inicio_em', start.toISOString())
|
||||
.lte('inicio_em', end.toISOString())
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useLayout } from '@/layout/composables/layout';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import Menu from 'primevue/menu';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useAgendaEvents } from '@/features/agenda/composables/useAgendaEvents';
|
||||
import AgendaEventDialog from '@/features/agenda/components/AgendaEventDialog.vue';
|
||||
@@ -255,7 +256,6 @@ function _isUuid(v) {
|
||||
|
||||
function _pickDbFields(obj) {
|
||||
const allowed = [
|
||||
'tenant_id',
|
||||
'owner_id',
|
||||
'terapeuta_id',
|
||||
'patient_id',
|
||||
@@ -294,10 +294,6 @@ async function onAgendaDialogSave(arg) {
|
||||
|
||||
if (!normalized.owner_id && ownerId.value) normalized.owner_id = ownerId.value;
|
||||
|
||||
const tid = clinicTenantId.value;
|
||||
if (!tid) throw new Error('tenant_id não encontrado.');
|
||||
normalized.tenant_id = tid;
|
||||
|
||||
if (!normalized.visibility_scope) normalized.visibility_scope = 'public';
|
||||
if (!normalized.status) normalized.status = 'agendado';
|
||||
if (!normalized.tipo) normalized.tipo = 'sessao';
|
||||
@@ -566,7 +562,7 @@ const solicitacoesPendentes = computed(() => solicitacoes.value.length);
|
||||
|
||||
async function aceitarSol(id) {
|
||||
try {
|
||||
await supabase.from('agendador_solicitacoes').update({ status: 'autorizado' }).eq('id', id);
|
||||
await tenantDb().from('agendador_solicitacoes').update({ status: 'autorizado' }).eq('id', id);
|
||||
_solicitacoesBruto.value = _solicitacoesBruto.value.filter((s) => s.id !== id);
|
||||
} catch (e) {
|
||||
console.error('[TherapistDashboard] aceitarSol:', e);
|
||||
@@ -574,7 +570,7 @@ async function aceitarSol(id) {
|
||||
}
|
||||
async function recusarSol(id) {
|
||||
try {
|
||||
await supabase.from('agendador_solicitacoes').update({ status: 'recusado' }).eq('id', id);
|
||||
await tenantDb().from('agendador_solicitacoes').update({ status: 'recusado' }).eq('id', id);
|
||||
_solicitacoesBruto.value = _solicitacoesBruto.value.filter((s) => s.id !== id);
|
||||
} catch (e) {
|
||||
console.error('[TherapistDashboard] recusarSol:', e);
|
||||
@@ -888,34 +884,29 @@ async function load() {
|
||||
try {
|
||||
const [eventosRes, recRes, solRes, cadRes] = await Promise.all([
|
||||
(() => {
|
||||
let q = supabase
|
||||
.from('agenda_eventos')
|
||||
let q = tenantDb().from('agenda_eventos')
|
||||
.select('id, inicio_em, fim_em, status, modalidade, tipo, titulo, titulo_custom, patient_id, recurrence_id, determined_commitment_id, patients(nome_completo), determined_commitments(bg_color, text_color)')
|
||||
.eq('owner_id', ownerId.value)
|
||||
.gte('inicio_em', mesInicio)
|
||||
.lte('inicio_em', mesFim)
|
||||
.order('inicio_em', { ascending: true });
|
||||
if (tid) q = q.eq('tenant_id', tid);
|
||||
return q;
|
||||
})(),
|
||||
(() => {
|
||||
let q = supabase
|
||||
.from('recurrence_rules')
|
||||
let q = tenantDb().from('recurrence_rules')
|
||||
.select('id, patient_id, type, interval, weekdays, start_date, end_date, max_occurrences, start_time')
|
||||
.eq('owner_id', ownerId.value)
|
||||
.eq('status', 'ativo')
|
||||
.order('start_date', { ascending: false });
|
||||
if (tid) q = q.eq('tenant_id', tid);
|
||||
return q;
|
||||
})(),
|
||||
supabase
|
||||
.from('agendador_solicitacoes')
|
||||
tenantDb().from('agendador_solicitacoes')
|
||||
.select('id, paciente_nome, paciente_sobrenome, tipo, modalidade, data_solicitada, hora_solicitada')
|
||||
.eq('owner_id', ownerId.value)
|
||||
.eq('status', 'pendente')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(10),
|
||||
supabase.from('patient_intake_requests').select('id, nome_completo, status, created_at').eq('owner_id', ownerId.value).eq('status', 'new').order('created_at', { ascending: false }).limit(10)
|
||||
tenantDb().from('patient_intake_requests').select('id, nome_completo, status, created_at').eq('owner_id', ownerId.value).eq('status', 'new').order('created_at', { ascending: false }).limit(10)
|
||||
]);
|
||||
eventosDoMes.value = eventosRes.data || [];
|
||||
_solicitacoesBruto.value = solRes.data || [];
|
||||
@@ -923,7 +914,7 @@ async function load() {
|
||||
const rawRules = recRes.data || [];
|
||||
const patientIds = [...new Set(rawRules.map((r) => r.patient_id).filter(Boolean))];
|
||||
if (patientIds.length) {
|
||||
const { data: pts } = await supabase.from('patients').select('id, nome_completo').in('id', patientIds);
|
||||
const { data: pts } = await tenantDb().from('patients').select('id, nome_completo').in('id', patientIds);
|
||||
const ptMap = Object.fromEntries((pts || []).map((p) => [p.id, p.nome_completo]));
|
||||
for (const r of rawRules) r._patientNome = ptMap[r.patient_id] || '—';
|
||||
} else {
|
||||
@@ -931,8 +922,7 @@ async function load() {
|
||||
}
|
||||
const rulesComLimite = rawRules.filter((r) => r.max_occurrences);
|
||||
if (rulesComLimite.length) {
|
||||
const { data: sessData } = await supabase
|
||||
.from('agenda_eventos')
|
||||
const { data: sessData } = await tenantDb().from('agenda_eventos')
|
||||
.select('id, recurrence_id')
|
||||
.in(
|
||||
'recurrence_id',
|
||||
|
||||
Reference in New Issue
Block a user