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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}%`);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 })
|
||||
|
||||
Reference in New Issue
Block a user