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:
@@ -11,6 +11,7 @@
|
||||
|
||||
import { ref, computed } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
function normalizePhoneBR(raw) {
|
||||
@@ -38,15 +39,11 @@ export function useConversationOptouts() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const [optsRes, kwsRes] = await Promise.all([
|
||||
supabase
|
||||
.from('conversation_optouts')
|
||||
tenantDb().from('conversation_optouts')
|
||||
.select('id, phone, patient_id, source, keyword_matched, original_message, notes, opted_out_at, opted_back_in_at, blocked_by')
|
||||
.eq('tenant_id', tenantId)
|
||||
.order('opted_out_at', { ascending: false }),
|
||||
supabase
|
||||
.from('conversation_optout_keywords')
|
||||
.select('id, tenant_id, keyword, enabled, is_system')
|
||||
.or(`tenant_id.is.null,tenant_id.eq.${tenantId}`)
|
||||
tenantDb().from('conversation_optout_keywords')
|
||||
.select('id, keyword, enabled, is_system')
|
||||
.order('is_system', { ascending: false })
|
||||
.order('keyword', { ascending: true })
|
||||
]);
|
||||
@@ -56,7 +53,7 @@ export function useConversationOptouts() {
|
||||
// Enriquece com nome do paciente
|
||||
const patIds = [...new Set(optouts.value.map((o) => o.patient_id).filter(Boolean))];
|
||||
if (patIds.length) {
|
||||
const { data: pats } = await supabase.from('patients').select('id, nome_completo').in('id', patIds);
|
||||
const { data: pats } = await tenantDb().from('patients').select('id, nome_completo').in('id', patIds);
|
||||
const patMap = Object.fromEntries((pats || []).map((p) => [p.id, p.nome_completo]));
|
||||
optouts.value = optouts.value.map((o) => ({ ...o, _patient_name: patMap[o.patient_id] || null }));
|
||||
}
|
||||
@@ -79,19 +76,15 @@ export function useConversationOptouts() {
|
||||
const userId = authData?.user?.id;
|
||||
|
||||
// Verifica se já existe ativo
|
||||
const { data: existing } = await supabase
|
||||
.from('conversation_optouts')
|
||||
const { data: existing } = await tenantDb().from('conversation_optouts')
|
||||
.select('id')
|
||||
.eq('tenant_id', tenantId)
|
||||
.eq('phone', cleanPhone)
|
||||
.is('opted_back_in_at', null)
|
||||
.maybeSingle();
|
||||
if (existing) return { ok: false, error: 'already_opted_out' };
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('conversation_optouts')
|
||||
const { data, error } = await tenantDb().from('conversation_optouts')
|
||||
.insert({
|
||||
tenant_id: tenantId,
|
||||
phone: cleanPhone,
|
||||
patient_id: patientId,
|
||||
source: 'manual',
|
||||
@@ -115,8 +108,7 @@ export function useConversationOptouts() {
|
||||
saving.value = true;
|
||||
try {
|
||||
const now = new Date().toISOString();
|
||||
const { error } = await supabase
|
||||
.from('conversation_optouts')
|
||||
const { error } = await tenantDb().from('conversation_optouts')
|
||||
.update({ opted_back_in_at: now })
|
||||
.eq('id', id);
|
||||
if (error) throw error;
|
||||
@@ -136,10 +128,9 @@ export function useConversationOptouts() {
|
||||
if (!tenantId || !clean) return { ok: false, error: 'invalid_params' };
|
||||
saving.value = true;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('conversation_optout_keywords')
|
||||
.insert({ tenant_id: tenantId, keyword: clean, is_system: false, enabled: true })
|
||||
.select('id, tenant_id, keyword, enabled, is_system')
|
||||
const { data, error } = await tenantDb().from('conversation_optout_keywords')
|
||||
.insert({ keyword: clean, is_system: false, enabled: true })
|
||||
.select('id, keyword, enabled, is_system')
|
||||
.single();
|
||||
if (error) throw error;
|
||||
keywords.value = [...keywords.value, data];
|
||||
@@ -154,8 +145,7 @@ export function useConversationOptouts() {
|
||||
async function toggleKeyword(id, enabled) {
|
||||
saving.value = true;
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('conversation_optout_keywords')
|
||||
const { error } = await tenantDb().from('conversation_optout_keywords')
|
||||
.update({ enabled })
|
||||
.eq('id', id);
|
||||
if (error) throw error;
|
||||
@@ -172,8 +162,7 @@ export function useConversationOptouts() {
|
||||
async function deleteKeyword(id) {
|
||||
saving.value = true;
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('conversation_optout_keywords')
|
||||
const { error } = await tenantDb().from('conversation_optout_keywords')
|
||||
.delete()
|
||||
.eq('id', id);
|
||||
if (error) throw error;
|
||||
|
||||
Reference in New Issue
Block a user