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
+6 -10
View File
@@ -16,6 +16,7 @@
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
// ── Helpers ──────────────────────────────────────────────────
async function getOwnerId() {
@@ -55,11 +56,9 @@ export async function logAccess(documentoId, acao) {
const ownerId = await getOwnerId();
const tenantId = await getActiveTenantId(ownerId);
const { error } = await supabase
.from('document_access_logs')
const { error } = await tenantDb().from('document_access_logs')
.insert({
documento_id: documentoId,
tenant_id: tenantId,
acao,
user_id: ownerId
});
@@ -76,8 +75,7 @@ export async function logAccess(documentoId, acao) {
export async function listAccessLogs(documentoId) {
if (!documentoId) return [];
const { data, error } = await supabase
.from('document_access_logs')
const { data, error } = await tenantDb().from('document_access_logs')
.select('*, profiles:user_id(full_name)')
.eq('documento_id', documentoId)
.order('acessado_em', { ascending: false });
@@ -97,10 +95,9 @@ export async function listAllAccessLogs(filters = {}, limit = 100) {
const ownerId = await getOwnerId();
const tenantId = await getActiveTenantId(ownerId);
let query = supabase
.from('document_access_logs')
let query = tenantDb().from('document_access_logs')
.select('*, profiles:user_id(full_name), documents:documento_id(nome_original, patient_id)')
.eq('tenant_id', tenantId)
.order('acessado_em', { ascending: false })
.limit(limit);
@@ -129,8 +126,7 @@ export async function listAllAccessLogs(filters = {}, limit = 100) {
export async function countAccessByAction(documentoId) {
if (!documentoId) return {};
const { data, error } = await supabase
.from('document_access_logs')
const { data, error } = await tenantDb().from('document_access_logs')
.select('acao')
.eq('documento_id', documentoId);
+14 -29
View File
@@ -15,6 +15,7 @@
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { valorExtenso } from '@/utils/valorExtenso';
const BUCKET = 'generated-docs';
@@ -78,8 +79,7 @@ async function getActiveTenantId(uid) {
* Busca dados do paciente para preencher variaveis do template.
*/
export async function loadPatientData(patientId) {
const { data, error } = await supabase
.from('patients')
const { data, error } = await tenantDb().from('patients')
.select(`
nome_completo, nome_social, cpf, data_nascimento,
telefone, email_principal,
@@ -113,8 +113,7 @@ export async function loadPatientData(patientId) {
export async function loadSessionData(agendaEventoId) {
if (!agendaEventoId) return {};
const { data, error } = await supabase
.from('agenda_eventos')
const { data, error } = await tenantDb().from('agenda_eventos')
.select('inicio_em, fim_em, modalidade, price')
.eq('id', agendaEventoId)
.single();
@@ -433,8 +432,7 @@ export async function saveGeneratedDocument({ templateId, patientId, dadosPreenc
// Re-edicao: preserva documents.id (e o audit trail), substitui o PDF
// no Storage, atualiza metadados. Best-effort cleanup do PDF antigo.
if (editingDocId) {
const { data: oldDoc } = await supabase
.from('documents')
const { data: oldDoc } = await tenantDb().from('documents')
.select('bucket_path, storage_bucket')
.eq('id', editingDocId)
.single();
@@ -449,16 +447,14 @@ export async function saveGeneratedDocument({ templateId, patientId, dadosPreenc
docPatch.tamanho_bytes = pdfBlob?.size || null;
docPatch.nome_original = filename.replace(/_/g, ' ').replace('.pdf', '') + '.pdf';
}
const { error: upDocErr } = await supabase
.from('documents')
const { error: upDocErr } = await tenantDb().from('documents')
.update(docPatch)
.eq('id', editingDocId);
if (upDocErr) throw upDocErr;
// Atualiza document_generated. Pode nao existir (docs legados sem
// linkage) — INSERT nesse caso, com documento_id apontando pro doc.
const { data: existingGen } = await supabase
.from('document_generated')
const { data: existingGen } = await tenantDb().from('document_generated')
.select('id')
.eq('documento_id', editingDocId)
.maybeSingle();
@@ -470,8 +466,7 @@ export async function saveGeneratedDocument({ templateId, patientId, dadosPreenc
dados_preenchidos: dadosPreenchidos || {}
};
if (pdfPath) genPatch.pdf_path = pdfPath;
const { data: updated, error: upGenErr } = await supabase
.from('document_generated')
const { data: updated, error: upGenErr } = await tenantDb().from('document_generated')
.update(genPatch)
.eq('id', existingGen.id)
.select('*')
@@ -479,12 +474,10 @@ export async function saveGeneratedDocument({ templateId, patientId, dadosPreenc
if (upGenErr) throw upGenErr;
data = updated;
} else {
const { data: inserted, error: insGenErr } = await supabase
.from('document_generated')
const { data: inserted, error: insGenErr } = await tenantDb().from('document_generated')
.insert({
template_id: templateId,
patient_id: patientId,
tenant_id: tenantId,
dados_preenchidos: dadosPreenchidos || {},
pdf_path: pdfPath,
storage_bucket: BUCKET,
@@ -512,11 +505,9 @@ export async function saveGeneratedDocument({ templateId, patientId, dadosPreenc
// document_generated via documento_id (FK).
let documentoId = null;
if (pdfPath) {
const { data: newDoc, error: insDocErr } = await supabase
.from('documents')
const { data: newDoc, error: insDocErr } = await tenantDb().from('documents')
.insert({
owner_id: ownerId,
tenant_id: tenantId,
patient_id: patientId,
bucket_path: pdfPath,
storage_bucket: BUCKET,
@@ -537,12 +528,10 @@ export async function saveGeneratedDocument({ templateId, patientId, dadosPreenc
}
// Registra em document_generated com o linkage documento_id preenchido
const { data, error } = await supabase
.from('document_generated')
const { data, error } = await tenantDb().from('document_generated')
.insert({
template_id: templateId,
patient_id: patientId,
tenant_id: tenantId,
dados_preenchidos: dadosPreenchidos || {},
pdf_path: pdfPath,
storage_bucket: BUCKET,
@@ -566,8 +555,7 @@ export async function saveGeneratedDocument({ templateId, patientId, dadosPreenc
*/
export async function loadGeneratedFromDocId(documentoId) {
if (!documentoId) return null;
const { data, error } = await supabase
.from('document_generated')
const { data, error } = await tenantDb().from('document_generated')
.select('id, template_id, dados_preenchidos, pdf_path, gerado_em')
.eq('documento_id', documentoId)
.maybeSingle();
@@ -599,8 +587,7 @@ export async function emitirReciboParaSessao(agendaEventoId, { patientId, valor,
// Resolve patient_id pela sessão se não veio
let resolvedPatientId = patientId;
if (!resolvedPatientId && agendaEventoId) {
const { data, error } = await supabase
.from('agenda_eventos')
const { data, error } = await tenantDb().from('agenda_eventos')
.select('paciente_id')
.eq('id', agendaEventoId)
.single();
@@ -610,8 +597,7 @@ export async function emitirReciboParaSessao(agendaEventoId, { patientId, valor,
}
// Busca template global recibo_pagamento
const { data: tpl, error: tplErr } = await supabase
.from('document_templates')
const { data: tpl, error: tplErr } = await tenantDb().from('document_templates')
.select('*')
.eq('tipo', 'recibo_pagamento')
.eq('is_global', true)
@@ -660,8 +646,7 @@ export async function emitirReciboParaSessao(agendaEventoId, { patientId, valor,
export async function listGeneratedDocuments(patientId) {
const ownerId = await getOwnerId();
const { data, error } = await supabase
.from('document_generated')
const { data, error } = await tenantDb().from('document_generated')
.select('*, document_templates(nome_template, tipo)')
.eq('gerado_por', ownerId)
.eq('patient_id', patientId)
+4 -7
View File
@@ -16,6 +16,7 @@
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
// ── Helpers ──────────────────────────────────────────────────
async function getOwnerId() {
@@ -59,11 +60,9 @@ export async function createShareLink(documentoId, opts = {}) {
const expiraEm = new Date();
expiraEm.setHours(expiraEm.getHours() + expiracaoHoras);
const { data, error } = await supabase
.from('document_share_links')
const { data, error } = await tenantDb().from('document_share_links')
.insert({
documento_id: documentoId,
tenant_id: tenantId,
expira_em: expiraEm.toISOString(),
usos_max: opts.usosMax || 5,
criado_por: ownerId
@@ -82,8 +81,7 @@ export async function listShareLinks(documentoId) {
const ownerId = await getOwnerId();
const { data, error } = await supabase
.from('document_share_links')
const { data, error } = await tenantDb().from('document_share_links')
.select('*')
.eq('documento_id', documentoId)
.eq('criado_por', ownerId)
@@ -131,8 +129,7 @@ export async function deactivateShareLink(linkId) {
const ownerId = await getOwnerId();
const { error } = await supabase
.from('document_share_links')
const { error } = await tenantDb().from('document_share_links')
.update({ ativo: false })
.eq('id', linkId)
.eq('criado_por', ownerId);
+5 -9
View File
@@ -16,6 +16,7 @@
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
// ── Helpers ──────────────────────────────────────────────────
async function getOwnerId() {
@@ -69,7 +70,6 @@ export async function createSignatureRequests(documentoId, signatarios = []) {
const rows = signatarios.map((s, idx) => ({
documento_id: documentoId,
tenant_id: tenantId,
signatario_tipo: s.tipo || 'paciente',
signatario_id: s.id || null,
signatario_nome: s.nome || null,
@@ -78,8 +78,7 @@ export async function createSignatureRequests(documentoId, signatarios = []) {
status: 'pendente'
}));
const { data, error } = await supabase
.from('document_signatures')
const { data, error } = await tenantDb().from('document_signatures')
.insert(rows)
.select('*');
@@ -98,8 +97,7 @@ export async function createSignatureRequests(documentoId, signatarios = []) {
export async function registerSignature(signatureId, meta = {}) {
if (!signatureId) throw new Error('ID da assinatura inválido.');
const { data, error } = await supabase
.from('document_signatures')
const { data, error } = await tenantDb().from('document_signatures')
.update({
status: 'assinado',
ip: meta.ip || null,
@@ -120,8 +118,7 @@ export async function registerSignature(signatureId, meta = {}) {
export async function listSignatures(documentoId) {
if (!documentoId) return [];
const { data, error } = await supabase
.from('document_signatures')
const { data, error } = await tenantDb().from('document_signatures')
.select('*')
.eq('documento_id', documentoId)
.order('ordem', { ascending: true });
@@ -157,8 +154,7 @@ export async function getSignatureStatus(documentoId) {
export async function refuseSignature(signatureId) {
if (!signatureId) throw new Error('ID da assinatura inválido.');
const { data, error } = await supabase
.from('document_signatures')
const { data, error } = await tenantDb().from('document_signatures')
.update({
status: 'recusado',
atualizado_em: new Date().toISOString()
+7 -15
View File
@@ -16,6 +16,7 @@
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
// ── Helpers ──────────────────────────────────────────────────
async function getOwnerId() {
@@ -105,10 +106,8 @@ export async function listTemplates() {
const ownerId = await getOwnerId();
const tenantId = await getActiveTenantId(ownerId);
const { data, error } = await supabase
.from('document_templates')
const { data, error } = await tenantDb().from('document_templates')
.select('*')
.or(`is_global.eq.true,tenant_id.eq.${tenantId}`)
.eq('ativo', true)
.order('nome_template', { ascending: true });
@@ -123,10 +122,8 @@ export async function listAllTemplates() {
const ownerId = await getOwnerId();
const tenantId = await getActiveTenantId(ownerId);
const { data, error } = await supabase
.from('document_templates')
const { data, error } = await tenantDb().from('document_templates')
.select('*')
.or(`is_global.eq.true,tenant_id.eq.${tenantId}`)
.order('is_global', { ascending: false })
.order('nome_template', { ascending: true });
@@ -137,8 +134,7 @@ export async function listAllTemplates() {
// ── Get one ─────────────────────────────────────────────────
export async function getTemplate(id) {
const { data, error } = await supabase
.from('document_templates')
const { data, error } = await tenantDb().from('document_templates')
.select('*')
.eq('id', id)
.single();
@@ -158,7 +154,6 @@ export async function createTemplate(payload) {
const row = {
owner_id: ownerId,
tenant_id: tenantId,
nome_template: nome,
tipo: payload.tipo || 'outro',
descricao: payload.descricao || null,
@@ -171,8 +166,7 @@ export async function createTemplate(payload) {
ativo: true
};
const { data, error } = await supabase
.from('document_templates')
const { data, error } = await tenantDb().from('document_templates')
.insert(row)
.select('*')
.single();
@@ -200,8 +194,7 @@ export async function updateTemplate(id, payload) {
row.updated_at = new Date().toISOString();
const { data, error } = await supabase
.from('document_templates')
const { data, error } = await tenantDb().from('document_templates')
.update(row)
.eq('id', id)
.eq('owner_id', ownerId)
@@ -218,8 +211,7 @@ export async function deleteTemplate(id) {
const ownerId = await getOwnerId();
if (!id) throw new Error('ID inválido.');
const { error } = await supabase
.from('document_templates')
const { error } = await tenantDb().from('document_templates')
.update({ ativo: false, updated_at: new Date().toISOString() })
.eq('id', id)
.eq('owner_id', ownerId);
+10 -19
View File
@@ -16,6 +16,7 @@
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
const BUCKET = 'documents';
// ── Helpers ──────────────────────────────────────────────────
@@ -93,7 +94,6 @@ export async function uploadDocument(file, patientId, meta = {}) {
// Insert na tabela
const row = {
owner_id: ownerId,
tenant_id: tenantId,
patient_id: patientId,
bucket_path: path,
storage_bucket: BUCKET,
@@ -114,8 +114,7 @@ export async function uploadDocument(file, patientId, meta = {}) {
uploaded_by: ownerId
};
const { data, error } = await supabase
.from('documents')
const { data, error } = await tenantDb().from('documents')
.insert(row)
.select('*')
.single();
@@ -140,8 +139,7 @@ export async function uploadDocument(file, patientId, meta = {}) {
export async function listDocuments(patientId, filters = {}) {
const ownerId = await getOwnerId();
let query = supabase
.from('documents')
let query = tenantDb().from('documents')
.select('*')
.eq('owner_id', ownerId)
.eq('patient_id', patientId)
@@ -172,8 +170,7 @@ export async function listDocuments(patientId, filters = {}) {
export async function listAllDocuments(filters = {}) {
const ownerId = await getOwnerId();
let query = supabase
.from('documents')
let query = tenantDb().from('documents')
.select('*, patients!inner(nome_completo)')
.eq('owner_id', ownerId)
.is('deleted_at', null)
@@ -196,8 +193,7 @@ export async function listAllDocuments(filters = {}) {
export async function getDocument(id) {
const ownerId = await getOwnerId();
const { data, error } = await supabase
.from('documents')
const { data, error } = await tenantDb().from('documents')
.select('*')
.eq('id', id)
.eq('owner_id', ownerId)
@@ -229,8 +225,7 @@ export async function updateDocument(id, payload) {
row.updated_at = new Date().toISOString();
const { data, error } = await supabase
.from('documents')
const { data, error } = await tenantDb().from('documents')
.update(row)
.eq('id', id)
.eq('owner_id', ownerId)
@@ -254,8 +249,7 @@ export async function softDeleteDocument(id, retencaoAnos = 5) {
const retencaoAte = new Date();
retencaoAte.setFullYear(retencaoAte.getFullYear() + retencaoAnos);
const { error } = await supabase
.from('documents')
const { error } = await tenantDb().from('documents')
.update({
deleted_at: new Date().toISOString(),
deleted_by: ownerId,
@@ -276,8 +270,7 @@ export async function restoreDocument(id) {
const ownerId = await getOwnerId();
if (!id) throw new Error('ID inválido.');
const { error } = await supabase
.from('documents')
const { error } = await tenantDb().from('documents')
.update({
deleted_at: null,
deleted_by: null,
@@ -313,8 +306,7 @@ export async function getDownloadUrl(bucketPath, expiresIn = 60, bucket = BUCKET
export async function getUsedTags() {
const ownerId = await getOwnerId();
const { data, error } = await supabase
.from('documents')
const { data, error } = await tenantDb().from('documents')
.select('tags')
.eq('owner_id', ownerId)
.is('deleted_at', null);
@@ -336,8 +328,7 @@ export async function getUsedTags() {
* @returns {{ ok: boolean, expected: string|null, actual: string|null }}
*/
export async function verifyDocumentIntegrity(docId) {
const { data: doc, error } = await supabase
.from('documents')
const { data: doc, error } = await tenantDb().from('documents')
.select('id, bucket_path, storage_bucket, content_sha256')
.eq('id', docId)
.single();
+9 -10
View File
@@ -16,6 +16,7 @@
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
function pickCount(row) {
return row?.patients_count ?? row?.patient_count ?? 0;
}
@@ -58,7 +59,7 @@ export async function listGroupsWithCounts() {
const ownerId = await getOwnerId();
// 1) View (preferencial) — agora já é a fonte correta
const { data: vData, error: vErr } = await supabase.from('v_patient_groups_with_counts').select('*').or(`owner_id.eq.${ownerId},is_system.eq.true`).order('nome', { ascending: true });
const { data: vData, error: vErr } = await tenantDb().from('v_patient_groups_with_counts').select('*').or(`owner_id.eq.${ownerId},is_system.eq.true`).order('nome', { ascending: true });
if (!vErr) {
return (vData || []).map((r) => ({
@@ -68,14 +69,14 @@ export async function listGroupsWithCounts() {
}
// 2) Fallback (caso view não exista / erro de schema)
const { data: groups, error: gErr } = await supabase.from('patient_groups').select('id,nome,cor,is_system,is_active,owner_id,created_at,updated_at').or(`owner_id.eq.${ownerId},is_system.eq.true`).order('nome', { ascending: true });
const { data: groups, error: gErr } = await tenantDb().from('patient_groups').select('id,nome,cor,is_system,is_active,owner_id,created_at,updated_at').or(`owner_id.eq.${ownerId},is_system.eq.true`).order('nome', { ascending: true });
if (gErr) throw gErr;
const ids = (groups || []).map((g) => g.id).filter(Boolean);
if (!ids.length) return [];
// conta pacientes por grupo na pivot
const { data: rel, error: rErr } = await supabase.from('patient_group_patient').select('patient_group_id').in('patient_group_id', ids);
const { data: rel, error: rErr } = await tenantDb().from('patient_group_patient').select('patient_group_id').in('patient_group_id', ids);
if (rErr) throw rErr;
const counts = new Map();
@@ -102,7 +103,7 @@ export async function createGroup(nome, cor = null) {
// proteção extra no front: busca por igualdade "normalizada"
// (mantém RLS como autoridade final, mas evita UX ruim)
const { data: existing, error: exErr } = await supabase.from('patient_groups').select('id,nome').eq('owner_id', ownerId).eq('is_system', false).limit(50);
const { data: existing, error: exErr } = await tenantDb().from('patient_groups').select('id,nome').eq('owner_id', ownerId).eq('is_system', false).limit(50);
if (!exErr && (existing || []).some((r) => normalizeNome(r.nome) === nNorm)) {
throw new Error('Já existe um grupo com esse nome.');
@@ -110,12 +111,11 @@ export async function createGroup(nome, cor = null) {
const payload = {
owner_id: ownerId,
tenant_id: tenantId,
nome: raw,
cor: cor || null
};
const { data, error } = await supabase.from('patient_groups').insert(payload).select('id,nome,cor,is_system,owner_id,is_active,created_at,updated_at').single();
const { data, error } = await tenantDb().from('patient_groups').insert(payload).select('id,nome,cor,is_system,owner_id,is_active,created_at,updated_at').single();
if (error) {
if (isUniqueViolation(error)) throw new Error('Já existe um grupo com esse nome.');
@@ -134,14 +134,13 @@ export async function updateGroup(id, nome, cor = null) {
// (opcional) valida duplicidade entre os grupos do owner (não-system)
const nNorm = normalizeNome(raw);
const { data: existing, error: exErr } = await supabase.from('patient_groups').select('id,nome').eq('owner_id', ownerId).eq('is_system', false).neq('id', id).limit(80);
const { data: existing, error: exErr } = await tenantDb().from('patient_groups').select('id,nome').eq('owner_id', ownerId).eq('is_system', false).neq('id', id).limit(80);
if (!exErr && (existing || []).some((r) => normalizeNome(r.nome) === nNorm)) {
throw new Error('Já existe um grupo com esse nome.');
}
const { data, error } = await supabase
.from('patient_groups')
const { data, error } = await tenantDb().from('patient_groups')
.update({ nome: raw, cor: cor || null, updated_at: new Date().toISOString() })
.eq('id', id)
.eq('owner_id', ownerId)
@@ -162,7 +161,7 @@ export async function deleteGroup(id) {
if (!id) throw new Error('ID inválido.');
const { error } = await supabase.from('patient_groups').delete().eq('id', id).eq('owner_id', ownerId).eq('is_system', false);
const { error } = await tenantDb().from('patient_groups').delete().eq('id', id).eq('owner_id', ownerId).eq('is_system', false);
if (error) throw error;
return true;
+8 -14
View File
@@ -16,6 +16,7 @@
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
// ── Helpers ──────────────────────────────────────────────────
async function getOwnerId() {
@@ -60,9 +61,8 @@ function isUniqueViolation(err) {
export async function listMedicosWithPatientCounts() {
const ownerId = await getOwnerId();
const { data: medicos, error } = await supabase
.from('medicos')
.select('id, nome, crm, especialidade, telefone_profissional, telefone_pessoal, email, clinica, cidade, estado, observacoes, ativo, owner_id, tenant_id, created_at, updated_at')
const { data: medicos, error } = await tenantDb().from('medicos')
.select('id, nome, crm, especialidade, telefone_profissional, telefone_pessoal, email, clinica, cidade, estado, observacoes, ativo, owner_id, created_at, updated_at')
.eq('owner_id', ownerId)
.eq('ativo', true)
.order('nome', { ascending: true });
@@ -70,8 +70,7 @@ export async function listMedicosWithPatientCounts() {
if (error) throw error;
// Busca pacientes do owner para contar encaminhamentos por médico
const { data: patients, error: pErr } = await supabase
.from('patients')
const { data: patients, error: pErr } = await tenantDb().from('patients')
.select('id, encaminhado_por')
.eq('owner_id', ownerId);
@@ -110,7 +109,6 @@ export async function createMedico(payload) {
const row = {
owner_id: ownerId,
tenant_id: tenantId,
nome,
crm: String(payload.crm || '').trim() || null,
especialidade: payload.especialidade || null,
@@ -124,8 +122,7 @@ export async function createMedico(payload) {
ativo: true
};
const { data, error } = await supabase
.from('medicos')
const { data, error } = await tenantDb().from('medicos')
.insert(row)
.select('*')
.single();
@@ -161,8 +158,7 @@ export async function updateMedico(id, payload) {
updated_at: new Date().toISOString()
};
const { data, error } = await supabase
.from('medicos')
const { data, error } = await tenantDb().from('medicos')
.update(row)
.eq('id', id)
.eq('owner_id', ownerId)
@@ -183,8 +179,7 @@ export async function deleteMedico(id) {
const ownerId = await getOwnerId();
if (!id) throw new Error('ID inválido.');
const { error } = await supabase
.from('medicos')
const { error } = await tenantDb().from('medicos')
.update({ ativo: false, updated_at: new Date().toISOString() })
.eq('id', id)
.eq('owner_id', ownerId);
@@ -203,8 +198,7 @@ export async function fetchPatientsByMedicoNome(medicoNome) {
const nomeLower = String(medicoNome || '').trim().toLowerCase();
if (!nomeLower) return [];
const { data, error } = await supabase
.from('patients')
const { data, error } = await tenantDb().from('patients')
.select('id, nome_completo, email_principal, telefone, avatar_url, encaminhado_por')
.eq('owner_id', ownerId)
.ilike('encaminhado_por', `%${nomeLower}%`);
+3 -2
View File
@@ -16,6 +16,7 @@
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
export async function getOwnerId() {
const { data, error } = await supabase.auth.getUser();
if (error) throw error;
@@ -25,7 +26,7 @@ export async function getOwnerId() {
}
export async function fetchSlotsRegras(ownerId) {
const { data, error } = await supabase.from('agenda_slots_regras').select('*').eq('owner_id', ownerId).order('dia_semana', { ascending: true });
const { data, error } = await tenantDb().from('agenda_slots_regras').select('*').eq('owner_id', ownerId).order('dia_semana', { ascending: true });
if (error) throw error;
return data || [];
}
@@ -42,7 +43,7 @@ export async function upsertSlotRegra(ownerId, payload) {
ativo: !!payload.ativo
};
const { data, error } = await supabase.from('agenda_slots_regras').upsert(row, { onConflict: 'owner_id,dia_semana' }).select('*').single();
const { data, error } = await tenantDb().from('agenda_slots_regras').upsert(row, { onConflict: 'owner_id,dia_semana' }).select('*').single();
if (error) throw error;
return data;
+4 -3
View File
@@ -16,8 +16,9 @@
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
export async function fetchSlotsBloqueados(ownerId, diaSemana) {
const { data, error } = await supabase.from('agenda_slots_bloqueados_semanais').select('*').eq('owner_id', ownerId).eq('dia_semana', diaSemana).eq('ativo', true).order('hora_inicio', { ascending: true });
const { data, error } = await tenantDb().from('agenda_slots_bloqueados_semanais').select('*').eq('owner_id', ownerId).eq('dia_semana', diaSemana).eq('ativo', true).order('hora_inicio', { ascending: true });
if (error) throw error;
return data || [];
@@ -25,7 +26,7 @@ export async function fetchSlotsBloqueados(ownerId, diaSemana) {
export async function setSlotBloqueado(ownerId, diaSemana, horaInicio, isBloqueado, motivo = null) {
if (isBloqueado) {
const { error } = await supabase.from('agenda_slots_bloqueados_semanais').upsert(
const { error } = await tenantDb().from('agenda_slots_bloqueados_semanais').upsert(
{
owner_id: ownerId,
dia_semana: diaSemana,
@@ -40,7 +41,7 @@ export async function setSlotBloqueado(ownerId, diaSemana, horaInicio, isBloquea
}
// "desbloquear": deletar (ou marcar ativo=false; aqui vou deletar por simplicidade)
const { error } = await supabase.from('agenda_slots_bloqueados_semanais').delete().eq('owner_id', ownerId).eq('dia_semana', diaSemana).eq('hora_inicio', horaInicio);
const { error } = await tenantDb().from('agenda_slots_bloqueados_semanais').delete().eq('owner_id', ownerId).eq('dia_semana', diaSemana).eq('hora_inicio', horaInicio);
if (error) throw error;
return true;
+8 -7
View File
@@ -16,6 +16,7 @@
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
async function getOwnerId() {
const { data, error } = await supabase.auth.getUser();
if (error) throw error;
@@ -26,11 +27,11 @@ async function getOwnerId() {
export async function listTagsWithCounts() {
const ownerId = await getOwnerId();
const v = await supabase.from('v_tag_patient_counts').select('*').eq('owner_id', ownerId).order('name', { ascending: true });
const v = await tenantDb().from('v_tag_patient_counts').select('*').eq('owner_id', ownerId).order('name', { ascending: true });
if (!v.error) return v.data || [];
const t = 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 t = 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 (t.error) throw t.error;
return (t.data || []).map((r) => ({ ...r, patient_count: 0 }));
@@ -38,13 +39,13 @@ export async function listTagsWithCounts() {
export async function createTag({ name, color = null }) {
const ownerId = await getOwnerId();
const { error } = await supabase.from('patient_tags').insert({ owner_id: ownerId, name, color });
const { error } = await tenantDb().from('patient_tags').insert({ owner_id: ownerId, name, color });
if (error) throw error;
}
export async function updateTag({ id, name, color = null }) {
const ownerId = await getOwnerId();
const { error } = await supabase.from('patient_tags').update({ name, color, updated_at: new Date().toISOString() }).eq('id', id).eq('owner_id', ownerId);
const { error } = await tenantDb().from('patient_tags').update({ name, color, updated_at: new Date().toISOString() }).eq('id', id).eq('owner_id', ownerId);
if (error) throw error;
}
@@ -52,16 +53,16 @@ export async function deleteTagsByIds(ids = []) {
const ownerId = await getOwnerId();
if (!ids.length) 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;
}
export async function fetchPatientsByTagId(tagId) {
const ownerId = await getOwnerId();
const { data, error } = await supabase.from('patient_patient_tag').select('patient_id, patients:patients(id, name, email, phone)').eq('owner_id', ownerId).eq('tag_id', tagId);
const { data, error } = await tenantDb().from('patient_patient_tag').select('patient_id, patients:patients(id, name, email, phone)').eq('owner_id', ownerId).eq('tag_id', tagId);
if (error) throw error;
return (data || []).map((r) => r.patients).filter(Boolean);
+8 -12
View File
@@ -12,6 +12,7 @@
import { supabase } from '@/lib/supabase/client'
import { tenantDb } from '@/lib/supabase/tenantClient';
const PROVISION_FN = 'twilio-whatsapp-provision'
// ── Helpers ────────────────────────────────────────────────────────────────
@@ -142,10 +143,9 @@ export async function testSend(channelId, toNumber, message = 'Mensagem de teste
* @param {string} tenantId
*/
export async function getChannel(tenantId) {
const { data, error } = await supabase
.from('notification_channels')
const { data, error } = await tenantDb().from('notification_channels')
.select('*')
.eq('tenant_id', tenantId)
.eq('channel', 'whatsapp')
.eq('provider', 'twilio')
.is('deleted_at', null)
@@ -172,8 +172,7 @@ export async function getAllChannels() {
* @param {object} pricing - { cost_per_message_usd, price_per_message_brl }
*/
export async function updatePricing(channelId, pricing) {
const { data, error } = await supabase
.from('notification_channels')
const { data, error } = await tenantDb().from('notification_channels')
.update({
cost_per_message_usd: pricing.cost_per_message_usd,
price_per_message_brl: pricing.price_per_message_brl,
@@ -187,15 +186,13 @@ export async function updatePricing(channelId, pricing) {
/**
* Busca dados de uso mensal (painel admin).
* @param {object} filters - { tenant_id?, channel_id?, period_start?, months? }
* @param {object} filters - { channel_id?, period_start?, months? }
*/
export async function getUsageReport(filters = {}) {
let query = supabase
.from('twilio_subaccount_usage')
let query = tenantDb().from('twilio_subaccount_usage')
.select('*')
.order('period_start', { ascending: false })
if (filters.tenant_id) query = query.eq('tenant_id', filters.tenant_id)
if (filters.channel_id) query = query.eq('channel_id', filters.channel_id)
if (filters.period_start) query = query.gte('period_start', filters.period_start)
@@ -213,10 +210,9 @@ export async function getUsageReport(filters = {}) {
* @param {number} [limit=50]
*/
export async function getMessageLogs(tenantId, limit = 50) {
const { data, error } = await supabase
.from('notification_logs')
const { data, error } = await tenantDb().from('notification_logs')
.select('id, recipient_address, status, sent_at, delivered_at, read_at, failed_at, failure_reason, estimated_cost_brl, created_at')
.eq('tenant_id', tenantId)
.eq('channel', 'whatsapp')
.eq('provider', 'twilio')
.order('created_at', { ascending: false })