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:
@@ -0,0 +1,33 @@
|
||||
-- =============================================================================
|
||||
-- F3 — my_tenants() passa a devolver slug (e nome) do tenant
|
||||
--
|
||||
-- O frontend resolve o schema físico do tenant ativo no cliente:
|
||||
-- tenantStore guarda memberships de my_tenants(); slug -> 'tenant_<slug>'.
|
||||
-- Campo extra é inofensivo pro frontend atual (main) que ignora colunas novas.
|
||||
-- (mudança de RETURNS TABLE exige DROP + CREATE)
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP FUNCTION IF EXISTS public.my_tenants();
|
||||
|
||||
CREATE FUNCTION public.my_tenants()
|
||||
RETURNS TABLE(tenant_id uuid, role text, status text, kind text, slug text, tenant_name text)
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
AS $function$
|
||||
select
|
||||
tm.tenant_id,
|
||||
tm.role,
|
||||
tm.status,
|
||||
t.kind,
|
||||
t.slug,
|
||||
t.name
|
||||
from public.tenant_members tm
|
||||
join public.tenants t on t.id = tm.tenant_id
|
||||
where tm.user_id = auth.uid();
|
||||
$function$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.my_tenants() TO authenticated;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,184 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
F3 schema-per-tenant: codemod do frontend.
|
||||
|
||||
1. supabase.from('<tabela/view tenant>') -> tenantDb().from('...') (84 tabelas + 6 views)
|
||||
2. injeta import { tenantDb } from '@/lib/supabase/tenantClient'
|
||||
3. remove .eq('tenant_id', <expr>) APENAS dentro de cadeias tenantDb().from(...)
|
||||
4. relatorio de sobras pra passada manual:
|
||||
- tenant_id em payloads dentro de cadeias tenantDb (insert/upsert/update)
|
||||
- onConflict com tenant_id em cadeias tenantDb
|
||||
- supabase.from(<nao-literal>) pra auditoria
|
||||
|
||||
Uso: python scripts/codemod-tenant-db.py [--apply] (default: dry-run)
|
||||
"""
|
||||
import io, os, re, sys
|
||||
|
||||
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'src')
|
||||
APPLY = '--apply' in sys.argv
|
||||
|
||||
TENANT_RELS = [
|
||||
# 84 tabelas (docs/F0_categorizacao.md §1.1 + §1.2)
|
||||
'agenda_bloqueios','agenda_configuracoes','agenda_eventos','agenda_online_slots',
|
||||
'agenda_regras_semanais','agenda_slots_bloqueados_semanais','agenda_slots_regras',
|
||||
'agendador_configuracoes','agendador_solicitacoes','asaas_customers','asaas_payments',
|
||||
'billing_contracts','clinical_note_templates','clinical_note_versions','clinical_notes',
|
||||
'commitment_services','commitment_time_logs','company_profiles','contact_email_types',
|
||||
'contact_emails','contact_phones','contact_types','conversation_assignments',
|
||||
'conversation_autoreply_log','conversation_autoreply_settings','conversation_bot_sessions',
|
||||
'conversation_bots','conversation_messages','conversation_notes','conversation_optout_keywords',
|
||||
'conversation_optouts','conversation_sla_breaches','conversation_sla_rules','conversation_tags',
|
||||
'conversation_thread_tags','determined_commitment_fields','determined_commitments',
|
||||
'document_access_logs','document_generated','document_share_links','document_signatures',
|
||||
'document_templates','documents','email_layout_config','email_templates_tenant','feriados',
|
||||
'financial_categories','financial_exceptions','financial_records','insurance_plan_services',
|
||||
'insurance_plans','medicos','notification_channels','notification_logs','notification_preferences',
|
||||
'notification_queue','notification_schedules','notification_templates','notifications',
|
||||
'patient_contacts','patient_discounts','patient_group_patient','patient_groups',
|
||||
'patient_intake_requests','patient_invite_attempts','patient_invites','patient_patient_tag',
|
||||
'patient_status_history','patient_support_contacts','patient_tags','patient_timeline','patients',
|
||||
'payment_settings','professional_pricing','recurrence_exceptions','recurrence_rule_services',
|
||||
'recurrence_rules','services','session_reminder_logs','session_reminder_settings',
|
||||
'therapist_payout_records','therapist_payouts','twilio_subaccount_usage','whatsapp_connection_incidents',
|
||||
# 6 views clonadas por schema
|
||||
'conversation_threads','audit_log_unified','v_cashflow_projection','v_commitment_totals',
|
||||
'v_patient_groups_with_counts','v_tag_patient_counts',
|
||||
]
|
||||
SKIP_FILES = {'tenantClient.js', 'useTenantDb.js', 'client.js'}
|
||||
|
||||
names = '|'.join(sorted(TENANT_RELS, key=len, reverse=True))
|
||||
FROM_RE = re.compile(r"supabase\s*\.\s*from\(\s*(['\"])(" + names + r")\1\s*\)")
|
||||
IMPORT_LINE = "import { tenantDb } from '@/lib/supabase/tenantClient';"
|
||||
|
||||
def skip_string(s, p):
|
||||
q = s[p]; p += 1
|
||||
while p < len(s):
|
||||
if s[p] == '\\': p += 2; continue
|
||||
if s[p] == q: return p + 1
|
||||
p += 1
|
||||
return p
|
||||
|
||||
def balanced_end(s, open_paren):
|
||||
"""indice logo apos o ')' que fecha o '(' em open_paren"""
|
||||
depth = 0; p = open_paren
|
||||
while p < len(s):
|
||||
c = s[p]
|
||||
if c in '\'"`':
|
||||
p = skip_string(s, p); continue
|
||||
if c == '(': depth += 1
|
||||
elif c == ')':
|
||||
depth -= 1
|
||||
if depth == 0: return p + 1
|
||||
p += 1
|
||||
return p
|
||||
|
||||
def chain_end(s, start):
|
||||
"""fim da cadeia de metodos iniciada logo apos from(...)"""
|
||||
i = start
|
||||
while True:
|
||||
j = i
|
||||
while j < len(s) and s[j] in ' \t\r\n': j += 1
|
||||
if j < len(s) and s[j] == '.':
|
||||
m = re.match(r'[A-Za-z_$][\w$]*', s[j+1:])
|
||||
if not m: return i
|
||||
k = j + 1 + m.end()
|
||||
while k < len(s) and s[k] in ' \t\r\n': k += 1
|
||||
if k < len(s) and s[k] == '(':
|
||||
i = balanced_end(s, k)
|
||||
elif k < len(s) and s[k] == ';':
|
||||
return k
|
||||
else:
|
||||
# acesso a propriedade sem chamada (ex.: .then? sempre tem parens) — para
|
||||
return i
|
||||
else:
|
||||
return i
|
||||
|
||||
EQ_RE = re.compile(r"\.\s*eq\(\s*(['\"])tenant_id\1\s*,")
|
||||
|
||||
report = {'files': 0, 'from': 0, 'eq': 0, 'payload': [], 'onconflict': [], 'dynamic_from': []}
|
||||
|
||||
for dirpath, dirnames, filenames in os.walk(ROOT):
|
||||
dirnames[:] = [d for d in dirnames if d not in ('node_modules', '__tests__')]
|
||||
for fn in filenames:
|
||||
if not fn.endswith(('.js', '.vue', '.ts')) or fn in SKIP_FILES:
|
||||
continue
|
||||
path = os.path.join(dirpath, fn)
|
||||
text = io.open(path, encoding='utf-8').read()
|
||||
orig = text
|
||||
|
||||
# 1. from() replacement
|
||||
text, n_from = FROM_RE.subn(lambda m: "tenantDb().from(%s%s%s)" % (m.group(1), m.group(2), m.group(1)), text)
|
||||
report['from'] += n_from
|
||||
|
||||
# 3. eq removal dentro de cadeias tenantDb
|
||||
n_eq = 0
|
||||
while True:
|
||||
removed = False
|
||||
for m in re.finditer(r"tenantDb\(\)\s*\.\s*from\(", text):
|
||||
fstart = m.end() - 1
|
||||
fend = balanced_end(text, fstart)
|
||||
cend = chain_end(text, fend)
|
||||
span = text[fend:cend]
|
||||
em = EQ_RE.search(span)
|
||||
if em:
|
||||
eq_open = fend + span.index('(', em.start() + 1)
|
||||
# acha o '(' do .eq
|
||||
eq_paren = fend + em.end() - len(em.group(0)) + span[em.start():].index('(')
|
||||
eq_paren = fend + em.start() + text[fend + em.start():].index('(')
|
||||
eq_close = balanced_end(text, eq_paren)
|
||||
eq_dot = fend + em.start()
|
||||
text = text[:eq_dot] + text[eq_close:]
|
||||
n_eq += 1
|
||||
removed = True
|
||||
break
|
||||
if not removed:
|
||||
break
|
||||
report['eq'] += n_eq
|
||||
|
||||
# 2. import injection
|
||||
if 'tenantDb(' in text and "from '@/lib/supabase/tenantClient'" not in text:
|
||||
anchor = re.search(r"^import .*from '@/lib/supabase/client';?\s*$", text, re.M)
|
||||
if anchor:
|
||||
text = text[:anchor.end()] + '\n' + IMPORT_LINE + text[anchor.end():]
|
||||
else:
|
||||
first_import = re.search(r"^import .*$", text, re.M)
|
||||
if first_import:
|
||||
text = text[:first_import.end()] + '\n' + IMPORT_LINE + text[first_import.end():]
|
||||
else:
|
||||
report['payload'].append((path, 0, 'SEM PONTO DE IMPORT — inserir manualmente'))
|
||||
|
||||
# 4. relatorios de sobras dentro de cadeias tenantDb
|
||||
for m in re.finditer(r"tenantDb\(\)\s*\.\s*from\(", text):
|
||||
fstart = m.end() - 1
|
||||
fend = balanced_end(text, fstart)
|
||||
cend = chain_end(text, fend)
|
||||
span = text[fend:cend]
|
||||
line = text[:m.start()].count('\n') + 1
|
||||
if re.search(r"\btenant_id\b", span):
|
||||
if 'onConflict' in span and 'tenant_id' in span:
|
||||
report['onconflict'].append((path, line))
|
||||
else:
|
||||
report['payload'].append((path, line, 'tenant_id na cadeia'))
|
||||
|
||||
# from() dinamico com supabase (auditoria)
|
||||
for m in re.finditer(r"supabase\s*\.\s*from\(\s*[^'\")]", text):
|
||||
line = text[:m.start()].count('\n') + 1
|
||||
report['dynamic_from'].append((path, line))
|
||||
|
||||
if text != orig:
|
||||
report['files'] += 1
|
||||
if APPLY:
|
||||
io.open(path, 'w', encoding='utf-8', newline='').write(text)
|
||||
|
||||
mode = 'APPLY' if APPLY else 'DRY-RUN'
|
||||
print('[%s] arquivos alterados: %d | from substituidos: %d | eq removidos: %d' %
|
||||
(mode, report['files'], report['from'], report['eq']))
|
||||
print('\n-- tenant_id sobrando em cadeias tenantDb (payloads, passada manual): %d' % len(report['payload']))
|
||||
for p, l, why in report['payload'][:80]:
|
||||
print(' %s:%s (%s)' % (os.path.relpath(p, ROOT), l, why))
|
||||
print('\n-- onConflict com tenant_id em cadeias tenantDb: %d' % len(report['onconflict']))
|
||||
for p, l in report['onconflict']:
|
||||
print(' %s:%s' % (os.path.relpath(p, ROOT), l))
|
||||
print('\n-- supabase.from(nao-literal) pra auditoria: %d' % len(report['dynamic_from']))
|
||||
for p, l in report['dynamic_from'][:40]:
|
||||
print(' %s:%s' % (os.path.relpath(p, ROOT), l))
|
||||
+2
-2
@@ -18,6 +18,7 @@
|
||||
import { onMounted, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { fetchDocsForPath } from '@/composables/useAjuda';
|
||||
|
||||
@@ -51,8 +52,7 @@ async function checkSetupWizard() {
|
||||
// Se já confirmamos que este uid passou o setup, não verifica de novo
|
||||
if (_setupClearedUid === uid && _setupClearedIsClinic === isClinic) return;
|
||||
|
||||
const { data } = await supabase
|
||||
.from('agenda_configuracoes')
|
||||
const { data } = await tenantDb().from('agenda_configuracoes')
|
||||
.select('setup_concluido, setup_clinica_concluido, atendimento_mode')
|
||||
.eq('owner_id', uid)
|
||||
.maybeSingle();
|
||||
|
||||
@@ -35,6 +35,7 @@ import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useAgendaFinanceiro } from '@/composables/useAgendaFinanceiro';
|
||||
import { emitirReciboParaSessao } from '@/services/DocumentGenerate.service';
|
||||
|
||||
@@ -126,8 +127,7 @@ async function fetchRecord() {
|
||||
// após cancelar (caso comum: cancelou sem querer ou quer recobrar).
|
||||
// Sem esse filtro, o scenario ficava em 'com-cobranca' mostrando
|
||||
// o cancelado, e o botão "Gerar cobrança" sumia.
|
||||
const { data, error } = await supabase
|
||||
.from('financial_records')
|
||||
const { data, error } = await tenantDb().from('financial_records')
|
||||
.select('id, amount, discount_amount, final_amount, status, due_date, paid_at, payment_method')
|
||||
.eq('agenda_evento_id', props.evento.id)
|
||||
.neq('status', 'cancelled')
|
||||
@@ -213,7 +213,7 @@ function requestCancel() {
|
||||
acceptSeverity: 'danger',
|
||||
accept: async () => {
|
||||
try {
|
||||
const { error } = await supabase.from('financial_records').update({ status: 'cancelled', updated_at: new Date().toISOString() }).eq('id', record.value.id);
|
||||
const { error } = await tenantDb().from('financial_records').update({ status: 'cancelled', updated_at: new Date().toISOString() }).eq('id', record.value.id);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import { gerarSlotsDoDia } from '@/utils/slotsGenerator';
|
||||
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
const toast = useToast();
|
||||
|
||||
const props = defineProps({
|
||||
@@ -51,7 +52,7 @@ const regrasSemanais = ref([]); // agenda_regras_semanais
|
||||
const bloqueadosByDia = ref({}); // {dia: Set('09:00'...)}
|
||||
|
||||
async function loadRegrasSemanais() {
|
||||
const { data, error } = await supabase.from('agenda_regras_semanais').select('*').eq('owner_id', props.ownerId).order('dia_semana', { ascending: true }).order('hora_inicio', { ascending: true });
|
||||
const { data, error } = await tenantDb().from('agenda_regras_semanais').select('*').eq('owner_id', props.ownerId).order('dia_semana', { ascending: true }).order('hora_inicio', { ascending: true });
|
||||
|
||||
if (error) throw error;
|
||||
regrasSemanais.value = data || [];
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useConversationNotes } from '@/composables/useConversationNotes';
|
||||
import { useConversationTags } from '@/composables/useConversationTags';
|
||||
import { useConversationAssignment } from '@/composables/useConversationAssignment';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import ComponentCadastroRapido from '@/components/ComponentCadastroRapido.vue';
|
||||
|
||||
const toast = useToast();
|
||||
@@ -69,10 +70,8 @@ async function loadPatients() {
|
||||
linkPatientLoading.value = true;
|
||||
try {
|
||||
// Carrega todos os pacientes do tenant (até 500) — filter é client-side
|
||||
const { data, error } = await supabase
|
||||
.from('patients')
|
||||
const { data, error } = await tenantDb().from('patients')
|
||||
.select('id, nome_completo, telefone, email_principal, status')
|
||||
.eq('tenant_id', tenantId)
|
||||
.order('nome_completo', { ascending: true })
|
||||
.limit(500);
|
||||
if (error) throw error;
|
||||
@@ -99,8 +98,7 @@ async function confirmLinkPatient() {
|
||||
const tenantId = store.thread.tenant_id;
|
||||
|
||||
// 1) Vincula conversation_messages
|
||||
const { error } = await supabase
|
||||
.from('conversation_messages')
|
||||
const { error } = await tenantDb().from('conversation_messages')
|
||||
.update({ patient_id: patient.id })
|
||||
.or(`from_number.eq.${phone},to_number.eq.${phone}`)
|
||||
.is('patient_id', null);
|
||||
@@ -137,8 +135,7 @@ async function upsertWhatsappForExisting(tenantId, patientId, threadPhone) {
|
||||
const phoneDigits = String(threadPhone).replace(/\D/g, '');
|
||||
|
||||
// Busca se já tem esse número cadastrado
|
||||
const { data: existing } = await supabase
|
||||
.from('contact_phones')
|
||||
const { data: existing } = await tenantDb().from('contact_phones')
|
||||
.select('id, contact_type_id, whatsapp_linked_at')
|
||||
.eq('entity_type', 'patient')
|
||||
.eq('entity_id', patientId)
|
||||
@@ -149,8 +146,7 @@ async function upsertWhatsappForExisting(tenantId, patientId, threadPhone) {
|
||||
if (existing) {
|
||||
// Atualiza vinculado_at se ainda não tinha
|
||||
if (!existing.whatsapp_linked_at) {
|
||||
await supabase
|
||||
.from('contact_phones')
|
||||
await tenantDb().from('contact_phones')
|
||||
.update({ whatsapp_linked_at: new Date().toISOString() })
|
||||
.eq('id', existing.id);
|
||||
}
|
||||
@@ -158,17 +154,15 @@ async function upsertWhatsappForExisting(tenantId, patientId, threadPhone) {
|
||||
}
|
||||
|
||||
// Não tem — cria novo com type='whatsapp'
|
||||
const { data: types } = await supabase
|
||||
.from('contact_types')
|
||||
const { data: types } = await tenantDb().from('contact_types')
|
||||
.select('id, slug')
|
||||
.is('tenant_id', null)
|
||||
.eq('is_system', true)
|
||||
.eq('slug', 'whatsapp')
|
||||
.maybeSingle();
|
||||
const whatsappTypeId = types?.id;
|
||||
if (!whatsappTypeId) return;
|
||||
|
||||
await supabase.from('contact_phones').insert({
|
||||
tenant_id: tenantId,
|
||||
await tenantDb().from('contact_phones').insert({
|
||||
entity_type: 'patient',
|
||||
entity_id: patientId,
|
||||
contact_type_id: whatsappTypeId,
|
||||
@@ -201,8 +195,7 @@ async function onPatientCreated(row) {
|
||||
}
|
||||
try {
|
||||
// 1) Vincula TODAS as mensagens do thread (anon) a esse patient_id
|
||||
const { error: msgErr } = await supabase
|
||||
.from('conversation_messages')
|
||||
const { error: msgErr } = await tenantDb().from('conversation_messages')
|
||||
.update({ patient_id: newPatientId })
|
||||
.or(`from_number.eq.${phone},to_number.eq.${phone}`)
|
||||
.is('patient_id', null);
|
||||
@@ -238,10 +231,9 @@ async function insertWhatsappContactPhone(tenantId, patientId, threadPhone, form
|
||||
if (!tenantId || !patientId || !threadPhone) return;
|
||||
try {
|
||||
// Busca tipos system
|
||||
const { data: types } = await supabase
|
||||
.from('contact_types')
|
||||
const { data: types } = await tenantDb().from('contact_types')
|
||||
.select('id, slug')
|
||||
.is('tenant_id', null);
|
||||
.eq('is_system', true);
|
||||
const celularType = types?.find((t) => t.slug === 'celular');
|
||||
const whatsappType = types?.find((t) => t.slug === 'whatsapp');
|
||||
|
||||
@@ -257,7 +249,6 @@ async function insertWhatsappContactPhone(tenantId, patientId, threadPhone, form
|
||||
// Celular primary (from form — o que o user digitou no cadastro rápido)
|
||||
if (celularType && formDigits && formDigits.length >= 8) {
|
||||
rows.push({
|
||||
tenant_id: tenantId,
|
||||
entity_type: 'patient',
|
||||
entity_id: patientId,
|
||||
contact_type_id: celularType.id,
|
||||
@@ -270,7 +261,6 @@ async function insertWhatsappContactPhone(tenantId, patientId, threadPhone, form
|
||||
// WhatsApp linked (from thread) — só se diferente do celular
|
||||
if (whatsappType && phoneDigits && formNoDdi !== threadNoDdi) {
|
||||
rows.push({
|
||||
tenant_id: tenantId,
|
||||
entity_type: 'patient',
|
||||
entity_id: patientId,
|
||||
contact_type_id: whatsappType.id,
|
||||
@@ -285,7 +275,7 @@ async function insertWhatsappContactPhone(tenantId, patientId, threadPhone, form
|
||||
}
|
||||
|
||||
if (rows.length > 0) {
|
||||
await supabase.from('contact_phones').insert(rows);
|
||||
await tenantDb().from('contact_phones').insert(rows);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[ConversationDrawer] insert whatsapp contact_phones:', e?.message);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, onUnmounted } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useConversationDrawerStore } from '@/stores/conversationDrawerStore';
|
||||
import { logEvent, logError } from '@/support/supportLogger';
|
||||
@@ -90,7 +91,7 @@ async function showNotif(msg) {
|
||||
|
||||
let name = msg.from_number || 'Desconhecido';
|
||||
if (msg.patient_id) {
|
||||
const { data } = await supabase.from('patients').select('nome_completo').eq('id', msg.patient_id).maybeSingle();
|
||||
const { data } = await tenantDb().from('patients').select('nome_completo').eq('id', msg.patient_id).maybeSingle();
|
||||
if (data?.nome_completo) name = data.nome_completo;
|
||||
}
|
||||
|
||||
@@ -142,7 +143,8 @@ function channelIcon(ch) {
|
||||
|
||||
function subscribe() {
|
||||
const tenantId = tenantStore.activeTenantId;
|
||||
if (!tenantId) {
|
||||
const tenantSchema = tenantStore.activeTenantSchema;
|
||||
if (!tenantId || !tenantSchema) {
|
||||
logEvent(LOG_SRC, 'subscribe skipped — sem tenant');
|
||||
return;
|
||||
}
|
||||
@@ -154,9 +156,8 @@ function subscribe() {
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'INSERT',
|
||||
schema: 'public',
|
||||
table: 'conversation_messages',
|
||||
filter: `tenant_id=eq.${tenantId}`
|
||||
schema: tenantSchema,
|
||||
table: 'conversation_messages'
|
||||
},
|
||||
(payload) => {
|
||||
const m = payload.new;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
-->
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
@@ -121,10 +122,9 @@ async function openConversationByThreadKey(threadKey) {
|
||||
try {
|
||||
const tenantId = tenantStore.activeTenantId;
|
||||
const { supabase } = await import('@/lib/supabase/client');
|
||||
const { data } = await supabase
|
||||
.from('conversation_threads')
|
||||
const { data } = await tenantDb().from('conversation_threads')
|
||||
.select('*')
|
||||
.eq('tenant_id', tenantId)
|
||||
|
||||
.eq('thread_key', threadKey)
|
||||
.maybeSingle();
|
||||
if (!data) return false;
|
||||
|
||||
@@ -32,6 +32,7 @@ import Popover from 'primevue/popover';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
const emit = defineEmits(['quick-create', 'go-complete', 'show', 'hide']);
|
||||
const toast = useToast();
|
||||
|
||||
@@ -52,7 +53,7 @@ async function loadToken() {
|
||||
const { data: authData } = await supabase.auth.getUser();
|
||||
const uid = authData?.user?.id;
|
||||
if (!uid) return;
|
||||
const { data } = await supabase.from('patient_invites').select('token').eq('owner_id', uid).eq('active', true).order('created_at', { ascending: false }).limit(1);
|
||||
const { data } = await tenantDb().from('patient_invites').select('token').eq('owner_id', uid).eq('active', true).order('created_at', { ascending: false }).limit(1);
|
||||
if (data?.[0]?.token) {
|
||||
inviteToken.value = data[0].token;
|
||||
tokenLoaded = true;
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
// ─── Cache de exceções financeiras (vive enquanto o módulo estiver carregado) ─
|
||||
@@ -84,10 +85,9 @@ export function useAgendaFinanceiro() {
|
||||
|
||||
const uid = await getUid();
|
||||
|
||||
const { data, error: err } = await supabase
|
||||
.from('financial_exceptions')
|
||||
const { data, error: err } = await tenantDb().from('financial_exceptions')
|
||||
.select('*')
|
||||
.eq('tenant_id', tenantId)
|
||||
|
||||
.eq('exception_type', exceptionType)
|
||||
.or(`owner_id.eq.${uid},owner_id.is.null`)
|
||||
.order('owner_id', { ascending: false, nullsLast: true }) // owner próprio tem prioridade
|
||||
@@ -188,10 +188,10 @@ export function useAgendaFinanceiro() {
|
||||
if (!rule || rule.charge_mode === 'none') {
|
||||
// Cancelar cobrança existente, se houver
|
||||
if (evento.billed) {
|
||||
const { data: existingRec } = await supabase.from('financial_records').select('id, status').eq('agenda_evento_id', evento.id).in('status', ['pending', 'overdue']).maybeSingle();
|
||||
const { data: existingRec } = await tenantDb().from('financial_records').select('id, status').eq('agenda_evento_id', evento.id).in('status', ['pending', 'overdue']).maybeSingle();
|
||||
|
||||
if (existingRec) {
|
||||
await supabase.from('financial_records').update({ status: 'cancelled', updated_at: new Date().toISOString() }).eq('id', existingRec.id);
|
||||
await tenantDb().from('financial_records').update({ status: 'cancelled', updated_at: new Date().toISOString() }).eq('id', existingRec.id);
|
||||
}
|
||||
}
|
||||
return { ok: true };
|
||||
@@ -202,11 +202,10 @@ export function useAgendaFinanceiro() {
|
||||
|
||||
if (evento.billed) {
|
||||
// Atualiza o valor da cobrança existente
|
||||
const { data: existingRec } = await supabase.from('financial_records').select('id').eq('agenda_evento_id', evento.id).in('status', ['pending', 'overdue']).maybeSingle();
|
||||
const { data: existingRec } = await tenantDb().from('financial_records').select('id').eq('agenda_evento_id', evento.id).in('status', ['pending', 'overdue']).maybeSingle();
|
||||
|
||||
if (existingRec) {
|
||||
await supabase
|
||||
.from('financial_records')
|
||||
await tenantDb().from('financial_records')
|
||||
.update({
|
||||
amount: chargeAmount,
|
||||
final_amount: chargeAmount,
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import { ref, computed } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
// ─── helpers ────────────────────────────────────────────────────────────────
|
||||
@@ -102,10 +103,8 @@ export function useAuditoria() {
|
||||
try {
|
||||
const { from, to } = dateRange.value;
|
||||
|
||||
let query = supabase
|
||||
.from('audit_log_unified')
|
||||
.select('uid, tenant_id, user_id, entity_type, entity_id, action, description, occurred_at, source, details')
|
||||
.eq('tenant_id', tenantId)
|
||||
let query = tenantDb().from('audit_log_unified')
|
||||
.select('uid, user_id, entity_type, entity_id, action, description, occurred_at, source, details')
|
||||
.gte('occurred_at', from.toISOString())
|
||||
.lte('occurred_at', to.toISOString())
|
||||
.order('occurred_at', { ascending: false })
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
@@ -37,10 +38,8 @@ export function useAutoReplySettings() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const { data, error: err } = await supabase
|
||||
.from('conversation_autoreply_settings')
|
||||
const { data, error: err } = await tenantDb().from('conversation_autoreply_settings')
|
||||
.select('enabled, message, cooldown_minutes, schedule_mode, business_hours, custom_window')
|
||||
.eq('tenant_id', tenantId)
|
||||
.maybeSingle();
|
||||
if (err) throw err;
|
||||
if (data) {
|
||||
@@ -80,9 +79,8 @@ export function useAutoReplySettings() {
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
const { error: err } = await supabase
|
||||
.from('conversation_autoreply_settings')
|
||||
.upsert({ tenant_id: tenantId, ...payload }, { onConflict: 'tenant_id' });
|
||||
const { error: err } = await tenantDb().from('conversation_autoreply_settings')
|
||||
.upsert({ ...payload }, { onConflict: 'singleton' });
|
||||
if (err) throw err;
|
||||
settings.value = payload;
|
||||
return { ok: true };
|
||||
@@ -98,10 +96,8 @@ export function useAutoReplySettings() {
|
||||
const tenantId = tenantStore.activeTenantId;
|
||||
if (!tenantId) return [];
|
||||
try {
|
||||
const { data } = await supabase
|
||||
.from('agenda_regras_semanais')
|
||||
const { data } = await tenantDb().from('agenda_regras_semanais')
|
||||
.select('dia_semana, hora_inicio, hora_fim, ativo')
|
||||
.eq('tenant_id', tenantId)
|
||||
.eq('ativo', true)
|
||||
.order('dia_semana');
|
||||
return (data || []).map((r) => ({
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import { ref, computed } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
function startOfMonth(d = new Date()) {
|
||||
@@ -82,41 +83,36 @@ export function useClinicKPIs() {
|
||||
try {
|
||||
const [finRes, pendRes, patRes, eventRes, finSeriesRes] = await Promise.all([
|
||||
// 1) financial_records PAGO no mês (para MRR)
|
||||
supabase
|
||||
.from('financial_records')
|
||||
tenantDb().from('financial_records')
|
||||
.select('final_amount, patient_id')
|
||||
.eq('tenant_id', tenantId)
|
||||
|
||||
.eq('status', 'paid')
|
||||
.gte('paid_at', monthStart)
|
||||
.lte('paid_at', monthEnd),
|
||||
|
||||
// 2) financial_records pending/overdue (qualquer data)
|
||||
supabase
|
||||
.from('financial_records')
|
||||
tenantDb().from('financial_records')
|
||||
.select('status, final_amount')
|
||||
.eq('tenant_id', tenantId)
|
||||
|
||||
.in('status', ['pending', 'overdue']),
|
||||
|
||||
// 3) patients por status
|
||||
supabase
|
||||
.from('patients')
|
||||
tenantDb().from('patients')
|
||||
.select('status')
|
||||
.eq('tenant_id', tenantId),
|
||||
,
|
||||
|
||||
// 4) eventos de agenda no mês (para realizado/cancelado/faltou)
|
||||
supabase
|
||||
.from('agenda_eventos')
|
||||
tenantDb().from('agenda_eventos')
|
||||
.select('status, tipo')
|
||||
.eq('tenant_id', tenantId)
|
||||
|
||||
.gte('inicio_em', monthStart)
|
||||
.lte('inicio_em', monthEnd)
|
||||
.neq('tipo', 'bloqueio'),
|
||||
|
||||
// 5) financial_records pagos últimos 6 meses (série + top pacientes)
|
||||
supabase
|
||||
.from('financial_records')
|
||||
tenantDb().from('financial_records')
|
||||
.select('final_amount, paid_at, patient_id, patients!patient_id(nome_completo)')
|
||||
.eq('tenant_id', tenantId)
|
||||
|
||||
.eq('status', 'paid')
|
||||
.gte('paid_at', sixMonthsAgo)
|
||||
.lte('paid_at', monthEnd)
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
const EMAIL_RE = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i;
|
||||
@@ -37,9 +38,8 @@ export function useContactEmails() {
|
||||
|
||||
async function loadTypes() {
|
||||
try {
|
||||
const { data } = await supabase
|
||||
.from('contact_email_types')
|
||||
.select('id, tenant_id, name, slug, icon, is_system, position')
|
||||
const { data } = await tenantDb().from('contact_email_types')
|
||||
.select('id, name, slug, icon, is_system, position')
|
||||
.order('position', { ascending: true })
|
||||
.order('name', { ascending: true });
|
||||
types.value = data || [];
|
||||
@@ -56,8 +56,7 @@ export function useContactEmails() {
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('contact_emails')
|
||||
const { data, error } = await tenantDb().from('contact_emails')
|
||||
.select('id, contact_email_type_id, email, is_primary, notes, position, created_at')
|
||||
.eq('entity_type', entityType)
|
||||
.eq('entity_id', entityId)
|
||||
@@ -74,8 +73,7 @@ export function useContactEmails() {
|
||||
}
|
||||
|
||||
async function unsetOtherPrimaries(entityType, entityId, exceptId = null) {
|
||||
const q = supabase
|
||||
.from('contact_emails')
|
||||
const q = tenantDb().from('contact_emails')
|
||||
.update({ is_primary: false })
|
||||
.eq('entity_type', entityType)
|
||||
.eq('entity_id', entityId)
|
||||
@@ -122,10 +120,8 @@ export function useContactEmails() {
|
||||
}
|
||||
|
||||
const maxPos = emails.value.reduce((m, e) => Math.max(m, e.position || 0), 0);
|
||||
const { data, error } = await supabase
|
||||
.from('contact_emails')
|
||||
const { data, error } = await tenantDb().from('contact_emails')
|
||||
.insert({
|
||||
tenant_id: tenantId,
|
||||
entity_type: entityType,
|
||||
entity_id: entityId,
|
||||
contact_email_type_id,
|
||||
@@ -172,7 +168,7 @@ export function useContactEmails() {
|
||||
if (sanitized.is_primary === true) {
|
||||
await unsetOtherPrimaries(entityType, entityId, id);
|
||||
}
|
||||
const { error } = await supabase.from('contact_emails').update(sanitized).eq('id', id);
|
||||
const { error } = await tenantDb().from('contact_emails').update(sanitized).eq('id', id);
|
||||
if (error) throw error;
|
||||
await loadEmails(entityType, entityId);
|
||||
return { ok: true };
|
||||
@@ -200,12 +196,12 @@ export function useContactEmails() {
|
||||
saving.value = true;
|
||||
try {
|
||||
const wasPrimary = emails.value.find((e) => e.id === id)?.is_primary;
|
||||
const { error } = await supabase.from('contact_emails').delete().eq('id', id);
|
||||
const { error } = await tenantDb().from('contact_emails').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
if (wasPrimary) {
|
||||
const remaining = emails.value.filter((e) => e.id !== id).sort((a, b) => (a.position || 0) - (b.position || 0));
|
||||
if (remaining.length > 0) {
|
||||
await supabase.from('contact_emails').update({ is_primary: true }).eq('id', remaining[0].id);
|
||||
await tenantDb().from('contact_emails').update({ is_primary: true }).eq('id', remaining[0].id);
|
||||
}
|
||||
}
|
||||
await loadEmails(entityType, entityId);
|
||||
@@ -227,7 +223,6 @@ export function useContactEmails() {
|
||||
saving.value = true;
|
||||
try {
|
||||
const rows = pendingItems.map((e) => ({
|
||||
tenant_id: tenantId,
|
||||
entity_type: entityType,
|
||||
entity_id: entityId,
|
||||
contact_email_type_id: e.contact_email_type_id,
|
||||
@@ -236,7 +231,7 @@ export function useContactEmails() {
|
||||
notes: e.notes || null,
|
||||
position: e.position
|
||||
}));
|
||||
const { error } = await supabase.from('contact_emails').insert(rows);
|
||||
const { error } = await tenantDb().from('contact_emails').insert(rows);
|
||||
if (error) throw error;
|
||||
await loadEmails(entityType, entityId);
|
||||
return { ok: true, count: rows.length };
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
function normalizeDigits(raw) {
|
||||
@@ -36,9 +37,8 @@ export function useContactPhones() {
|
||||
|
||||
async function loadTypes() {
|
||||
try {
|
||||
const { data } = await supabase
|
||||
.from('contact_types')
|
||||
.select('id, tenant_id, name, slug, icon, is_mobile, is_system, position')
|
||||
const { data } = await tenantDb().from('contact_types')
|
||||
.select('id, name, slug, icon, is_mobile, is_system, position')
|
||||
.order('position', { ascending: true })
|
||||
.order('name', { ascending: true });
|
||||
types.value = data || [];
|
||||
@@ -55,8 +55,7 @@ export function useContactPhones() {
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('contact_phones')
|
||||
const { data, error } = await tenantDb().from('contact_phones')
|
||||
.select('id, contact_type_id, number, is_primary, whatsapp_linked_at, notes, position, created_at')
|
||||
.eq('entity_type', entityType)
|
||||
.eq('entity_id', entityId)
|
||||
@@ -74,8 +73,7 @@ export function useContactPhones() {
|
||||
|
||||
// Ensure só 1 primary por entidade — seta outros pra false antes de inserir/atualizar
|
||||
async function unsetOtherPrimaries(entityType, entityId, exceptId = null) {
|
||||
const q = supabase
|
||||
.from('contact_phones')
|
||||
const q = tenantDb().from('contact_phones')
|
||||
.update({ is_primary: false })
|
||||
.eq('entity_type', entityType)
|
||||
.eq('entity_id', entityId)
|
||||
@@ -127,10 +125,8 @@ export function useContactPhones() {
|
||||
}
|
||||
|
||||
const maxPos = phones.value.reduce((m, p) => Math.max(m, p.position || 0), 0);
|
||||
const { data, error } = await supabase
|
||||
.from('contact_phones')
|
||||
const { data, error } = await tenantDb().from('contact_phones')
|
||||
.insert({
|
||||
tenant_id: tenantId,
|
||||
entity_type: entityType,
|
||||
entity_id: entityId,
|
||||
contact_type_id,
|
||||
@@ -177,8 +173,7 @@ export function useContactPhones() {
|
||||
await unsetOtherPrimaries(entityType, entityId, id);
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from('contact_phones')
|
||||
const { error } = await tenantDb().from('contact_phones')
|
||||
.update(sanitized)
|
||||
.eq('id', id);
|
||||
if (error) throw error;
|
||||
@@ -208,15 +203,14 @@ export function useContactPhones() {
|
||||
saving.value = true;
|
||||
try {
|
||||
const wasPrimary = phones.value.find((p) => p.id === id)?.is_primary;
|
||||
const { error } = await supabase.from('contact_phones').delete().eq('id', id);
|
||||
const { error } = await tenantDb().from('contact_phones').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
|
||||
// Se removeu o primary, promove o próximo pra primary
|
||||
if (wasPrimary) {
|
||||
const remaining = phones.value.filter((p) => p.id !== id).sort((a, b) => (a.position || 0) - (b.position || 0));
|
||||
if (remaining.length > 0) {
|
||||
await supabase
|
||||
.from('contact_phones')
|
||||
await tenantDb().from('contact_phones')
|
||||
.update({ is_primary: true })
|
||||
.eq('id', remaining[0].id);
|
||||
}
|
||||
@@ -242,7 +236,6 @@ export function useContactPhones() {
|
||||
saving.value = true;
|
||||
try {
|
||||
const rows = pendingItems.map((p) => ({
|
||||
tenant_id: tenantId,
|
||||
entity_type: entityType,
|
||||
entity_id: entityId,
|
||||
contact_type_id: p.contact_type_id,
|
||||
@@ -252,7 +245,7 @@ export function useContactPhones() {
|
||||
notes: p.notes || null,
|
||||
position: p.position
|
||||
}));
|
||||
const { error } = await supabase.from('contact_phones').insert(rows);
|
||||
const { error } = await tenantDb().from('contact_phones').insert(rows);
|
||||
if (error) throw error;
|
||||
// Recarrega do DB pra ter IDs reais — substitui os pending_* por uuids.
|
||||
await loadPhones(entityType, entityId);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
export function useConversationAssignment() {
|
||||
@@ -55,10 +56,8 @@ export function useConversationAssignment() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const { data, error: err } = await supabase
|
||||
.from('conversation_assignments')
|
||||
.select('tenant_id, thread_key, patient_id, contact_number, assigned_to, assigned_by, assigned_at')
|
||||
.eq('tenant_id', tenantId)
|
||||
const { data, error: err } = await tenantDb().from('conversation_assignments')
|
||||
.select('thread_key, patient_id, contact_number, assigned_to, assigned_by, assigned_at')
|
||||
.eq('thread_key', threadKey)
|
||||
.maybeSingle();
|
||||
if (err) throw err;
|
||||
@@ -96,7 +95,6 @@ export function useConversationAssignment() {
|
||||
if (!userId) return { ok: false, error: 'not_authenticated' };
|
||||
|
||||
const payload = {
|
||||
tenant_id: tenantId,
|
||||
thread_key: threadKey,
|
||||
patient_id: patientId || null,
|
||||
contact_number: contactNumber || null,
|
||||
@@ -105,10 +103,9 @@ export function useConversationAssignment() {
|
||||
assigned_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
const { data, error: err } = await supabase
|
||||
.from('conversation_assignments')
|
||||
.upsert(payload, { onConflict: 'tenant_id,thread_key' })
|
||||
.select('tenant_id, thread_key, patient_id, contact_number, assigned_to, assigned_by, assigned_at')
|
||||
const { data, error: err } = await tenantDb().from('conversation_assignments')
|
||||
.upsert(payload, { onConflict: 'thread_key' })
|
||||
.select('thread_key, patient_id, contact_number, assigned_to, assigned_by, assigned_at')
|
||||
.single();
|
||||
if (err) throw err;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
import { ref, computed } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
function sanitizeBody(raw) {
|
||||
@@ -42,10 +43,8 @@ export function useConversationNotes() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const { data, error: err } = await supabase
|
||||
.from('conversation_notes')
|
||||
const { data, error: err } = await tenantDb().from('conversation_notes')
|
||||
.select('id, thread_key, patient_id, contact_number, body, created_by, created_at, updated_at')
|
||||
.eq('tenant_id', tenantId)
|
||||
.eq('thread_key', threadKey)
|
||||
.is('deleted_at', null)
|
||||
.order('created_at', { ascending: false });
|
||||
@@ -82,10 +81,8 @@ export function useConversationNotes() {
|
||||
const userId = authData?.user?.id;
|
||||
if (!userId) return { ok: false, error: 'not_authenticated' };
|
||||
|
||||
const { data, error: err } = await supabase
|
||||
.from('conversation_notes')
|
||||
const { data, error: err } = await tenantDb().from('conversation_notes')
|
||||
.insert({
|
||||
tenant_id: tenantId,
|
||||
thread_key: threadKey,
|
||||
patient_id: patientId,
|
||||
contact_number: contactNumber,
|
||||
@@ -122,8 +119,7 @@ export function useConversationNotes() {
|
||||
if (!id || !clean) return { ok: false, error: 'invalid_params' };
|
||||
saving.value = true;
|
||||
try {
|
||||
const { error: err } = await supabase
|
||||
.from('conversation_notes')
|
||||
const { error: err } = await tenantDb().from('conversation_notes')
|
||||
.update({ body: clean })
|
||||
.eq('id', id);
|
||||
if (err) throw err;
|
||||
@@ -144,8 +140,7 @@ export function useConversationNotes() {
|
||||
if (!id) return { ok: false, error: 'invalid_id' };
|
||||
saving.value = true;
|
||||
try {
|
||||
const { error: err } = await supabase
|
||||
.from('conversation_notes')
|
||||
const { error: err } = await tenantDb().from('conversation_notes')
|
||||
.update({ deleted_at: new Date().toISOString() })
|
||||
.eq('id', id);
|
||||
if (err) throw err;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
import { ref, computed } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
function sanitizeName(raw) {
|
||||
@@ -46,9 +47,8 @@ export function useConversationTags() {
|
||||
loading.value = true;
|
||||
try {
|
||||
// RLS filtra automaticamente: system (tenant_id IS NULL) + custom do tenant ativo
|
||||
const { data, error } = await supabase
|
||||
.from('conversation_tags')
|
||||
.select('id, tenant_id, name, slug, color, icon, position, is_system')
|
||||
const { data, error } = await tenantDb().from('conversation_tags')
|
||||
.select('id, name, slug, color, icon, position, is_system')
|
||||
.order('position', { ascending: true })
|
||||
.order('name', { ascending: true });
|
||||
if (error) throw error;
|
||||
@@ -67,10 +67,8 @@ export function useConversationTags() {
|
||||
const tenantId = tenantStore.activeTenantId;
|
||||
if (!tenantId || !Array.isArray(threadKeys) || !threadKeys.length) return new Map();
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('conversation_thread_tags')
|
||||
const { data, error } = await tenantDb().from('conversation_thread_tags')
|
||||
.select('thread_key, tag_id')
|
||||
.eq('tenant_id', tenantId)
|
||||
.in('thread_key', threadKeys);
|
||||
if (error) throw error;
|
||||
const map = new Map();
|
||||
@@ -93,10 +91,8 @@ export function useConversationTags() {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('conversation_thread_tags')
|
||||
const { data, error } = await tenantDb().from('conversation_thread_tags')
|
||||
.select('tag_id')
|
||||
.eq('tenant_id', tenantId)
|
||||
.eq('thread_key', threadKey);
|
||||
if (error) throw error;
|
||||
threadTagIds.value = new Set((data || []).map((r) => r.tag_id));
|
||||
@@ -116,10 +112,8 @@ export function useConversationTags() {
|
||||
|
||||
try {
|
||||
if (hasTag) {
|
||||
const { error } = await supabase
|
||||
.from('conversation_thread_tags')
|
||||
const { error } = await tenantDb().from('conversation_thread_tags')
|
||||
.delete()
|
||||
.eq('tenant_id', tenantId)
|
||||
.eq('thread_key', threadKey)
|
||||
.eq('tag_id', tagId);
|
||||
if (error) throw error;
|
||||
@@ -130,10 +124,8 @@ export function useConversationTags() {
|
||||
const { data: authData } = await supabase.auth.getUser();
|
||||
const userId = authData?.user?.id;
|
||||
if (!userId) return { ok: false, error: 'not_authenticated' };
|
||||
const { error } = await supabase
|
||||
.from('conversation_thread_tags')
|
||||
const { error } = await tenantDb().from('conversation_thread_tags')
|
||||
.insert({
|
||||
tenant_id: tenantId,
|
||||
thread_key: threadKey,
|
||||
tag_id: tagId,
|
||||
tagged_by: userId
|
||||
@@ -162,17 +154,15 @@ export function useConversationTags() {
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('conversation_tags')
|
||||
const { data, error } = await tenantDb().from('conversation_tags')
|
||||
.insert({
|
||||
tenant_id: tenantId,
|
||||
name: cleanName,
|
||||
slug,
|
||||
color,
|
||||
icon,
|
||||
is_system: false
|
||||
})
|
||||
.select('id, tenant_id, name, slug, color, icon, position, is_system')
|
||||
.select('id, name, slug, color, icon, position, is_system')
|
||||
.single();
|
||||
if (error) throw error;
|
||||
allTags.value = [...allTags.value, data].sort((a, b) => (a.position - b.position) || a.name.localeCompare(b.name));
|
||||
@@ -201,11 +191,10 @@ export function useConversationTags() {
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('conversation_tags')
|
||||
const { data, error } = await tenantDb().from('conversation_tags')
|
||||
.update(patch)
|
||||
.eq('id', id)
|
||||
.select('id, tenant_id, name, slug, color, icon, position, is_system')
|
||||
.select('id, name, slug, color, icon, position, is_system')
|
||||
.single();
|
||||
if (error) throw error;
|
||||
allTags.value = allTags.value
|
||||
@@ -224,7 +213,7 @@ export function useConversationTags() {
|
||||
if (!id) return { ok: false, error: 'invalid_id' };
|
||||
saving.value = true;
|
||||
try {
|
||||
const { error } = await supabase.from('conversation_tags').delete().eq('id', id);
|
||||
const { error } = await tenantDb().from('conversation_tags').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
allTags.value = allTags.value.filter((t) => t.id !== id);
|
||||
const next = new Set(threadTagIds.value);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import { ref, computed, onUnmounted } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
// Metadata canonica das colunas do kanban — fonte unica consumida pelo
|
||||
@@ -82,10 +83,8 @@ export function useConversations() {
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const { data, error: qErr } = await supabase
|
||||
.from('conversation_threads')
|
||||
const { data, error: qErr } = await tenantDb().from('conversation_threads')
|
||||
.select('*')
|
||||
.eq('tenant_id', tenantId)
|
||||
.order('last_message_at', { ascending: false })
|
||||
.limit(500);
|
||||
if (qErr) throw qErr;
|
||||
@@ -100,7 +99,8 @@ export function useConversations() {
|
||||
|
||||
function subscribeRealtime() {
|
||||
const tenantId = tenantStore.activeTenantId;
|
||||
if (!tenantId) return;
|
||||
const tenantSchema = tenantStore.activeTenantSchema;
|
||||
if (!tenantId || !tenantSchema) return;
|
||||
if (realtimeChannel) {
|
||||
supabase.removeChannel(realtimeChannel);
|
||||
}
|
||||
@@ -110,9 +110,8 @@ export function useConversations() {
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'INSERT',
|
||||
schema: 'public',
|
||||
table: 'conversation_messages',
|
||||
filter: `tenant_id=eq.${tenantId}`
|
||||
schema: tenantSchema,
|
||||
table: 'conversation_messages'
|
||||
},
|
||||
(payload) => {
|
||||
// refetch da lista (view agrega tudo) — debounced
|
||||
@@ -129,9 +128,8 @@ export function useConversations() {
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'UPDATE',
|
||||
schema: 'public',
|
||||
table: 'conversation_messages',
|
||||
filter: `tenant_id=eq.${tenantId}`
|
||||
schema: tenantSchema,
|
||||
table: 'conversation_messages'
|
||||
},
|
||||
(payload) => {
|
||||
_scheduleLoad();
|
||||
@@ -226,10 +224,8 @@ export function useConversations() {
|
||||
}
|
||||
threadLoading.value = true;
|
||||
try {
|
||||
let q = supabase
|
||||
.from('conversation_messages')
|
||||
let q = tenantDb().from('conversation_messages')
|
||||
.select('*')
|
||||
.eq('tenant_id', tenantStore.activeTenantId)
|
||||
.order('created_at', { ascending: true })
|
||||
.limit(500);
|
||||
|
||||
@@ -253,10 +249,8 @@ export function useConversations() {
|
||||
// Marca unread do inbound como lido
|
||||
const nowIso = new Date().toISOString();
|
||||
const tenantId = tenantStore.activeTenantId;
|
||||
let q = supabase
|
||||
.from('conversation_messages')
|
||||
let q = tenantDb().from('conversation_messages')
|
||||
.update({ read_at: nowIso })
|
||||
.eq('tenant_id', tenantId)
|
||||
.eq('direction', 'inbound')
|
||||
.is('read_at', null);
|
||||
if (thread.patient_id) q = q.eq('patient_id', thread.patient_id);
|
||||
@@ -271,7 +265,7 @@ export function useConversations() {
|
||||
const patch = { kanban_status: newStatus };
|
||||
if (newStatus === 'resolved') patch.resolved_at = new Date().toISOString();
|
||||
|
||||
let q = supabase.from('conversation_messages').update(patch).eq('tenant_id', tenantId);
|
||||
let q = tenantDb().from('conversation_messages').update(patch);
|
||||
if (thread.patient_id) q = q.eq('patient_id', thread.patient_id);
|
||||
else q = q.eq('from_number', thread.contact_number).is('patient_id', null);
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import { ref, computed } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { getFeriadosNacionais } from '@/utils/feriadosBR';
|
||||
import { useMelissaCacheStore, MELISSA_CACHE_TTL } from '@/stores/melissaCacheStore';
|
||||
|
||||
@@ -59,10 +60,8 @@ export function useFeriados(opts = {}) {
|
||||
}
|
||||
|
||||
async function _doFetch(tenantId, cacheKey) {
|
||||
const { data, error } = await supabase
|
||||
.from('feriados')
|
||||
const { data, error } = await tenantDb().from('feriados')
|
||||
.select('*')
|
||||
.or(`tenant_id.eq.${tenantId},tenant_id.is.null`)
|
||||
.gte('data', `${ano.value}-01-01`)
|
||||
.lte('data', `${ano.value}-12-31`)
|
||||
.order('data');
|
||||
@@ -98,10 +97,8 @@ export function useFeriados(opts = {}) {
|
||||
// Comportamento legado (sem cache) — páginas de admin que editam.
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('feriados')
|
||||
const { data, error } = await tenantDb().from('feriados')
|
||||
.select('*')
|
||||
.or(`tenant_id.eq.${tenantId},tenant_id.is.null`)
|
||||
.gte('data', `${ano.value}-01-01`)
|
||||
.lte('data', `${ano.value}-12-31`)
|
||||
.order('data');
|
||||
@@ -114,7 +111,7 @@ export function useFeriados(opts = {}) {
|
||||
|
||||
// ── Criar feriado municipal ───────────────────────────────
|
||||
async function criar(payload) {
|
||||
const { data, error } = await supabase.from('feriados').insert(payload).select().single();
|
||||
const { data, error } = await tenantDb().from('feriados').insert(payload).select().single();
|
||||
if (error) throw error;
|
||||
municipais.value = [...municipais.value, data].sort((a, b) => a.data.localeCompare(b.data));
|
||||
if (cache) cache.invalidate('feriados');
|
||||
@@ -123,7 +120,7 @@ export function useFeriados(opts = {}) {
|
||||
|
||||
// ── Remover feriado municipal ─────────────────────────────
|
||||
async function remover(id) {
|
||||
const { error } = await supabase.from('feriados').delete().eq('id', id);
|
||||
const { error } = await tenantDb().from('feriados').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
municipais.value = municipais.value.filter((f) => f.id !== id);
|
||||
if (cache) cache.invalidate('feriados');
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import { ref, computed } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
// ─── helpers internos ────────────────────────────────────────────────────────
|
||||
@@ -38,7 +39,7 @@ async function getUid() {
|
||||
// ─── select base com joins ───────────────────────────────────────────────────
|
||||
|
||||
const BASE_SELECT = `
|
||||
id, tenant_id, owner_id, patient_id, agenda_evento_id,
|
||||
id, owner_id, patient_id, agenda_evento_id,
|
||||
type, amount, discount_amount, final_amount,
|
||||
status, due_date, paid_at, payment_method, payment_link,
|
||||
description, notes, created_at, updated_at,
|
||||
@@ -117,10 +118,8 @@ export function useFinancialRecords() {
|
||||
const offset = filters.offset ?? 0;
|
||||
|
||||
try {
|
||||
let query = supabase
|
||||
.from('financial_records')
|
||||
let query = tenantDb().from('financial_records')
|
||||
.select(BASE_SELECT, { count: 'exact' })
|
||||
.eq('tenant_id', tenantId)
|
||||
.is('deleted_at', null)
|
||||
.order('due_date', { ascending: false })
|
||||
.range(offset, offset + limit - 1);
|
||||
@@ -214,11 +213,9 @@ export function useFinancialRecords() {
|
||||
const discount = payload.discount_amount ?? 0;
|
||||
const amount = payload.amount ?? 0;
|
||||
|
||||
const { data, error: err } = await supabase
|
||||
.from('financial_records')
|
||||
const { data, error: err } = await tenantDb().from('financial_records')
|
||||
.insert([
|
||||
{
|
||||
tenant_id: tenantId,
|
||||
owner_id: ownerId,
|
||||
patient_id: payload.patient_id ?? null,
|
||||
agenda_evento_id: null,
|
||||
@@ -291,7 +288,7 @@ export function useFinancialRecords() {
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const { error: err } = await supabase.from('financial_records').update({ status: 'cancelled', updated_at: new Date().toISOString() }).eq('id', recordId);
|
||||
const { error: err } = await tenantDb().from('financial_records').update({ status: 'cancelled', updated_at: new Date().toISOString() }).eq('id', recordId);
|
||||
|
||||
if (err) throw err;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
// ─── estado compartilhado ──────────────────────────────────
|
||||
@@ -50,34 +51,30 @@ async function _refresh() {
|
||||
|
||||
// 1. Agenda hoje
|
||||
{
|
||||
let q = supabase.from('agenda_eventos').select('id', { count: 'exact', head: true }).gte('inicio_em', startDay).lt('inicio_em', endDay);
|
||||
if (isClinic && tenantId) q = q.eq('tenant_id', tenantId);
|
||||
else q = q.eq('owner_id', ownerId);
|
||||
let q = tenantDb().from('agenda_eventos').select('id', { count: 'exact', head: true }).gte('inicio_em', startDay).lt('inicio_em', endDay);
|
||||
if (!(isClinic && tenantId)) q = q.eq('owner_id', ownerId);
|
||||
const { count } = await q;
|
||||
agendaHoje.value = count || 0;
|
||||
}
|
||||
|
||||
// 2. Cadastros recebidos (status = 'new') — RLS filtra pelo owner
|
||||
{
|
||||
const { count } = await supabase.from('patient_intake_requests').select('id', { count: 'exact', head: true }).eq('status', 'new');
|
||||
const { count } = await tenantDb().from('patient_intake_requests').select('id', { count: 'exact', head: true }).eq('status', 'new');
|
||||
cadastrosRecebidos.value = count || 0;
|
||||
}
|
||||
|
||||
// 3. Agendamentos recebidos (status = 'pendente')
|
||||
{
|
||||
let q = supabase.from('agendador_solicitacoes').select('id', { count: 'exact', head: true }).eq('status', 'pendente');
|
||||
if (isClinic && tenantId) q = q.eq('tenant_id', tenantId);
|
||||
else q = q.eq('owner_id', ownerId);
|
||||
let q = tenantDb().from('agendador_solicitacoes').select('id', { count: 'exact', head: true }).eq('status', 'pendente');
|
||||
if (!(isClinic && tenantId)) q = q.eq('owner_id', ownerId);
|
||||
const { count } = await q;
|
||||
agendamentosRecebidos.value = count || 0;
|
||||
}
|
||||
|
||||
// 4. Conversas não lidas (mensagens inbound sem read_at)
|
||||
if (tenantId) {
|
||||
const { count } = await supabase
|
||||
.from('conversation_messages')
|
||||
const { count } = await tenantDb().from('conversation_messages')
|
||||
.select('id', { count: 'exact', head: true })
|
||||
.eq('tenant_id', tenantId)
|
||||
.eq('direction', 'inbound')
|
||||
.is('read_at', null);
|
||||
conversasUnread.value = count || 0;
|
||||
@@ -92,7 +89,8 @@ function _subscribeRealtime() {
|
||||
try {
|
||||
const tenantStore = useTenantStore();
|
||||
const tenantId = tenantStore.activeTenantId || tenantStore.tenantId || null;
|
||||
if (!tenantId) return;
|
||||
const tenantSchema = tenantStore.activeTenantSchema;
|
||||
if (!tenantId || !tenantSchema) return;
|
||||
if (_realtimeChannel) {
|
||||
supabase.removeChannel(_realtimeChannel);
|
||||
}
|
||||
@@ -100,12 +98,12 @@ function _subscribeRealtime() {
|
||||
.channel(`menu_badges_conv_${tenantId}`)
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{ event: 'INSERT', schema: 'public', table: 'conversation_messages', filter: `tenant_id=eq.${tenantId}` },
|
||||
{ event: 'INSERT', schema: tenantSchema, table: 'conversation_messages' },
|
||||
() => _refresh()
|
||||
)
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{ event: 'UPDATE', schema: 'public', table: 'conversation_messages', filter: `tenant_id=eq.${tenantId}` },
|
||||
{ event: 'UPDATE', schema: tenantSchema, table: 'conversation_messages' },
|
||||
() => _refresh()
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
@@ -18,6 +18,7 @@ import { onMounted, onUnmounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useNotificationStore, fireBrowserNotification } from '@/stores/notificationStore';
|
||||
import { useConversationDrawerStore } from '@/stores/conversationDrawerStore';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
@@ -91,10 +92,9 @@ export function useNotifications() {
|
||||
if (payload.thread_key) {
|
||||
try {
|
||||
const tenantId = tenantStore.activeTenantId;
|
||||
const { data } = await supabase
|
||||
.from('conversation_threads')
|
||||
const { data } = await tenantDb().from('conversation_threads')
|
||||
.select('*')
|
||||
.eq('tenant_id', tenantId)
|
||||
|
||||
.eq('thread_key', payload.thread_key)
|
||||
.maybeSingle();
|
||||
if (data) {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
export function usePatientLifecycle() {
|
||||
async function canDelete(patientId) {
|
||||
const { data, error } = await supabase.rpc('can_delete_patient', { p_patient_id: patientId });
|
||||
@@ -32,8 +33,8 @@ export function usePatientLifecycle() {
|
||||
async function checkActiveSchedule(patientId) {
|
||||
const now = new Date().toISOString();
|
||||
const [evts, recs] = await Promise.all([
|
||||
supabase.from('agenda_eventos').select('id', { count: 'exact', head: true }).eq('patient_id', patientId).eq('status', 'agendado').gt('inicio_em', now),
|
||||
supabase.from('recurrence_rules').select('id', { count: 'exact', head: true }).eq('patient_id', patientId).eq('status', 'ativo')
|
||||
tenantDb().from('agenda_eventos').select('id', { count: 'exact', head: true }).eq('patient_id', patientId).eq('status', 'agendado').gt('inicio_em', now),
|
||||
tenantDb().from('recurrence_rules').select('id', { count: 'exact', head: true }).eq('patient_id', patientId).eq('status', 'ativo')
|
||||
]);
|
||||
return {
|
||||
hasFutureSessions: (evts.count ?? 0) > 0,
|
||||
@@ -42,17 +43,17 @@ export function usePatientLifecycle() {
|
||||
}
|
||||
|
||||
async function deactivatePatient(patientId) {
|
||||
const { error } = await supabase.from('patients').update({ status: 'Inativo', updated_at: new Date().toISOString() }).eq('id', patientId);
|
||||
const { error } = await tenantDb().from('patients').update({ status: 'Inativo', updated_at: new Date().toISOString() }).eq('id', patientId);
|
||||
return error ? { ok: false, error } : { ok: true };
|
||||
}
|
||||
|
||||
async function archivePatient(patientId) {
|
||||
const { error } = await supabase.from('patients').update({ status: 'Arquivado', updated_at: new Date().toISOString() }).eq('id', patientId);
|
||||
const { error } = await tenantDb().from('patients').update({ status: 'Arquivado', updated_at: new Date().toISOString() }).eq('id', patientId);
|
||||
return error ? { ok: false, error } : { ok: true };
|
||||
}
|
||||
|
||||
async function reactivatePatient(patientId) {
|
||||
const { error } = await supabase.from('patients').update({ status: 'Ativo', updated_at: new Date().toISOString() }).eq('id', patientId);
|
||||
const { error } = await tenantDb().from('patients').update({ status: 'Ativo', updated_at: new Date().toISOString() }).eq('id', patientId);
|
||||
return error ? { ok: false, error } : { ok: true };
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
const DEFAULTS = {
|
||||
@@ -38,15 +39,11 @@ export function useSessionReminders() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const [settingsRes, logsRes] = await Promise.all([
|
||||
supabase
|
||||
.from('session_reminder_settings')
|
||||
tenantDb().from('session_reminder_settings')
|
||||
.select('*')
|
||||
.eq('tenant_id', tenantId)
|
||||
.maybeSingle(),
|
||||
supabase
|
||||
.from('session_reminder_logs')
|
||||
tenantDb().from('session_reminder_logs')
|
||||
.select('id, event_id, reminder_type, sent_at, provider, skip_reason, to_phone')
|
||||
.eq('tenant_id', tenantId)
|
||||
.order('sent_at', { ascending: false })
|
||||
.limit(30)
|
||||
]);
|
||||
@@ -88,9 +85,8 @@ export function useSessionReminders() {
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('session_reminder_settings')
|
||||
.upsert({ tenant_id: tenantId, ...payload }, { onConflict: 'tenant_id' });
|
||||
const { error } = await tenantDb().from('session_reminder_settings')
|
||||
.upsert({ ...payload }, { onConflict: 'singleton' });
|
||||
if (error) throw error;
|
||||
return { ok: true };
|
||||
} catch (e) {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/useTenantDb.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Composable reativo sobre tenantClient: use em componentes que precisam
|
||||
| aguardar o tenant ativo (isReady) ou reagir à troca de tenant.
|
||||
| Em services/repositories, importe tenantDb direto de
|
||||
| '@/lib/supabase/tenantClient'.
|
||||
*/
|
||||
import { computed } from 'vue';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { tenantDb, tenantSchemaName } from '@/lib/supabase/tenantClient';
|
||||
|
||||
export function useTenantDb() {
|
||||
const tenantStore = useTenantStore();
|
||||
const schemaName = computed(() => tenantSchemaName(tenantStore.activeTenantSlug));
|
||||
const isReady = computed(() => Boolean(schemaName.value));
|
||||
|
||||
function db() {
|
||||
return tenantDb();
|
||||
}
|
||||
|
||||
return { db, schemaName, isReady };
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import Message from 'primevue/message';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import ComponentCadastroRapido from '@/components/ComponentCadastroRapido.vue';
|
||||
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue';
|
||||
import AgendaEventoFinanceiroPanel from '@/components/agenda/AgendaEventoFinanceiroPanel.vue';
|
||||
@@ -803,8 +804,7 @@ async function openSessionRecordsDialog() {
|
||||
sessionRecordsDialogOpen.value = true;
|
||||
sessionRecordsLoading.value = true;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('financial_records')
|
||||
const { data, error } = await tenantDb().from('financial_records')
|
||||
.select('id, description, amount, final_amount, status, due_date, paid_at, payment_method, created_at')
|
||||
.eq('agenda_evento_id', eid)
|
||||
.is('deleted_at', null)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useFeriados } from '@/composables/useFeriados';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import DatePicker from 'primevue/datepicker';
|
||||
@@ -168,7 +169,6 @@ async function confirmar() {
|
||||
try {
|
||||
const base = {
|
||||
owner_id: props.ownerId,
|
||||
tenant_id: props.tenantId,
|
||||
tipo: 'bloqueio',
|
||||
recorrente: false
|
||||
};
|
||||
@@ -204,7 +204,7 @@ async function confirmar() {
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await supabase.from('agenda_bloqueios').insert(rows);
|
||||
const { error } = await tenantDb().from('agenda_bloqueios').insert(rows);
|
||||
if (error) throw error;
|
||||
|
||||
// Marcar sessões existentes como "remarcado"
|
||||
@@ -229,7 +229,7 @@ async function marcarSessoesParaRemarcar(bloqueios) {
|
||||
// Para cada bloqueio, tenta marcar sessões existentes como 'remarcado'
|
||||
for (const b of bloqueios) {
|
||||
try {
|
||||
let query = supabase.from('agenda_eventos').update({ status: 'remarcado' }).eq('owner_id', props.ownerId).eq('tipo', 'sessao').gte('inicio_em', `${b.data_inicio}T00:00:00`).lte('inicio_em', `${b.data_fim}T23:59:59`);
|
||||
let query = tenantDb().from('agenda_eventos').update({ status: 'remarcado' }).eq('owner_id', props.ownerId).eq('tipo', 'sessao').gte('inicio_em', `${b.data_inicio}T00:00:00`).lte('inicio_em', `${b.data_fim}T23:59:59`);
|
||||
|
||||
if (b.hora_inicio && b.hora_fim) {
|
||||
// filtra pela hora aproximada — comparação UTC simplificada
|
||||
@@ -250,7 +250,6 @@ async function salvarFeriadoMunicipal() {
|
||||
const iso = toISO(fform.value.data);
|
||||
try {
|
||||
await criarFeriado({
|
||||
tenant_id: props.tenantId,
|
||||
owner_id: props.ownerId,
|
||||
tipo: 'municipal',
|
||||
nome: fform.value.nome.trim(),
|
||||
|
||||
@@ -20,6 +20,7 @@ import { ref, watch } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
insurancePlanId: { type: String, default: '' },
|
||||
@@ -61,7 +62,7 @@ async function onSave() {
|
||||
value: Number(form.value.value),
|
||||
active: true
|
||||
};
|
||||
const { data, error } = await supabase.from('insurance_plan_services').insert(payload).select().single();
|
||||
const { data, error } = await tenantDb().from('insurance_plan_services').insert(payload).select().single();
|
||||
if (error) throw error;
|
||||
toast.add({ severity: 'success', summary: 'Procedimento cadastrado', life: 2200 });
|
||||
emit('created', data);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useFeriados } from '@/composables/useFeriados';
|
||||
@@ -109,7 +110,7 @@ async function loadBloqueiosMes() {
|
||||
const end = `${ano}-${String(mesAtual).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
|
||||
loadingBloqueios.value = true;
|
||||
try {
|
||||
const { data } = await supabase.from('agenda_bloqueios').select('data_inicio').eq('owner_id', _ownerId.value).in('origem', ['agenda_feriado', 'agenda_dia']).gte('data_inicio', start).lte('data_inicio', end);
|
||||
const { data } = await tenantDb().from('agenda_bloqueios').select('data_inicio').eq('owner_id', _ownerId.value).in('origem', ['agenda_feriado', 'agenda_dia']).gte('data_inicio', start).lte('data_inicio', end);
|
||||
bloqueiosDatas.value = new Set((data || []).map((r) => r.data_inicio));
|
||||
} catch {
|
||||
/* silencioso */
|
||||
@@ -152,7 +153,6 @@ async function confirmarBloqueio(feriado) {
|
||||
try {
|
||||
const row = {
|
||||
owner_id: _ownerId.value,
|
||||
tenant_id: _tenantId.value,
|
||||
tipo: 'bloqueio',
|
||||
recorrente: false,
|
||||
titulo: `Feriado: ${feriado.nome}`,
|
||||
@@ -163,11 +163,11 @@ async function confirmarBloqueio(feriado) {
|
||||
origem: 'agenda_feriado'
|
||||
};
|
||||
|
||||
const { error } = await supabase.from('agenda_bloqueios').insert([row]);
|
||||
const { error } = await tenantDb().from('agenda_bloqueios').insert([row]);
|
||||
if (error) throw error;
|
||||
|
||||
// Marcar sessões existentes no dia como 'remarcado'
|
||||
await supabase.from('agenda_eventos').update({ status: 'remarcado' }).eq('owner_id', _ownerId.value).eq('tipo', 'sessao').gte('inicio_em', `${feriado.data}T00:00:00`).lte('inicio_em', `${feriado.data}T23:59:59`);
|
||||
await tenantDb().from('agenda_eventos').update({ status: 'remarcado' }).eq('owner_id', _ownerId.value).eq('tipo', 'sessao').gte('inicio_em', `${feriado.data}T00:00:00`).lte('inicio_em', `${feriado.data}T23:59:59`);
|
||||
|
||||
bloqueiosDatas.value = new Set([...bloqueiosDatas.value, feriado.data]);
|
||||
toast.add({
|
||||
@@ -212,7 +212,6 @@ async function salvar() {
|
||||
saving.value = true;
|
||||
try {
|
||||
await criar({
|
||||
tenant_id: _tenantId.value,
|
||||
owner_id: _ownerId.value,
|
||||
tipo: 'municipal',
|
||||
nome: form.value.nome.trim(),
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
| o id pra que o parent pré-selecione no select de serviços.
|
||||
|
|
||||
| Campos mínimos (obrigatórios no schema):
|
||||
| name, price, owner_id, tenant_id
|
||||
| name, price, owner_id
|
||||
| Opcionais úteis:
|
||||
| duration_min, description
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -20,6 +20,7 @@
|
||||
import { ref, watch } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -72,7 +73,7 @@ async function onSave() {
|
||||
// Nome unico por owner (case-insensitive) — espelha a validacao
|
||||
// do useServices.save() pra impedir duplicata tambem quando o
|
||||
// cadastro vem do quick-create dentro do AgendaEventDialog.
|
||||
const { data: dups, error: dupErr } = await supabase.from('services').select('id').eq('owner_id', ownerId).ilike('name', name).limit(1);
|
||||
const { data: dups, error: dupErr } = await tenantDb().from('services').select('id').eq('owner_id', ownerId).ilike('name', name).limit(1);
|
||||
if (dupErr) throw dupErr;
|
||||
if (dups && dups.length > 0) {
|
||||
toast.add({ severity: 'warn', summary: 'Nome em uso', detail: 'Já existe um serviço com este nome.', life: 3500 });
|
||||
@@ -82,14 +83,13 @@ async function onSave() {
|
||||
|
||||
const payload = {
|
||||
owner_id: ownerId,
|
||||
tenant_id: tid,
|
||||
name,
|
||||
price: Number(form.value.price),
|
||||
duration_min: form.value.duration_min ? Number(form.value.duration_min) : null,
|
||||
description: form.value.description?.trim().slice(0, 500) || null,
|
||||
active: true
|
||||
};
|
||||
const { data, error } = await supabase.from('services').insert(payload).select().single();
|
||||
const { data, error } = await tenantDb().from('services').insert(payload).select().single();
|
||||
if (error) throw error;
|
||||
toast.add({ severity: 'success', summary: 'Serviço criado', life: 2200 });
|
||||
emit('created', data);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
Acessível via SupportDebugBanner → botão "Docs". -->
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false }
|
||||
@@ -141,7 +142,7 @@ const activeTab = ref(0);
|
||||
<!-- ── Tab 1: Tabelas ─────────────────────────────────── -->
|
||||
<TabPanel header="Tabelas">
|
||||
<div class="dd-section">
|
||||
<p class="dd-p">Todas as tabelas usam <strong>Row Level Security (RLS)</strong> habilitada.</p>
|
||||
<p class="dd-p">Todas as tabelas usam <strong>Row Level Security (RLS)</strong> habilitada. As tabelas da agenda vivem no schema do tenant (<code>tenant_<slug></code>, sem coluna <code>tenant_id</code>) e são acessadas via <code>tenantDb().from(...)</code>.</p>
|
||||
|
||||
<h3 class="dd-h3">Core</h3>
|
||||
<table class="dd-table">
|
||||
@@ -156,12 +157,12 @@ const activeTab = ref(0);
|
||||
<tr>
|
||||
<td><code>agenda_configuracoes</code></td>
|
||||
<td>Configurações da agenda por owner (terapeuta ou clínica)</td>
|
||||
<td>owner_id, tenant_id, slot_duration_minutes, start_time, end_time, days_of_week</td>
|
||||
<td>owner_id, slot_duration_minutes, start_time, end_time, days_of_week</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>agenda_eventos</code></td>
|
||||
<td>Eventos individuais (sessões, bloqueios avulsos)</td>
|
||||
<td>id, owner_id, tenant_id, patient_id, starts_at, ends_at, status, recurrence_rule_id, tipo</td>
|
||||
<td>id, owner_id, patient_id, starts_at, ends_at, status, recurrence_rule_id, tipo</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>agenda_bloqueios</code></td>
|
||||
@@ -217,7 +218,7 @@ const activeTab = ref(0);
|
||||
<tr>
|
||||
<td><code>determined_commitments</code></td>
|
||||
<td>Tipos de compromisso determinístico (ex: Avaliação, Supervisão)</td>
|
||||
<td>id, owner_id, tenant_id, name, color, duration_minutes</td>
|
||||
<td>id, owner_id, name, color, duration_minutes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>determined_commitment_fields</code></td>
|
||||
@@ -232,7 +233,7 @@ const activeTab = ref(0);
|
||||
<tr>
|
||||
<td><code>services</code></td>
|
||||
<td>Catálogo de serviços do terapeuta/clínica</td>
|
||||
<td>id, owner_id, tenant_id, name, default_price, active</td>
|
||||
<td>id, owner_id, name, default_price, active</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>professional_pricing</code></td>
|
||||
@@ -636,8 +637,7 @@ async function loadEvents (ownerId, range) {
|
||||
logAPI('useAgendaEvents', 'loadEvents start', { ownerId, range })
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_eventos')
|
||||
const { data, error } = await tenantDb().from('agenda_eventos')
|
||||
.select('*')
|
||||
.eq('owner_id', ownerId)
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { buildBloqueioBackgroundEvents } from '@/features/agenda/services/agendaMappers';
|
||||
|
||||
export function useAgendaBloqueios() {
|
||||
@@ -55,14 +56,12 @@ export function useAgendaBloqueios() {
|
||||
// Query: recorrentes (qualquer data) OU não-recorrentes com
|
||||
// data_inicio <= isoEnd e (data_fim ?? data_inicio) >= isoStart.
|
||||
// 2 queries simples + merge pra evitar string-building frágil.
|
||||
const baseNonRec = supabase
|
||||
.from('agenda_bloqueios')
|
||||
const baseNonRec = tenantDb().from('agenda_bloqueios')
|
||||
.select('*')
|
||||
.eq('recorrente', false)
|
||||
.lte('data_inicio', isoEnd)
|
||||
.or(`data_fim.gte.${isoStart},and(data_fim.is.null,data_inicio.gte.${isoStart})`);
|
||||
const baseRec = supabase
|
||||
.from('agenda_bloqueios')
|
||||
const baseRec = tenantDb().from('agenda_bloqueios')
|
||||
.select('*')
|
||||
.eq('recorrente', true);
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import { ref, watch } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { labelStatusSessao } from './agendaEventHelpers';
|
||||
|
||||
const EVENTO_TIPO_SESSAO = 'sessao';
|
||||
@@ -157,7 +158,7 @@ export function useAgendaEventActions({
|
||||
toast.add({ severity: 'success', summary: 'Status atualizado', detail: `Sessão marcada como ${labelStatusSessao(newVal)}.`, life: 3000 });
|
||||
return;
|
||||
}
|
||||
const { data, error } = await supabase.from('agenda_eventos').update({ status: newVal }).eq('id', formId).select().single();
|
||||
const { data, error } = await tenantDb().from('agenda_eventos').update({ status: newVal }).eq('id', formId).select().single();
|
||||
if (error) throw error;
|
||||
toast.add({ severity: 'success', summary: 'Status atualizado', detail: `Sessão marcada como ${labelStatusSessao(newVal)}.`, life: 3000 });
|
||||
emit('updated', data);
|
||||
@@ -213,8 +214,7 @@ export function useAgendaEventActions({
|
||||
const dayStart = new Date(d.getFullYear(), d.getMonth(), d.getDate()).toISOString();
|
||||
const dayEnd = new Date(d.getFullYear(), d.getMonth(), d.getDate() + 1).toISOString();
|
||||
|
||||
let q = supabase
|
||||
.from('agenda_eventos')
|
||||
let q = tenantDb().from('agenda_eventos')
|
||||
.select('id, inicio_em, fim_em, titulo')
|
||||
.eq('patient_id', pid)
|
||||
.gte('inicio_em', dayStart)
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
import { ref, computed, watch, nextTick } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
export function generateRuleDates(rule) {
|
||||
const { type, interval = 1, weekdays = [], start_date, end_date, max_occurrences } = rule || {};
|
||||
if (!start_date || !weekdays?.length) return [];
|
||||
@@ -150,14 +151,13 @@ export function useAgendaEventLifecycle({
|
||||
}
|
||||
serieLoading.value = true;
|
||||
try {
|
||||
const { data: rule, error: ruleErr } = await supabase.from('recurrence_rules').select('*').eq('id', rid).maybeSingle();
|
||||
const { data: rule, error: ruleErr } = await tenantDb().from('recurrence_rules').select('*').eq('id', rid).maybeSingle();
|
||||
if (ruleErr) throw ruleErr;
|
||||
|
||||
const { data: excData } = await supabase.from('recurrence_exceptions').select('original_date, type, reason').eq('recurrence_id', rid);
|
||||
const { data: excData } = await tenantDb().from('recurrence_exceptions').select('original_date, type, reason').eq('recurrence_id', rid);
|
||||
const exMap = new Map((excData || []).map((e) => [e.original_date, e]));
|
||||
|
||||
const { data: realData } = await supabase
|
||||
.from('agenda_eventos')
|
||||
const { data: realData } = await tenantDb().from('agenda_eventos')
|
||||
.select('id, inicio_em, fim_em, status, recurrence_date')
|
||||
.eq('recurrence_id', rid)
|
||||
.is('mirror_of_event_id', null)
|
||||
@@ -236,8 +236,7 @@ export function useAgendaEventLifecycle({
|
||||
// 1) Record direto (materializada que tem agenda_evento_id real)
|
||||
const isVirtualId = typeof evId === 'string' && evId.startsWith('rec::');
|
||||
if (evId && !isVirtualId) {
|
||||
const { data, error } = await supabase
|
||||
.from('financial_records')
|
||||
const { data, error } = await tenantDb().from('financial_records')
|
||||
.select('id, amount, final_amount, status, due_date, paid_at, payment_method')
|
||||
.eq('agenda_evento_id', evId)
|
||||
.in('status', ['pending', 'paid', 'overdue'])
|
||||
@@ -255,8 +254,7 @@ export function useAgendaEventLifecycle({
|
||||
// materializadas sem cobrança individual) herdam status do
|
||||
// contrato pra UI mostrar "Cobrança paga" coerentemente.
|
||||
if (ruleId && patientId) {
|
||||
const { data: contracts } = await supabase
|
||||
.from('billing_contracts')
|
||||
const { data: contracts } = await tenantDb().from('billing_contracts')
|
||||
.select('id, package_price, charging_style, status')
|
||||
.eq('patient_id', patientId)
|
||||
.eq('type', 'package')
|
||||
@@ -266,8 +264,7 @@ export function useAgendaEventLifecycle({
|
||||
if (upfront) {
|
||||
// Confere se há record PAGO ligado a qualquer evento do
|
||||
// mesmo recurrence_id (ou seja, contrato foi quitado).
|
||||
const { data: siblingEvents } = await supabase
|
||||
.from('agenda_eventos')
|
||||
const { data: siblingEvents } = await tenantDb().from('agenda_eventos')
|
||||
.select('id')
|
||||
.eq('recurrence_id', ruleId);
|
||||
const ids = (siblingEvents || []).map((e) => e.id);
|
||||
@@ -276,8 +273,7 @@ export function useAgendaEventLifecycle({
|
||||
// pending OU overdue). Pacote upfront tem 1 record
|
||||
// unico cobrindo toda a serie — qualquer status dele
|
||||
// trava as siblings (cobranca ja emitida, imutavel).
|
||||
const { data: anyRec } = await supabase
|
||||
.from('financial_records')
|
||||
const { data: anyRec } = await tenantDb().from('financial_records')
|
||||
.select('id, amount, final_amount, status, due_date, paid_at, payment_method')
|
||||
.in('agenda_evento_id', ids)
|
||||
.in('status', ['paid', 'pending', 'overdue'])
|
||||
@@ -315,8 +311,7 @@ export function useAgendaEventLifecycle({
|
||||
const evId = props.eventRow?.id;
|
||||
if (!evId) return;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('financial_records')
|
||||
const { data, error } = await tenantDb().from('financial_records')
|
||||
.select('id, amount, final_amount, status, due_date, paid_at, payment_method')
|
||||
.eq('agenda_evento_id', evId)
|
||||
.in('status', ['pending', 'paid', 'overdue'])
|
||||
@@ -341,8 +336,7 @@ export function useAgendaEventLifecycle({
|
||||
// Só faz sentido pra sessão de série
|
||||
if (!patientId || !ruleId) return;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('billing_contracts')
|
||||
const { data, error } = await tenantDb().from('billing_contracts')
|
||||
.select('id, type, total_sessions, sessions_used, package_price, charging_style, status, active_from')
|
||||
.eq('patient_id', patientId)
|
||||
.eq('type', 'package')
|
||||
@@ -522,8 +516,7 @@ export function useAgendaEventLifecycle({
|
||||
if (serieValorMode) serieValorMode.value = 'multiplicar';
|
||||
|
||||
if (composer.isEdit.value && composer.form.value.paciente_id && !composer.form.value.paciente_nome) {
|
||||
supabase
|
||||
.from('patients')
|
||||
tenantDb().from('patients')
|
||||
.select('id, nome_completo')
|
||||
.eq('id', composer.form.value.paciente_id)
|
||||
.maybeSingle()
|
||||
@@ -602,8 +595,7 @@ export function useAgendaEventLifecycle({
|
||||
|
||||
const d = new Date(composer.form.value.dia);
|
||||
const isoDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
const { data } = await supabase
|
||||
.from('agendador_solicitacoes')
|
||||
const { data } = await tenantDb().from('agendador_solicitacoes')
|
||||
.select('id, paciente_nome, paciente_sobrenome, paciente_email')
|
||||
.eq('owner_id', props.ownerId)
|
||||
.eq('status', 'pendente')
|
||||
@@ -625,8 +617,7 @@ export function useAgendaEventLifecycle({
|
||||
const dow = new Date(dia).getDay();
|
||||
loadingOnlineSlots.value = true;
|
||||
try {
|
||||
const { data } = await supabase
|
||||
.from('agenda_online_slots')
|
||||
const { data } = await tenantDb().from('agenda_online_slots')
|
||||
.select('time')
|
||||
.eq('owner_id', props.ownerId)
|
||||
.eq('weekday', dow)
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
*/
|
||||
import { ref, watch, nextTick } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { calcFinalPrice } from './agendaEventHelpers';
|
||||
|
||||
export function useAgendaEventPickerBilling({
|
||||
@@ -254,13 +255,11 @@ export function useAgendaEventPickerBilling({
|
||||
pacientesError.value = '';
|
||||
pacientesLoading.value = true;
|
||||
|
||||
let q = supabase
|
||||
.from('patients')
|
||||
.select('id,nome_completo,email_principal,telefone,status,avatar_url,tenant_id,responsible_member_id,created_at')
|
||||
let q = tenantDb().from('patients')
|
||||
.select('id,nome_completo,email_principal,telefone,status,avatar_url,responsible_member_id,created_at')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(500);
|
||||
|
||||
if (props.tenantId) q = q.eq('tenant_id', props.tenantId);
|
||||
if (props.restrictPatientsToOwner && props.patientScopeOwnerId) {
|
||||
q = q.eq('responsible_member_id', props.patientScopeOwnerId);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
// Shape interno de CommitmentItem:
|
||||
// {
|
||||
// service_id: uuid,
|
||||
@@ -56,7 +57,7 @@ export function useCommitmentServices() {
|
||||
async function loadItems(eventId) {
|
||||
if (!eventId) return [];
|
||||
|
||||
const { data, error } = await supabase.from('commitment_services').select('service_id, quantity, unit_price, discount_pct, discount_flat, final_price, services(name)').eq('commitment_id', eventId).order('created_at', { ascending: true });
|
||||
const { data, error } = await tenantDb().from('commitment_services').select('service_id, quantity, unit_price, discount_pct, discount_flat, final_price, services(name)').eq('commitment_id', eventId).order('created_at', { ascending: true });
|
||||
|
||||
if (error) throw error;
|
||||
return (data || []).map(_mapRow);
|
||||
@@ -73,7 +74,7 @@ export function useCommitmentServices() {
|
||||
if (!eventId) throw new Error('eventId é obrigatório para salvar commitment_services.');
|
||||
|
||||
// 1. Remove itens existentes deste evento
|
||||
const { error: deleteError } = await supabase.from('commitment_services').delete().eq('commitment_id', eventId);
|
||||
const { error: deleteError } = await tenantDb().from('commitment_services').delete().eq('commitment_id', eventId);
|
||||
|
||||
if (deleteError) throw deleteError;
|
||||
|
||||
@@ -89,14 +90,14 @@ export function useCommitmentServices() {
|
||||
final_price: item.final_price
|
||||
}));
|
||||
|
||||
const { error: insertError } = await supabase.from('commitment_services').insert(rows);
|
||||
const { error: insertError } = await tenantDb().from('commitment_services').insert(rows);
|
||||
|
||||
if (insertError) throw insertError;
|
||||
}
|
||||
|
||||
// 3. Marca a ocorrência como customizada (impede sobrescrita por edições do raiz)
|
||||
if (markCustomized) {
|
||||
const { error: updateError } = await supabase.from('agenda_eventos').update({ services_customized: true }).eq('id', eventId);
|
||||
const { error: updateError } = await tenantDb().from('agenda_eventos').update({ services_customized: true }).eq('id', eventId);
|
||||
|
||||
if (updateError) throw updateError;
|
||||
}
|
||||
@@ -107,7 +108,7 @@ export function useCommitmentServices() {
|
||||
async function loadRuleItems(ruleId) {
|
||||
if (!ruleId) return [];
|
||||
|
||||
const { data, error } = await supabase.from('recurrence_rule_services').select('service_id, quantity, unit_price, discount_pct, discount_flat, final_price, services(name)').eq('rule_id', ruleId).order('created_at', { ascending: true });
|
||||
const { data, error } = await tenantDb().from('recurrence_rule_services').select('service_id, quantity, unit_price, discount_pct, discount_flat, final_price, services(name)').eq('rule_id', ruleId).order('created_at', { ascending: true });
|
||||
|
||||
if (error) throw error;
|
||||
return (data || []).map(_mapRow);
|
||||
@@ -120,7 +121,7 @@ export function useCommitmentServices() {
|
||||
async function saveRuleItems(ruleId, items) {
|
||||
if (!ruleId) throw new Error('ruleId é obrigatório para salvar recurrence_rule_services.');
|
||||
|
||||
const { error: deleteError } = await supabase.from('recurrence_rule_services').delete().eq('rule_id', ruleId);
|
||||
const { error: deleteError } = await tenantDb().from('recurrence_rule_services').delete().eq('rule_id', ruleId);
|
||||
|
||||
if (deleteError) throw deleteError;
|
||||
|
||||
@@ -136,7 +137,7 @@ export function useCommitmentServices() {
|
||||
final_price: item.final_price
|
||||
}));
|
||||
|
||||
const { error: insertError } = await supabase.from('recurrence_rule_services').insert(rows);
|
||||
const { error: insertError } = await tenantDb().from('recurrence_rule_services').insert(rows);
|
||||
|
||||
if (insertError) throw insertError;
|
||||
}
|
||||
@@ -171,7 +172,7 @@ export function useCommitmentServices() {
|
||||
if (!ruleId) return;
|
||||
|
||||
// Busca IDs das ocorrências materializadas elegíveis
|
||||
let q = supabase.from('agenda_eventos').select('id').eq('recurrence_id', ruleId);
|
||||
let q = tenantDb().from('agenda_eventos').select('id').eq('recurrence_id', ruleId);
|
||||
|
||||
if (!ignoreCustomized) {
|
||||
q = q.eq('services_customized', false);
|
||||
@@ -189,8 +190,7 @@ export function useCommitmentServices() {
|
||||
// em batch evita N round-trips. Status considerados imutáveis: pending,
|
||||
// paid, overdue. cancelled é ok propagar (record foi descartado).
|
||||
const eventIds = events.map((e) => e.id);
|
||||
const { data: lockedEvents, error: frErr } = await supabase
|
||||
.from('financial_records')
|
||||
const { data: lockedEvents, error: frErr } = await tenantDb().from('financial_records')
|
||||
.select('agenda_evento_id')
|
||||
.in('agenda_evento_id', eventIds)
|
||||
.in('status', ['pending', 'paid', 'overdue']);
|
||||
@@ -202,7 +202,7 @@ export function useCommitmentServices() {
|
||||
|
||||
// Para cada evento elegível: delete + insert (padrão idempotente)
|
||||
for (const ev of eligibleEvents) {
|
||||
const { error: delErr } = await supabase.from('commitment_services').delete().eq('commitment_id', ev.id);
|
||||
const { error: delErr } = await tenantDb().from('commitment_services').delete().eq('commitment_id', ev.id);
|
||||
if (delErr) throw delErr;
|
||||
|
||||
if (items?.length) {
|
||||
@@ -215,7 +215,7 @@ export function useCommitmentServices() {
|
||||
discount_flat: item.discount_flat ?? 0,
|
||||
final_price: item.final_price
|
||||
}));
|
||||
const { error: insErr } = await supabase.from('commitment_services').insert(rows);
|
||||
const { error: insErr } = await tenantDb().from('commitment_services').insert(rows);
|
||||
if (insErr) throw insErr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
export function useDeterminedCommitments(tenantIdRef) {
|
||||
const loading = ref(false);
|
||||
const error = ref('');
|
||||
@@ -39,10 +40,9 @@ export function useDeterminedCommitments(tenantIdRef) {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
|
||||
const { data, error: err } = await supabase
|
||||
.from('determined_commitments')
|
||||
.select('id,tenant_id,created_by,is_native,native_key,is_locked,active,name,description,bg_color,text_color,created_at,determined_commitment_fields(id,key,label,field_type,required,sort_order)')
|
||||
.eq('tenant_id', tenantId.value) // ✅ SOMENTE tenant corrente
|
||||
const { data, error: err } = await tenantDb().from('determined_commitments')
|
||||
.select('id,created_by,is_native,native_key,is_locked,active,name,description,bg_color,text_color,created_at,determined_commitment_fields(id,key,label,field_type,required,sort_order)')
|
||||
// ✅ SOMENTE tenant corrente
|
||||
.eq('active', true)
|
||||
.order('is_native', { ascending: false })
|
||||
.order('name', { ascending: true });
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
import { ref } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
export function useFinancialExceptions() {
|
||||
const exceptions = ref([]);
|
||||
const loading = ref(false);
|
||||
@@ -39,7 +40,7 @@ export function useFinancialExceptions() {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const { data, error: err } = await supabase.from('financial_exceptions').select('*').or(`owner_id.eq.${ownerId},owner_id.is.null`).order('exception_type', { ascending: true }).order('created_at', { ascending: true });
|
||||
const { data, error: err } = await tenantDb().from('financial_exceptions').select('*').or(`owner_id.eq.${ownerId},owner_id.is.null`).order('exception_type', { ascending: true }).order('created_at', { ascending: true });
|
||||
|
||||
if (err) throw err;
|
||||
exceptions.value = data || [];
|
||||
@@ -60,8 +61,7 @@ export function useFinancialExceptions() {
|
||||
error.value = '';
|
||||
try {
|
||||
if (payload.id) {
|
||||
const { error: err } = await supabase
|
||||
.from('financial_exceptions')
|
||||
const { error: err } = await tenantDb().from('financial_exceptions')
|
||||
.update({
|
||||
charge_mode: payload.charge_mode,
|
||||
charge_value: payload.charge_value ?? null,
|
||||
@@ -72,9 +72,8 @@ export function useFinancialExceptions() {
|
||||
.eq('id', payload.id);
|
||||
if (err) throw err;
|
||||
} else {
|
||||
const { error: err } = await supabase.from('financial_exceptions').insert({
|
||||
const { error: err } = await tenantDb().from('financial_exceptions').insert({
|
||||
owner_id: payload.owner_id,
|
||||
tenant_id: payload.tenant_id ?? null,
|
||||
exception_type: payload.exception_type,
|
||||
charge_mode: payload.charge_mode,
|
||||
charge_value: payload.charge_value ?? null,
|
||||
@@ -96,7 +95,7 @@ export function useFinancialExceptions() {
|
||||
async function remove(id) {
|
||||
error.value = '';
|
||||
try {
|
||||
const { error: err } = await supabase.from('financial_exceptions').delete().eq('id', id);
|
||||
const { error: err } = await tenantDb().from('financial_exceptions').delete().eq('id', id);
|
||||
if (err) throw err;
|
||||
exceptions.value = exceptions.value.filter((e) => e.id !== id);
|
||||
} catch (e) {
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
import { ref } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
export function useInsurancePlans() {
|
||||
const plans = ref([]);
|
||||
const loading = ref(false);
|
||||
@@ -40,8 +41,7 @@ export function useInsurancePlans() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const { data, error: err } = await supabase
|
||||
.from('insurance_plans')
|
||||
const { data, error: err } = await tenantDb().from('insurance_plans')
|
||||
.select(
|
||||
`
|
||||
*,
|
||||
@@ -66,8 +66,7 @@ export function useInsurancePlans() {
|
||||
error.value = null;
|
||||
try {
|
||||
if (payload.id) {
|
||||
const { error: err } = await supabase
|
||||
.from('insurance_plans')
|
||||
const { error: err } = await tenantDb().from('insurance_plans')
|
||||
.update({
|
||||
name: payload.name,
|
||||
notes: payload.notes || null,
|
||||
@@ -76,9 +75,8 @@ export function useInsurancePlans() {
|
||||
.eq('id', payload.id);
|
||||
if (err) throw err;
|
||||
} else {
|
||||
const { error: err } = await supabase.from('insurance_plans').insert({
|
||||
const { error: err } = await tenantDb().from('insurance_plans').insert({
|
||||
owner_id: payload.owner_id,
|
||||
tenant_id: payload.tenant_id,
|
||||
name: payload.name,
|
||||
notes: payload.notes || null
|
||||
});
|
||||
@@ -93,7 +91,7 @@ export function useInsurancePlans() {
|
||||
async function toggle(id, active) {
|
||||
error.value = null;
|
||||
try {
|
||||
const { error: err } = await supabase.from('insurance_plans').update({ active }).eq('id', id);
|
||||
const { error: err } = await tenantDb().from('insurance_plans').update({ active }).eq('id', id);
|
||||
if (err) throw err;
|
||||
const plan = plans.value.find((p) => p.id === id);
|
||||
if (plan) plan.active = active;
|
||||
@@ -106,7 +104,7 @@ export function useInsurancePlans() {
|
||||
async function remove(id) {
|
||||
error.value = null;
|
||||
try {
|
||||
const { error: err } = await supabase.from('insurance_plans').update({ active: false }).eq('id', id);
|
||||
const { error: err } = await tenantDb().from('insurance_plans').update({ active: false }).eq('id', id);
|
||||
if (err) throw err;
|
||||
const plan = plans.value.find((p) => p.id === id);
|
||||
if (plan) plan.active = false;
|
||||
@@ -120,8 +118,7 @@ export function useInsurancePlans() {
|
||||
error.value = null;
|
||||
try {
|
||||
if (payload.id) {
|
||||
const { error: err } = await supabase
|
||||
.from('insurance_plan_services')
|
||||
const { error: err } = await tenantDb().from('insurance_plan_services')
|
||||
.update({
|
||||
name: payload.name,
|
||||
value: payload.value
|
||||
@@ -129,7 +126,7 @@ export function useInsurancePlans() {
|
||||
.eq('id', payload.id);
|
||||
if (err) throw err;
|
||||
} else {
|
||||
const { error: err } = await supabase.from('insurance_plan_services').insert({
|
||||
const { error: err } = await tenantDb().from('insurance_plan_services').insert({
|
||||
insurance_plan_id: payload.insurance_plan_id,
|
||||
name: payload.name,
|
||||
value: payload.value
|
||||
@@ -145,7 +142,7 @@ export function useInsurancePlans() {
|
||||
async function togglePlanService(id, active) {
|
||||
error.value = null;
|
||||
try {
|
||||
const { error: err } = await supabase.from('insurance_plan_services').update({ active }).eq('id', id);
|
||||
const { error: err } = await tenantDb().from('insurance_plan_services').update({ active }).eq('id', id);
|
||||
if (err) throw err;
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Erro ao atualizar procedimento';
|
||||
@@ -156,7 +153,7 @@ export function useInsurancePlans() {
|
||||
async function removeDefinitivo(id) {
|
||||
error.value = null;
|
||||
try {
|
||||
const { error: err } = await supabase.from('insurance_plans').delete().eq('id', id);
|
||||
const { error: err } = await tenantDb().from('insurance_plans').delete().eq('id', id);
|
||||
if (err) throw err;
|
||||
plans.value = plans.value.filter((p) => p.id !== id);
|
||||
} catch (e) {
|
||||
@@ -168,7 +165,7 @@ export function useInsurancePlans() {
|
||||
async function removePlanService(id) {
|
||||
error.value = null;
|
||||
try {
|
||||
const { error: err } = await supabase.from('insurance_plan_services').delete().eq('id', id);
|
||||
const { error: err } = await tenantDb().from('insurance_plan_services').delete().eq('id', id);
|
||||
if (err) throw err;
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Erro ao remover procedimento';
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
import { ref } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
export function usePatientDiscounts() {
|
||||
const discounts = ref([]);
|
||||
const loading = ref(false);
|
||||
@@ -40,7 +41,7 @@ export function usePatientDiscounts() {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const { data, error: err } = await supabase.from('patient_discounts').select('*').eq('owner_id', ownerId).order('created_at', { ascending: false });
|
||||
const { data, error: err } = await tenantDb().from('patient_discounts').select('*').eq('owner_id', ownerId).order('created_at', { ascending: false });
|
||||
|
||||
if (err) throw err;
|
||||
discounts.value = data || [];
|
||||
@@ -53,17 +54,19 @@ export function usePatientDiscounts() {
|
||||
}
|
||||
|
||||
// ── Criar ou atualizar um desconto ───────────────────────────────────
|
||||
// payload deve conter: { owner_id, tenant_id, patient_id, discount_pct, discount_flat, ... }
|
||||
// payload deve conter: { owner_id, patient_id, discount_pct, discount_flat, ... }
|
||||
// Se payload.id estiver presente, faz UPDATE; caso contrário, INSERT.
|
||||
async function save(payload) {
|
||||
error.value = '';
|
||||
try {
|
||||
if (payload.id) {
|
||||
const { id, owner_id, tenant_id, ...fields } = payload;
|
||||
const { error: err } = await supabase.from('patient_discounts').update(fields).eq('id', id).eq('owner_id', owner_id);
|
||||
const { error: err } = await tenantDb().from('patient_discounts').update(fields).eq('id', id).eq('owner_id', owner_id);
|
||||
if (err) throw err;
|
||||
} else {
|
||||
const { error: err } = await supabase.from('patient_discounts').insert(payload);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { tenant_id: _dropTenantId, ...insertFields } = payload;
|
||||
const { error: err } = await tenantDb().from('patient_discounts').insert(insertFields);
|
||||
if (err) throw err;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -76,7 +79,7 @@ export function usePatientDiscounts() {
|
||||
async function remove(id) {
|
||||
error.value = '';
|
||||
try {
|
||||
const { error: err } = await supabase.from('patient_discounts').update({ active: false }).eq('id', id);
|
||||
const { error: err } = await tenantDb().from('patient_discounts').update({ active: false }).eq('id', id);
|
||||
if (err) throw err;
|
||||
discounts.value = discounts.value.filter((d) => d.id !== id);
|
||||
} catch (e) {
|
||||
@@ -95,8 +98,7 @@ export function usePatientDiscounts() {
|
||||
if (!ownerId || !patientId) return null;
|
||||
try {
|
||||
const now = new Date().toISOString();
|
||||
const { data, error: err } = await supabase
|
||||
.from('patient_discounts')
|
||||
const { data, error: err } = await tenantDb().from('patient_discounts')
|
||||
.select('*')
|
||||
.eq('owner_id', ownerId)
|
||||
.eq('patient_id', patientId)
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import { ref } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
export function useProfessionalPricing() {
|
||||
const rows = ref([]); // professional_pricing rows
|
||||
const loading = ref(false);
|
||||
@@ -34,7 +35,7 @@ export function useProfessionalPricing() {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const { data, error: err } = await supabase.from('professional_pricing').select('id, determined_commitment_id, price, notes').eq('owner_id', ownerId);
|
||||
const { data, error: err } = await tenantDb().from('professional_pricing').select('id, determined_commitment_id, price, notes').eq('owner_id', ownerId);
|
||||
|
||||
if (err) throw err;
|
||||
rows.value = data || [];
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { assertTenantId } from '@/features/agenda/services/_tenantGuards';
|
||||
import { logRecurrence, logError, logPerf } from '@/support/supportLogger';
|
||||
@@ -326,7 +327,6 @@ function buildOccurrence(rule, date, originalIso, exception) {
|
||||
owner_id: rule.owner_id,
|
||||
therapist_id: rule.therapist_id,
|
||||
terapeuta_id: rule.therapist_id,
|
||||
tenant_id: rule.tenant_id,
|
||||
|
||||
// nome do paciente — injetado pelo loadAndExpand via _patient
|
||||
paciente_nome: rule._patient?.nome_completo ?? null,
|
||||
@@ -452,12 +452,7 @@ export function useRecurrence() {
|
||||
// Busca regras sem end_date (abertas) + regras com end_date >= rangeStart
|
||||
// Dois selects separados evitam problemas com .or() + .is.null no Supabase JS
|
||||
const baseQuery = () => {
|
||||
let q = supabase.from('recurrence_rules').select('*').eq('owner_id', ownerId).eq('status', 'ativo').lte('start_date', endISO).order('start_date', { ascending: true });
|
||||
// Filtra por tenant quando disponível — defesa em profundidade
|
||||
if (tenantId && tenantId !== 'null' && tenantId !== 'undefined') {
|
||||
q = q.eq('tenant_id', tenantId);
|
||||
}
|
||||
return q;
|
||||
return tenantDb().from('recurrence_rules').select('*').eq('owner_id', ownerId).eq('status', 'ativo').lte('start_date', endISO).order('start_date', { ascending: true });
|
||||
};
|
||||
|
||||
const [resOpen, resWithEnd] = await Promise.all([baseQuery().is('end_date', null), baseQuery().gte('end_date', startISO).not('end_date', 'is', null)]);
|
||||
@@ -504,11 +499,11 @@ export function useRecurrence() {
|
||||
const endISO = toISO(rangeEnd);
|
||||
|
||||
// Query 1 — comportamento original: exceções cujo original_date está no range
|
||||
const q1 = supabase.from('recurrence_exceptions').select('*').in('recurrence_id', ids).gte('original_date', startISO).lte('original_date', endISO);
|
||||
const q1 = tenantDb().from('recurrence_exceptions').select('*').in('recurrence_id', ids).gte('original_date', startISO).lte('original_date', endISO);
|
||||
|
||||
// Query 2 — bug fix: remarcações cujo new_date cai neste range
|
||||
// (original_date pode estar antes ou depois do range)
|
||||
const q2 = supabase.from('recurrence_exceptions').select('*').in('recurrence_id', ids).eq('type', 'reschedule_session').not('new_date', 'is', null).gte('new_date', startISO).lte('new_date', endISO);
|
||||
const q2 = tenantDb().from('recurrence_exceptions').select('*').in('recurrence_id', ids).eq('type', 'reschedule_session').not('new_date', 'is', null).gte('new_date', startISO).lte('new_date', endISO);
|
||||
|
||||
const [res1, res2] = await Promise.all([q1, q2]);
|
||||
|
||||
@@ -550,7 +545,7 @@ export function useRecurrence() {
|
||||
// Busca nomes dos pacientes das regras carregadas
|
||||
const patientIds = [...new Set(rules.value.map((r) => r.patient_id).filter(Boolean))];
|
||||
if (patientIds.length) {
|
||||
const { data: patients } = await supabase.from('patients').select('id, nome_completo, avatar_url').in('id', patientIds);
|
||||
const { data: patients } = await tenantDb().from('patients').select('id, nome_completo, avatar_url').in('id', patientIds);
|
||||
// injeta nome diretamente na regra para o buildOccurrence usar
|
||||
const pMap = new Map((patients || []).map((p) => [p.id, p]));
|
||||
for (const rule of rules.value) {
|
||||
@@ -579,15 +574,14 @@ export function useRecurrence() {
|
||||
|
||||
/**
|
||||
* Cria uma nova regra de recorrência.
|
||||
* tenant_id é injetado do tenantStore se não vier no payload (defesa em profundidade).
|
||||
* tenant_id é dropado defensivamente — schema-per-tenant não tem essa coluna.
|
||||
* @param {Object} rule - campos da tabela recurrence_rules
|
||||
* @returns {Object} regra criada
|
||||
*/
|
||||
async function createRule(rule) {
|
||||
const tenantId = currentTenantId();
|
||||
logRecurrence('createRule →', { patient_id: rule?.patient_id, type: rule?.type });
|
||||
const safeRule = { ...rule, tenant_id: rule?.tenant_id || tenantId };
|
||||
const { data, error: err } = await supabase.from('recurrence_rules').insert([safeRule]).select('*').single();
|
||||
const { tenant_id: _dropTenantId, ...safeRule } = rule || {};
|
||||
const { data, error: err } = await tenantDb().from('recurrence_rules').insert([safeRule]).select('*').single();
|
||||
if (err) {
|
||||
logError('useRecurrence', 'createRule ERRO', err);
|
||||
throw err;
|
||||
@@ -598,15 +592,14 @@ export function useRecurrence() {
|
||||
|
||||
/**
|
||||
* Atualiza a regra toda (editar todos).
|
||||
* Filtro adicional por tenant_id — defesa em profundidade (RLS cobre, mas reforçamos).
|
||||
* Isolamento multi-tenant garantido pelo schema do tenant (tenantDb).
|
||||
*/
|
||||
async function updateRule(id, patch) {
|
||||
const tenantId = currentTenantId();
|
||||
const { data, error: err } = await supabase
|
||||
.from('recurrence_rules')
|
||||
const { data, error: err } = await tenantDb().from('recurrence_rules')
|
||||
.update({ ...patch, updated_at: new Date().toISOString() })
|
||||
.eq('id', id)
|
||||
.eq('tenant_id', tenantId)
|
||||
|
||||
.select('*')
|
||||
.single();
|
||||
if (err) throw err;
|
||||
@@ -614,15 +607,14 @@ export function useRecurrence() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancela a série inteira (filtro por tenant_id — defesa em profundidade).
|
||||
* Cancela a série inteira.
|
||||
*/
|
||||
async function cancelRule(id) {
|
||||
const tenantId = currentTenantId();
|
||||
const { error: err } = await supabase
|
||||
.from('recurrence_rules')
|
||||
const { error: err } = await tenantDb().from('recurrence_rules')
|
||||
.update({ status: 'cancelado', updated_at: new Date().toISOString() })
|
||||
.eq('id', id)
|
||||
.eq('tenant_id', tenantId);
|
||||
;
|
||||
if (err) throw err;
|
||||
}
|
||||
|
||||
@@ -654,13 +646,11 @@ export function useRecurrence() {
|
||||
|
||||
/**
|
||||
* Cria ou atualiza uma exceção para uma ocorrência específica.
|
||||
* tenant_id é injetado do tenantStore se não vier no payload.
|
||||
* tenant_id é dropado defensivamente — schema-per-tenant não tem essa coluna.
|
||||
*/
|
||||
async function upsertException(ex) {
|
||||
const tenantId = currentTenantId();
|
||||
const safeEx = { ...ex, tenant_id: ex?.tenant_id || tenantId };
|
||||
const { data, error: err } = await supabase
|
||||
.from('recurrence_exceptions')
|
||||
const { tenant_id: _dropTenantId, ...safeEx } = ex || {};
|
||||
const { data, error: err } = await tenantDb().from('recurrence_exceptions')
|
||||
.upsert([safeEx], { onConflict: 'recurrence_id,original_date' })
|
||||
.select('*')
|
||||
.single();
|
||||
@@ -670,16 +660,14 @@ export function useRecurrence() {
|
||||
|
||||
/**
|
||||
* Remove uma exceção (restaura a ocorrência ao normal).
|
||||
* Filtro por tenant_id — defesa em profundidade.
|
||||
*/
|
||||
async function deleteException(recurrenceId, originalDate) {
|
||||
const tenantId = currentTenantId();
|
||||
const { error: err } = await supabase
|
||||
.from('recurrence_exceptions')
|
||||
const { error: err } = await tenantDb().from('recurrence_exceptions')
|
||||
.delete()
|
||||
.eq('recurrence_id', recurrenceId)
|
||||
.eq('original_date', originalDate)
|
||||
.eq('tenant_id', tenantId);
|
||||
;
|
||||
if (err) throw err;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
import { ref } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
export function useServices() {
|
||||
const services = ref([]);
|
||||
const loading = ref(false);
|
||||
@@ -39,7 +40,7 @@ export function useServices() {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const { data, error: err } = await supabase.from('services').select('id, name, description, price, duration_min, active').eq('owner_id', ownerId).order('created_at', { ascending: true });
|
||||
const { data, error: err } = await tenantDb().from('services').select('id, name, description, price, duration_min, active').eq('owner_id', ownerId).order('created_at', { ascending: true });
|
||||
|
||||
if (err) throw err;
|
||||
services.value = data || [];
|
||||
@@ -61,7 +62,7 @@ export function useServices() {
|
||||
// Nome unico por owner (case-insensitive). No update,
|
||||
// ignora o proprio id pra nao conflitar consigo mesmo
|
||||
// quando o usuario salva sem mudar o nome.
|
||||
let dupQuery = supabase.from('services').select('id').eq('owner_id', payload.owner_id).ilike('name', name).limit(1);
|
||||
let dupQuery = tenantDb().from('services').select('id').eq('owner_id', payload.owner_id).ilike('name', name).limit(1);
|
||||
if (payload.id) dupQuery = dupQuery.neq('id', payload.id);
|
||||
const { data: dups, error: dupErr } = await dupQuery;
|
||||
if (dupErr) throw dupErr;
|
||||
@@ -71,10 +72,12 @@ export function useServices() {
|
||||
|
||||
if (payload.id) {
|
||||
const { id, owner_id, tenant_id, ...fields } = payload;
|
||||
const { error: err } = await supabase.from('services').update(fields).eq('id', id).eq('owner_id', owner_id);
|
||||
const { error: err } = await tenantDb().from('services').update(fields).eq('id', id).eq('owner_id', owner_id);
|
||||
if (err) throw err;
|
||||
} else {
|
||||
const { error: err } = await supabase.from('services').insert(payload);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { tenant_id: _dropTenantId, ...insertFields } = payload;
|
||||
const { error: err } = await tenantDb().from('services').insert(insertFields);
|
||||
if (err) throw err;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -86,7 +89,7 @@ export function useServices() {
|
||||
async function toggle(id, active) {
|
||||
error.value = '';
|
||||
try {
|
||||
const { error: err } = await supabase.from('services').update({ active }).eq('id', id);
|
||||
const { error: err } = await tenantDb().from('services').update({ active }).eq('id', id);
|
||||
if (err) throw err;
|
||||
const svc = services.value.find((s) => s.id === id);
|
||||
if (svc) svc.active = active;
|
||||
@@ -99,7 +102,7 @@ export function useServices() {
|
||||
async function remove(id) {
|
||||
error.value = '';
|
||||
try {
|
||||
const { error: err } = await supabase.from('services').delete().eq('id', id);
|
||||
const { error: err } = await tenantDb().from('services').delete().eq('id', id);
|
||||
if (err) throw err;
|
||||
services.value = services.value.filter((s) => s.id !== id);
|
||||
} catch (e) {
|
||||
|
||||
@@ -47,6 +47,7 @@ import { useAgendaSettings } from '@/features/agenda/composables/useAgendaSettin
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const toast = useToast();
|
||||
@@ -677,12 +678,11 @@ async function loadMonthSearchRows() {
|
||||
const end = new Date(d.getFullYear(), d.getMonth() + 1, 1).toISOString();
|
||||
monthSearchLoading.value = true;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_eventos')
|
||||
const { data, error } = await tenantDb().from('agenda_eventos')
|
||||
.select(
|
||||
'id, owner_id, tenant_id, tipo, status, titulo, inicio_em, fim_em, observacoes, modalidade, determined_commitment_id, insurance_plan_id, insurance_guide_number, insurance_value, insurance_plan_service_id, patients!agenda_eventos_patient_id_fkey(nome_completo)'
|
||||
'id, owner_id, tipo, status, titulo, inicio_em, fim_em, observacoes, modalidade, determined_commitment_id, insurance_plan_id, insurance_guide_number, insurance_value, insurance_plan_service_id, patients!agenda_eventos_patient_id_fkey(nome_completo)'
|
||||
)
|
||||
.eq('tenant_id', tid)
|
||||
|
||||
.in('owner_id', ids)
|
||||
.is('mirror_of_event_id', null)
|
||||
.gte('inicio_em', start)
|
||||
@@ -915,7 +915,7 @@ async function debugPatientsForColumn(staffUserId) {
|
||||
console.log('tenant_member_id (mapeado):', memberId);
|
||||
|
||||
try {
|
||||
const { count, error } = await supabase.from('patients').select('id', { count: 'exact', head: true }).eq('tenant_id', tid);
|
||||
const { count, error } = await tenantDb().from('patients').select('id', { count: 'exact', head: true });
|
||||
if (error) throw error;
|
||||
console.log('patients total no tenant:', count);
|
||||
} catch (e) {
|
||||
@@ -924,7 +924,7 @@ async function debugPatientsForColumn(staffUserId) {
|
||||
|
||||
if (memberId && isUuid(memberId)) {
|
||||
try {
|
||||
const { count, error } = await supabase.from('patients').select('id', { count: 'exact', head: true }).eq('tenant_id', tid).eq('responsible_member_id', memberId);
|
||||
const { count, error } = await tenantDb().from('patients').select('id', { count: 'exact', head: true }).eq('responsible_member_id', memberId);
|
||||
if (error) throw error;
|
||||
console.log('patients por responsible_member_id:', count);
|
||||
} catch (e) {
|
||||
@@ -932,10 +932,9 @@ async function debugPatientsForColumn(staffUserId) {
|
||||
}
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('patients')
|
||||
const { data, error } = await tenantDb().from('patients')
|
||||
.select('id,nome_completo,email_principal,telefone,responsible_member_id,created_at')
|
||||
.eq('tenant_id', tid)
|
||||
|
||||
.eq('responsible_member_id', memberId)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(5);
|
||||
@@ -1212,8 +1211,7 @@ async function onUpdateSeriesEvent({ id, status, recurrence_date, inicio_em, fim
|
||||
if (!is_virtual || !inicio_em) return;
|
||||
const rid = row.recurrence_id ?? row.serie_id ?? null;
|
||||
const rDate = recurrence_date || inicio_em?.slice(0, 10);
|
||||
const { data: existing } = await supabase
|
||||
.from('agenda_eventos')
|
||||
const { data: existing } = await tenantDb().from('agenda_eventos')
|
||||
.select('id')
|
||||
.eq('recurrence_id', rid)
|
||||
.eq('recurrence_date', rDate)
|
||||
@@ -1281,9 +1279,8 @@ async function _offerBillingContract(basePayload, recorrencia, tenantId) {
|
||||
rejectLabel: 'Agora não',
|
||||
accept: async () => {
|
||||
try {
|
||||
const { error } = await supabase.from('billing_contracts').insert({
|
||||
const { error } = await tenantDb().from('billing_contracts').insert({
|
||||
owner_id: basePayload.owner_id,
|
||||
tenant_id: tenantId,
|
||||
patient_id: basePayload.paciente_id,
|
||||
type: 'package',
|
||||
total_sessions: n,
|
||||
@@ -1454,7 +1451,7 @@ async function onDialogSave(arg) {
|
||||
extra_fields: basePayload.extra_fields ?? null
|
||||
});
|
||||
if (arg.onSaved) {
|
||||
const { data: existing } = await supabase.from('agenda_eventos').select('id').eq('recurrence_id', recurrenceId).eq('recurrence_date', originalDate).maybeSingle();
|
||||
const { data: existing } = await tenantDb().from('agenda_eventos').select('id').eq('recurrence_id', recurrenceId).eq('recurrence_date', originalDate).maybeSingle();
|
||||
if (existing?.id) {
|
||||
eventId = existing.id;
|
||||
} else {
|
||||
@@ -1550,8 +1547,7 @@ async function onDialogSave(arg) {
|
||||
});
|
||||
|
||||
// Propaga campos não-serviço para sessões já materializadas da série
|
||||
await supabase
|
||||
.from('agenda_eventos')
|
||||
await tenantDb().from('agenda_eventos')
|
||||
.update({
|
||||
modalidade: basePayload.modalidade ?? 'presencial',
|
||||
titulo_custom: basePayload.titulo_custom ?? null,
|
||||
@@ -1599,8 +1595,7 @@ async function onDialogSave(arg) {
|
||||
});
|
||||
|
||||
// Propaga TODOS os campos para TODAS as sessões materializadas (sem exceção)
|
||||
await supabase
|
||||
.from('agenda_eventos')
|
||||
await tenantDb().from('agenda_eventos')
|
||||
.update({
|
||||
modalidade: basePayload.modalidade ?? 'presencial',
|
||||
titulo_custom: basePayload.titulo_custom ?? null,
|
||||
@@ -1708,7 +1703,7 @@ async function onDialogDelete(arg) {
|
||||
|
||||
if (isVirtual) {
|
||||
const rDate = row.original_date || row.inicio_em?.slice(0, 10);
|
||||
const existing = await supabase.from('agenda_eventos').select('id').eq('recurrence_id', recurrenceId).eq('recurrence_date', rDate).maybeSingle();
|
||||
const existing = await tenantDb().from('agenda_eventos').select('id').eq('recurrence_id', recurrenceId).eq('recurrence_date', rDate).maybeSingle();
|
||||
|
||||
if (existing.data?.id) {
|
||||
await updateClinic(existing.data.id, { recurrence_id: null, recurrence_date: null }, { tenantId: tid });
|
||||
@@ -1926,8 +1921,7 @@ async function loadMiniMonthEvents(refDate) {
|
||||
try {
|
||||
const tid = tenantId.value;
|
||||
// 1. Eventos normais (bolinhas)
|
||||
let evQ = supabase.from('agenda_eventos').select('inicio_em').gte('inicio_em', start.toISOString()).lte('inicio_em', end.toISOString());
|
||||
if (tid) evQ = evQ.eq('tenant_id', tid);
|
||||
let evQ = tenantDb().from('agenda_eventos').select('inicio_em').gte('inicio_em', start.toISOString()).lte('inicio_em', end.toISOString());
|
||||
const { data: evData } = await evQ;
|
||||
|
||||
const evSet = new Set();
|
||||
@@ -1961,7 +1955,7 @@ async function loadMiniMonthEvents(refDate) {
|
||||
const isoStart = `${year}-${pad(month + 1)}-01`;
|
||||
const lastDay = new Date(year, month + 1, 0).getDate();
|
||||
const isoEnd = `${year}-${pad(month + 1)}-${pad(lastDay)}`;
|
||||
let blkQ = supabase.from('agenda_bloqueios').select('data_inicio').is('hora_inicio', null).gte('data_inicio', isoStart).lte('data_inicio', isoEnd);
|
||||
let blkQ = tenantDb().from('agenda_bloqueios').select('data_inicio').is('hora_inicio', null).gte('data_inicio', isoStart).lte('data_inicio', isoEnd);
|
||||
if (clinicOwnerId.value) blkQ = blkQ.eq('owner_id', clinicOwnerId.value);
|
||||
const { data: blkData } = await blkQ;
|
||||
miniBlockedDaySet.value = new Set((blkData || []).map((r) => r.data_inicio));
|
||||
@@ -2050,10 +2044,9 @@ async function bloquearFeriadoDoAlerta(feriado) {
|
||||
if (!clinicOwnerId.value || !tenantId.value) return;
|
||||
feriadosAlertaSalvando.value = feriado.data;
|
||||
try {
|
||||
const { error } = await supabase.from('agenda_bloqueios').insert([
|
||||
const { error } = await tenantDb().from('agenda_bloqueios').insert([
|
||||
{
|
||||
owner_id: clinicOwnerId.value,
|
||||
tenant_id: tenantId.value,
|
||||
tipo: 'bloqueio',
|
||||
recorrente: false,
|
||||
titulo: `Feriado: ${feriado.nome}`,
|
||||
@@ -2065,7 +2058,7 @@ async function bloquearFeriadoDoAlerta(feriado) {
|
||||
}
|
||||
]);
|
||||
if (error) throw error;
|
||||
await supabase.from('agenda_eventos').update({ status: 'remarcado' }).eq('owner_id', clinicOwnerId.value).eq('tipo', 'sessao').gte('inicio_em', `${feriado.data}T00:00:00`).lte('inicio_em', `${feriado.data}T23:59:59`);
|
||||
await tenantDb().from('agenda_eventos').update({ status: 'remarcado' }).eq('owner_id', clinicOwnerId.value).eq('tipo', 'sessao').gte('inicio_em', `${feriado.data}T00:00:00`).lte('inicio_em', `${feriado.data}T23:59:59`);
|
||||
|
||||
feriadosAlertaBloqueados.value = new Set([...feriadosAlertaBloqueados.value, feriado.data]);
|
||||
miniBlockedDaySet.value = new Set([...miniBlockedDaySet.value, feriado.data]);
|
||||
@@ -2088,7 +2081,7 @@ async function desbloquearFeriadoDoAlerta(feriado) {
|
||||
if (!clinicOwnerId.value) return;
|
||||
feriadosAlertaSalvando.value = `unblock_${feriado.data}`;
|
||||
try {
|
||||
const { error } = await supabase.from('agenda_bloqueios').delete().eq('owner_id', clinicOwnerId.value).eq('data_inicio', feriado.data).in('origem', ['agenda_feriado', 'agenda_dia']);
|
||||
const { error } = await tenantDb().from('agenda_bloqueios').delete().eq('owner_id', clinicOwnerId.value).eq('data_inicio', feriado.data).in('origem', ['agenda_feriado', 'agenda_dia']);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useAgendaClinicStaff } from '@/features/agenda/composables/useAgendaClinicStaff';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
@@ -78,11 +79,10 @@ async function load() {
|
||||
if (!userId.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
let q = supabase.from('recurrence_rules').select('*').order('start_date', { ascending: false });
|
||||
let q = tenantDb().from('recurrence_rules').select('*').order('start_date', { ascending: false });
|
||||
|
||||
if (isClinic.value) {
|
||||
if (!tenantId.value) return;
|
||||
q = q.eq('tenant_id', tenantId.value);
|
||||
if (filterOwner.value) q = q.eq('owner_id', filterOwner.value);
|
||||
} else {
|
||||
q = q.eq('owner_id', userId.value);
|
||||
@@ -97,7 +97,7 @@ async function load() {
|
||||
const patientIds = [...new Set(rawRules.map((r) => r.patient_id).filter(Boolean))];
|
||||
const patientMap = {};
|
||||
if (patientIds.length) {
|
||||
const { data: pts } = await supabase.from('patients').select('id, nome_completo, avatar_url').in('id', patientIds);
|
||||
const { data: pts } = await tenantDb().from('patients').select('id, nome_completo, avatar_url').in('id', patientIds);
|
||||
for (const p of pts || []) patientMap[p.id] = p;
|
||||
}
|
||||
for (const r of rawRules) r._patient = patientMap[r.patient_id] || null;
|
||||
@@ -115,8 +115,8 @@ async function load() {
|
||||
|
||||
async function reloadSessions(ruleIds) {
|
||||
const [exRes, sessRes] = await Promise.all([
|
||||
supabase.from('recurrence_exceptions').select('*').in('recurrence_id', ruleIds).order('original_date'),
|
||||
supabase.from('agenda_eventos').select('id, recurrence_id, recurrence_date, status, inicio_em, fim_em').in('recurrence_id', ruleIds).order('inicio_em')
|
||||
tenantDb().from('recurrence_exceptions').select('*').in('recurrence_id', ruleIds).order('original_date'),
|
||||
tenantDb().from('agenda_eventos').select('id, recurrence_id, recurrence_date, status, inicio_em, fim_em').in('recurrence_id', ruleIds).order('inicio_em')
|
||||
]);
|
||||
const exm = {};
|
||||
for (const ex of exRes.data || []) {
|
||||
@@ -254,17 +254,16 @@ const PILL_CLASS = {
|
||||
async function onPillStatusChange(rule, s, newStatus) {
|
||||
try {
|
||||
if (s.real_id) {
|
||||
await supabase.from('agenda_eventos').update({ status: newStatus }).eq('id', s.real_id);
|
||||
await tenantDb().from('agenda_eventos').update({ status: newStatus }).eq('id', s.real_id);
|
||||
} else {
|
||||
const { data: ex } = await supabase.from('agenda_eventos').select('id').eq('recurrence_id', rule.id).eq('recurrence_date', s.date).maybeSingle();
|
||||
const { data: ex } = await tenantDb().from('agenda_eventos').select('id').eq('recurrence_id', rule.id).eq('recurrence_date', s.date).maybeSingle();
|
||||
if (ex?.id) {
|
||||
await supabase.from('agenda_eventos').update({ status: newStatus }).eq('id', ex.id);
|
||||
await tenantDb().from('agenda_eventos').update({ status: newStatus }).eq('id', ex.id);
|
||||
} else {
|
||||
await supabase.from('agenda_eventos').insert({
|
||||
await tenantDb().from('agenda_eventos').insert({
|
||||
recurrence_id: rule.id,
|
||||
recurrence_date: s.date,
|
||||
owner_id: rule.owner_id,
|
||||
tenant_id: rule.tenant_id,
|
||||
tipo: 'sessao',
|
||||
status: newStatus,
|
||||
inicio_em: s.date + 'T' + (rule.start_time || '00:00') + ':00',
|
||||
@@ -287,7 +286,7 @@ async function onCancelRule(rule) {
|
||||
const name = rule._patient?.nome_completo || 'paciente';
|
||||
if (!confirm(`Encerrar a série de "${name}"?\n\nSessões futuras deixarão de ser geradas. Sessões passadas já registradas são mantidas.`)) return;
|
||||
try {
|
||||
await supabase.from('recurrence_rules').update({ status: 'cancelado', updated_at: new Date().toISOString() }).eq('id', rule.id);
|
||||
await tenantDb().from('recurrence_rules').update({ status: 'cancelado', updated_at: new Date().toISOString() }).eq('id', rule.id);
|
||||
toast.add({ severity: 'success', summary: 'Série encerrada', life: 2000 });
|
||||
await load();
|
||||
} catch (e) {
|
||||
@@ -297,7 +296,7 @@ async function onCancelRule(rule) {
|
||||
|
||||
async function onReactivateRule(rule) {
|
||||
try {
|
||||
await supabase.from('recurrence_rules').update({ status: 'ativo', updated_at: new Date().toISOString() }).eq('id', rule.id);
|
||||
await tenantDb().from('recurrence_rules').update({ status: 'ativo', updated_at: new Date().toISOString() }).eq('id', rule.id);
|
||||
toast.add({ severity: 'success', summary: 'Série reativada', life: 2000 });
|
||||
await load();
|
||||
} catch (e) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useRouter, useRoute } from 'vue-router';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
|
||||
@@ -606,8 +607,7 @@ async function loadMonthSearchRows() {
|
||||
try {
|
||||
// 1. Eventos reais do banco — inclui recurrence_id/recurrence_date para
|
||||
// mergeWithStoredSessions deduplicar sessões materializadas de séries.
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_eventos')
|
||||
const { data, error } = await tenantDb().from('agenda_eventos')
|
||||
.select(
|
||||
'id, owner_id, tipo, status, titulo, inicio_em, fim_em, observacoes, modalidade, determined_commitment_id, insurance_plan_id, insurance_guide_number, insurance_value, insurance_plan_service_id, recurrence_id, recurrence_date, patients!agenda_eventos_patient_id_fkey(nome_completo, status)'
|
||||
)
|
||||
@@ -981,7 +981,7 @@ async function loadAgendadorSlug() {
|
||||
const uid = ownerId.value;
|
||||
if (!uid) return;
|
||||
try {
|
||||
const { data } = await supabase.from('agendador_configuracoes').select('link_slug').eq('owner_id', uid).eq('ativo', true).maybeSingle();
|
||||
const { data } = await tenantDb().from('agendador_configuracoes').select('link_slug').eq('owner_id', uid).eq('ativo', true).maybeSingle();
|
||||
agendadorSlug.value = data?.link_slug || '';
|
||||
} catch {
|
||||
agendadorSlug.value = '';
|
||||
@@ -993,7 +993,7 @@ async function loadCadastroToken() {
|
||||
const { data: authData } = await supabase.auth.getUser();
|
||||
const uid = authData?.user?.id;
|
||||
if (!uid) return;
|
||||
const { data } = await supabase.from('patient_invites').select('token').eq('owner_id', uid).eq('active', true).order('created_at', { ascending: false }).limit(1);
|
||||
const { data } = await tenantDb().from('patient_invites').select('token').eq('owner_id', uid).eq('active', true).order('created_at', { ascending: false }).limit(1);
|
||||
cadastroToken.value = data?.[0]?.token || '';
|
||||
} catch {
|
||||
cadastroToken.value = '';
|
||||
@@ -1031,7 +1031,7 @@ const desativadoFcRef = ref(null);
|
||||
async function loadDesativados() {
|
||||
if (!ownerId.value) return;
|
||||
try {
|
||||
const { data: pats, error: pErr } = await supabase.from('patients').select('id, nome_completo, status').eq('owner_id', ownerId.value).in('status', ['Inativo', 'Arquivado']);
|
||||
const { data: pats, error: pErr } = await tenantDb().from('patients').select('id, nome_completo, status').eq('owner_id', ownerId.value).in('status', ['Inativo', 'Arquivado']);
|
||||
|
||||
if (pErr) {
|
||||
console.warn('[loadDesativados] patients error:', pErr);
|
||||
@@ -1044,9 +1044,8 @@ async function loadDesativados() {
|
||||
}
|
||||
|
||||
const patIds = pats.map((p) => p.id);
|
||||
const sessQ = supabase.from('agenda_eventos').select('id, patient_id, inicio_em, fim_em, status, titulo, modalidade, determined_commitment_id').in('patient_id', patIds).order('inicio_em', { ascending: true });
|
||||
const sessQ = tenantDb().from('agenda_eventos').select('id, patient_id, inicio_em, fim_em, status, titulo, modalidade, determined_commitment_id').in('patient_id', patIds).order('inicio_em', { ascending: true });
|
||||
if (ownerId.value) sessQ.eq('owner_id', ownerId.value);
|
||||
if (clinicTenantId.value) sessQ.eq('tenant_id', clinicTenantId.value);
|
||||
const { data: sessions, error: sErr } = await sessQ;
|
||||
|
||||
if (sErr) {
|
||||
@@ -1278,7 +1277,7 @@ async function loadMiniMonthEvents(refDate) {
|
||||
|
||||
try {
|
||||
// 1. Eventos reais (agenda_eventos)
|
||||
const { data: evData } = await supabase.from('agenda_eventos').select('inicio_em').eq('owner_id', ownerId.value).gte('inicio_em', start.toISOString()).lte('inicio_em', end.toISOString());
|
||||
const { data: evData } = await tenantDb().from('agenda_eventos').select('inicio_em').eq('owner_id', ownerId.value).gte('inicio_em', start.toISOString()).lte('inicio_em', end.toISOString());
|
||||
|
||||
const evSet = new Set();
|
||||
for (const r of evData || []) {
|
||||
@@ -1303,8 +1302,7 @@ async function loadMiniMonthEvents(refDate) {
|
||||
const isoStart = `${year}-${pad(month + 1)}-01`;
|
||||
const lastDay = new Date(year, month + 1, 0).getDate();
|
||||
const isoEnd = `${year}-${pad(month + 1)}-${pad(lastDay)}`;
|
||||
const { data: blkData } = await supabase
|
||||
.from('agenda_bloqueios')
|
||||
const { data: blkData } = await tenantDb().from('agenda_bloqueios')
|
||||
.select('data_inicio')
|
||||
.eq('owner_id', ownerId.value || '')
|
||||
.is('hora_inicio', null)
|
||||
@@ -1398,10 +1396,9 @@ async function bloquearFeriadoDoAlerta(feriado) {
|
||||
if (!ownerId.value || !clinicTenantId.value) return;
|
||||
feriadosAlertaSalvando.value = feriado.data;
|
||||
try {
|
||||
const { error } = await supabase.from('agenda_bloqueios').insert([
|
||||
const { error } = await tenantDb().from('agenda_bloqueios').insert([
|
||||
{
|
||||
owner_id: ownerId.value,
|
||||
tenant_id: clinicTenantId.value,
|
||||
tipo: 'bloqueio',
|
||||
recorrente: false,
|
||||
titulo: `Feriado: ${feriado.nome}`,
|
||||
@@ -1413,7 +1410,7 @@ async function bloquearFeriadoDoAlerta(feriado) {
|
||||
}
|
||||
]);
|
||||
if (error) throw error;
|
||||
await supabase.from('agenda_eventos').update({ status: 'remarcado' }).eq('owner_id', ownerId.value).eq('tipo', 'sessao').gte('inicio_em', `${feriado.data}T00:00:00`).lte('inicio_em', `${feriado.data}T23:59:59`);
|
||||
await tenantDb().from('agenda_eventos').update({ status: 'remarcado' }).eq('owner_id', ownerId.value).eq('tipo', 'sessao').gte('inicio_em', `${feriado.data}T00:00:00`).lte('inicio_em', `${feriado.data}T23:59:59`);
|
||||
|
||||
feriadosAlertaBloqueados.value = new Set([...feriadosAlertaBloqueados.value, feriado.data]);
|
||||
miniBlockedDaySet.value = new Set([...miniBlockedDaySet.value, feriado.data]);
|
||||
@@ -1438,7 +1435,7 @@ async function desbloquearFeriadoDoAlerta(feriado) {
|
||||
if (!ownerId.value) return;
|
||||
feriadosAlertaSalvando.value = `unblock_${feriado.data}`;
|
||||
try {
|
||||
const { error } = await supabase.from('agenda_bloqueios').delete().eq('owner_id', ownerId.value).eq('data_inicio', feriado.data).in('origem', ['agenda_feriado', 'agenda_dia']);
|
||||
const { error } = await tenantDb().from('agenda_bloqueios').delete().eq('owner_id', ownerId.value).eq('data_inicio', feriado.data).in('origem', ['agenda_feriado', 'agenda_dia']);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
@@ -1736,8 +1733,7 @@ async function onUpdateSeriesEvent({ id, status, recurrence_date, inicio_em, fim
|
||||
if (!is_virtual || !inicio_em) return;
|
||||
const rid = row.recurrence_id ?? row.serie_id ?? null;
|
||||
const rDate = recurrence_date || inicio_em?.slice(0, 10);
|
||||
const { data: existing } = await supabase
|
||||
.from('agenda_eventos')
|
||||
const { data: existing } = await tenantDb().from('agenda_eventos')
|
||||
.select('id')
|
||||
.eq('recurrence_id', rid)
|
||||
.eq('recurrence_date', rDate)
|
||||
@@ -1807,9 +1803,8 @@ async function _offerBillingContract(normalized, recorrencia, tenantId) {
|
||||
rejectLabel: 'Agora não',
|
||||
accept: async () => {
|
||||
try {
|
||||
const { error } = await supabase.from('billing_contracts').insert({
|
||||
const { error } = await tenantDb().from('billing_contracts').insert({
|
||||
owner_id: normalized.owner_id,
|
||||
tenant_id: tenantId,
|
||||
patient_id: normalized.paciente_id,
|
||||
type: 'package',
|
||||
total_sessions: n,
|
||||
@@ -1933,12 +1928,11 @@ async function onDialogSave(arg) {
|
||||
if (recorrencia?.conflitos?.length && createdRule?.id) {
|
||||
const exceptions = recorrencia.conflitos.map((c) => ({
|
||||
recurrence_id: createdRule.id,
|
||||
tenant_id: clinicId,
|
||||
original_date: c.date,
|
||||
type: c.conflict.type === 'feriado' ? 'holiday_block' : c.conflict.type === 'bloqueado' ? 'cancel_session' : c.conflict.type === 'folga' ? 'cancel_session' : 'cancel_session',
|
||||
reason: c.conflict.label
|
||||
}));
|
||||
const { error: exErr } = await supabase.from('recurrence_exceptions').insert(exceptions);
|
||||
const { error: exErr } = await tenantDb().from('recurrence_exceptions').insert(exceptions);
|
||||
if (exErr) logError('AgendaTerapeutaPage', 'onDialogSave: erro ao inserir exceptions', exErr);
|
||||
}
|
||||
|
||||
@@ -1998,7 +1992,7 @@ async function onDialogSave(arg) {
|
||||
extra_fields: normalized.extra_fields ?? null
|
||||
});
|
||||
if (arg.onSaved) {
|
||||
const { data: existing } = await supabase.from('agenda_eventos').select('id').eq('recurrence_id', recurrenceId).eq('recurrence_date', originalDate).maybeSingle();
|
||||
const { data: existing } = await tenantDb().from('agenda_eventos').select('id').eq('recurrence_id', recurrenceId).eq('recurrence_date', originalDate).maybeSingle();
|
||||
if (existing?.id) {
|
||||
eventId = existing.id;
|
||||
} else {
|
||||
@@ -2091,8 +2085,7 @@ async function onDialogSave(arg) {
|
||||
});
|
||||
|
||||
// Propaga campos não-serviço para sessões já materializadas da série
|
||||
await supabase
|
||||
.from('agenda_eventos')
|
||||
await tenantDb().from('agenda_eventos')
|
||||
.update({
|
||||
modalidade: normalized.modalidade ?? 'presencial',
|
||||
titulo_custom: normalized.titulo_custom ?? null,
|
||||
@@ -2140,8 +2133,7 @@ async function onDialogSave(arg) {
|
||||
});
|
||||
|
||||
// Propaga TODOS os campos para TODAS as sessões materializadas (sem exceção)
|
||||
await supabase
|
||||
.from('agenda_eventos')
|
||||
await tenantDb().from('agenda_eventos')
|
||||
.update({
|
||||
modalidade: normalized.modalidade ?? 'presencial',
|
||||
titulo_custom: normalized.titulo_custom ?? null,
|
||||
@@ -2205,8 +2197,7 @@ async function onDialogSave(arg) {
|
||||
let detail = 'Já existe um compromisso nesse horário. Verifique a agenda e escolha outro horário.';
|
||||
try {
|
||||
if (normalized?.inicio_em && normalized?.fim_em && normalized?.owner_id) {
|
||||
const { data: conflicting } = await supabase
|
||||
.from('agenda_eventos')
|
||||
const { data: conflicting } = await tenantDb().from('agenda_eventos')
|
||||
.select('titulo, inicio_em, fim_em')
|
||||
.eq('owner_id', normalized.owner_id)
|
||||
.lt('inicio_em', normalized.fim_em)
|
||||
@@ -2276,7 +2267,7 @@ async function onDialogDelete(arg) {
|
||||
if (isVirtual) {
|
||||
// Ocorrência virtual: materializa como evento avulso (sem recurrence_id)
|
||||
const rDate = row.original_date || row.inicio_em?.slice(0, 10);
|
||||
const existing = await supabase.from('agenda_eventos').select('id').eq('recurrence_id', recurrenceId).eq('recurrence_date', rDate).maybeSingle();
|
||||
const existing = await tenantDb().from('agenda_eventos').select('id').eq('recurrence_id', recurrenceId).eq('recurrence_date', rDate).maybeSingle();
|
||||
|
||||
if (existing.data?.id) {
|
||||
await update(existing.data.id, { recurrence_id: null, recurrence_date: null });
|
||||
|
||||
@@ -19,6 +19,7 @@ import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
|
||||
import AgendaEventDialog from '@/features/agenda/components/AgendaEventDialog.vue';
|
||||
@@ -63,14 +64,12 @@ async function load() {
|
||||
if (!ownerId.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
let q = supabase
|
||||
.from('agendador_solicitacoes')
|
||||
.select('id, owner_id, tenant_id, paciente_nome, paciente_sobrenome, paciente_email, paciente_celular, paciente_cpf, tipo, modalidade, data_solicitada, hora_solicitada, reservado_ate, motivo, como_conheceu, status, created_at')
|
||||
let q = tenantDb().from('agendador_solicitacoes')
|
||||
.select('id, owner_id, paciente_nome, paciente_sobrenome, paciente_email, paciente_celular, paciente_cpf, tipo, modalidade, data_solicitada, hora_solicitada, reservado_ate, motivo, como_conheceu, status, created_at')
|
||||
.order('data_solicitada', { ascending: false })
|
||||
.order('hora_solicitada', { ascending: true });
|
||||
|
||||
if (isClinic.value) q = q.eq('tenant_id', tenantId.value);
|
||||
else q = q.eq('owner_id', ownerId.value);
|
||||
if (!isClinic.value) q = q.eq('owner_id', ownerId.value);
|
||||
|
||||
if (filtroStatus.value) q = q.eq('status', filtroStatus.value);
|
||||
|
||||
@@ -79,9 +78,8 @@ async function load() {
|
||||
solicitacoes.value = data || [];
|
||||
|
||||
if (filtroStatus.value !== 'pendente') {
|
||||
let qp = supabase.from('agendador_solicitacoes').select('id', { count: 'exact', head: true }).eq('status', 'pendente');
|
||||
if (isClinic.value) qp = qp.eq('tenant_id', tenantId.value);
|
||||
else qp = qp.eq('owner_id', ownerId.value);
|
||||
let qp = tenantDb().from('agendador_solicitacoes').select('id', { count: 'exact', head: true }).eq('status', 'pendente');
|
||||
if (!isClinic.value) qp = qp.eq('owner_id', ownerId.value);
|
||||
const { count } = await qp;
|
||||
totalPendentes.value = count || 0;
|
||||
} else {
|
||||
@@ -90,9 +88,8 @@ async function load() {
|
||||
|
||||
// Conta autorizados (sempre, independente do filtro ativo)
|
||||
if (filtroStatus.value !== 'autorizado') {
|
||||
let qa = supabase.from('agendador_solicitacoes').select('id', { count: 'exact', head: true }).eq('status', 'autorizado');
|
||||
if (isClinic.value) qa = qa.eq('tenant_id', tenantId.value);
|
||||
else qa = qa.eq('owner_id', ownerId.value);
|
||||
let qa = tenantDb().from('agendador_solicitacoes').select('id', { count: 'exact', head: true }).eq('status', 'autorizado');
|
||||
if (!isClinic.value) qa = qa.eq('owner_id', ownerId.value);
|
||||
const { count: ca } = await qa;
|
||||
totalAutorizados.value = ca || 0;
|
||||
} else {
|
||||
@@ -158,7 +155,7 @@ const aprovando = ref(null);
|
||||
async function aprovar(s) {
|
||||
aprovando.value = s.id;
|
||||
try {
|
||||
const { error } = await supabase.from('agendador_solicitacoes').update({ status: 'autorizado', autorizado_em: new Date().toISOString() }).eq('id', s.id);
|
||||
const { error } = await tenantDb().from('agendador_solicitacoes').update({ status: 'autorizado', autorizado_em: new Date().toISOString() }).eq('id', s.id);
|
||||
if (error) throw error;
|
||||
toast.add({ severity: 'success', summary: 'Autorizado', detail: `Solicitação de ${nomeCompleto(s)} autorizada.`, life: 3000 });
|
||||
await load();
|
||||
@@ -187,8 +184,7 @@ async function confirmarRecusa() {
|
||||
if (!s) return;
|
||||
recusandoId.value = s.id;
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('agendador_solicitacoes')
|
||||
const { error } = await tenantDb().from('agendador_solicitacoes')
|
||||
.update({ status: 'recusado', recusado_motivo: recusaMotivo.value || null })
|
||||
.eq('id', s.id);
|
||||
if (error) throw error;
|
||||
@@ -241,17 +237,15 @@ async function converterEmSessao(s) {
|
||||
async function encontrarOuCriarPaciente(s) {
|
||||
const email = s.paciente_email?.toLowerCase().trim();
|
||||
if (email) {
|
||||
const { data: found } = await supabase.from('patients').select('id').eq('tenant_id', tenantId.value).ilike('email_principal', email).maybeSingle();
|
||||
const { data: found } = await tenantDb().from('patients').select('id').ilike('email_principal', email).maybeSingle();
|
||||
if (found?.id) return found.id;
|
||||
}
|
||||
const { data: memberData, error: memberErr } = await supabase.from('tenant_members').select('id').eq('tenant_id', tenantId.value).eq('user_id', ownerId.value).eq('status', 'active').maybeSingle();
|
||||
if (memberErr || !memberData?.id) throw new Error('Membro ativo não encontrado para criação do paciente.');
|
||||
const scope = isClinic.value ? 'clinic' : 'therapist';
|
||||
const nomeCompleto_ = [s.paciente_nome, s.paciente_sobrenome].filter(Boolean).join(' ');
|
||||
const { data: novo, error: criErr } = await supabase
|
||||
.from('patients')
|
||||
const { data: novo, error: criErr } = await tenantDb().from('patients')
|
||||
.insert({
|
||||
tenant_id: tenantId.value,
|
||||
responsible_member_id: memberData.id,
|
||||
owner_id: ownerId.value,
|
||||
nome_completo: nomeCompleto_,
|
||||
@@ -299,7 +293,7 @@ async function onEventSaved(arg) {
|
||||
if (normalized[k] !== undefined) dbPayload[k] = normalized[k];
|
||||
}
|
||||
await createEvento(dbPayload);
|
||||
const { error } = await supabase.from('agendador_solicitacoes').update({ status: 'convertido' }).eq('id', target.id);
|
||||
const { error } = await tenantDb().from('agendador_solicitacoes').update({ status: 'convertido' }).eq('id', target.id);
|
||||
if (error) throw error;
|
||||
toast.add({ severity: 'success', summary: 'Convertido!', detail: `Sessão criada para ${nomeCompleto(target)}.`, life: 4000 });
|
||||
await load();
|
||||
|
||||
@@ -26,6 +26,7 @@ import Menu from 'primevue/menu';
|
||||
import DeterminedCommitmentDialog from '@/features/agenda/components/DeterminedCommitmentDialog.vue';
|
||||
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
const toast = useToast();
|
||||
@@ -161,10 +162,9 @@ async function fetchAll() {
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data: cData, error: cErr } = await supabase
|
||||
.from('determined_commitments')
|
||||
.select('id, tenant_id, is_native, native_key, is_locked, active, name, description, bg_color, text_color, created_at, updated_at')
|
||||
.eq('tenant_id', tenantId)
|
||||
const { data: cData, error: cErr } = await tenantDb().from('determined_commitments')
|
||||
.select('id, is_native, native_key, is_locked, active, name, description, bg_color, text_color, created_at, updated_at')
|
||||
|
||||
.order('is_native', { ascending: false })
|
||||
.order('created_at', { ascending: false });
|
||||
if (cErr) throw cErr;
|
||||
@@ -172,10 +172,9 @@ async function fetchAll() {
|
||||
const ids = (cData || []).map((x) => x.id);
|
||||
let fieldsByCommitmentId = {};
|
||||
if (ids.length > 0) {
|
||||
const { data: fData, error: fErr } = await supabase
|
||||
.from('determined_commitment_fields')
|
||||
.select('id, tenant_id, commitment_id, key, label, field_type, required, sort_order')
|
||||
.eq('tenant_id', tenantId)
|
||||
const { data: fData, error: fErr } = await tenantDb().from('determined_commitment_fields')
|
||||
.select('id, commitment_id, key, label, field_type, required, sort_order')
|
||||
|
||||
.in('commitment_id', ids)
|
||||
.order('sort_order', { ascending: true });
|
||||
if (fErr) throw fErr;
|
||||
@@ -193,7 +192,7 @@ async function fetchAll() {
|
||||
}, {});
|
||||
}
|
||||
|
||||
const { data: lData, error: lErr } = await supabase.from('commitment_time_logs').select('commitment_id, minutes').eq('tenant_id', tenantId);
|
||||
const { data: lData, error: lErr } = await tenantDb().from('commitment_time_logs').select('commitment_id, minutes');
|
||||
if (lErr) throw lErr;
|
||||
const totals = {};
|
||||
for (const row of lData || []) {
|
||||
@@ -253,7 +252,7 @@ async function onToggleActive(c) {
|
||||
if (!tenantId) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
const { error } = await supabase.from('determined_commitments').update({ active: !!c.active }).eq('tenant_id', tenantId).eq('id', c.id);
|
||||
const { error } = await tenantDb().from('determined_commitments').update({ active: !!c.active }).eq('id', c.id);
|
||||
if (error) throw error;
|
||||
toast.add({ severity: 'success', summary: 'Atualizado', detail: `"${c.name}" ${c.active ? 'ativo' : 'inativo'}.`, life: 2500 });
|
||||
} catch (e) {
|
||||
@@ -271,10 +270,8 @@ async function onSave(payload) {
|
||||
try {
|
||||
await supabase.auth.getUser();
|
||||
if (dlgMode.value === 'create') {
|
||||
const { data: newC, error: cErr } = await supabase
|
||||
.from('determined_commitments')
|
||||
const { data: newC, error: cErr } = await tenantDb().from('determined_commitments')
|
||||
.insert({
|
||||
tenant_id: tenantId,
|
||||
is_native: false,
|
||||
native_key: null,
|
||||
is_locked: false,
|
||||
@@ -284,14 +281,13 @@ async function onSave(payload) {
|
||||
bg_color: payload.bg_color || null,
|
||||
text_color: payload.text_color || null
|
||||
})
|
||||
.select('id, tenant_id, is_native, native_key, is_locked, active, name, description, bg_color, text_color, created_at, updated_at')
|
||||
.select('id, is_native, native_key, is_locked, active, name, description, bg_color, text_color, created_at, updated_at')
|
||||
.single();
|
||||
if (cErr) throw cErr;
|
||||
const fields = Array.isArray(payload.fields) ? payload.fields : [];
|
||||
if (fields.length > 0) {
|
||||
const { error: fErr } = await supabase.from('determined_commitment_fields').insert(
|
||||
const { error: fErr } = await tenantDb().from('determined_commitment_fields').insert(
|
||||
fields.map((f, idx) => ({
|
||||
tenant_id: tenantId,
|
||||
commitment_id: newC.id,
|
||||
key: f.key,
|
||||
label: f.label,
|
||||
@@ -304,8 +300,7 @@ async function onSave(payload) {
|
||||
}
|
||||
toast.add({ severity: 'success', summary: 'Criado', detail: 'Compromisso criado.', life: 2500 });
|
||||
} else {
|
||||
const { error: upErr } = await supabase
|
||||
.from('determined_commitments')
|
||||
const { error: upErr } = await tenantDb().from('determined_commitments')
|
||||
.update({
|
||||
name: payload.name,
|
||||
description: payload.description,
|
||||
@@ -313,16 +308,15 @@ async function onSave(payload) {
|
||||
bg_color: payload.bg_color || null,
|
||||
text_color: payload.text_color || null
|
||||
})
|
||||
.eq('tenant_id', tenantId)
|
||||
|
||||
.eq('id', payload.id);
|
||||
if (upErr) throw upErr;
|
||||
const { error: delErr } = await supabase.from('determined_commitment_fields').delete().eq('tenant_id', tenantId).eq('commitment_id', payload.id);
|
||||
const { error: delErr } = await tenantDb().from('determined_commitment_fields').delete().eq('commitment_id', payload.id);
|
||||
if (delErr) throw delErr;
|
||||
const fields = Array.isArray(payload.fields) ? payload.fields : [];
|
||||
if (fields.length > 0) {
|
||||
const { error: insErr } = await supabase.from('determined_commitment_fields').insert(
|
||||
const { error: insErr } = await tenantDb().from('determined_commitment_fields').insert(
|
||||
fields.map((f, idx) => ({
|
||||
tenant_id: tenantId,
|
||||
commitment_id: payload.id,
|
||||
key: f.key,
|
||||
label: f.label,
|
||||
@@ -358,11 +352,11 @@ async function onDelete(c) {
|
||||
if (!tenantId) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
const { error: fErr } = await supabase.from('determined_commitment_fields').delete().eq('tenant_id', tenantId).eq('commitment_id', c.id);
|
||||
const { error: fErr } = await tenantDb().from('determined_commitment_fields').delete().eq('commitment_id', c.id);
|
||||
if (fErr) throw fErr;
|
||||
const { error: lErr } = await supabase.from('commitment_time_logs').delete().eq('tenant_id', tenantId).eq('commitment_id', c.id);
|
||||
const { error: lErr } = await tenantDb().from('commitment_time_logs').delete().eq('commitment_id', c.id);
|
||||
if (lErr) throw lErr;
|
||||
const { data: delRows, error: dErr } = await supabase.from('determined_commitments').delete().eq('tenant_id', tenantId).eq('id', c.id).eq('is_native', false).select('id');
|
||||
const { data: delRows, error: dErr } = await tenantDb().from('determined_commitments').delete().eq('id', c.id).eq('is_native', false).select('id');
|
||||
if (dErr) throw dErr;
|
||||
if (!delRows?.length) throw new Error('DELETE bloqueado por RLS.');
|
||||
toast.add({ severity: 'success', summary: 'Excluído', detail: 'Compromisso removido.', life: 2500 });
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
*/
|
||||
|
||||
import { dateToISO } from '@/features/agenda/utils/timeHelpers';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
|
||||
// ── Helpers puros ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -155,10 +156,9 @@ export async function loadStatusChangeContext({ supabase, row, eventoId, status,
|
||||
const excType = exceptionTypeMap[status];
|
||||
if (excType && tenantId) {
|
||||
try {
|
||||
const { data } = await supabase
|
||||
.from('financial_exceptions')
|
||||
const { data } = await tenantDb().from('financial_exceptions')
|
||||
.select('*')
|
||||
.eq('tenant_id', tenantId)
|
||||
|
||||
.eq('exception_type', excType)
|
||||
.or(`owner_id.eq.${ownerId},owner_id.is.null`)
|
||||
.order('owner_id', { ascending: false, nullsLast: true })
|
||||
@@ -177,8 +177,7 @@ export async function loadStatusChangeContext({ supabase, row, eventoId, status,
|
||||
const contractId = row?.billing_contract_id ?? null;
|
||||
if (contractId) {
|
||||
try {
|
||||
const { data } = await supabase
|
||||
.from('billing_contracts')
|
||||
const { data } = await tenantDb().from('billing_contracts')
|
||||
.select('*')
|
||||
.eq('id', contractId)
|
||||
.maybeSingle();
|
||||
@@ -189,14 +188,12 @@ export async function loadStatusChangeContext({ supabase, row, eventoId, status,
|
||||
}
|
||||
if (!ctx.billingContract && eventoId) {
|
||||
try {
|
||||
const { data: ev } = await supabase
|
||||
.from('agenda_eventos')
|
||||
const { data: ev } = await tenantDb().from('agenda_eventos')
|
||||
.select('billing_contract_id')
|
||||
.eq('id', eventoId)
|
||||
.maybeSingle();
|
||||
if (ev?.billing_contract_id) {
|
||||
const { data: c } = await supabase
|
||||
.from('billing_contracts')
|
||||
const { data: c } = await tenantDb().from('billing_contracts')
|
||||
.select('*')
|
||||
.eq('id', ev.billing_contract_id)
|
||||
.maybeSingle();
|
||||
@@ -208,10 +205,9 @@ export async function loadStatusChangeContext({ supabase, row, eventoId, status,
|
||||
}
|
||||
if (!ctx.billingContract && patientId && tenantId) {
|
||||
try {
|
||||
const { data: c } = await supabase
|
||||
.from('billing_contracts')
|
||||
const { data: c } = await tenantDb().from('billing_contracts')
|
||||
.select('*')
|
||||
.eq('tenant_id', tenantId)
|
||||
|
||||
.eq('patient_id', patientId)
|
||||
.eq('status', 'active')
|
||||
.eq('type', 'package')
|
||||
@@ -227,8 +223,7 @@ export async function loadStatusChangeContext({ supabase, row, eventoId, status,
|
||||
// 3) Pending record
|
||||
if (eventoId) {
|
||||
try {
|
||||
const { data } = await supabase
|
||||
.from('financial_records')
|
||||
const { data } = await tenantDb().from('financial_records')
|
||||
.select('*')
|
||||
.eq('agenda_evento_id', eventoId)
|
||||
.in('status', ['pending', 'overdue'])
|
||||
@@ -244,8 +239,7 @@ export async function loadStatusChangeContext({ supabase, row, eventoId, status,
|
||||
// 3b) Paid record pré-existente (caso C12: antecipar pagamento).
|
||||
if (eventoId) {
|
||||
try {
|
||||
const { data } = await supabase
|
||||
.from('financial_records')
|
||||
const { data } = await tenantDb().from('financial_records')
|
||||
.select('id, status, amount, final_amount, paid_at, payment_method')
|
||||
.eq('agenda_evento_id', eventoId)
|
||||
.eq('status', 'paid')
|
||||
@@ -266,16 +260,14 @@ export async function loadStatusChangeContext({ supabase, row, eventoId, status,
|
||||
saldoConsumed: false
|
||||
};
|
||||
try {
|
||||
const { data: evRow } = await supabase
|
||||
.from('agenda_eventos')
|
||||
const { data: evRow } = await tenantDb().from('agenda_eventos')
|
||||
.select('status, billing_contract_id')
|
||||
.eq('id', eventoId)
|
||||
.maybeSingle();
|
||||
if (evRow) {
|
||||
ctx.reverseArtifacts.previousStatus = evRow.status;
|
||||
}
|
||||
const { data: recs } = await supabase
|
||||
.from('financial_records')
|
||||
const { data: recs } = await tenantDb().from('financial_records')
|
||||
.select('id, status, amount, final_amount, description, paid_at, payment_method')
|
||||
.eq('agenda_evento_id', eventoId)
|
||||
.neq('status', 'cancelled')
|
||||
@@ -336,8 +328,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const reason = `Cancelada via reversão de status (${r.previousStatus} → agendado) em ${today}`;
|
||||
for (const id of pendingIds) {
|
||||
const { error: cErr } = await supabase
|
||||
.from('financial_records')
|
||||
const { error: cErr } = await tenantDb().from('financial_records')
|
||||
.update({
|
||||
status: 'cancelled',
|
||||
notes: `[${today}] ${reason}`,
|
||||
@@ -356,8 +347,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
|
||||
// 2) Devolver saldo
|
||||
if (decision.reverseRestoreSaldo && r.saldoConsumed && ctx.billingContract?.id) {
|
||||
try {
|
||||
const { data: freshContract, error: fetchErr } = await supabase
|
||||
.from('billing_contracts')
|
||||
const { data: freshContract, error: fetchErr } = await tenantDb().from('billing_contracts')
|
||||
.select('sessions_used, total_sessions, status')
|
||||
.eq('id', ctx.billingContract.id)
|
||||
.maybeSingle();
|
||||
@@ -369,7 +359,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
|
||||
if (currentUsed >= totalSessions) {
|
||||
patch.status = 'active';
|
||||
}
|
||||
const { error: dErr } = await supabase.from('billing_contracts').update(patch).eq('id', ctx.billingContract.id);
|
||||
const { error: dErr } = await tenantDb().from('billing_contracts').update(patch).eq('id', ctx.billingContract.id);
|
||||
if (dErr) throw dErr;
|
||||
} catch (e) {
|
||||
console.error('[agendaBilling/reverse] erro decrementando saldo:', e?.message);
|
||||
@@ -380,7 +370,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
|
||||
// 3) Desamarrar billing_contract_id (só se devolveu saldo)
|
||||
if (decision.reverseRestoreSaldo && r.saldoConsumed) {
|
||||
try {
|
||||
await supabase.from('agenda_eventos').update({ billing_contract_id: null, updated_at: new Date().toISOString() }).eq('id', eventoId);
|
||||
await tenantDb().from('agenda_eventos').update({ billing_contract_id: null, updated_at: new Date().toISOString() }).eq('id', eventoId);
|
||||
} catch (e) {
|
||||
console.warn('[agendaBilling/reverse] erro desamarrando billing_contract_id:', e?.message);
|
||||
}
|
||||
@@ -393,8 +383,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
|
||||
// 1) Consumir saldo
|
||||
if (decision.consumeSaldo && ctx.billingContract?.id) {
|
||||
tasks.push(
|
||||
supabase
|
||||
.from('billing_contracts')
|
||||
tenantDb().from('billing_contracts')
|
||||
.update({ sessions_used: (ctx.billingContract.sessions_used ?? 0) + 1 })
|
||||
.eq('id', ctx.billingContract.id)
|
||||
);
|
||||
@@ -404,8 +393,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
|
||||
const isForwardStatus = novoStatus === 'realizado' || novoStatus === 'faltou' || novoStatus === 'cancelado';
|
||||
if (isForwardStatus && ctx.billingContract?.id && eventoId) {
|
||||
tasks.push(
|
||||
supabase
|
||||
.from('agenda_eventos')
|
||||
tenantDb().from('agenda_eventos')
|
||||
.update({ billing_contract_id: ctx.billingContract.id, updated_at: new Date().toISOString() })
|
||||
.eq('id', eventoId)
|
||||
);
|
||||
@@ -418,7 +406,6 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
|
||||
const fineDesc = novoStatus === 'faltou' ? `Multa por falta · sessão ${sessaoLabel}` : `Taxa de cancelamento tardio · sessão ${sessaoLabel}`;
|
||||
const finePayload = {
|
||||
owner_id: uid,
|
||||
tenant_id: tenantId,
|
||||
patient_id: patientId,
|
||||
agenda_evento_id: eventoId,
|
||||
amount: decision.fineAmount,
|
||||
@@ -429,8 +416,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
|
||||
type: 'receita'
|
||||
};
|
||||
tasks.push(
|
||||
supabase
|
||||
.from('financial_records')
|
||||
tenantDb().from('financial_records')
|
||||
.insert(finePayload)
|
||||
.then(({ error }) => {
|
||||
if (error) {
|
||||
@@ -455,8 +441,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
|
||||
const noteEntry = `[${today}] ${reasonText}`;
|
||||
const noteText = ctx.pendingRecord.notes ? `${ctx.pendingRecord.notes}\n${noteEntry}` : noteEntry;
|
||||
tasks.push(
|
||||
supabase
|
||||
.from('financial_records')
|
||||
tenantDb().from('financial_records')
|
||||
.update({
|
||||
status: 'cancelled',
|
||||
notes: noteText,
|
||||
@@ -469,8 +454,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
|
||||
// 3) Realizado avulsa pendente: marcar pendingRecord como pago (ou só status)
|
||||
if (decision.markPaid && ctx.pendingRecord?.id) {
|
||||
tasks.push(
|
||||
supabase
|
||||
.from('financial_records')
|
||||
tenantDb().from('financial_records')
|
||||
.update({
|
||||
status: 'paid',
|
||||
paid_at: new Date().toISOString(),
|
||||
@@ -492,8 +476,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
|
||||
}
|
||||
}
|
||||
try {
|
||||
const { data: freshContract, error: fetchErr } = await supabase
|
||||
.from('billing_contracts')
|
||||
const { data: freshContract, error: fetchErr } = await tenantDb().from('billing_contracts')
|
||||
.select('sessions_used, total_sessions, status')
|
||||
.eq('id', ctx.billingContract.id)
|
||||
.maybeSingle();
|
||||
@@ -504,7 +487,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
|
||||
if (newUsed >= (freshContract?.total_sessions ?? 0)) {
|
||||
patch.status = 'completed';
|
||||
}
|
||||
const { error: incErr } = await supabase.from('billing_contracts').update(patch).eq('id', ctx.billingContract.id);
|
||||
const { error: incErr } = await tenantDb().from('billing_contracts').update(patch).eq('id', ctx.billingContract.id);
|
||||
if (incErr) throw incErr;
|
||||
tx({ severity: 'success', summary: 'Sessão consumida', detail: `Saldo: ${newUsed}/${freshContract?.total_sessions ?? '?'}. Pagamento já estava registrado.`, life: 4000 });
|
||||
} catch (e) {
|
||||
@@ -520,7 +503,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
|
||||
const dueIso = row.inicio_em ? new Date(row.inicio_em).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10);
|
||||
|
||||
try {
|
||||
const { error: linkErr } = await supabase.from('agenda_eventos').update({ billing_contract_id: ctx.billingContract.id, updated_at: new Date().toISOString() }).eq('id', eventoId);
|
||||
const { error: linkErr } = await tenantDb().from('agenda_eventos').update({ billing_contract_id: ctx.billingContract.id, updated_at: new Date().toISOString() }).eq('id', eventoId);
|
||||
if (linkErr) throw linkErr;
|
||||
} catch (e) {
|
||||
console.error('[agendaBilling] erro amarrando billing_contract_id:', e?.message);
|
||||
@@ -548,7 +531,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
|
||||
if (newUsed >= (ctx.billingContract.total_sessions ?? 0)) {
|
||||
patchContract.status = 'completed';
|
||||
}
|
||||
const { error: incErr } = await supabase.from('billing_contracts').update(patchContract).eq('id', ctx.billingContract.id);
|
||||
const { error: incErr } = await tenantDb().from('billing_contracts').update(patchContract).eq('id', ctx.billingContract.id);
|
||||
if (incErr) throw incErr;
|
||||
} catch (e) {
|
||||
console.error('[agendaBilling] erro incrementando sessions_used:', e?.message);
|
||||
@@ -570,8 +553,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
|
||||
// Pós-processamento do record gerado pelo pacote saldo
|
||||
if (decision.generatePackageCharge && eventoId) {
|
||||
try {
|
||||
const { data: newRec } = await supabase
|
||||
.from('financial_records')
|
||||
const { data: newRec } = await tenantDb().from('financial_records')
|
||||
.select('id')
|
||||
.eq('agenda_evento_id', eventoId)
|
||||
.order('created_at', { ascending: false })
|
||||
@@ -579,8 +561,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
|
||||
.single();
|
||||
if (newRec?.id) {
|
||||
if (decision.markPaid) {
|
||||
await supabase
|
||||
.from('financial_records')
|
||||
await tenantDb().from('financial_records')
|
||||
.update({
|
||||
status: 'paid',
|
||||
paid_at: new Date().toISOString(),
|
||||
@@ -589,8 +570,7 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
|
||||
})
|
||||
.eq('id', newRec.id);
|
||||
} else if (decision.paymentMethod === 'link') {
|
||||
await supabase
|
||||
.from('financial_records')
|
||||
await tenantDb().from('financial_records')
|
||||
.update({ payment_method: 'asaas', updated_at: new Date().toISOString() })
|
||||
.eq('id', newRec.id);
|
||||
}
|
||||
@@ -609,11 +589,9 @@ export async function applyStatusDecisions({ supabase, toast, eventoId, row, nov
|
||||
export async function createPackageContract({ supabase, rule, normalized, recorrencia, tenantId, packageStyle = 'upfront', paymentMethod = 'link', markPaidNow = false }) {
|
||||
const { n, packagePrice } = computeSeriePrice(recorrencia);
|
||||
try {
|
||||
const { data: createdContract, error: contractErr } = await supabase
|
||||
.from('billing_contracts')
|
||||
const { data: createdContract, error: contractErr } = await tenantDb().from('billing_contracts')
|
||||
.insert({
|
||||
owner_id: normalized.owner_id,
|
||||
tenant_id: tenantId,
|
||||
patient_id: normalized.paciente_id,
|
||||
type: 'package',
|
||||
total_sessions: n,
|
||||
@@ -645,11 +623,9 @@ export async function createPackageContract({ supabase, rule, normalized, recorr
|
||||
startDt.setHours(hh, mm, 0, 0);
|
||||
const endDt = new Date(startDt.getTime() + durMin * 60 * 1000);
|
||||
|
||||
const { data: createdEvent, error: evErr } = await supabase
|
||||
.from('agenda_eventos')
|
||||
const { data: createdEvent, error: evErr } = await tenantDb().from('agenda_eventos')
|
||||
.insert({
|
||||
owner_id: rule.owner_id,
|
||||
tenant_id: tenantId,
|
||||
terapeuta_id: rule.therapist_id ?? null,
|
||||
recurrence_id: rule.id,
|
||||
recurrence_date: firstISO,
|
||||
@@ -680,8 +656,7 @@ export async function createPackageContract({ supabase, rule, normalized, recorr
|
||||
if (cobErr) throw cobErr;
|
||||
|
||||
const paidNow = markPaidNow === true && paymentMethod !== 'link';
|
||||
const { data: recRow } = await supabase
|
||||
.from('financial_records')
|
||||
const { data: recRow } = await tenantDb().from('financial_records')
|
||||
.select('id')
|
||||
.eq('agenda_evento_id', createdEvent.id)
|
||||
.order('created_at', { ascending: false })
|
||||
@@ -696,7 +671,7 @@ export async function createPackageContract({ supabase, rule, normalized, recorr
|
||||
patch.status = 'paid';
|
||||
patch.paid_at = new Date().toISOString();
|
||||
}
|
||||
await supabase.from('financial_records').update(patch).eq('id', recRow.id);
|
||||
await tenantDb().from('financial_records').update(patch).eq('id', recRow.id);
|
||||
}
|
||||
|
||||
const methodLabel = {
|
||||
@@ -745,7 +720,6 @@ export async function materializeAndChargePerSession({ supabase, rule, normalize
|
||||
const endDt = new Date(startDt.getTime() + durMin * 60 * 1000);
|
||||
return {
|
||||
owner_id: rule.owner_id,
|
||||
tenant_id: tenantId,
|
||||
terapeuta_id: rule.therapist_id ?? null,
|
||||
recurrence_id: rule.id,
|
||||
recurrence_date: iso,
|
||||
@@ -762,7 +736,7 @@ export async function materializeAndChargePerSession({ supabase, rule, normalize
|
||||
};
|
||||
});
|
||||
|
||||
const { data: createdEvents, error: evErr } = await supabase.from('agenda_eventos').insert(rows).select('id, inicio_em');
|
||||
const { data: createdEvents, error: evErr } = await tenantDb().from('agenda_eventos').insert(rows).select('id, inicio_em');
|
||||
if (evErr) throw evErr;
|
||||
|
||||
let okCount = 0;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import {
|
||||
assertTenantId as assertValidTenantId,
|
||||
assertIsoRange as assertValidIsoRange,
|
||||
@@ -24,7 +25,7 @@ import { AGENDA_EVENT_SELECT, flattenAgendaRow } from './agendaSelects';
|
||||
|
||||
/**
|
||||
* Lista eventos para mosaico da clínica (admin/secretaria) dentro de um tenant específico.
|
||||
* IMPORTANTE: SEM tenant_id aqui vira vazamento multi-tenant.
|
||||
* Isolamento multi-tenant garantido pelo schema do tenant (tenantDb).
|
||||
*/
|
||||
export async function listClinicEvents({ tenantId, ownerIds, startISO, endISO } = {}) {
|
||||
assertValidTenantId(tenantId);
|
||||
@@ -34,10 +35,9 @@ export async function listClinicEvents({ tenantId, ownerIds, startISO, endISO }
|
||||
const safeOwnerIds = sanitizeOwnerIds(ownerIds);
|
||||
if (!safeOwnerIds.length) return [];
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_eventos')
|
||||
const { data, error } = await tenantDb().from('agenda_eventos')
|
||||
.select(AGENDA_EVENT_SELECT)
|
||||
.eq('tenant_id', tenantId)
|
||||
|
||||
.in('owner_id', safeOwnerIds)
|
||||
.gte('inicio_em', startISO)
|
||||
.lt('inicio_em', endISO)
|
||||
@@ -78,13 +78,11 @@ export async function createClinicAgendaEvento(payload, { tenantId } = {}) {
|
||||
throw new Error('owner_id é obrigatório para criação pela clínica.');
|
||||
}
|
||||
|
||||
const insertPayload = {
|
||||
...payload,
|
||||
tenant_id: tenantId
|
||||
};
|
||||
// dropa tenant_id se vier no payload (schema-per-tenant não tem a coluna)
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { tenant_id: _dropTenantId, ...insertPayload } = payload;
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_eventos')
|
||||
const { data, error } = await tenantDb().from('agenda_eventos')
|
||||
.insert(insertPayload)
|
||||
.select(AGENDA_EVENT_SELECT)
|
||||
.single();
|
||||
@@ -95,7 +93,7 @@ export async function createClinicAgendaEvento(payload, { tenantId } = {}) {
|
||||
|
||||
/**
|
||||
* Atualização segura para clínica:
|
||||
* - filtra por id + tenant_id (evita update cruzado)
|
||||
* - filtra por id (isolamento via schema do tenant)
|
||||
* - permite editar owner_id (caso você mova evento para outro profissional)
|
||||
*/
|
||||
export async function updateClinicAgendaEvento(id, patch, { tenantId } = {}) {
|
||||
@@ -103,11 +101,13 @@ export async function updateClinicAgendaEvento(id, patch, { tenantId } = {}) {
|
||||
if (!patch) throw new Error('Patch vazio.');
|
||||
assertValidTenantId(tenantId);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_eventos')
|
||||
.update(patch)
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { tenant_id: _dropTenantId, ...safePatch } = patch;
|
||||
|
||||
const { data, error } = await tenantDb().from('agenda_eventos')
|
||||
.update(safePatch)
|
||||
.eq('id', id)
|
||||
.eq('tenant_id', tenantId)
|
||||
|
||||
.select(AGENDA_EVENT_SELECT)
|
||||
.single();
|
||||
|
||||
@@ -117,13 +117,13 @@ export async function updateClinicAgendaEvento(id, patch, { tenantId } = {}) {
|
||||
|
||||
/**
|
||||
* Delete seguro para clínica:
|
||||
* - filtra por id + tenant_id
|
||||
* - filtra por id (isolamento via schema do tenant)
|
||||
*/
|
||||
export async function deleteClinicAgendaEvento(id, { tenantId } = {}) {
|
||||
if (!id) throw new Error('ID inválido.');
|
||||
assertValidTenantId(tenantId);
|
||||
|
||||
const { error } = await supabase.from('agenda_eventos').delete().eq('id', id).eq('tenant_id', tenantId);
|
||||
const { error } = await tenantDb().from('agenda_eventos').delete().eq('id', id);
|
||||
|
||||
if (error) throw error;
|
||||
return true;
|
||||
|
||||
@@ -143,8 +143,7 @@ function _mapRow(r) {
|
||||
|
||||
// timestamps
|
||||
inicio_em: r.inicio_em,
|
||||
fim_em: r.fim_em,
|
||||
tenant_id: r.tenant_id ?? null
|
||||
fim_em: r.fim_em
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { assertTenantId as assertValidTenantId, assertIsoRange, getUid } from './_tenantGuards';
|
||||
import { AGENDA_EVENT_SELECT, flattenAgendaRow } from './agendaSelects';
|
||||
@@ -23,8 +24,7 @@ import { AGENDA_EVENT_SELECT, flattenAgendaRow } from './agendaSelects';
|
||||
|
||||
export async function getMyAgendaSettings() {
|
||||
const uid = await getUid();
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_configuracoes')
|
||||
const { data, error } = await tenantDb().from('agenda_configuracoes')
|
||||
.select('*')
|
||||
.eq('owner_id', uid)
|
||||
.order('created_at', { ascending: false })
|
||||
@@ -36,8 +36,7 @@ export async function getMyAgendaSettings() {
|
||||
|
||||
export async function getMyWorkSchedule() {
|
||||
const uid = await getUid();
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_regras_semanais')
|
||||
const { data, error } = await tenantDb().from('agenda_regras_semanais')
|
||||
.select('dia_semana, hora_inicio, hora_fim, ativo')
|
||||
.eq('owner_id', uid)
|
||||
.eq('ativo', true)
|
||||
@@ -78,10 +77,9 @@ export async function listMyAgendaEvents({ startISO, endISO, ownerId, tenantId,
|
||||
const uid = ownerId || (await getUid());
|
||||
const tid = resolveTenantId(tenantId);
|
||||
|
||||
let q = supabase
|
||||
.from('agenda_eventos')
|
||||
let q = tenantDb().from('agenda_eventos')
|
||||
.select(AGENDA_EVENT_SELECT)
|
||||
.eq('tenant_id', tid)
|
||||
|
||||
.eq('owner_id', uid)
|
||||
.gte('inicio_em', startISO)
|
||||
.lt('inicio_em', endISO)
|
||||
@@ -96,9 +94,8 @@ export async function listMyAgendaEvents({ startISO, endISO, ownerId, tenantId,
|
||||
|
||||
/**
|
||||
* Criação segura:
|
||||
* - injeta tenant_id do tenantStore
|
||||
* - injeta owner_id do usuário logado (ignora owner_id vindo de fora)
|
||||
* - dropa paciente_id (campo legado) se vier no payload
|
||||
* - dropa paciente_id (campo legado) e tenant_id (schema-per-tenant não tem a coluna) se vierem no payload
|
||||
*/
|
||||
export async function createAgendaEvento(payload) {
|
||||
if (!payload) throw new Error('Payload vazio.');
|
||||
@@ -106,11 +103,10 @@ export async function createAgendaEvento(payload) {
|
||||
const tid = resolveTenantId();
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { paciente_id: _dropped, ...rest } = payload;
|
||||
const insertPayload = { ...rest, tenant_id: tid, owner_id: uid };
|
||||
const { paciente_id: _dropped, tenant_id: _dropTenantId, ...rest } = payload;
|
||||
const insertPayload = { ...rest, owner_id: uid };
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_eventos')
|
||||
const { data, error } = await tenantDb().from('agenda_eventos')
|
||||
.insert([insertPayload])
|
||||
.select(AGENDA_EVENT_SELECT)
|
||||
.single();
|
||||
@@ -120,7 +116,7 @@ export async function createAgendaEvento(payload) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualização segura: filtra por id + tenant_id (RLS reforça no banco).
|
||||
* Atualização segura: filtra por id (isolamento via schema do tenant; RLS reforça no banco).
|
||||
*/
|
||||
export async function updateAgendaEvento(id, patch, { tenantId } = {}) {
|
||||
if (!id) throw new Error('ID inválido.');
|
||||
@@ -128,13 +124,12 @@ export async function updateAgendaEvento(id, patch, { tenantId } = {}) {
|
||||
const tid = resolveTenantId(tenantId);
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { paciente_id: _dropped, ...safePatch } = patch;
|
||||
const { paciente_id: _dropped, tenant_id: _dropTenantId, ...safePatch } = patch;
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_eventos')
|
||||
const { data, error } = await tenantDb().from('agenda_eventos')
|
||||
.update(safePatch)
|
||||
.eq('id', id)
|
||||
.eq('tenant_id', tid)
|
||||
|
||||
.select(AGENDA_EVENT_SELECT)
|
||||
.single();
|
||||
|
||||
@@ -143,13 +138,13 @@ export async function updateAgendaEvento(id, patch, { tenantId } = {}) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete seguro: filtra por id + tenant_id.
|
||||
* Delete seguro: filtra por id (isolamento via schema do tenant).
|
||||
*/
|
||||
export async function deleteAgendaEvento(id, { tenantId } = {}) {
|
||||
if (!id) throw new Error('ID inválido.');
|
||||
const tid = resolveTenantId(tenantId);
|
||||
|
||||
const { error } = await supabase.from('agenda_eventos').delete().eq('id', id).eq('tenant_id', tid);
|
||||
const { error } = await tenantDb().from('agenda_eventos').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
export const AGENDA_EVENT_SELECT = `
|
||||
id, owner_id, patient_id, tipo, status,
|
||||
titulo, titulo_custom, observacoes, inicio_em, fim_em,
|
||||
terapeuta_id, tenant_id, visibility_scope,
|
||||
terapeuta_id, visibility_scope,
|
||||
determined_commitment_id, link_online, extra_fields, modalidade,
|
||||
recurrence_id, recurrence_date,
|
||||
mirror_of_event_id, price,
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
*/
|
||||
|
||||
const ALLOWED_FIELDS = [
|
||||
'tenant_id', 'owner_id', 'terapeuta_id', 'patient_id',
|
||||
'owner_id', 'terapeuta_id', 'patient_id',
|
||||
'tipo', 'status', 'titulo', 'observacoes', 'modalidade',
|
||||
'inicio_em', 'fim_em', 'visibility_scope',
|
||||
'mirror_of_event_id', 'mirror_source',
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { assertTenantId } from './_tenantGuards';
|
||||
import {
|
||||
@@ -41,7 +42,7 @@ function resolveTenantId(tenantIdArg) {
|
||||
export async function listThreads({ tenantId, limit = 500 } = {}) {
|
||||
const tid = resolveTenantId(tenantId);
|
||||
|
||||
const { data, error } = await supabase.from('conversation_threads').select(CONVERSATION_THREAD_SELECT).eq('tenant_id', tid).order('last_message_at', { ascending: false }).limit(limit);
|
||||
const { data, error } = await tenantDb().from('conversation_threads').select(CONVERSATION_THREAD_SELECT).order('last_message_at', { ascending: false }).limit(limit);
|
||||
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
@@ -54,7 +55,7 @@ export async function getThreadById(threadId, { tenantId } = {}) {
|
||||
if (!threadId) throw new Error('threadId obrigatório.');
|
||||
const tid = resolveTenantId(tenantId);
|
||||
|
||||
const { data, error } = await supabase.from('conversation_threads').select(CONVERSATION_THREAD_SELECT).eq('id', threadId).eq('tenant_id', tid).maybeSingle();
|
||||
const { data, error } = await tenantDb().from('conversation_threads').select(CONVERSATION_THREAD_SELECT).eq('id', threadId).maybeSingle();
|
||||
|
||||
if (error) throw error;
|
||||
return data || null;
|
||||
@@ -67,7 +68,7 @@ export async function updateThread(threadId, patch, { tenantId } = {}) {
|
||||
if (!threadId) throw new Error('threadId obrigatório.');
|
||||
const tid = resolveTenantId(tenantId);
|
||||
|
||||
const { data, error } = await supabase.from('conversation_threads').update({ ...patch, updated_at: new Date().toISOString() }).eq('id', threadId).eq('tenant_id', tid).select(CONVERSATION_THREAD_SELECT).single();
|
||||
const { data, error } = await tenantDb().from('conversation_threads').update({ ...patch, updated_at: new Date().toISOString() }).eq('id', threadId).select(CONVERSATION_THREAD_SELECT).single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
@@ -82,7 +83,7 @@ export async function listMessagesByThread(threadId, { tenantId, limit = 500 } =
|
||||
if (!threadId) return [];
|
||||
const tid = resolveTenantId(tenantId);
|
||||
|
||||
const { data, error } = await supabase.from('conversation_messages').select(CONVERSATION_MESSAGE_SELECT).eq('tenant_id', tid).eq('thread_id', threadId).order('created_at', { ascending: true }).limit(limit);
|
||||
const { data, error } = await tenantDb().from('conversation_messages').select(CONVERSATION_MESSAGE_SELECT).eq('thread_id', threadId).order('created_at', { ascending: true }).limit(limit);
|
||||
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
@@ -96,7 +97,7 @@ export async function listMessagesByPatient(patientId, { tenantId, limit = 200 }
|
||||
if (!patientId) return [];
|
||||
const tid = resolveTenantId(tenantId);
|
||||
|
||||
const { data, error } = await supabase.from('conversation_messages').select(CONVERSATION_MESSAGE_SELECT_BRIEF).eq('tenant_id', tid).eq('patient_id', patientId).order('created_at', { ascending: false }).limit(limit);
|
||||
const { data, error } = await tenantDb().from('conversation_messages').select(CONVERSATION_MESSAGE_SELECT_BRIEF).eq('patient_id', patientId).order('created_at', { ascending: false }).limit(limit);
|
||||
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
@@ -109,7 +110,7 @@ export async function updateMessageKanban(messageId, kanbanStatus, { tenantId }
|
||||
if (!messageId) throw new Error('messageId obrigatório.');
|
||||
const tid = resolveTenantId(tenantId);
|
||||
|
||||
const { error } = await supabase.from('conversation_messages').update({ kanban_status: kanbanStatus, updated_at: new Date().toISOString() }).eq('id', messageId).eq('tenant_id', tid);
|
||||
const { error } = await tenantDb().from('conversation_messages').update({ kanban_status: kanbanStatus, updated_at: new Date().toISOString() }).eq('id', messageId);
|
||||
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import { ref, reactive, watch, computed } from 'vue'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import {
|
||||
createSignatureRequests,
|
||||
listSignatures,
|
||||
@@ -61,8 +62,7 @@ function removeSignatario(idx) {
|
||||
async function fetchPatientEmails(patientId) {
|
||||
if (!patientId) { patientEmails.value = []; return }
|
||||
try {
|
||||
const { data } = await supabase
|
||||
.from('patients')
|
||||
const { data } = await tenantDb().from('patients')
|
||||
.select('email_principal, email_alternativo')
|
||||
.eq('id', patientId)
|
||||
.single()
|
||||
|
||||
@@ -20,6 +20,7 @@ import { ref, computed, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
// ─── helpers ─────────────────────────────────────────────────────────────────
|
||||
const router = useRouter();
|
||||
|
||||
@@ -75,7 +76,7 @@ async function loadSummary(uid) {
|
||||
totalDespesas.value = Number(s?.total_despesas ?? 0);
|
||||
|
||||
// Pending e overdue separados (sem filtro de mês)
|
||||
const { data: pendRows } = await supabase.from('financial_records').select('status, final_amount').eq('owner_id', uid).is('deleted_at', null).in('status', ['pending', 'overdue']);
|
||||
const { data: pendRows } = await tenantDb().from('financial_records').select('status, final_amount').eq('owner_id', uid).is('deleted_at', null).in('status', ['pending', 'overdue']);
|
||||
|
||||
let pen = 0,
|
||||
ove = 0;
|
||||
@@ -141,7 +142,7 @@ async function loadCashflow() {
|
||||
cashflowLoading.value = true;
|
||||
cashflowError.value = false;
|
||||
try {
|
||||
const { data, error } = await supabase.from('v_cashflow_projection').select('mes_label, receitas_projetadas, despesas_projetadas, saldo_projetado, count_registros').order('mes', { ascending: true });
|
||||
const { data, error } = await tenantDb().from('v_cashflow_projection').select('mes_label, receitas_projetadas, despesas_projetadas, saldo_projetado, count_registros').order('mes', { ascending: true });
|
||||
if (error) throw error;
|
||||
cashflowRows.value = data ?? [];
|
||||
} catch {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useFinancialRecords } from '@/composables/useFinancialRecords';
|
||||
|
||||
@@ -47,7 +48,7 @@ async function loadPatients() {
|
||||
const tenantId = tenantStore.activeTenantId;
|
||||
if (!tenantId) return;
|
||||
|
||||
const { data } = await supabase.from('patients').select('id, nome_completo, identification_color').eq('tenant_id', tenantId).order('nome_completo');
|
||||
const { data } = await tenantDb().from('patients').select('id, nome_completo, identification_color').order('nome_completo');
|
||||
patients.value = data ?? [];
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
// ─── Status mapping Asaas → financial_records.status ────────────────────────
|
||||
@@ -128,10 +129,9 @@ export async function getPaymentForRecord(financialRecordId) {
|
||||
if (!financialRecordId) return null;
|
||||
const tenantId = resolveTenantId();
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('asaas_payments')
|
||||
const { data, error } = await tenantDb().from('asaas_payments')
|
||||
.select('id, asaas_payment_id, billing_type, status, value, due_date, payment_date, invoice_url, payment_url, bank_slip_url, pix_qr_code, pix_copy_paste, cancelled_at')
|
||||
.eq('tenant_id', tenantId)
|
||||
|
||||
.eq('financial_record_id', financialRecordId)
|
||||
.is('cancelled_at', null)
|
||||
.order('created_at', { ascending: false })
|
||||
@@ -167,7 +167,7 @@ export async function syncPayment(asaasPaymentId) {
|
||||
*/
|
||||
export async function isGatewayEnabled() {
|
||||
const tenantId = resolveTenantId();
|
||||
const { data, error } = await supabase.from('payment_settings').select('asaas_enabled, asaas_environment').eq('tenant_id', tenantId).maybeSingle();
|
||||
const { data, error } = await tenantDb().from('payment_settings').select('asaas_enabled, asaas_environment').maybeSingle();
|
||||
if (error) return false;
|
||||
return !!data?.asaas_enabled;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { assertTenantId, getUid } from './_tenantGuards';
|
||||
import { BILLING_CONTRACT_SELECT } from './financialSelects';
|
||||
@@ -31,7 +32,7 @@ export async function listForPatient(patientId, { tenantId, includeDeleted = fal
|
||||
if (!patientId) return [];
|
||||
const tid = resolveTenantId(tenantId);
|
||||
|
||||
let q = supabase.from('billing_contracts').select(BILLING_CONTRACT_SELECT).eq('tenant_id', tid).eq('patient_id', patientId).order('created_at', { ascending: false });
|
||||
let q = tenantDb().from('billing_contracts').select(BILLING_CONTRACT_SELECT).eq('patient_id', patientId).order('created_at', { ascending: false });
|
||||
|
||||
if (!includeDeleted) q = q.is('deleted_at', null);
|
||||
|
||||
@@ -48,7 +49,7 @@ export async function getById(contractId, { tenantId } = {}) {
|
||||
if (!contractId) throw new Error('contractId obrigatório.');
|
||||
const tid = resolveTenantId(tenantId);
|
||||
|
||||
const { data, error } = await supabase.from('billing_contracts').select(BILLING_CONTRACT_SELECT).eq('id', contractId).eq('tenant_id', tid).maybeSingle();
|
||||
const { data, error } = await tenantDb().from('billing_contracts').select(BILLING_CONTRACT_SELECT).eq('id', contractId).maybeSingle();
|
||||
|
||||
if (error) throw error;
|
||||
return data || null;
|
||||
@@ -65,7 +66,6 @@ export async function create(payload) {
|
||||
const tid = resolveTenantId(payload.tenantId);
|
||||
|
||||
const row = {
|
||||
tenant_id: tid,
|
||||
owner_id: uid,
|
||||
patient_id: payload.patient_id,
|
||||
charging_style: payload.charging_style,
|
||||
@@ -77,7 +77,7 @@ export async function create(payload) {
|
||||
end_date: payload.end_date || null
|
||||
};
|
||||
|
||||
const { data, error } = await supabase.from('billing_contracts').insert([row]).select(BILLING_CONTRACT_SELECT).single();
|
||||
const { data, error } = await tenantDb().from('billing_contracts').insert([row]).select(BILLING_CONTRACT_SELECT).single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
@@ -93,7 +93,7 @@ export async function update(contractId, patch, { tenantId } = {}) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { updated_at: _dropped, ...safePatch } = patch || {};
|
||||
|
||||
const { data, error } = await supabase.from('billing_contracts').update(safePatch).eq('id', contractId).eq('tenant_id', tid).select(BILLING_CONTRACT_SELECT).single();
|
||||
const { data, error } = await tenantDb().from('billing_contracts').update(safePatch).eq('id', contractId).select(BILLING_CONTRACT_SELECT).single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
@@ -128,7 +128,7 @@ export async function findRecordsByRecurrence(recurrenceId, { tenantId } = {}) {
|
||||
if (!recurrenceId) return [];
|
||||
const tid = resolveTenantId(tenantId);
|
||||
|
||||
const { data, error } = await supabase.from('financial_records').select('id, status, agenda_evento_id, billing_contract_id').eq('tenant_id', tid).is('deleted_at', null).not('agenda_evento_id', 'is', null);
|
||||
const { data, error } = await tenantDb().from('financial_records').select('id, status, agenda_evento_id, billing_contract_id').is('deleted_at', null).not('agenda_evento_id', 'is', null);
|
||||
// NOTE: filter por recurrence_id requer join — fica como TODO no orchestrator
|
||||
// (memória project_cross_week_propagation: query records cross-week por recurrence_id).
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { assertTenantId, getUid } from './_tenantGuards';
|
||||
import { FINANCIAL_EXCEPTION_SELECT } from './financialSelects';
|
||||
@@ -35,10 +36,9 @@ export async function getRule(exceptionType, { tenantId } = {}) {
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const uid = await getUid();
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('financial_exceptions')
|
||||
const { data, error } = await tenantDb().from('financial_exceptions')
|
||||
.select(FINANCIAL_EXCEPTION_SELECT)
|
||||
.eq('tenant_id', tid)
|
||||
|
||||
.eq('exception_type', exceptionType)
|
||||
.or(`owner_id.eq.${uid},owner_id.is.null`)
|
||||
.order('owner_id', { ascending: false, nullsLast: true })
|
||||
@@ -54,7 +54,7 @@ export async function getRule(exceptionType, { tenantId } = {}) {
|
||||
*/
|
||||
export async function listAll({ tenantId } = {}) {
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const { data, error } = await supabase.from('financial_exceptions').select(FINANCIAL_EXCEPTION_SELECT).eq('tenant_id', tid).order('exception_type', { ascending: true });
|
||||
const { data, error } = await tenantDb().from('financial_exceptions').select(FINANCIAL_EXCEPTION_SELECT).order('exception_type', { ascending: true });
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
}
|
||||
@@ -72,7 +72,6 @@ export async function upsertRule(payload) {
|
||||
const tid = resolveTenantId(payload.tenantId);
|
||||
|
||||
const row = {
|
||||
tenant_id: tid,
|
||||
owner_id: payload.ownerScoped ? uid : null,
|
||||
exception_type: payload.exception_type,
|
||||
charge_mode: payload.charge_mode || 'none',
|
||||
@@ -83,7 +82,8 @@ export async function upsertRule(payload) {
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
const { data, error } = await supabase.from('financial_exceptions').upsert(row, { onConflict: 'tenant_id,owner_id,exception_type' }).select(FINANCIAL_EXCEPTION_SELECT).single();
|
||||
// TODO(schema-per-tenant): conferir unique (PK é só id; antes era tenant_id,owner_id,exception_type)
|
||||
const { data, error } = await tenantDb().from('financial_exceptions').upsert(row, { onConflict: 'owner_id,exception_type' }).select(FINANCIAL_EXCEPTION_SELECT).single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { assertTenantId, getUid } from './_tenantGuards';
|
||||
import { FINANCIAL_RECORD_SELECT, flattenFinancialRecord } from './financialSelects';
|
||||
@@ -46,10 +47,9 @@ export async function list(filters = {}) {
|
||||
const limit = filters.limit ?? 50;
|
||||
const offset = filters.offset ?? 0;
|
||||
|
||||
let q = supabase
|
||||
.from('financial_records')
|
||||
let q = tenantDb().from('financial_records')
|
||||
.select(FINANCIAL_RECORD_SELECT, { count: 'exact' })
|
||||
.eq('tenant_id', tid)
|
||||
|
||||
.is('deleted_at', null)
|
||||
.order('due_date', { ascending: false })
|
||||
.range(offset, offset + limit - 1);
|
||||
@@ -75,7 +75,7 @@ export async function getById(recordId, { tenantId } = {}) {
|
||||
if (!recordId) throw new Error('recordId obrigatório.');
|
||||
const tid = resolveTenantId(tenantId);
|
||||
|
||||
const { data, error } = await supabase.from('financial_records').select(FINANCIAL_RECORD_SELECT).eq('id', recordId).eq('tenant_id', tid).maybeSingle();
|
||||
const { data, error } = await tenantDb().from('financial_records').select(FINANCIAL_RECORD_SELECT).eq('id', recordId).maybeSingle();
|
||||
|
||||
if (error) throw error;
|
||||
return data ? flattenFinancialRecord(data) : null;
|
||||
@@ -89,7 +89,7 @@ export async function listByEvent(eventId, { tenantId } = {}) {
|
||||
if (!eventId) return [];
|
||||
const tid = resolveTenantId(tenantId);
|
||||
|
||||
const { data, error } = await supabase.from('financial_records').select(FINANCIAL_RECORD_SELECT).eq('tenant_id', tid).eq('agenda_evento_id', eventId).is('deleted_at', null);
|
||||
const { data, error } = await tenantDb().from('financial_records').select(FINANCIAL_RECORD_SELECT).eq('agenda_evento_id', eventId).is('deleted_at', null);
|
||||
|
||||
if (error) throw error;
|
||||
return (data || []).map(flattenFinancialRecord);
|
||||
@@ -140,7 +140,6 @@ export async function createManual(payload) {
|
||||
const amount = Number(payload.amount);
|
||||
|
||||
const row = {
|
||||
tenant_id: tid,
|
||||
owner_id: uid,
|
||||
patient_id: payload.patient_id ?? null,
|
||||
agenda_evento_id: null,
|
||||
@@ -155,7 +154,7 @@ export async function createManual(payload) {
|
||||
notes: payload.notes ? String(payload.notes).trim() || null : null
|
||||
};
|
||||
|
||||
const { data, error } = await supabase.from('financial_records').insert([row]).select(FINANCIAL_RECORD_SELECT).single();
|
||||
const { data, error } = await tenantDb().from('financial_records').insert([row]).select(FINANCIAL_RECORD_SELECT).single();
|
||||
|
||||
if (error) throw error;
|
||||
return flattenFinancialRecord(data);
|
||||
@@ -184,8 +183,7 @@ export async function markAsUnpaid(recordId, { tenantId } = {}) {
|
||||
if (!recordId) throw new Error('recordId obrigatório.');
|
||||
const tid = resolveTenantId(tenantId);
|
||||
|
||||
const { error } = await supabase
|
||||
.from('financial_records')
|
||||
const { error } = await tenantDb().from('financial_records')
|
||||
.update({
|
||||
status: 'pending',
|
||||
paid_at: null,
|
||||
@@ -193,13 +191,13 @@ export async function markAsUnpaid(recordId, { tenantId } = {}) {
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', recordId)
|
||||
.eq('tenant_id', tid);
|
||||
;
|
||||
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancela record (soft — status='cancelled'). Defesa em profundidade: .eq('tenant_id').
|
||||
* Cancela record (soft — status='cancelled').
|
||||
*/
|
||||
export async function cancel(recordId, { tenantId, reason } = {}) {
|
||||
if (!recordId) throw new Error('recordId obrigatório.');
|
||||
@@ -208,7 +206,7 @@ export async function cancel(recordId, { tenantId, reason } = {}) {
|
||||
const patch = { status: 'cancelled', updated_at: new Date().toISOString() };
|
||||
if (reason) patch.notes = String(reason).trim() || null;
|
||||
|
||||
const { error } = await supabase.from('financial_records').update(patch).eq('id', recordId).eq('tenant_id', tid);
|
||||
const { error } = await tenantDb().from('financial_records').update(patch).eq('id', recordId);
|
||||
|
||||
if (error) throw error;
|
||||
}
|
||||
@@ -223,7 +221,7 @@ export async function update(recordId, patch, { tenantId } = {}) {
|
||||
|
||||
const safePatch = { ...patch, updated_at: new Date().toISOString() };
|
||||
|
||||
const { data, error } = await supabase.from('financial_records').update(safePatch).eq('id', recordId).eq('tenant_id', tid).select(FINANCIAL_RECORD_SELECT).single();
|
||||
const { data, error } = await tenantDb().from('financial_records').update(safePatch).eq('id', recordId).select(FINANCIAL_RECORD_SELECT).single();
|
||||
|
||||
if (error) throw error;
|
||||
return flattenFinancialRecord(data);
|
||||
|
||||
@@ -8,12 +8,13 @@
|
||||
| Pure functions seguindo blueprints/repository-blueprint.md.
|
||||
|
|
||||
| Schema (servicos_prontuarios.sql):
|
||||
| id, owner_id, tenant_id,
|
||||
| id, owner_id,
|
||||
| name text, notes text, default_value numeric(10,2),
|
||||
| active boolean DEFAULT true, created_at, updated_at
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { assertTenantId, getUid } from './_tenantGuards';
|
||||
import { INSURANCE_PLAN_SELECT } from './insurancePlansSelects';
|
||||
@@ -37,7 +38,7 @@ export async function listForOwner({ ownerId, tenantId, includeInactive = false
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const uid = ownerId || (await getUid());
|
||||
|
||||
let q = supabase.from('insurance_plans').select(INSURANCE_PLAN_SELECT).eq('tenant_id', tid).eq('owner_id', uid).order('name', { ascending: true });
|
||||
let q = tenantDb().from('insurance_plans').select(INSURANCE_PLAN_SELECT).eq('owner_id', uid).order('name', { ascending: true });
|
||||
|
||||
if (!includeInactive) q = q.eq('active', true);
|
||||
|
||||
@@ -47,14 +48,14 @@ export async function listForOwner({ ownerId, tenantId, includeInactive = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Lê convênio por id. Filtra owner_id + tenant_id por segurança.
|
||||
* Lê convênio por id. Filtra owner_id por segurança.
|
||||
*/
|
||||
export async function getById(id, { tenantId } = {}) {
|
||||
if (!id) throw new Error('ID inválido.');
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const uid = await getUid();
|
||||
|
||||
const { data, error } = await supabase.from('insurance_plans').select(INSURANCE_PLAN_SELECT).eq('id', id).eq('tenant_id', tid).eq('owner_id', uid).maybeSingle();
|
||||
const { data, error } = await tenantDb().from('insurance_plans').select(INSURANCE_PLAN_SELECT).eq('id', id).eq('owner_id', uid).maybeSingle();
|
||||
|
||||
if (error) throw error;
|
||||
return data || null;
|
||||
@@ -76,7 +77,7 @@ export async function findByName({ name, ownerId, tenantId } = {}) {
|
||||
const safeName = String(name).trim();
|
||||
if (!safeName) return null;
|
||||
|
||||
const { data, error } = await supabase.from('insurance_plans').select(INSURANCE_PLAN_SELECT).eq('tenant_id', tid).eq('owner_id', uid).eq('active', true).ilike('name', safeName).limit(1).maybeSingle();
|
||||
const { data, error } = await tenantDb().from('insurance_plans').select(INSURANCE_PLAN_SELECT).eq('owner_id', uid).eq('active', true).ilike('name', safeName).limit(1).maybeSingle();
|
||||
|
||||
if (error) throw error;
|
||||
return data || null;
|
||||
@@ -84,7 +85,7 @@ export async function findByName({ name, ownerId, tenantId } = {}) {
|
||||
|
||||
/**
|
||||
* Cria convênio. Pré-checa duplicidade por nome (case-insensitive) — se já
|
||||
* existe ativo, lança erro PT-BR. Repository injeta owner_id + tenant_id.
|
||||
* existe ativo, lança erro PT-BR. Repository injeta owner_id.
|
||||
*/
|
||||
export async function create(payload) {
|
||||
if (!payload) throw new Error('Payload vazio.');
|
||||
@@ -103,21 +104,20 @@ export async function create(payload) {
|
||||
|
||||
const insertPayload = {
|
||||
owner_id: uid,
|
||||
tenant_id: tid,
|
||||
name: name.slice(0, 120),
|
||||
notes: payload.notes ? String(payload.notes).trim().slice(0, 500) || null : null,
|
||||
default_value: payload.default_value != null && payload.default_value !== '' ? Number(payload.default_value) : null,
|
||||
active: payload.active !== false
|
||||
};
|
||||
|
||||
const { data, error } = await supabase.from('insurance_plans').insert([insertPayload]).select(INSURANCE_PLAN_SELECT).single();
|
||||
const { data, error } = await tenantDb().from('insurance_plans').insert([insertPayload]).select(INSURANCE_PLAN_SELECT).single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualiza convênio. Filtra por id + tenant_id.
|
||||
* Atualiza convênio. Filtra por id.
|
||||
*/
|
||||
export async function update(id, patch, { tenantId } = {}) {
|
||||
if (!id) throw new Error('ID inválido.');
|
||||
@@ -127,7 +127,7 @@ export async function update(id, patch, { tenantId } = {}) {
|
||||
const safePatch = sanitize(patch);
|
||||
safePatch.updated_at = new Date().toISOString();
|
||||
|
||||
const { data, error } = await supabase.from('insurance_plans').update(safePatch).eq('id', id).eq('tenant_id', tid).select(INSURANCE_PLAN_SELECT).single();
|
||||
const { data, error } = await tenantDb().from('insurance_plans').update(safePatch).eq('id', id).select(INSURANCE_PLAN_SELECT).single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
@@ -140,7 +140,7 @@ export async function softDelete(id, { tenantId } = {}) {
|
||||
if (!id) throw new Error('ID inválido.');
|
||||
const tid = resolveTenantId(tenantId);
|
||||
|
||||
const { error } = await supabase.from('insurance_plans').update({ active: false, updated_at: new Date().toISOString() }).eq('id', id).eq('tenant_id', tid);
|
||||
const { error } = await tenantDb().from('insurance_plans').update({ active: false, updated_at: new Date().toISOString() }).eq('id', id);
|
||||
|
||||
if (error) throw error;
|
||||
return true;
|
||||
@@ -149,7 +149,9 @@ export async function softDelete(id, { tenantId } = {}) {
|
||||
// ─── helpers internos ────────────────────────────────────────────────────────
|
||||
|
||||
function sanitize(payload) {
|
||||
const out = { ...payload };
|
||||
// Dropa tenant_id defensivamente (schema-per-tenant: coluna não existe mais)
|
||||
const { tenant_id: _drop, ...rest } = payload;
|
||||
const out = { ...rest };
|
||||
if ('name' in out && typeof out.name === 'string') {
|
||||
const t = out.name.trim();
|
||||
out.name = t === '' ? null : t.slice(0, 120);
|
||||
|
||||
@@ -8,12 +8,13 @@
|
||||
| blueprints/repository-blueprint.md.
|
||||
|
|
||||
| Schema (servicos_prontuarios.sql):
|
||||
| id, owner_id, tenant_id, nome, crm, especialidade,
|
||||
| id, owner_id, nome, crm, especialidade,
|
||||
| telefone_profissional, telefone_pessoal, email, clinica,
|
||||
| cidade, estado='SP', observacoes, ativo=true, created_at, updated_at
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { assertTenantId, getUid } from './_tenantGuards';
|
||||
import { MEDICO_LIST_SELECT, MEDICO_FULL_SELECT } from './medicosSelects';
|
||||
@@ -38,7 +39,7 @@ export async function listForOwner({ ownerId, tenantId, includeInactive = false
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const uid = ownerId || (await getUid());
|
||||
|
||||
let q = supabase.from('medicos').select(MEDICO_LIST_SELECT).eq('tenant_id', tid).eq('owner_id', uid).order('nome', { ascending: true });
|
||||
let q = tenantDb().from('medicos').select(MEDICO_LIST_SELECT).eq('owner_id', uid).order('nome', { ascending: true });
|
||||
|
||||
if (!includeInactive) q = q.eq('ativo', true);
|
||||
|
||||
@@ -48,7 +49,7 @@ export async function listForOwner({ ownerId, tenantId, includeInactive = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Lê um médico completo (pra edit). Filtra owner_id + tenant_id por segurança.
|
||||
* Lê um médico completo (pra edit). Filtra owner_id por segurança.
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {Object} [opts]
|
||||
@@ -59,14 +60,14 @@ export async function getById(id, { tenantId } = {}) {
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const uid = await getUid();
|
||||
|
||||
const { data, error } = await supabase.from('medicos').select(MEDICO_FULL_SELECT).eq('id', id).eq('tenant_id', tid).eq('owner_id', uid).maybeSingle();
|
||||
const { data, error } = await tenantDb().from('medicos').select(MEDICO_FULL_SELECT).eq('id', id).eq('owner_id', uid).maybeSingle();
|
||||
|
||||
if (error) throw error;
|
||||
return data || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria médico. Injeta owner_id (uid logado) + tenant_id (store).
|
||||
* Cria médico. Injeta owner_id (uid logado).
|
||||
* Payload aceita os campos canônicos da tabela; o repository sanitiza
|
||||
* trims e nullif vazio.
|
||||
*
|
||||
@@ -83,18 +84,17 @@ export async function create(payload) {
|
||||
const insertPayload = {
|
||||
...sanitize(payload),
|
||||
owner_id: uid,
|
||||
tenant_id: tid,
|
||||
ativo: payload.ativo !== false
|
||||
};
|
||||
|
||||
const { data, error } = await supabase.from('medicos').insert([insertPayload]).select(MEDICO_FULL_SELECT).single();
|
||||
const { data, error } = await tenantDb().from('medicos').insert([insertPayload]).select(MEDICO_FULL_SELECT).single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualiza médico. Filtra por id + tenant_id (defesa em profundidade — RLS reforça).
|
||||
* Atualiza médico. Filtra por id (defesa em profundidade — RLS reforça).
|
||||
* updated_at é atualizado server-side ou aqui se não houver trigger.
|
||||
*
|
||||
* @param {string} id
|
||||
@@ -112,7 +112,7 @@ export async function update(id, patch, { tenantId } = {}) {
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
const { data, error } = await supabase.from('medicos').update(safePatch).eq('id', id).eq('tenant_id', tid).select(MEDICO_FULL_SELECT).single();
|
||||
const { data, error } = await tenantDb().from('medicos').update(safePatch).eq('id', id).select(MEDICO_FULL_SELECT).single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
@@ -130,7 +130,7 @@ export async function softDelete(id, { tenantId } = {}) {
|
||||
if (!id) throw new Error('ID inválido.');
|
||||
const tid = resolveTenantId(tenantId);
|
||||
|
||||
const { error } = await supabase.from('medicos').update({ ativo: false, updated_at: new Date().toISOString() }).eq('id', id).eq('tenant_id', tid);
|
||||
const { error } = await tenantDb().from('medicos').update({ ativo: false, updated_at: new Date().toISOString() }).eq('id', id);
|
||||
|
||||
if (error) throw error;
|
||||
return true;
|
||||
@@ -141,12 +141,14 @@ export async function softDelete(id, { tenantId } = {}) {
|
||||
/**
|
||||
* Sanitiza payload: trim em strings, nullif vazio.
|
||||
* Não sanitiza telefones (já chegam digits-only do componente)
|
||||
* nem owner_id/tenant_id/ativo (controlados pelo repository).
|
||||
* nem owner_id/ativo (controlados pelo repository).
|
||||
* Dropa tenant_id defensivamente (schema-per-tenant: coluna não existe mais).
|
||||
*/
|
||||
function sanitize(payload) {
|
||||
const stringFields = ['nome', 'crm', 'especialidade', 'telefone_profissional', 'telefone_pessoal', 'email', 'clinica', 'cidade', 'estado', 'observacoes'];
|
||||
|
||||
const out = { ...payload };
|
||||
const { tenant_id: _drop, ...rest } = payload;
|
||||
const out = { ...rest };
|
||||
for (const f of stringFields) {
|
||||
if (f in out) {
|
||||
const v = out[f];
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
-->
|
||||
<script setup>
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import {
|
||||
listGroupsByPatient,
|
||||
listTagsByPatient,
|
||||
@@ -76,13 +77,12 @@ async function abrirSessoes(pat) {
|
||||
recorrencias.value = [];
|
||||
try {
|
||||
const [evts, recs] = await Promise.all([
|
||||
supabase
|
||||
.from('agenda_eventos')
|
||||
tenantDb().from('agenda_eventos')
|
||||
.select('id, titulo, tipo, status, inicio_em, fim_em, modalidade, insurance_guide_number, insurance_value, insurance_plans(name)')
|
||||
.eq('patient_id', pat.id)
|
||||
.order('inicio_em', { ascending: false })
|
||||
.limit(100),
|
||||
supabase.from('recurrence_rules').select('id, type, interval, weekdays, start_date, end_date, start_time, duration_min, status').eq('patient_id', pat.id).order('start_date', { ascending: false })
|
||||
tenantDb().from('recurrence_rules').select('id, type, interval, weekdays, start_date, end_date, start_time, duration_min, status').eq('patient_id', pat.id).order('start_date', { ascending: false })
|
||||
]);
|
||||
sessoesLista.value = evts.data || [];
|
||||
recorrencias.value = recs.data || [];
|
||||
@@ -487,11 +487,9 @@ function withOwnerFilter(q) {
|
||||
return uid.value ? q.eq('owner_id', uid.value) : q;
|
||||
}
|
||||
|
||||
// Defesa em profundidade: filtra por tenant_id do tenantStore em todas as queries.
|
||||
// RLS cobre no backend, mas blindamos no cliente (padrão do projeto).
|
||||
// Schema-per-tenant: isolamento via schema tenant_<slug>; tabela não tem mais coluna tenant_id.
|
||||
function withTenantFilter(q) {
|
||||
const tid = tenantStore.activeTenantId;
|
||||
return tid ? q.eq('tenant_id', tid) : q;
|
||||
return q;
|
||||
}
|
||||
|
||||
// ── Filtered rows ─────────────────────────────────────────
|
||||
@@ -547,13 +545,13 @@ async function fetchAll() {
|
||||
discountMap.value = {};
|
||||
if (uid.value) {
|
||||
const now = new Date().toISOString();
|
||||
const { data: discRows } = await supabase.from('patient_discounts').select('patient_id, discount_pct, discount_flat').eq('owner_id', uid.value).eq('active', true).or(`active_to.is.null,active_to.gte.${now}`);
|
||||
const { data: discRows } = await tenantDb().from('patient_discounts').select('patient_id, discount_pct, discount_flat').eq('owner_id', uid.value).eq('active', true).or(`active_to.is.null,active_to.gte.${now}`);
|
||||
if (discRows) discountMap.value = Object.fromEntries(discRows.map((d) => [d.patient_id, d]));
|
||||
}
|
||||
|
||||
insuranceMap.value = {};
|
||||
if (uid.value) {
|
||||
const { data: insRows } = await supabase.from('agenda_eventos').select('patient_id, insurance_plan_id, insurance_plans(name)').eq('owner_id', uid.value).not('insurance_plan_id', 'is', null).order('inicio_em', { ascending: false });
|
||||
const { data: insRows } = await tenantDb().from('agenda_eventos').select('patient_id, insurance_plan_id, insurance_plans(name)').eq('owner_id', uid.value).not('insurance_plan_id', 'is', null).order('inicio_em', { ascending: false });
|
||||
if (insRows) {
|
||||
for (const row of insRows) {
|
||||
if (!insuranceMap.value[row.patient_id]) insuranceMap.value[row.patient_id] = row.insurance_plans?.name ?? null;
|
||||
@@ -574,7 +572,7 @@ async function fetchAll() {
|
||||
}
|
||||
|
||||
async function listPatients() {
|
||||
let q = supabase.from('patients').select('id, owner_id, nome_completo, email_principal, telefone, avatar_url, status, last_attended_at, created_at, updated_at').order('created_at', { ascending: false });
|
||||
let q = tenantDb().from('patients').select('id, owner_id, nome_completo, email_principal, telefone, avatar_url, status, last_attended_at, created_at, updated_at').order('created_at', { ascending: false });
|
||||
q = withTenantFilter(withOwnerFilter(q));
|
||||
const { data, error } = await q;
|
||||
if (error) throw error;
|
||||
@@ -590,7 +588,7 @@ async function listPatients() {
|
||||
}
|
||||
|
||||
async function listGroups() {
|
||||
let q = supabase.from('patient_groups').select('id, owner_id, nome, cor, is_system, is_active').eq('is_active', true).order('nome', { ascending: true });
|
||||
let q = tenantDb().from('patient_groups').select('id, owner_id, nome, cor, is_system, is_active').eq('is_active', true).order('nome', { ascending: true });
|
||||
q = withTenantFilter(q);
|
||||
if (uid.value) q = q.or(`is_system.eq.true,owner_id.eq.${uid.value}`);
|
||||
else q = q.eq('is_system', true);
|
||||
@@ -600,7 +598,7 @@ async function listGroups() {
|
||||
}
|
||||
|
||||
async function listTags() {
|
||||
let q = supabase.from('patient_tags').select('id, owner_id, nome, cor').order('nome', { ascending: true });
|
||||
let q = tenantDb().from('patient_tags').select('id, owner_id, nome, cor').order('nome', { ascending: true });
|
||||
q = withTenantFilter(q);
|
||||
if (uid.value) q = q.eq('owner_id', uid.value);
|
||||
const { data, error } = await q;
|
||||
|
||||
@@ -64,6 +64,7 @@ import { useRoleGuard } from '@/composables/useRoleGuard'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { useConfirm } from 'primevue/useconfirm'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore'
|
||||
import { logError } from '@/support/supportLogger'
|
||||
import { digitsOnly, fmtCPF, fmtRG, fmtPhone, toISODate, generateCPF } from '@/utils/validators'
|
||||
@@ -647,7 +648,7 @@ async function onSubmit () {
|
||||
await openPanel(0); return
|
||||
}
|
||||
const payload = sanitizePayload(form.value, ownerId)
|
||||
payload.tenant_id = tenantId; payload.responsible_member_id = memberId
|
||||
payload.responsible_member_id = memberId
|
||||
if (isEdit.value) {
|
||||
await updatePatient(patientId.value, payload)
|
||||
await maybeUploadAvatar(ownerId, patientId.value)
|
||||
@@ -706,7 +707,7 @@ async function doDelete () {
|
||||
['patients', 'id'],
|
||||
]
|
||||
for (const [tbl, col] of tables) {
|
||||
const { error } = await supabase.from(tbl).delete().eq(col, pid); if (error) throw error
|
||||
const { error } = await tenantDb().from(tbl).delete().eq(col, pid); if (error) throw error
|
||||
}
|
||||
toast.add({ severity:'success', summary:'Excluído', detail:'Paciente removido.', life:2500 })
|
||||
if (props.dialogMode) { emit('created', null); return }
|
||||
@@ -766,7 +767,7 @@ async function createGroupPersist () {
|
||||
createGroupSaving.value=true
|
||||
try {
|
||||
const ownerId=await getOwnerId(); const { tenantId }=await resolveTenantContextOrFail()
|
||||
const { data, error }=await supabase.from('patient_groups').insert({ owner_id:ownerId, tenant_id:tenantId, nome:name, cor:color, is_system:false, is_active:true }).select('id').single()
|
||||
const { data, error }=await tenantDb().from('patient_groups').insert({ owner_id:ownerId, nome:name, cor:color, is_system:false, is_active:true }).select('id').single()
|
||||
if (error) throw error
|
||||
groups.value=await listGroups(); if (data?.id) grupoIdSelecionado.value=data.id
|
||||
toast.add({ severity:'success', summary:'Grupo criado.', life:2500 }); createGroupDialog.value=false
|
||||
@@ -782,7 +783,7 @@ async function createTagPersist () {
|
||||
createTagSaving.value=true
|
||||
try {
|
||||
const ownerId=await getOwnerId(); const { tenantId }=await resolveTenantContextOrFail()
|
||||
const { data, error }=await supabase.from('patient_tags').insert({ owner_id:ownerId, tenant_id:tenantId, nome:name, cor:color }).select('id').single()
|
||||
const { data, error }=await tenantDb().from('patient_tags').insert({ owner_id:ownerId, nome:name, cor:color }).select('id').single()
|
||||
if (error) throw error
|
||||
tags.value=await listTags()
|
||||
if (data?.id) { const s=new Set([...(tagIdsSelecionadas.value||[]),data.id]); tagIdsSelecionadas.value=Array.from(s) }
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
-->
|
||||
<script setup>
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
// Fase 2 (Graphify hotspot): convertToPatient duplicado em 2 pages — INSERT/UPDATE
|
||||
// extraídos pro repository pra remover duplicação.
|
||||
@@ -275,7 +276,7 @@ const intakeSections = computed(() => {
|
||||
async function fetchIntakes() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data, error } = await supabase.from('patient_intake_requests').select('*').order('created_at', { ascending: false });
|
||||
const { data, error } = await tenantDb().from('patient_intake_requests').select('*').order('created_at', { ascending: false });
|
||||
if (error) throw error;
|
||||
const weight = (s) => (s === 'new' ? 0 : s === 'converted' ? 1 : s === 'rejected' ? 2 : 9);
|
||||
rows.value = (data || []).slice().sort((a, b) => {
|
||||
@@ -322,7 +323,7 @@ async function markRejected() {
|
||||
dlg.value.saving = true;
|
||||
try {
|
||||
const reason = String(dlg.value.reject_note || '').trim() || null;
|
||||
const { error } = await supabase.from('patient_intake_requests').update({ status: 'rejected', rejected_reason: reason, updated_at: new Date().toISOString() }).eq('id', item.id);
|
||||
const { error } = await tenantDb().from('patient_intake_requests').update({ status: 'rejected', rejected_reason: reason, updated_at: new Date().toISOString() }).eq('id', item.id);
|
||||
if (error) throw error;
|
||||
toast.add({ severity: 'success', summary: 'Rejeitado', detail: 'Solicitação rejeitada.', life: 2500 });
|
||||
await fetchIntakes();
|
||||
@@ -371,7 +372,6 @@ async function convertToPatient() {
|
||||
const intakeAvatar = cleanStr(item.avatar_url) || cleanStr(item.foto_url) || cleanStr(item.photo_url) || null;
|
||||
|
||||
const patientPayload = {
|
||||
tenant_id: tenantId,
|
||||
responsible_member_id: responsibleMemberId,
|
||||
owner_id: ownerId,
|
||||
nome_completo: cleanStr(fNome(item)),
|
||||
|
||||
@@ -21,6 +21,7 @@ import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import Checkbox from 'primevue/checkbox';
|
||||
import Menu from 'primevue/menu';
|
||||
|
||||
@@ -201,7 +202,7 @@ function applyRealCountsToGroups(groupsArr, countMap) {
|
||||
async function fetchRealGroupCountsForOwner() {
|
||||
const ownerId = (await supabase.auth.getUser())?.data?.user?.id;
|
||||
if (!ownerId) throw new Error('Sessão inválida.');
|
||||
const { data, error } = await supabase.from('patient_group_patient').select('patient_group_id, patient:patients!inner(id, owner_id)').eq('patient.owner_id', ownerId);
|
||||
const { data, error } = await tenantDb().from('patient_group_patient').select('patient_group_id, patient:patients!inner(id, owner_id)').eq('patient.owner_id', ownerId);
|
||||
if (error) throw error;
|
||||
const map = Object.create(null);
|
||||
for (const row of data || []) {
|
||||
@@ -362,7 +363,7 @@ async function openGroupPatientsModal(groupRow) {
|
||||
patientsDialog.items = [];
|
||||
patientsDialog.search = '';
|
||||
try {
|
||||
const { data, error } = await supabase.from('patient_group_patient').select('patient_id, patient:patients(id, nome_completo, email_principal, telefone, avatar_url)').eq('patient_group_id', groupRow.id);
|
||||
const { data, error } = await tenantDb().from('patient_group_patient').select('patient_id, patient:patients(id, nome_completo, email_principal, telefone, avatar_url)').eq('patient_group_id', groupRow.id);
|
||||
if (error) throw error;
|
||||
patientsDialog.items = (data || [])
|
||||
.map((r) => r.patient)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useConversationDrawerStore } from '@/stores/conversationDrawerStore';
|
||||
import { formatDistanceToNow, format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
@@ -31,8 +32,7 @@ async function load() {
|
||||
if (!props.patientId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('conversation_messages')
|
||||
const { data, error } = await tenantDb().from('conversation_messages')
|
||||
.select('id, channel, direction, from_number, to_number, body, media_url, media_mime, provider, kanban_status, received_at, created_at, responded_at, delivery_status')
|
||||
.eq('patient_id', props.patientId)
|
||||
.order('created_at', { ascending: true })
|
||||
|
||||
@@ -27,6 +27,7 @@ import Popover from 'primevue/popover';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useConversationDrawerStore } from '@/stores/conversationDrawerStore';
|
||||
|
||||
@@ -379,8 +380,7 @@ async function updateSessionStatus(ev, novoStatus, msg) {
|
||||
if (!ev?.id || sessionBusy.value) return;
|
||||
sessionBusy.value = true;
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('agenda_eventos')
|
||||
const { error } = await tenantDb().from('agenda_eventos')
|
||||
.update({ status: novoStatus })
|
||||
.eq('id', ev.id);
|
||||
if (error) throw error;
|
||||
@@ -432,8 +432,7 @@ function openAddPhone() {
|
||||
// bloqueamos save por falha no check.
|
||||
async function findPhoneOwner(digits, excludeId) {
|
||||
try {
|
||||
const { data: byPat } = await supabase
|
||||
.from('patients')
|
||||
const { data: byPat } = await tenantDb().from('patients')
|
||||
.select('id, nome_completo')
|
||||
.eq('telefone', digits)
|
||||
.neq('id', excludeId)
|
||||
@@ -441,8 +440,7 @@ async function findPhoneOwner(digits, excludeId) {
|
||||
.maybeSingle();
|
||||
if (byPat?.id) return byPat;
|
||||
|
||||
const { data: byCp } = await supabase
|
||||
.from('contact_phones')
|
||||
const { data: byCp } = await tenantDb().from('contact_phones')
|
||||
.select('entity_id')
|
||||
.eq('entity_type', 'patient')
|
||||
.eq('number', digits)
|
||||
@@ -450,8 +448,7 @@ async function findPhoneOwner(digits, excludeId) {
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
if (byCp?.entity_id) {
|
||||
const { data: p } = await supabase
|
||||
.from('patients')
|
||||
const { data: p } = await tenantDb().from('patients')
|
||||
.select('id, nome_completo')
|
||||
.eq('id', byCp.entity_id)
|
||||
.maybeSingle();
|
||||
@@ -508,8 +505,7 @@ async function _persistPhone(id, digits, tenantId) {
|
||||
|
||||
// 1) Garante o contact_type "whatsapp" (system, slug fixo via
|
||||
// seed_014_global_data).
|
||||
const { data: ctype, error: errType } = await supabase
|
||||
.from('contact_types')
|
||||
const { data: ctype, error: errType } = await tenantDb().from('contact_types')
|
||||
.select('id')
|
||||
.eq('slug', 'whatsapp')
|
||||
.order('is_system', { ascending: false })
|
||||
@@ -519,8 +515,7 @@ async function _persistPhone(id, digits, tenantId) {
|
||||
if (!ctype?.id) throw new Error('Tipo de contato "WhatsApp" não encontrado.');
|
||||
|
||||
// 2) Insere ou atualiza em contact_phones (entity_type=patient).
|
||||
const { data: existing } = await supabase
|
||||
.from('contact_phones')
|
||||
const { data: existing } = await tenantDb().from('contact_phones')
|
||||
.select('id, is_primary')
|
||||
.eq('entity_type', 'patient')
|
||||
.eq('entity_id', id)
|
||||
@@ -529,22 +524,18 @@ async function _persistPhone(id, digits, tenantId) {
|
||||
.maybeSingle();
|
||||
|
||||
if (existing?.id) {
|
||||
const { error: errUpd } = await supabase
|
||||
.from('contact_phones')
|
||||
const { error: errUpd } = await tenantDb().from('contact_phones')
|
||||
.update({ number: digits, whatsapp_linked_at: new Date().toISOString() })
|
||||
.eq('id', existing.id);
|
||||
if (errUpd) throw errUpd;
|
||||
} else {
|
||||
const { count } = await supabase
|
||||
.from('contact_phones')
|
||||
const { count } = await tenantDb().from('contact_phones')
|
||||
.select('id', { count: 'exact', head: true })
|
||||
.eq('entity_type', 'patient')
|
||||
.eq('entity_id', id);
|
||||
const isPrimary = (count || 0) === 0;
|
||||
const { error: errIns } = await supabase
|
||||
.from('contact_phones')
|
||||
const { error: errIns } = await tenantDb().from('contact_phones')
|
||||
.insert({
|
||||
tenant_id: tenantId,
|
||||
entity_type: 'patient',
|
||||
entity_id: id,
|
||||
contact_type_id: ctype.id,
|
||||
@@ -820,8 +811,7 @@ async function loadSessions(patientId) {
|
||||
sessionsLoading.value = true;
|
||||
sessions.value = [];
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_eventos')
|
||||
const { data, error } = await tenantDb().from('agenda_eventos')
|
||||
.select('id, inicio_em, fim_em, status, modalidade, tipo, titulo, titulo_custom, observacoes')
|
||||
.eq('patient_id', patientId)
|
||||
.order('inicio_em', { ascending: false })
|
||||
@@ -840,8 +830,7 @@ async function loadRecentMessages(patientId) {
|
||||
messagesLoading.value = true;
|
||||
recentMessages.value = [];
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('conversation_messages')
|
||||
const { data, error } = await tenantDb().from('conversation_messages')
|
||||
.select('id, body, direction, created_at, channel, kanban_status')
|
||||
.eq('patient_id', patientId)
|
||||
.order('created_at', { ascending: false })
|
||||
@@ -858,8 +847,7 @@ async function loadDocumentsList(patientId) {
|
||||
documentsLoading.value = true;
|
||||
documentsList.value = [];
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('documents')
|
||||
const { data, error } = await tenantDb().from('documents')
|
||||
.select('id, tipo_documento, created_at, status_revisao, tamanho_bytes')
|
||||
.eq('patient_id', patientId)
|
||||
.is('deleted_at', null)
|
||||
@@ -878,8 +866,7 @@ async function loadFinancialRecent(patientId) {
|
||||
financialLoading.value = true;
|
||||
financialRecords.value = [];
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('financial_records')
|
||||
const { data, error } = await tenantDb().from('financial_records')
|
||||
.select('id, type, amount, due_date, paid_at, description, payment_method, category, created_at')
|
||||
.eq('patient_id', patientId)
|
||||
.eq('type', 'receita')
|
||||
@@ -892,15 +879,15 @@ async function loadFinancialRecent(patientId) {
|
||||
}
|
||||
|
||||
async function getPatientById(id) {
|
||||
const { data, error } = await supabase.from('patients').select('*').eq('id', id).maybeSingle();
|
||||
const { data, error } = await tenantDb().from('patients').select('*').eq('id', id).maybeSingle();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
|
||||
async function getPatientRelations(id) {
|
||||
const { data: g, error: ge } = await supabase.from('patient_group_patient').select('patient_group_id').eq('patient_id', id);
|
||||
const { data: g, error: ge } = await tenantDb().from('patient_group_patient').select('patient_group_id').eq('patient_id', id);
|
||||
if (ge) throw ge;
|
||||
const { data: t, error: te } = await supabase.from('patient_patient_tag').select('tag_id').eq('patient_id', id);
|
||||
const { data: t, error: te } = await tenantDb().from('patient_patient_tag').select('tag_id').eq('patient_id', id);
|
||||
if (te) throw te;
|
||||
return {
|
||||
groupIds: (g || []).map(x => x.patient_group_id).filter(Boolean),
|
||||
@@ -910,14 +897,14 @@ async function getPatientRelations(id) {
|
||||
|
||||
async function getGroupsByIds(ids) {
|
||||
if (!ids?.length) return [];
|
||||
const { data, error } = await supabase.from('patient_groups').select('id, nome').in('id', ids).order('nome', { ascending: true });
|
||||
const { data, error } = await tenantDb().from('patient_groups').select('id, nome').in('id', ids).order('nome', { ascending: true });
|
||||
if (error) throw error;
|
||||
return (data || []).map(g => ({ id: g.id, name: g.nome }));
|
||||
}
|
||||
|
||||
async function getTagsByIds(ids) {
|
||||
if (!ids?.length) return [];
|
||||
const { data, error } = await supabase.from('patient_tags').select('id, nome, cor').in('id', ids).order('nome', { ascending: true });
|
||||
const { data, error } = await tenantDb().from('patient_tags').select('id, nome, cor').in('id', ids).order('nome', { ascending: true });
|
||||
if (error) throw error;
|
||||
return (data || []).map(t => ({ id: t.id, name: t.nome, color: t.cor }));
|
||||
}
|
||||
@@ -989,8 +976,7 @@ async function setProximaSessaoStatus(novoStatus, msgSucesso) {
|
||||
if (!ev?.id || sessionBusy.value) return;
|
||||
sessionBusy.value = true;
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('agenda_eventos')
|
||||
const { error } = await tenantDb().from('agenda_eventos')
|
||||
.update({ status: novoStatus })
|
||||
.eq('id', ev.id);
|
||||
if (error) throw error;
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
| Arquivo: src/features/patients/prontuario/services/clinicalNoteTemplatesRepository.js
|
||||
|
|
||||
| Repository de clinical_note_templates. Escopo escalonado:
|
||||
| - Sistema (is_system=true, tenant_id NULL) — todos authenticated leem
|
||||
| - Tenant-wide (tenant_id, owner_id NULL) — membros do tenant
|
||||
| - Owner (tenant_id + owner_id) — só o owner
|
||||
| - Sistema (is_system=true) — todos authenticated leem
|
||||
| - Tenant-wide (owner_id NULL) — membros do tenant (schema do tenant)
|
||||
| - Owner (owner_id) — só o owner
|
||||
|
|
||||
| RLS bloqueia INSERT/UPDATE/DELETE de templates is_system — só via seed.
|
||||
| Templates do tenant podem ser criados/editados pelo tenant_admin.
|
||||
@@ -16,6 +16,7 @@
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { assertTenantId, getUid } from './_tenantGuards';
|
||||
import { CLINICAL_NOTE_TEMPLATE_SELECT } from './clinicalNotesSelects';
|
||||
@@ -41,7 +42,7 @@ function resolveTenantId(tenantIdArg) {
|
||||
export async function listAvailable({ noteType, tenantId, includeInactive = false } = {}) {
|
||||
resolveTenantId(tenantId); // garante tenant ativo (RLS depende)
|
||||
|
||||
let q = supabase.from('clinical_note_templates').select(CLINICAL_NOTE_TEMPLATE_SELECT).order('is_system', { ascending: false }).order('name', { ascending: true });
|
||||
let q = tenantDb().from('clinical_note_templates').select(CLINICAL_NOTE_TEMPLATE_SELECT).order('is_system', { ascending: false }).order('name', { ascending: true });
|
||||
|
||||
if (!includeInactive) q = q.eq('active', true);
|
||||
if (noteType) q = q.eq('note_type', noteType);
|
||||
@@ -57,7 +58,7 @@ export async function listAvailable({ noteType, tenantId, includeInactive = fals
|
||||
export async function getById(templateId) {
|
||||
if (!templateId) throw new Error('ID inválido.');
|
||||
|
||||
const { data, error } = await supabase.from('clinical_note_templates').select(CLINICAL_NOTE_TEMPLATE_SELECT).eq('id', templateId).maybeSingle();
|
||||
const { data, error } = await tenantDb().from('clinical_note_templates').select(CLINICAL_NOTE_TEMPLATE_SELECT).eq('id', templateId).maybeSingle();
|
||||
|
||||
if (error) throw error;
|
||||
return data || null;
|
||||
@@ -70,7 +71,7 @@ export async function getById(templateId) {
|
||||
export async function getByKey(key, { noteType } = {}) {
|
||||
if (!key) throw new Error('Key inválida.');
|
||||
|
||||
let q = supabase.from('clinical_note_templates').select(CLINICAL_NOTE_TEMPLATE_SELECT).eq('key', key).eq('active', true);
|
||||
let q = tenantDb().from('clinical_note_templates').select(CLINICAL_NOTE_TEMPLATE_SELECT).eq('key', key).eq('active', true);
|
||||
if (noteType) q = q.eq('note_type', noteType);
|
||||
|
||||
const { data, error } = await q.order('is_system', { ascending: false }).limit(1).maybeSingle();
|
||||
@@ -94,7 +95,6 @@ export async function create(payload) {
|
||||
const tid = resolveTenantId();
|
||||
|
||||
const row = {
|
||||
tenant_id: tid,
|
||||
owner_id: payload.ownerScoped ? uid : null,
|
||||
key: String(payload.key).trim(),
|
||||
name: String(payload.name).trim(),
|
||||
@@ -106,7 +106,7 @@ export async function create(payload) {
|
||||
active: payload.active !== false
|
||||
};
|
||||
|
||||
const { data, error } = await supabase.from('clinical_note_templates').insert([row]).select(CLINICAL_NOTE_TEMPLATE_SELECT).single();
|
||||
const { data, error } = await tenantDb().from('clinical_note_templates').insert([row]).select(CLINICAL_NOTE_TEMPLATE_SELECT).single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
@@ -120,8 +120,9 @@ export async function update(templateId, patch) {
|
||||
|
||||
const safePatch = { ...patch, updated_at: new Date().toISOString() };
|
||||
if ('is_system' in safePatch) delete safePatch.is_system; // RLS bloqueia mas defesa em profundidade
|
||||
if ('tenant_id' in safePatch) delete safePatch.tenant_id; // schema-per-tenant: coluna não existe mais
|
||||
|
||||
const { data, error } = await supabase.from('clinical_note_templates').update(safePatch).eq('id', templateId).select(CLINICAL_NOTE_TEMPLATE_SELECT).single();
|
||||
const { data, error } = await tenantDb().from('clinical_note_templates').update(safePatch).eq('id', templateId).select(CLINICAL_NOTE_TEMPLATE_SELECT).single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
@@ -133,7 +134,7 @@ export async function update(templateId, patch) {
|
||||
export async function softDelete(templateId) {
|
||||
if (!templateId) throw new Error('ID inválido.');
|
||||
|
||||
const { error } = await supabase.from('clinical_note_templates').update({ active: false, updated_at: new Date().toISOString() }).eq('id', templateId);
|
||||
const { error } = await tenantDb().from('clinical_note_templates').update({ active: false, updated_at: new Date().toISOString() }).eq('id', templateId);
|
||||
|
||||
if (error) throw error;
|
||||
return true;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { assertTenantId, getUid } from './_tenantGuards';
|
||||
import {
|
||||
@@ -55,10 +56,9 @@ export async function listForPatient(patientId, { tenantId, noteType = null, inc
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const select = brief ? CLINICAL_NOTE_SELECT_BRIEF : CLINICAL_NOTE_SELECT;
|
||||
|
||||
let q = supabase
|
||||
.from('clinical_notes')
|
||||
let q = tenantDb().from('clinical_notes')
|
||||
.select(select)
|
||||
.eq('tenant_id', tid)
|
||||
|
||||
.eq('patient_id', patientId)
|
||||
.order('pinned', { ascending: false })
|
||||
.order('created_at', { ascending: false });
|
||||
@@ -80,7 +80,7 @@ export async function listForSession(sessionEventId, { tenantId, brief = false }
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const select = brief ? CLINICAL_NOTE_SELECT_BRIEF : CLINICAL_NOTE_SELECT;
|
||||
|
||||
const { data, error } = await supabase.from('clinical_notes').select(select).eq('tenant_id', tid).eq('session_event_id', sessionEventId).is('deleted_at', null).order('created_at', { ascending: false });
|
||||
const { data, error } = await tenantDb().from('clinical_notes').select(select).eq('session_event_id', sessionEventId).is('deleted_at', null).order('created_at', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
return (data || []).map(flattenNoteRow);
|
||||
@@ -93,7 +93,7 @@ export async function getById(noteId, { tenantId } = {}) {
|
||||
if (!noteId) throw new Error('ID inválido.');
|
||||
const tid = resolveTenantId(tenantId);
|
||||
|
||||
const { data, error } = await supabase.from('clinical_notes').select(CLINICAL_NOTE_SELECT).eq('id', noteId).eq('tenant_id', tid).maybeSingle();
|
||||
const { data, error } = await tenantDb().from('clinical_notes').select(CLINICAL_NOTE_SELECT).eq('id', noteId).maybeSingle();
|
||||
|
||||
if (error) throw error;
|
||||
return data ? flattenNoteRow(data) : null;
|
||||
@@ -140,7 +140,7 @@ export async function create(payload) {
|
||||
created_by: uid
|
||||
};
|
||||
|
||||
const { data, error } = await supabase.from('clinical_notes').insert([row]).select(CLINICAL_NOTE_SELECT).single();
|
||||
const { data, error } = await tenantDb().from('clinical_notes').insert([row]).select(CLINICAL_NOTE_SELECT).single();
|
||||
|
||||
if (error) throw error;
|
||||
return flattenNoteRow(data);
|
||||
@@ -164,7 +164,7 @@ export async function update(noteId, patch, { tenantId } = {}) {
|
||||
|
||||
const safePatch = { ...sanitize(patch), updated_by: uid };
|
||||
|
||||
const { data, error } = await supabase.from('clinical_notes').update(safePatch).eq('id', noteId).eq('tenant_id', tid).select(CLINICAL_NOTE_SELECT).single();
|
||||
const { data, error } = await tenantDb().from('clinical_notes').update(safePatch).eq('id', noteId).select(CLINICAL_NOTE_SELECT).single();
|
||||
|
||||
if (error) throw error;
|
||||
return flattenNoteRow(data);
|
||||
@@ -179,11 +179,10 @@ export async function softDelete(noteId, { tenantId } = {}) {
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const uid = await getUid();
|
||||
|
||||
const { error } = await supabase
|
||||
.from('clinical_notes')
|
||||
const { error } = await tenantDb().from('clinical_notes')
|
||||
.update({ deleted_at: new Date().toISOString(), deleted_by: uid, updated_by: uid })
|
||||
.eq('id', noteId)
|
||||
.eq('tenant_id', tid);
|
||||
;
|
||||
|
||||
if (error) throw error;
|
||||
return true;
|
||||
@@ -197,7 +196,7 @@ export async function restore(noteId, { tenantId } = {}) {
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const uid = await getUid();
|
||||
|
||||
const { error } = await supabase.from('clinical_notes').update({ deleted_at: null, deleted_by: null, updated_by: uid }).eq('id', noteId).eq('tenant_id', tid);
|
||||
const { error } = await tenantDb().from('clinical_notes').update({ deleted_at: null, deleted_by: null, updated_by: uid }).eq('id', noteId);
|
||||
|
||||
if (error) throw error;
|
||||
return true;
|
||||
@@ -216,7 +215,7 @@ export async function setPinned(noteId, pinned, { tenantId } = {}) {
|
||||
export async function listVersions(noteId) {
|
||||
if (!noteId) return [];
|
||||
|
||||
const { data, error } = await supabase.from('clinical_note_versions').select(CLINICAL_NOTE_VERSION_SELECT).eq('note_id', noteId).order('version_number', { ascending: false });
|
||||
const { data, error } = await tenantDb().from('clinical_note_versions').select(CLINICAL_NOTE_VERSION_SELECT).eq('note_id', noteId).order('version_number', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
@@ -229,7 +228,7 @@ export async function getVersion(noteId, versionNumber) {
|
||||
if (!noteId) throw new Error('noteId obrigatório.');
|
||||
if (!versionNumber) throw new Error('versionNumber obrigatório.');
|
||||
|
||||
const { data, error } = await supabase.from('clinical_note_versions').select(CLINICAL_NOTE_VERSION_SELECT).eq('note_id', noteId).eq('version_number', versionNumber).maybeSingle();
|
||||
const { data, error } = await tenantDb().from('clinical_note_versions').select(CLINICAL_NOTE_VERSION_SELECT).eq('note_id', noteId).eq('version_number', versionNumber).maybeSingle();
|
||||
|
||||
if (error) throw error;
|
||||
return data || null;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
| V#3 — fundação: queries de patients centralizadas.
|
||||
|
|
||||
| Pages e composables devem chamar este repo em vez de fazer
|
||||
| supabase.from('patients') direto.
|
||||
| tenantDb().from('patients') direto.
|
||||
|
|
||||
| Inclui também reads cross-feature em escopo de paciente (agenda_eventos,
|
||||
| financial_records, documents, recurrence_rules, conversation_messages,
|
||||
@@ -16,6 +16,7 @@
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { assertTenantId, getUid } from '@/features/agenda/services/_tenantGuards';
|
||||
import {
|
||||
@@ -51,7 +52,7 @@ function resolveTenantId(tenantIdArg) {
|
||||
export async function listPatients({ tenantId, ownerId = null, includeInactive = true, limit = null } = {}) {
|
||||
const tid = resolveTenantId(tenantId);
|
||||
|
||||
let q = supabase.from('patients').select(PATIENTS_SELECT_BASE).eq('tenant_id', tid);
|
||||
let q = tenantDb().from('patients').select(PATIENTS_SELECT_BASE);
|
||||
if (ownerId) q = q.eq('owner_id', ownerId);
|
||||
if (!includeInactive) q = q.neq('status', 'Inativo');
|
||||
if (limit) q = q.limit(limit);
|
||||
@@ -65,7 +66,7 @@ export async function listPatients({ tenantId, ownerId = null, includeInactive =
|
||||
export async function getPatientById(id, { tenantId } = {}) {
|
||||
if (!id) throw new Error('id obrigatório');
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const { data, error } = await supabase.from('patients').select(PATIENTS_SELECT_BASE).eq('id', id).eq('tenant_id', tid).maybeSingle();
|
||||
const { data, error } = await tenantDb().from('patients').select(PATIENTS_SELECT_BASE).eq('id', id).maybeSingle();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
@@ -77,9 +78,9 @@ export async function createPatient(payload) {
|
||||
// criar pacientes "de outro terapeuta". Repository é defesa em profundidade.
|
||||
const ownerId = await getUid();
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { owner_id: _dropped, ...rest } = payload || {};
|
||||
const row = { ...rest, tenant_id: tid, owner_id: ownerId };
|
||||
const { data, error } = await supabase.from('patients').insert(row).select(PATIENTS_SELECT_BASE).single();
|
||||
const { owner_id: _dropped, tenant_id: _tenantDropped, ...rest } = payload || {};
|
||||
const row = { ...rest, owner_id: ownerId };
|
||||
const { data, error } = await tenantDb().from('patients').insert(row).select(PATIENTS_SELECT_BASE).single();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
@@ -87,7 +88,7 @@ export async function createPatient(payload) {
|
||||
export async function updatePatient(id, patch, { tenantId } = {}) {
|
||||
if (!id) throw new Error('id obrigatório');
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const { data, error } = await supabase.from('patients').update(patch).eq('id', id).eq('tenant_id', tid).select(PATIENTS_SELECT_BASE).single();
|
||||
const { data, error } = await tenantDb().from('patients').update(patch).eq('id', id).select(PATIENTS_SELECT_BASE).single();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
@@ -95,7 +96,7 @@ export async function updatePatient(id, patch, { tenantId } = {}) {
|
||||
export async function softDeletePatient(id, { tenantId } = {}) {
|
||||
if (!id) throw new Error('id obrigatório');
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const { error } = await supabase.from('patients').update({ status: 'Arquivado' }).eq('id', id).eq('tenant_id', tid);
|
||||
const { error } = await tenantDb().from('patients').update({ status: 'Arquivado' }).eq('id', id);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
@@ -111,8 +112,8 @@ export async function softDeletePatient(id, { tenantId } = {}) {
|
||||
export async function getPatientRelations(patientId) {
|
||||
if (!patientId) return { groupIds: [], tagIds: [] };
|
||||
const [{ data: g, error: ge }, { data: t, error: te }] = await Promise.all([
|
||||
supabase.from('patient_group_patient').select('patient_group_id').eq('patient_id', patientId),
|
||||
supabase.from('patient_patient_tag').select('tag_id').eq('patient_id', patientId)
|
||||
tenantDb().from('patient_group_patient').select('patient_group_id').eq('patient_id', patientId),
|
||||
tenantDb().from('patient_patient_tag').select('tag_id').eq('patient_id', patientId)
|
||||
]);
|
||||
if (ge) throw ge;
|
||||
if (te) throw te;
|
||||
@@ -126,7 +127,7 @@ export async function getPatientRelations(patientId) {
|
||||
|
||||
export async function listGroups({ tenantId, ownerId = null } = {}) {
|
||||
const tid = resolveTenantId(tenantId);
|
||||
let q = supabase.from('patient_groups').select(PATIENT_GROUPS_SELECT).eq('tenant_id', tid).eq('is_active', true);
|
||||
let q = tenantDb().from('patient_groups').select(PATIENT_GROUPS_SELECT).eq('is_active', true);
|
||||
if (ownerId) q = q.or(`is_system.eq.true,owner_id.eq.${ownerId}`);
|
||||
q = q.order('nome', { ascending: true });
|
||||
const { data, error } = await q;
|
||||
@@ -137,7 +138,7 @@ export async function listGroups({ tenantId, ownerId = null } = {}) {
|
||||
export async function listGroupsByPatient(patientIds, { tenantId } = {}) {
|
||||
if (!patientIds?.length) return [];
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const { data, error } = await supabase.from('patient_group_patient').select('patient_id, patient_group_id').eq('tenant_id', tid).in('patient_id', patientIds);
|
||||
const { data, error } = await tenantDb().from('patient_group_patient').select('patient_id, patient_group_id').in('patient_id', patientIds);
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
}
|
||||
@@ -147,7 +148,7 @@ export async function listGroupsByPatient(patientIds, { tenantId } = {}) {
|
||||
*/
|
||||
export async function getGroupsByIds(ids) {
|
||||
if (!ids?.length) return [];
|
||||
const { data, error } = await supabase.from('patient_groups').select(PATIENT_GROUPS_SELECT_BRIEF).in('id', ids).order('nome', { ascending: true });
|
||||
const { data, error } = await tenantDb().from('patient_groups').select(PATIENT_GROUPS_SELECT_BRIEF).in('id', ids).order('nome', { ascending: true });
|
||||
if (error) throw error;
|
||||
return (data || []).map((g) => ({ id: g.id, name: g.nome }));
|
||||
}
|
||||
@@ -158,10 +159,10 @@ export async function getGroupsByIds(ids) {
|
||||
export async function replacePatientGroup(patientId, groupId, { tenantId } = {}) {
|
||||
if (!patientId) throw new Error('patientId obrigatório');
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const { error: del } = await supabase.from('patient_group_patient').delete().eq('patient_id', patientId).eq('tenant_id', tid);
|
||||
const { error: del } = await tenantDb().from('patient_group_patient').delete().eq('patient_id', patientId);
|
||||
if (del) throw del;
|
||||
if (!groupId) return;
|
||||
const { error: ins } = await supabase.from('patient_group_patient').insert({ patient_id: patientId, patient_group_id: groupId, tenant_id: tid });
|
||||
const { error: ins } = await tenantDb().from('patient_group_patient').insert({ patient_id: patientId, patient_group_id: groupId });
|
||||
if (ins) throw ins;
|
||||
}
|
||||
|
||||
@@ -169,7 +170,7 @@ export async function replacePatientGroup(patientId, groupId, { tenantId } = {})
|
||||
|
||||
export async function listTags({ tenantId, ownerId = null } = {}) {
|
||||
const tid = resolveTenantId(tenantId);
|
||||
let q = supabase.from('patient_tags').select(PATIENT_TAGS_SELECT).eq('tenant_id', tid);
|
||||
let q = tenantDb().from('patient_tags').select(PATIENT_TAGS_SELECT);
|
||||
if (ownerId) q = q.eq('owner_id', ownerId);
|
||||
const { data, error } = await q;
|
||||
if (error) throw error;
|
||||
@@ -179,7 +180,7 @@ export async function listTags({ tenantId, ownerId = null } = {}) {
|
||||
export async function listTagsByPatient(patientIds, { tenantId } = {}) {
|
||||
if (!patientIds?.length) return [];
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const { data, error } = await supabase.from('patient_patient_tag').select('patient_id, tag_id').eq('tenant_id', tid).in('patient_id', patientIds);
|
||||
const { data, error } = await tenantDb().from('patient_patient_tag').select('patient_id, tag_id').in('patient_id', patientIds);
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
}
|
||||
@@ -189,7 +190,7 @@ export async function listTagsByPatient(patientIds, { tenantId } = {}) {
|
||||
*/
|
||||
export async function getTagsByIds(ids) {
|
||||
if (!ids?.length) return [];
|
||||
const { data, error } = await supabase.from('patient_tags').select(PATIENT_TAGS_SELECT_BRIEF).in('id', ids).order('nome', { ascending: true });
|
||||
const { data, error } = await tenantDb().from('patient_tags').select(PATIENT_TAGS_SELECT_BRIEF).in('id', ids).order('nome', { ascending: true });
|
||||
if (error) throw error;
|
||||
return (data || []).map((t) => ({ id: t.id, name: t.nome, color: t.cor }));
|
||||
}
|
||||
@@ -202,12 +203,12 @@ export async function replacePatientTags(patientId, tagIds, { tenantId, ownerId
|
||||
if (!ownerId) throw new Error('ownerId obrigatório');
|
||||
const tid = resolveTenantId(tenantId);
|
||||
|
||||
const { error: del } = await supabase.from('patient_patient_tag').delete().eq('patient_id', patientId).eq('owner_id', ownerId).eq('tenant_id', tid);
|
||||
const { error: del } = await tenantDb().from('patient_patient_tag').delete().eq('patient_id', patientId).eq('owner_id', ownerId);
|
||||
if (del) throw del;
|
||||
|
||||
const clean = Array.from(new Set((tagIds || []).filter(Boolean)));
|
||||
if (!clean.length) return;
|
||||
const { error: ins } = await supabase.from('patient_patient_tag').insert(clean.map((tag_id) => ({ owner_id: ownerId, patient_id: patientId, tag_id, tenant_id: tid })));
|
||||
const { error: ins } = await tenantDb().from('patient_patient_tag').insert(clean.map((tag_id) => ({ owner_id: ownerId, patient_id: patientId, tag_id })));
|
||||
if (ins) throw ins;
|
||||
}
|
||||
|
||||
@@ -224,10 +225,9 @@ export async function replacePatientTags(patientId, tagIds, { tenantId, ownerId
|
||||
export async function listSessionsByPatient(patientId, { tenantId } = {}) {
|
||||
if (!patientId) return [];
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_eventos')
|
||||
const { data, error } = await tenantDb().from('agenda_eventos')
|
||||
.select(PATIENT_SESSIONS_SELECT)
|
||||
.eq('tenant_id', tid)
|
||||
|
||||
.eq('patient_id', patientId)
|
||||
.order('inicio_em', { ascending: false })
|
||||
.limit(100);
|
||||
@@ -251,7 +251,6 @@ export async function createPatientSession(patientId, payload) {
|
||||
const row = {
|
||||
patient_id: patientId,
|
||||
owner_id: uid,
|
||||
tenant_id: tid,
|
||||
inicio_em: payload.inicio_em,
|
||||
fim_em: payload.fim_em,
|
||||
status: payload.status || 'agendado',
|
||||
@@ -266,7 +265,7 @@ export async function createPatientSession(patientId, payload) {
|
||||
price: payload.price ?? null
|
||||
};
|
||||
|
||||
const { data, error } = await supabase.from('agenda_eventos').insert([row]).select().single();
|
||||
const { data, error } = await tenantDb().from('agenda_eventos').insert([row]).select().single();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
@@ -277,7 +276,7 @@ export async function createPatientSession(patientId, payload) {
|
||||
export async function updatePatientSessionStatus(sessionId, status, { tenantId } = {}) {
|
||||
if (!sessionId) throw new Error('sessionId obrigatório');
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const { error } = await supabase.from('agenda_eventos').update({ status }).eq('id', sessionId).eq('tenant_id', tid);
|
||||
const { error } = await tenantDb().from('agenda_eventos').update({ status }).eq('id', sessionId);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
@@ -287,7 +286,7 @@ export async function updatePatientSessionStatus(sessionId, status, { tenantId }
|
||||
*/
|
||||
export async function findSessionByRecurrence(recurrenceId, recurrenceDate) {
|
||||
if (!recurrenceId || !recurrenceDate) return null;
|
||||
const { data, error } = await supabase.from('agenda_eventos').select('id').eq('recurrence_id', recurrenceId).eq('recurrence_date', recurrenceDate).maybeSingle();
|
||||
const { data, error } = await tenantDb().from('agenda_eventos').select('id').eq('recurrence_id', recurrenceId).eq('recurrence_date', recurrenceDate).maybeSingle();
|
||||
if (error) throw error;
|
||||
return data || null;
|
||||
}
|
||||
@@ -303,10 +302,9 @@ export async function findSessionByRecurrence(recurrenceId, recurrenceDate) {
|
||||
export async function listFinancialRecordsByPatient(patientId, { tenantId } = {}) {
|
||||
if (!patientId) return [];
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const { data, error } = await supabase
|
||||
.from('financial_records')
|
||||
const { data, error } = await tenantDb().from('financial_records')
|
||||
.select(PATIENT_FINANCIAL_RECORDS_SELECT)
|
||||
.eq('tenant_id', tid)
|
||||
|
||||
.eq('patient_id', patientId)
|
||||
.eq('type', 'receita')
|
||||
.order('created_at', { ascending: false })
|
||||
@@ -329,7 +327,6 @@ export async function createFinancialRecord(patientId, payload) {
|
||||
const row = {
|
||||
patient_id: patientId,
|
||||
owner_id: uid,
|
||||
tenant_id: tid,
|
||||
type: 'receita',
|
||||
amount: Number(payload.amount),
|
||||
due_date: payload.due_date || null,
|
||||
@@ -338,7 +335,7 @@ export async function createFinancialRecord(patientId, payload) {
|
||||
paid_at: null
|
||||
};
|
||||
|
||||
const { data, error } = await supabase.from('financial_records').insert([row]).select().single();
|
||||
const { data, error } = await tenantDb().from('financial_records').insert([row]).select().single();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
@@ -349,7 +346,7 @@ export async function createFinancialRecord(patientId, payload) {
|
||||
export async function markFinancialRecordPaid(recordId, { tenantId } = {}) {
|
||||
if (!recordId) throw new Error('recordId obrigatório');
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const { error } = await supabase.from('financial_records').update({ paid_at: new Date().toISOString() }).eq('id', recordId).eq('tenant_id', tid);
|
||||
const { error } = await tenantDb().from('financial_records').update({ paid_at: new Date().toISOString() }).eq('id', recordId);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
@@ -359,7 +356,7 @@ export async function markFinancialRecordPaid(recordId, { tenantId } = {}) {
|
||||
export async function markFinancialRecordUnpaid(recordId, { tenantId } = {}) {
|
||||
if (!recordId) throw new Error('recordId obrigatório');
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const { error } = await supabase.from('financial_records').update({ paid_at: null }).eq('id', recordId).eq('tenant_id', tid);
|
||||
const { error } = await tenantDb().from('financial_records').update({ paid_at: null }).eq('id', recordId);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
@@ -370,10 +367,9 @@ export async function markFinancialRecordUnpaid(recordId, { tenantId } = {}) {
|
||||
export async function listDocumentsByPatient(patientId, { tenantId } = {}) {
|
||||
if (!patientId) return [];
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const { data, error } = await supabase
|
||||
.from('documents')
|
||||
const { data, error } = await tenantDb().from('documents')
|
||||
.select(PATIENT_DOCUMENTS_SELECT)
|
||||
.eq('tenant_id', tid)
|
||||
|
||||
.eq('patient_id', patientId)
|
||||
.is('deleted_at', null)
|
||||
.order('created_at', { ascending: false })
|
||||
@@ -390,10 +386,9 @@ export async function listDocumentsByPatient(patientId, { tenantId } = {}) {
|
||||
export async function listMessagesByPatient(patientId, { tenantId } = {}) {
|
||||
if (!patientId) return [];
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const { data, error } = await supabase
|
||||
.from('conversation_messages')
|
||||
const { data, error } = await tenantDb().from('conversation_messages')
|
||||
.select(PATIENT_MESSAGES_SELECT)
|
||||
.eq('tenant_id', tid)
|
||||
|
||||
.eq('patient_id', patientId)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(200);
|
||||
@@ -409,10 +404,9 @@ export async function listMessagesByPatient(patientId, { tenantId } = {}) {
|
||||
export async function listRecurrencesByPatient(patientId, { tenantId } = {}) {
|
||||
if (!patientId) return [];
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const { data, error } = await supabase
|
||||
.from('recurrence_rules')
|
||||
const { data, error } = await tenantDb().from('recurrence_rules')
|
||||
.select(PATIENT_RECURRENCE_RULES_SELECT)
|
||||
.eq('tenant_id', tid)
|
||||
|
||||
.eq('patient_id', patientId)
|
||||
.order('start_date', { ascending: false });
|
||||
if (error) throw error;
|
||||
@@ -425,7 +419,7 @@ export async function listRecurrencesByPatient(patientId, { tenantId } = {}) {
|
||||
export async function updateRecurrenceStatus(ruleId, status, { tenantId } = {}) {
|
||||
if (!ruleId) throw new Error('ruleId obrigatório');
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const { error } = await supabase.from('recurrence_rules').update({ status, updated_at: new Date().toISOString() }).eq('id', ruleId).eq('tenant_id', tid);
|
||||
const { error } = await tenantDb().from('recurrence_rules').update({ status, updated_at: new Date().toISOString() }).eq('id', ruleId);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
@@ -436,7 +430,7 @@ export async function updateRecurrenceStatus(ruleId, status, { tenantId } = {})
|
||||
export async function listSupportContactsByPatient(patientId, { tenantId } = {}) {
|
||||
if (!patientId) return [];
|
||||
const tid = resolveTenantId(tenantId);
|
||||
const { data, error } = await supabase.from('patient_support_contacts').select(PATIENT_SUPPORT_CONTACTS_SELECT).eq('tenant_id', tid).eq('patient_id', patientId).order('is_primario', { ascending: false });
|
||||
const { data, error } = await tenantDb().from('patient_support_contacts').select(PATIENT_SUPPORT_CONTACTS_SELECT).eq('patient_id', patientId).order('is_primario', { ascending: false });
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
}
|
||||
@@ -452,7 +446,7 @@ export async function replacePatientSupportContacts(patientId, contacts, { tenan
|
||||
if (!ownerId) throw new Error('ownerId obrigatório');
|
||||
const tid = resolveTenantId(tenantId);
|
||||
|
||||
const { error: del } = await supabase.from('patient_support_contacts').delete().eq('patient_id', patientId).eq('owner_id', ownerId).eq('tenant_id', tid);
|
||||
const { error: del } = await tenantDb().from('patient_support_contacts').delete().eq('patient_id', patientId).eq('owner_id', ownerId);
|
||||
if (del) throw del;
|
||||
|
||||
if (!contacts?.length) return;
|
||||
@@ -460,11 +454,10 @@ export async function replacePatientSupportContacts(patientId, contacts, { tenan
|
||||
const rows = contacts.map((c) => ({
|
||||
...c,
|
||||
patient_id: patientId,
|
||||
owner_id: ownerId,
|
||||
tenant_id: tid
|
||||
owner_id: ownerId
|
||||
}));
|
||||
|
||||
const { error: ins } = await supabase.from('patient_support_contacts').insert(rows);
|
||||
const { error: ins } = await tenantDb().from('patient_support_contacts').insert(rows);
|
||||
if (ins) throw ins;
|
||||
}
|
||||
|
||||
@@ -490,8 +483,7 @@ export async function markIntakeConverted(intakeId, patientId, { tenantId } = {}
|
||||
|
||||
// tenant_id no patient_intake_requests pode ser nullable (intake público sem tenant)
|
||||
// — só filtramos se passado explícito.
|
||||
let q = supabase
|
||||
.from('patient_intake_requests')
|
||||
let q = tenantDb().from('patient_intake_requests')
|
||||
.update({
|
||||
status: 'converted',
|
||||
converted_patient_id: patientId,
|
||||
@@ -499,11 +491,6 @@ export async function markIntakeConverted(intakeId, patientId, { tenantId } = {}
|
||||
})
|
||||
.eq('id', intakeId);
|
||||
|
||||
if (tenantId) {
|
||||
const tid = resolveTenantId(tenantId);
|
||||
q = q.eq('tenant_id', tid);
|
||||
}
|
||||
|
||||
const { error } = await q;
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import { useRouter } from 'vue-router';
|
||||
|
||||
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { logError } from '@/support/supportLogger';
|
||||
|
||||
const router = useRouter();
|
||||
@@ -204,7 +205,7 @@ async function buscarEtiquetas() {
|
||||
const ownerId = await getOwnerId();
|
||||
|
||||
// 1) tenta view com contagem
|
||||
const v = await supabase.from('v_tag_patient_counts').select('*').eq('owner_id', ownerId).order('nome', { ascending: true });
|
||||
const v = await tenantDb().from('v_tag_patient_counts').select('*').eq('owner_id', ownerId).order('nome', { ascending: true });
|
||||
|
||||
if (!v.error) {
|
||||
etiquetas.value = (v.data || []).map(normalizarEtiquetaRow);
|
||||
@@ -212,10 +213,10 @@ async function buscarEtiquetas() {
|
||||
}
|
||||
|
||||
// 2) fallback tabela direta
|
||||
const t = await supabase.from('patient_tags').select('id, owner_id, nome, cor, is_padrao, name, color, is_native, created_at, updated_at').eq('owner_id', ownerId).order('nome', { ascending: true });
|
||||
const t = await tenantDb().from('patient_tags').select('id, owner_id, nome, cor, is_padrao, name, color, is_native, created_at, updated_at').eq('owner_id', ownerId).order('nome', { ascending: true });
|
||||
|
||||
if (t.error && /column .*nome/i.test(String(t.error.message || ''))) {
|
||||
const t2 = await supabase.from('patient_tags').select('id, owner_id, name, color, is_native, created_at, updated_at').eq('owner_id', ownerId).order('name', { ascending: true });
|
||||
const t2 = await tenantDb().from('patient_tags').select('id, owner_id, name, color, is_native, created_at, updated_at').eq('owner_id', ownerId).order('name', { ascending: true });
|
||||
if (t2.error) throw t2.error;
|
||||
etiquetas.value = (t2.data || []).map((r) => normalizarEtiquetaRow({ ...r, patient_count: 0 }));
|
||||
return;
|
||||
@@ -262,14 +263,14 @@ async function salvarDlg() {
|
||||
|
||||
if (dlg.mode === 'create') {
|
||||
const tenantId = await getActiveTenantId(ownerId);
|
||||
const res = await supabase.from('patient_tags').insert({ owner_id: ownerId, tenant_id: tenantId, nome, cor });
|
||||
const res = await tenantDb().from('patient_tags').insert({ owner_id: ownerId, nome, cor });
|
||||
if (res.error) throw res.error;
|
||||
toast.add({ severity: 'success', summary: 'Tag criada', detail: nome, life: 2500 });
|
||||
} else {
|
||||
let res = await supabase.from('patient_tags').update({ nome, cor, updated_at: new Date().toISOString() }).eq('id', dlg.id).eq('owner_id', ownerId);
|
||||
let res = await tenantDb().from('patient_tags').update({ nome, cor, updated_at: new Date().toISOString() }).eq('id', dlg.id).eq('owner_id', ownerId);
|
||||
// fallback legado
|
||||
if (res.error && /column .*nome/i.test(String(res.error.message || ''))) {
|
||||
res = await supabase.from('patient_tags').update({ name: nome, color: cor, updated_at: new Date().toISOString() }).eq('id', dlg.id).eq('owner_id', ownerId);
|
||||
res = await tenantDb().from('patient_tags').update({ name: nome, color: cor, updated_at: new Date().toISOString() }).eq('id', dlg.id).eq('owner_id', ownerId);
|
||||
}
|
||||
if (res.error) throw res.error;
|
||||
toast.add({ severity: 'success', summary: 'Tag atualizada', detail: nome, life: 2500 });
|
||||
@@ -326,9 +327,9 @@ async function excluirTags(rows) {
|
||||
toast.add({ severity: 'warn', summary: 'Nada para excluir', detail: 'Tags padrão não podem ser removidas.', life: 4000 });
|
||||
return;
|
||||
}
|
||||
const pivotDel = await supabase.from('patient_patient_tag').delete().eq('owner_id', ownerId).in('tag_id', ids);
|
||||
const pivotDel = await tenantDb().from('patient_patient_tag').delete().eq('owner_id', ownerId).in('tag_id', ids);
|
||||
if (pivotDel.error) throw pivotDel.error;
|
||||
const tagDel = await supabase.from('patient_tags').delete().eq('owner_id', ownerId).in('id', ids);
|
||||
const tagDel = await tenantDb().from('patient_tags').delete().eq('owner_id', ownerId).in('id', ids);
|
||||
if (tagDel.error) throw tagDel.error;
|
||||
etiquetasSelecionadas.value = [];
|
||||
toast.add({ severity: 'success', summary: 'Excluído', detail: `${ids.length} tag(s) removida(s).`, life: 3000 });
|
||||
@@ -359,7 +360,7 @@ async function carregarPacientesDaTag(tag) {
|
||||
modalPacientes.error = '';
|
||||
try {
|
||||
const ownerId = await getOwnerId();
|
||||
const { data, error } = await supabase.from('patient_patient_tag').select('patient_id, patients:patients(id, nome_completo, email_principal, telefone, avatar_url)').eq('owner_id', ownerId).eq('tag_id', tag.id);
|
||||
const { data, error } = await tenantDb().from('patient_patient_tag').select('patient_id, patients:patients(id, nome_completo, email_principal, telefone, avatar_url)').eq('owner_id', ownerId).eq('tag_id', tag.id);
|
||||
if (error) throw error;
|
||||
modalPacientes.items = (data || [])
|
||||
.map((r) => r.patients)
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useInsurancePlans } from '@/features/agenda/composables/useInsurancePla
|
||||
import { useServices } from '@/features/agenda/composables/useServices';
|
||||
import { useLayout } from '@/layout/composables/layout';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { applyThemeEngine } from '@/theme/theme.options';
|
||||
import InputMask from 'primevue/inputmask';
|
||||
@@ -412,7 +413,7 @@ onMounted(async () => {
|
||||
if (!uid.value) return;
|
||||
userEmail.value = user.email || '';
|
||||
|
||||
const { data: cfg } = await supabase.from('agenda_configuracoes').select('setup_concluido, setup_clinica_concluido, atendimento_mode').eq('owner_id', uid.value).maybeSingle();
|
||||
const { data: cfg } = await tenantDb().from('agenda_configuracoes').select('setup_concluido, setup_clinica_concluido, atendimento_mode').eq('owner_id', uid.value).maybeSingle();
|
||||
if (cfg && (cfg.setup_concluido || cfg.setup_clinica_concluido || !!cfg.atendimento_mode)) {
|
||||
router.replace('/pages/notfound');
|
||||
return;
|
||||
@@ -439,7 +440,7 @@ async function loadNegocio() {
|
||||
if (!tenantId.value) return;
|
||||
|
||||
// Fonte única: company_profiles
|
||||
const { data } = await supabase.from('company_profiles').select('nome_fantasia,tipo_empresa,logo_url,cep,logradouro,numero,complemento,bairro,cidade,estado,telefone,email,site,redes_sociais').eq('tenant_id', tenantId.value).maybeSingle();
|
||||
const { data } = await tenantDb().from('company_profiles').select('nome_fantasia,tipo_empresa,logo_url,cep,logradouro,numero,complemento,bairro,cidade,estado,telefone,email,site,redes_sociais').maybeSingle();
|
||||
|
||||
if (!data) return;
|
||||
|
||||
@@ -477,7 +478,7 @@ async function loadNegocio() {
|
||||
}
|
||||
async function loadAtendimento() {
|
||||
if (!uid.value) return;
|
||||
const { data } = await supabase.from('agenda_configuracoes').select('atendimento_mode').eq('owner_id', uid.value).maybeSingle();
|
||||
const { data } = await tenantDb().from('agenda_configuracoes').select('atendimento_mode').eq('owner_id', uid.value).maybeSingle();
|
||||
if (data?.atendimento_mode) {
|
||||
atendimento.value.mode = data.atendimento_mode;
|
||||
markSaved('atendimento');
|
||||
@@ -570,9 +571,8 @@ async function saveNegocio(silent = false) {
|
||||
}
|
||||
|
||||
// Fonte única: company_profiles (upsert garante insert ou update)
|
||||
const { error } = await supabase.from('company_profiles').upsert(
|
||||
const { error } = await tenantDb().from('company_profiles').upsert(
|
||||
{
|
||||
tenant_id: tenantId.value,
|
||||
nome_fantasia: negocio.value.name.trim() || null,
|
||||
tipo_empresa: negocio.value.type || null,
|
||||
logo_url: logoUrl || null,
|
||||
@@ -588,7 +588,7 @@ async function saveNegocio(silent = false) {
|
||||
site: negocio.value.siteUrl?.trim() || null,
|
||||
redes_sociais: redes
|
||||
},
|
||||
{ onConflict: 'tenant_id' }
|
||||
{ onConflict: 'singleton' }
|
||||
);
|
||||
|
||||
if (error) throw error;
|
||||
@@ -614,10 +614,9 @@ async function saveAtendimento(silent = false) {
|
||||
await saveService({ name: 'Atendimento padrão', price: 0, duration_min: 50, owner_id: uid.value, tenant_id: tenantId.value });
|
||||
await loadServices(uid.value);
|
||||
}
|
||||
const { error } = await supabase.from('agenda_configuracoes').upsert(
|
||||
const { error } = await tenantDb().from('agenda_configuracoes').upsert(
|
||||
{
|
||||
owner_id: uid.value,
|
||||
tenant_id: tenantId.value,
|
||||
atendimento_mode: atendimento.value.mode,
|
||||
updated_at: new Date().toISOString()
|
||||
},
|
||||
@@ -691,7 +690,7 @@ async function onFinish() {
|
||||
finishing.value = true;
|
||||
try {
|
||||
const now = new Date().toISOString();
|
||||
const { error: finErr } = await supabase.from('agenda_configuracoes').upsert({ owner_id: uid.value, tenant_id: tenantId.value, setup_concluido: true, setup_concluido_em: now }, { onConflict: 'owner_id' });
|
||||
const { error: finErr } = await tenantDb().from('agenda_configuracoes').upsert({ owner_id: uid.value, setup_concluido: true, setup_concluido_em: now }, { onConflict: 'owner_id' });
|
||||
if (finErr) throw finErr;
|
||||
done.value = true;
|
||||
} catch (e) {
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useInsurancePlans } from '@/features/agenda/composables/useInsurancePla
|
||||
import { useServices } from '@/features/agenda/composables/useServices';
|
||||
import { useLayout } from '@/layout/composables/layout';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { applyThemeEngine } from '@/theme/theme.options';
|
||||
import InputMask from 'primevue/inputmask';
|
||||
@@ -416,7 +417,7 @@ onMounted(async () => {
|
||||
if (!uid.value) return;
|
||||
userEmail.value = user.email || '';
|
||||
|
||||
const { data: cfg } = await supabase.from('agenda_configuracoes').select('setup_concluido, setup_clinica_concluido, atendimento_mode').eq('owner_id', uid.value).maybeSingle();
|
||||
const { data: cfg } = await tenantDb().from('agenda_configuracoes').select('setup_concluido, setup_clinica_concluido, atendimento_mode').eq('owner_id', uid.value).maybeSingle();
|
||||
if (cfg && (cfg.setup_concluido || cfg.setup_clinica_concluido || !!cfg.atendimento_mode)) {
|
||||
router.replace('/pages/notfound');
|
||||
return;
|
||||
@@ -443,7 +444,7 @@ async function loadNegocio() {
|
||||
if (!tenantId.value) return;
|
||||
|
||||
// Fonte única: company_profiles
|
||||
const { data } = await supabase.from('company_profiles').select('nome_fantasia,tipo_empresa,logo_url,cep,logradouro,numero,complemento,bairro,cidade,estado,telefone,email,site,redes_sociais').eq('tenant_id', tenantId.value).maybeSingle();
|
||||
const { data } = await tenantDb().from('company_profiles').select('nome_fantasia,tipo_empresa,logo_url,cep,logradouro,numero,complemento,bairro,cidade,estado,telefone,email,site,redes_sociais').maybeSingle();
|
||||
|
||||
if (!data) return;
|
||||
|
||||
@@ -481,7 +482,7 @@ async function loadNegocio() {
|
||||
}
|
||||
async function loadAtendimento() {
|
||||
if (!uid.value) return;
|
||||
const { data } = await supabase.from('agenda_configuracoes').select('atendimento_mode').eq('owner_id', uid.value).maybeSingle();
|
||||
const { data } = await tenantDb().from('agenda_configuracoes').select('atendimento_mode').eq('owner_id', uid.value).maybeSingle();
|
||||
if (data?.atendimento_mode) {
|
||||
atendimento.value.mode = data.atendimento_mode;
|
||||
markSaved('atendimento');
|
||||
@@ -574,7 +575,6 @@ async function saveNegocio(silent = false) {
|
||||
}
|
||||
|
||||
const payload = {
|
||||
tenant_id: tenantId.value,
|
||||
nome_fantasia: negocio.value.name.trim() || null,
|
||||
tipo_empresa: negocio.value.type || null,
|
||||
logo_url: logoUrl || null,
|
||||
@@ -592,15 +592,15 @@ async function saveNegocio(silent = false) {
|
||||
};
|
||||
|
||||
// Verificar se já existe registro para este tenant
|
||||
const { data: existing } = await supabase.from('company_profiles').select('id').eq('tenant_id', tenantId.value).maybeSingle();
|
||||
const { data: existing } = await tenantDb().from('company_profiles').select('id').maybeSingle();
|
||||
|
||||
let error;
|
||||
if (existing?.id) {
|
||||
// Já existe — usar UPDATE direto pelo id
|
||||
({ error } = await supabase.from('company_profiles').update(payload).eq('id', existing.id));
|
||||
({ error } = await tenantDb().from('company_profiles').update(payload).eq('id', existing.id));
|
||||
} else {
|
||||
// Não existe — INSERT
|
||||
({ error } = await supabase.from('company_profiles').insert(payload));
|
||||
({ error } = await tenantDb().from('company_profiles').insert(payload));
|
||||
}
|
||||
|
||||
if (error) throw error;
|
||||
@@ -626,10 +626,9 @@ async function saveAtendimento(silent = false) {
|
||||
await saveService({ name: 'Atendimento padrão', price: 0, duration_min: 50, owner_id: uid.value, tenant_id: tenantId.value });
|
||||
await loadServices(uid.value);
|
||||
}
|
||||
const { error } = await supabase.from('agenda_configuracoes').upsert(
|
||||
const { error } = await tenantDb().from('agenda_configuracoes').upsert(
|
||||
{
|
||||
owner_id: uid.value,
|
||||
tenant_id: tenantId.value,
|
||||
atendimento_mode: atendimento.value.mode,
|
||||
updated_at: new Date().toISOString()
|
||||
},
|
||||
@@ -703,7 +702,7 @@ async function onFinish() {
|
||||
finishing.value = true;
|
||||
try {
|
||||
const now = new Date().toISOString();
|
||||
const { error: finErr } = await supabase.from('agenda_configuracoes').upsert({ owner_id: uid.value, tenant_id: tenantId.value, setup_concluido: true, setup_concluido_em: now }, { onConflict: 'owner_id' });
|
||||
const { error: finErr } = await tenantDb().from('agenda_configuracoes').upsert({ owner_id: uid.value, setup_concluido: true, setup_concluido_em: now }, { onConflict: 'owner_id' });
|
||||
if (finErr) throw finErr;
|
||||
// Fecha o dialog ANTES de mostrar a tela de parabéns
|
||||
dialogVisible.value = false;
|
||||
|
||||
@@ -20,6 +20,7 @@ import { computed, onMounted, onBeforeUnmount, provide, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useConversationDrawerStore } from '@/stores/conversationDrawerStore';
|
||||
|
||||
const router = useRouter();
|
||||
@@ -56,10 +57,9 @@ async function openConversationDrawer(threadKey) {
|
||||
if (!threadKey) return;
|
||||
try {
|
||||
const tenantId = tenantStore.activeTenantId;
|
||||
const { data, error } = await supabase
|
||||
.from('conversation_threads')
|
||||
const { data, error } = await tenantDb().from('conversation_threads')
|
||||
.select('*')
|
||||
.eq('tenant_id', tenantId)
|
||||
|
||||
.eq('thread_key', threadKey)
|
||||
.maybeSingle();
|
||||
if (error) throw error;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
@@ -120,7 +121,7 @@ async function loadBloqueios() {
|
||||
if (!ownerId.value) return;
|
||||
loadingB.value = true;
|
||||
try {
|
||||
const { data, error } = await supabase.from('agenda_bloqueios').select('*').eq('owner_id', ownerId.value).gte('data_inicio', `${ano.value}-01-01`).lte('data_inicio', `${ano.value}-12-31`).order('data_inicio');
|
||||
const { data, error } = await tenantDb().from('agenda_bloqueios').select('*').eq('owner_id', ownerId.value).gte('data_inicio', `${ano.value}-01-01`).lte('data_inicio', `${ano.value}-12-31`).order('data_inicio');
|
||||
if (error) throw error;
|
||||
bloqueios.value = data || [];
|
||||
} catch (e) {
|
||||
@@ -241,11 +242,11 @@ async function salvarBloqueio() {
|
||||
};
|
||||
|
||||
if (dlgMode.value === 'edit') {
|
||||
const { error } = await supabase.from('agenda_bloqueios').update(payload).eq('id', form.value.id);
|
||||
const { error } = await tenantDb().from('agenda_bloqueios').update(payload).eq('id', form.value.id);
|
||||
if (error) throw error;
|
||||
toast.add({ severity: 'success', summary: 'Salvo', detail: 'Bloqueio atualizado.', life: 1800 });
|
||||
} else {
|
||||
const { error } = await supabase.from('agenda_bloqueios').insert(payload);
|
||||
const { error } = await tenantDb().from('agenda_bloqueios').insert(payload);
|
||||
if (error) throw error;
|
||||
toast.add({ severity: 'success', summary: 'Criado', detail: 'Bloqueio adicionado.', life: 1800 });
|
||||
}
|
||||
@@ -270,7 +271,7 @@ function excluirBloqueio(id) {
|
||||
acceptClass: 'p-button-danger',
|
||||
accept: async () => {
|
||||
try {
|
||||
const { error } = await supabase.from('agenda_bloqueios').delete().eq('id', id);
|
||||
const { error } = await tenantDb().from('agenda_bloqueios').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
bloqueios.value = bloqueios.value.filter((b) => b.id !== id);
|
||||
toast.add({ severity: 'success', summary: 'Removido', life: 1500 });
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
-->
|
||||
<script setup>
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
|
||||
@@ -381,11 +382,11 @@ async function getActiveTenantId(uid) {
|
||||
}
|
||||
|
||||
async function seedConfigIfMissing(uid) {
|
||||
const { data: existing } = await supabase.from('agenda_configuracoes').select('owner_id').eq('owner_id', uid).maybeSingle();
|
||||
const { data: existing } = await tenantDb().from('agenda_configuracoes').select('owner_id').eq('owner_id', uid).maybeSingle();
|
||||
if (existing) return; // já existe, não toca
|
||||
|
||||
const tenantId = await getActiveTenantId(uid);
|
||||
const { error } = await supabase.from('agenda_configuracoes').insert({ owner_id: uid, tenant_id: tenantId, session_duration_min: 40, session_break_min: 10 });
|
||||
const { error } = await tenantDb().from('agenda_configuracoes').insert({ owner_id: uid, session_duration_min: 40, session_break_min: 10 });
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
@@ -397,7 +398,7 @@ async function loadConfig() {
|
||||
|
||||
await seedConfigIfMissing(uid);
|
||||
|
||||
const { data, error } = await supabase.from('agenda_configuracoes').select('*').eq('owner_id', uid).order('created_at', { ascending: false }).limit(1).maybeSingle();
|
||||
const { data, error } = await tenantDb().from('agenda_configuracoes').select('*').eq('owner_id', uid).order('created_at', { ascending: false }).limit(1).maybeSingle();
|
||||
if (error) throw error;
|
||||
if (data) {
|
||||
cfg.value = { ...cfg.value, ...data };
|
||||
@@ -409,7 +410,7 @@ async function loadRegras() {
|
||||
const uid = ownerId.value || (await getOwnerId());
|
||||
ownerId.value = uid;
|
||||
|
||||
const { data, error } = await supabase.from('agenda_regras_semanais').select('*').eq('owner_id', uid).order('dia_semana').order('hora_inicio');
|
||||
const { data, error } = await tenantDb().from('agenda_regras_semanais').select('*').eq('owner_id', uid).order('dia_semana').order('hora_inicio');
|
||||
if (error) throw error;
|
||||
|
||||
const rows = (data || []).map((r) => ({
|
||||
@@ -427,7 +428,7 @@ async function loadOnlineSlots() {
|
||||
ownerId.value = uid;
|
||||
resetOnlineSlots();
|
||||
|
||||
const { data, error } = await supabase.from('agenda_online_slots').select('weekday,time,enabled').eq('owner_id', uid);
|
||||
const { data, error } = await tenantDb().from('agenda_online_slots').select('weekday,time,enabled').eq('owner_id', uid);
|
||||
if (error) {
|
||||
console.warn('[CFG] loadOnlineSlots:', error);
|
||||
return;
|
||||
@@ -492,12 +493,10 @@ async function saveJornada() {
|
||||
cfg.value.pausas_semanais = pausasToSave;
|
||||
|
||||
const igualTodos = jornadaIgualTodos.value !== false;
|
||||
const { data: cfgSaved, error: cfgErr } = await supabase
|
||||
.from('agenda_configuracoes')
|
||||
const { data: cfgSaved, error: cfgErr } = await tenantDb().from('agenda_configuracoes')
|
||||
.upsert(
|
||||
{
|
||||
owner_id: uid,
|
||||
tenant_id: tenantId,
|
||||
pausas_semanais: pausasToSave,
|
||||
jornada_igual_todos: igualTodos,
|
||||
timezone: cfg.value.timezone || 'America/Sao_Paulo',
|
||||
@@ -513,13 +512,13 @@ async function saveJornada() {
|
||||
const rows = selectedDays.value.map((d) => {
|
||||
const isWeekend = d.value === 6 || d.value === 0;
|
||||
const t = jornadaIgualTodos.value === false || isWeekend ? jornadaPorDia.value[d.value] || { inicio: jornadaStart.value, fim: jornadaEnd.value } : { inicio: jornadaStart.value, fim: jornadaEnd.value };
|
||||
return { owner_id: uid, tenant_id: tenantId, dia_semana: d.value, hora_inicio: normalizeTime(t.inicio), hora_fim: normalizeTime(t.fim), modalidade: 'ambos', ativo: true };
|
||||
return { owner_id: uid, dia_semana: d.value, hora_inicio: normalizeTime(t.inicio), hora_fim: normalizeTime(t.fim), modalidade: 'ambos', ativo: true };
|
||||
});
|
||||
|
||||
const { error: delErr } = await supabase.from('agenda_regras_semanais').delete().eq('owner_id', uid);
|
||||
const { error: delErr } = await tenantDb().from('agenda_regras_semanais').delete().eq('owner_id', uid);
|
||||
if (delErr) throw delErr;
|
||||
if (rows.length) {
|
||||
const { error: insErr } = await supabase.from('agenda_regras_semanais').insert(rows);
|
||||
const { error: insErr } = await tenantDb().from('agenda_regras_semanais').insert(rows);
|
||||
if (insErr) throw insErr;
|
||||
}
|
||||
|
||||
@@ -527,7 +526,7 @@ async function saveJornada() {
|
||||
const activeDays = new Set(selectedDays.value.map((d) => d.value));
|
||||
const orphanDays = [0, 1, 2, 3, 4, 5, 6].filter((d) => !activeDays.has(d));
|
||||
if (orphanDays.length) {
|
||||
const { error: orphanErr } = await supabase.from('agenda_online_slots').delete().eq('owner_id', uid).in('weekday', orphanDays);
|
||||
const { error: orphanErr } = await tenantDb().from('agenda_online_slots').delete().eq('owner_id', uid).in('weekday', orphanDays);
|
||||
if (orphanErr) console.warn('[CFG] limpeza órfãos:', orphanErr);
|
||||
else for (const d of orphanDays) _setDay(d, new Set());
|
||||
}
|
||||
@@ -563,7 +562,7 @@ async function saveRitmo() {
|
||||
const uid = ownerId.value || (await getOwnerId());
|
||||
ownerId.value = uid;
|
||||
|
||||
const { error } = await supabase.from('agenda_configuracoes').upsert(
|
||||
const { error } = await tenantDb().from('agenda_configuracoes').upsert(
|
||||
{
|
||||
owner_id: uid,
|
||||
session_duration_min: dur,
|
||||
@@ -589,22 +588,22 @@ async function saveOnline() {
|
||||
const tenantId = await getActiveTenantId(uid);
|
||||
|
||||
// salvar flag online_ativo
|
||||
const { error: cfgErr } = await supabase.from('agenda_configuracoes').upsert({ owner_id: uid, online_ativo: cfg.value.online_ativo }, { onConflict: 'owner_id' });
|
||||
const { error: cfgErr } = await tenantDb().from('agenda_configuracoes').upsert({ owner_id: uid, online_ativo: cfg.value.online_ativo }, { onConflict: 'owner_id' });
|
||||
if (cfgErr) throw cfgErr;
|
||||
|
||||
// salvar slots
|
||||
const { error: delErr } = await supabase.from('agenda_online_slots').delete().eq('owner_id', uid);
|
||||
const { error: delErr } = await tenantDb().from('agenda_online_slots').delete().eq('owner_id', uid);
|
||||
if (delErr) throw delErr;
|
||||
|
||||
if (cfg.value.online_ativo) {
|
||||
const rows = [];
|
||||
for (const d of selectedDays.value) {
|
||||
for (const hhmm of onlineSlotsByDay.value[d.value] || new Set()) {
|
||||
if (isValidHHMM(hhmm)) rows.push({ owner_id: uid, tenant_id: tenantId, weekday: Number(d.value), time: normalizeTime(hhmm), enabled: true });
|
||||
if (isValidHHMM(hhmm)) rows.push({ owner_id: uid, weekday: Number(d.value), time: normalizeTime(hhmm), enabled: true });
|
||||
}
|
||||
}
|
||||
if (rows.length) {
|
||||
const { error: insErr } = await supabase.from('agenda_online_slots').insert(rows);
|
||||
const { error: insErr } = await tenantDb().from('agenda_online_slots').insert(rows);
|
||||
if (insErr) throw insErr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { RouterLink, useRoute } from 'vue-router';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useEntitlementsStore } from '@/stores/entitlementsStore';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
@@ -78,7 +79,7 @@ async function onFileSelected(event, field) {
|
||||
// Persiste imediatamente no banco sem fechar o accordion
|
||||
const uid = ownerId.value;
|
||||
const tenantId = await getActiveTenantId(uid);
|
||||
await supabase.from('agendador_configuracoes').upsert({ owner_id: uid, tenant_id: tenantId, ...buildPayload('identidade'), updated_at: new Date().toISOString() }, { onConflict: 'owner_id' });
|
||||
await tenantDb().from('agendador_configuracoes').upsert({ owner_id: uid, ...buildPayload('identidade'), updated_at: new Date().toISOString() }, { onConflict: 'owner_id' });
|
||||
toast.add({ severity: 'success', summary: 'Imagem salva', life: 2000 });
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -283,7 +284,7 @@ const pixChaveEfetiva = computed(() => cfg.value.pix_chave || paymentSettings.va
|
||||
|
||||
async function loadPaymentSettings(uid) {
|
||||
try {
|
||||
const { data } = await supabase.from('payment_settings').select('pix_ativo, pix_chave, pix_tipo, deposito_ativo, dinheiro_ativo, cartao_ativo, convenio_ativo').eq('owner_id', uid).maybeSingle();
|
||||
const { data } = await tenantDb().from('payment_settings').select('pix_ativo, pix_chave, pix_tipo, deposito_ativo, dinheiro_ativo, cartao_ativo, convenio_ativo').eq('owner_id', uid).maybeSingle();
|
||||
paymentSettings.value = data || {};
|
||||
} catch {
|
||||
paymentSettings.value = {};
|
||||
@@ -360,7 +361,7 @@ async function load() {
|
||||
const uid = await getOwnerId();
|
||||
ownerId.value = uid;
|
||||
|
||||
const [{ data, error }] = await Promise.all([supabase.from('agendador_configuracoes').select('*').eq('owner_id', uid).maybeSingle(), loadPaymentSettings(uid)]);
|
||||
const [{ data, error }] = await Promise.all([tenantDb().from('agendador_configuracoes').select('*').eq('owner_id', uid).maybeSingle(), loadPaymentSettings(uid)]);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
@@ -399,7 +400,7 @@ async function toggleAtivo() {
|
||||
try {
|
||||
const tenantId = await getActiveTenantId(uid);
|
||||
|
||||
await supabase.from('agendador_configuracoes').upsert({ owner_id: uid, tenant_id: tenantId, ativo: novoAtivo, updated_at: new Date().toISOString() }, { onConflict: 'owner_id' });
|
||||
await tenantDb().from('agendador_configuracoes').upsert({ owner_id: uid, ativo: novoAtivo, updated_at: new Date().toISOString() }, { onConflict: 'owner_id' });
|
||||
|
||||
toast.add({
|
||||
severity: novoAtivo ? 'success' : 'info',
|
||||
@@ -421,7 +422,7 @@ async function saveCard(cardKey) {
|
||||
const tenantId = await getActiveTenantId(uid);
|
||||
const payload = buildPayload(cardKey);
|
||||
|
||||
await supabase.from('agendador_configuracoes').upsert({ owner_id: uid, tenant_id: tenantId, ...payload, updated_at: new Date().toISOString() }, { onConflict: 'owner_id' });
|
||||
await tenantDb().from('agendador_configuracoes').upsert({ owner_id: uid, ...payload, updated_at: new Date().toISOString() }, { onConflict: 'owner_id' });
|
||||
|
||||
toast.add({ severity: 'success', summary: 'Salvo', life: 2500 });
|
||||
expandedCard.value = new Set();
|
||||
|
||||
@@ -19,6 +19,7 @@ import { ref, computed, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
const router = useRouter();
|
||||
@@ -136,11 +137,11 @@ async function loadUser() {
|
||||
async function loadWhatsApp() {
|
||||
if (!tenantId.value) return;
|
||||
|
||||
let { data } = await supabase.from('notification_channels').select('credentials, connection_status').eq('tenant_id', tenantId.value).eq('channel', 'whatsapp').is('deleted_at', null).maybeSingle();
|
||||
let { data } = await tenantDb().from('notification_channels').select('credentials, connection_status').eq('channel', 'whatsapp').is('deleted_at', null).maybeSingle();
|
||||
|
||||
// Fallback owner_id
|
||||
if (!data && userId.value && userId.value !== tenantId.value) {
|
||||
const fb = await supabase.from('notification_channels').select('credentials, connection_status').eq('owner_id', userId.value).eq('channel', 'whatsapp').is('deleted_at', null).maybeSingle();
|
||||
const fb = await tenantDb().from('notification_channels').select('credentials, connection_status').eq('owner_id', userId.value).eq('channel', 'whatsapp').is('deleted_at', null).maybeSingle();
|
||||
data = fb.data;
|
||||
}
|
||||
|
||||
@@ -184,7 +185,7 @@ async function loadSms() {
|
||||
async function loadEmail() {
|
||||
if (!tenantId.value) return;
|
||||
|
||||
const { count } = await supabase.from('email_templates_tenant').select('id', { count: 'exact', head: true }).eq('tenant_id', tenantId.value);
|
||||
const { count } = await tenantDb().from('email_templates_tenant').select('id', { count: 'exact', head: true });
|
||||
|
||||
email.value.templatesCount = count || 0;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useConfirm } from 'primevue/useconfirm';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
const toast = useToast();
|
||||
const confirm = useConfirm();
|
||||
const tenantStore = useTenantStore();
|
||||
@@ -53,10 +54,9 @@ async function loadConfig() {
|
||||
if (!tenantId.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('conversation_bots')
|
||||
const { data, error } = await tenantDb().from('conversation_bots')
|
||||
.select('*')
|
||||
.eq('tenant_id', tenantId.value)
|
||||
|
||||
.maybeSingle();
|
||||
if (error) throw error;
|
||||
if (data) {
|
||||
@@ -139,7 +139,6 @@ async function saveConfig() {
|
||||
saving.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
tenant_id: tenantId.value,
|
||||
enabled: !!config.value.enabled,
|
||||
greeting_message: String(config.value.greeting_message || '').trim().slice(0, 1000),
|
||||
closing_message: String(config.value.closing_message || '').trim().slice(0, 1000),
|
||||
@@ -155,9 +154,8 @@ async function saveConfig() {
|
||||
idle_timeout_minutes: Math.max(5, Math.min(1440, Number(config.value.idle_timeout_minutes) || 30)),
|
||||
respect_optout: !!config.value.respect_optout
|
||||
};
|
||||
const { error } = await supabase
|
||||
.from('conversation_bots')
|
||||
.upsert(payload, { onConflict: 'tenant_id' });
|
||||
const { error } = await tenantDb().from('conversation_bots')
|
||||
.upsert(payload, { onConflict: 'singleton' });
|
||||
if (error) throw error;
|
||||
toast.add({ severity: 'success', summary: 'Configuração salva', life: 2500 });
|
||||
loadSessions();
|
||||
@@ -173,10 +171,9 @@ async function loadSessions() {
|
||||
sessionsLoading.value = true;
|
||||
try {
|
||||
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 3600 * 1000).toISOString();
|
||||
const { data, error } = await supabase
|
||||
.from('conversation_bot_sessions')
|
||||
const { data, error } = await tenantDb().from('conversation_bot_sessions')
|
||||
.select('id, thread_key, contact_number, current_step, collected_data, status, started_at, completed_at, abandoned_at')
|
||||
.eq('tenant_id', tenantId.value)
|
||||
|
||||
.gte('started_at', sevenDaysAgo)
|
||||
.order('started_at', { ascending: false })
|
||||
.limit(30);
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useToast } from 'primevue/usetoast';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
const toast = useToast();
|
||||
const tenantStore = useTenantStore();
|
||||
|
||||
@@ -67,10 +68,9 @@ async function loadRule() {
|
||||
if (!tenantId.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('conversation_sla_rules')
|
||||
const { data, error } = await tenantDb().from('conversation_sla_rules')
|
||||
.select('*')
|
||||
.eq('tenant_id', tenantId.value)
|
||||
|
||||
.maybeSingle();
|
||||
if (error) throw error;
|
||||
if (data) {
|
||||
@@ -128,7 +128,6 @@ async function saveRule() {
|
||||
saving.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
tenant_id: tenantId.value,
|
||||
enabled: !!rule.value.enabled,
|
||||
threshold_minutes: Math.round(Number(rule.value.threshold_minutes) || 60),
|
||||
respect_business_hours: !!rule.value.respect_business_hours,
|
||||
@@ -138,9 +137,8 @@ async function saveRule() {
|
||||
alert_scope: rule.value.alert_scope,
|
||||
notify_admin_on_breach: !!rule.value.notify_admin_on_breach
|
||||
};
|
||||
const { error } = await supabase
|
||||
.from('conversation_sla_rules')
|
||||
.upsert(payload, { onConflict: 'tenant_id' });
|
||||
const { error } = await tenantDb().from('conversation_sla_rules')
|
||||
.upsert(payload, { onConflict: 'singleton' });
|
||||
if (error) throw error;
|
||||
toast.add({ severity: 'success', summary: 'Configuração salva', life: 2500 });
|
||||
loadBreaches();
|
||||
@@ -156,10 +154,9 @@ async function loadBreaches() {
|
||||
breachesLoading.value = true;
|
||||
try {
|
||||
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 3600 * 1000).toISOString();
|
||||
const { data, error } = await supabase
|
||||
.from('conversation_sla_breaches')
|
||||
const { data, error } = await tenantDb().from('conversation_sla_breaches')
|
||||
.select('id, thread_key, assigned_to, last_inbound_at, threshold_minutes_at_breach, breached_at, resolved_at, notification_count')
|
||||
.eq('tenant_id', tenantId.value)
|
||||
|
||||
.gte('breached_at', sevenDaysAgo)
|
||||
.order('breached_at', { ascending: false })
|
||||
.limit(30);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ref, computed, onMounted, reactive, watch } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useConversationTags } from '@/composables/useConversationTags';
|
||||
|
||||
@@ -28,10 +29,9 @@ async function loadUsage() {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('conversation_thread_tags')
|
||||
const { data, error } = await tenantDb().from('conversation_thread_tags')
|
||||
.select('tag_id')
|
||||
.eq('tenant_id', tenantId);
|
||||
;
|
||||
if (error) throw error;
|
||||
const counts = {};
|
||||
for (const row of data || []) {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { usePatientDiscounts } from '@/features/agenda/composables/usePatientDiscounts';
|
||||
@@ -177,7 +178,7 @@ onMounted(async () => {
|
||||
ownerId.value = uid;
|
||||
tenantId.value = tenantStore.activeTenantId || null;
|
||||
|
||||
const [, { data: pData }] = await Promise.all([load(uid), supabase.from('patients').select('id, nome_completo').eq('owner_id', uid).eq('status', 'Ativo').order('nome_completo', { ascending: true })]);
|
||||
const [, { data: pData }] = await Promise.all([load(uid), tenantDb().from('patients').select('id, nome_completo').eq('owner_id', uid).eq('status', 'Ativo').order('nome_completo', { ascending: true })]);
|
||||
|
||||
patients.value = pData || [];
|
||||
} catch (e) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import JoditEmailEditor from '@/components/ui/JoditEmailEditor.vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { renderEmail, renderTemplate, generateLayoutSection } from '@/lib/email/emailTemplateService';
|
||||
import { MOCK_DATA, TEMPLATE_DOMAINS } from '@/lib/email/emailTemplateConstants';
|
||||
|
||||
@@ -46,7 +47,7 @@ async function loadUser() {
|
||||
} = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
tenantId.value = user.id;
|
||||
const { data } = await supabase.from('company_profiles').select('logo_url').eq('tenant_id', user.id).maybeSingle();
|
||||
const { data } = await tenantDb().from('company_profiles').select('logo_url').maybeSingle();
|
||||
profileLogoUrl.value = data?.logo_url || null;
|
||||
}
|
||||
|
||||
@@ -60,7 +61,7 @@ function defaultSection() {
|
||||
|
||||
async function loadLayoutConfig() {
|
||||
if (!tenantId.value) return;
|
||||
const { data } = await supabase.from('email_layout_config').select('*').eq('tenant_id', tenantId.value).maybeSingle();
|
||||
const { data } = await tenantDb().from('email_layout_config').select('*').maybeSingle();
|
||||
if (data) {
|
||||
layoutConfigId.value = data.id;
|
||||
layoutConfig.value.header = { ...defaultSection(), ...(data.header_config || {}) };
|
||||
@@ -80,7 +81,7 @@ async function load() {
|
||||
try {
|
||||
const [{ data: gData, error: gErr }, { data: oData, error: oErr }] = await Promise.all([
|
||||
supabase.from('email_templates_global').select('*').eq('is_active', true).order('domain').order('key'),
|
||||
supabase.from('email_templates_tenant').select('*').eq('tenant_id', tenantId.value).is('owner_id', null)
|
||||
tenantDb().from('email_templates_tenant').select('*').is('owner_id', null)
|
||||
]);
|
||||
if (gErr) throw gErr;
|
||||
if (oErr) throw oErr;
|
||||
@@ -165,10 +166,10 @@ async function saveLayout() {
|
||||
footer_config: layoutForm.value.footer
|
||||
};
|
||||
if (layoutConfigId.value) {
|
||||
const { error } = await supabase.from('email_layout_config').update(payload).eq('id', layoutConfigId.value);
|
||||
const { error } = await tenantDb().from('email_layout_config').update(payload).eq('id', layoutConfigId.value);
|
||||
if (error) throw error;
|
||||
} else {
|
||||
const { data, error } = await supabase.from('email_layout_config').insert(payload).select('id').single();
|
||||
const { data, error } = await tenantDb().from('email_layout_config').insert(payload).select('id').single();
|
||||
if (error) throw error;
|
||||
layoutConfigId.value = data.id;
|
||||
}
|
||||
@@ -241,11 +242,11 @@ async function save() {
|
||||
synced_version: form.value.synced_version
|
||||
};
|
||||
if (dlg.value.mode === 'create') {
|
||||
const { error } = await supabase.from('email_templates_tenant').insert(payload);
|
||||
const { error } = await tenantDb().from('email_templates_tenant').insert(payload);
|
||||
if (error) throw error;
|
||||
toast.add({ severity: 'success', summary: 'Personalização salva', life: 3000 });
|
||||
} else {
|
||||
const { error } = await supabase.from('email_templates_tenant').update(payload).eq('id', overrideMap.value[form.value.template_key].id);
|
||||
const { error } = await tenantDb().from('email_templates_tenant').update(payload).eq('id', overrideMap.value[form.value.template_key].id);
|
||||
if (error) throw error;
|
||||
toast.add({ severity: 'success', summary: 'Personalização salva', life: 3000 });
|
||||
}
|
||||
@@ -266,7 +267,7 @@ function confirmRevert(row) {
|
||||
acceptClass: 'p-button-danger',
|
||||
accept: async () => {
|
||||
try {
|
||||
const { error } = await supabase.from('email_templates_tenant').delete().eq('id', row.override.id);
|
||||
const { error } = await tenantDb().from('email_templates_tenant').delete().eq('id', row.override.id);
|
||||
if (error) throw error;
|
||||
toast.add({ severity: 'success', summary: 'Revertido para o padrão', life: 3000 });
|
||||
await load();
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
const toast = useToast();
|
||||
const confirm = useConfirm();
|
||||
|
||||
@@ -200,7 +201,7 @@ async function load() {
|
||||
} = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
tenantId.value = user.id;
|
||||
const { data } = await supabase.from('company_profiles').select('*').eq('tenant_id', user.id).maybeSingle();
|
||||
const { data } = await tenantDb().from('company_profiles').select('*').maybeSingle();
|
||||
if (data) {
|
||||
recordId.value = data.id;
|
||||
Object.keys(form.value).forEach((k) => {
|
||||
@@ -227,7 +228,6 @@ async function save() {
|
||||
if (logoFile.value) form.value.logo_url = await uploadLogo();
|
||||
|
||||
const payload = {
|
||||
tenant_id: tenantId.value,
|
||||
nome_fantasia: form.value.nome_fantasia || null,
|
||||
razao_social: form.value.razao_social || null,
|
||||
tipo_empresa: form.value.tipo_empresa || null,
|
||||
@@ -249,10 +249,10 @@ async function save() {
|
||||
};
|
||||
|
||||
if (recordId.value) {
|
||||
const { error } = await supabase.from('company_profiles').update(payload).eq('id', recordId.value);
|
||||
const { error } = await tenantDb().from('company_profiles').update(payload).eq('id', recordId.value);
|
||||
if (error) throw error;
|
||||
} else {
|
||||
const { data, error } = await supabase.from('company_profiles').insert(payload).select('id').single();
|
||||
const { data, error } = await tenantDb().from('company_profiles').insert(payload).select('id').single();
|
||||
if (error) throw error;
|
||||
recordId.value = data.id;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
|
||||
@@ -121,7 +122,7 @@ async function load() {
|
||||
if (!uid) return;
|
||||
ownerId.value = uid;
|
||||
|
||||
const { data } = await supabase.from('payment_settings').select('*').eq('owner_id', uid).maybeSingle();
|
||||
const { data } = await tenantDb().from('payment_settings').select('*').eq('owner_id', uid).maybeSingle();
|
||||
|
||||
if (data) {
|
||||
cfg.value = { ...DEFAULT, ...data };
|
||||
@@ -137,8 +138,7 @@ async function saveCard(cardKey) {
|
||||
savingCard.value = cardKey;
|
||||
|
||||
const payload = {
|
||||
owner_id: ownerId.value,
|
||||
tenant_id: tenantStore.activeTenantId || null
|
||||
owner_id: ownerId.value
|
||||
};
|
||||
|
||||
if (cardKey === 'pix') {
|
||||
@@ -175,7 +175,7 @@ async function saveCard(cardKey) {
|
||||
}
|
||||
|
||||
try {
|
||||
const { error } = await supabase.from('payment_settings').upsert(payload, { onConflict: 'owner_id' });
|
||||
const { error } = await tenantDb().from('payment_settings').upsert(payload, { onConflict: 'owner_id' });
|
||||
|
||||
if (error) throw error;
|
||||
toast.add({ severity: 'success', summary: 'Salvo', detail: 'Configurações de pagamento atualizadas.', life: 2500 });
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useServices } from '@/features/agenda/composables/useServices';
|
||||
@@ -143,7 +144,7 @@ onMounted(async () => {
|
||||
ownerId.value = uid;
|
||||
tenantId.value = tenantStore.activeTenantId || null;
|
||||
|
||||
const { data: cfg } = await supabase.from('agenda_configuracoes').select('slot_mode').eq('owner_id', uid).maybeSingle();
|
||||
const { data: cfg } = await tenantDb().from('agenda_configuracoes').select('slot_mode').eq('owner_id', uid).maybeSingle();
|
||||
|
||||
slotMode.value = cfg?.slot_mode ?? 'fixed';
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
const toast = useToast();
|
||||
@@ -106,11 +107,9 @@ async function loadTemplates() {
|
||||
if (!tenantId.value) return;
|
||||
templatesLoading.value = true;
|
||||
try {
|
||||
// 1. Busca templates globais do SaaS (tenant_id IS NULL)
|
||||
const { data: globals, error: gErr } = await supabase
|
||||
.from('notification_templates')
|
||||
// 1. Busca templates default semeados no schema do tenant
|
||||
const { data: globals, error: gErr } = await tenantDb().from('notification_templates')
|
||||
.select('*')
|
||||
.is('tenant_id', null)
|
||||
.eq('channel', 'sms')
|
||||
.eq('is_default', true)
|
||||
.eq('is_active', true)
|
||||
@@ -120,7 +119,7 @@ async function loadTemplates() {
|
||||
if (gErr) throw gErr;
|
||||
|
||||
// 2. Busca customizações do tenant
|
||||
const { data: customs, error: cErr } = await supabase.from('notification_templates').select('*').eq('tenant_id', tenantId.value).eq('channel', 'sms').is('deleted_at', null);
|
||||
const { data: customs, error: cErr } = await tenantDb().from('notification_templates').select('*').eq('channel', 'sms').is('deleted_at', null);
|
||||
if (cErr) throw cErr;
|
||||
|
||||
const customMap = {};
|
||||
@@ -177,21 +176,19 @@ async function saveTemplate(tpl) {
|
||||
templateSaving.value[tpl.key] = true;
|
||||
try {
|
||||
if (tpl.id) {
|
||||
const { error } = await supabase.from('notification_templates').update({ body_text: tpl.body_text }).eq('id', tpl.id);
|
||||
const { error } = await tenantDb().from('notification_templates').update({ body_text: tpl.body_text }).eq('id', tpl.id);
|
||||
if (error) throw error;
|
||||
} else {
|
||||
const { data: existing } = await supabase.from('notification_templates').select('id').eq('tenant_id', tenantId.value).eq('key', tpl.key).is('deleted_at', null).maybeSingle();
|
||||
const { data: existing } = await tenantDb().from('notification_templates').select('id').eq('key', tpl.key).is('deleted_at', null).maybeSingle();
|
||||
|
||||
if (existing?.id) {
|
||||
const { error } = await supabase.from('notification_templates').update({ body_text: tpl.body_text, is_active: true }).eq('id', existing.id);
|
||||
const { error } = await tenantDb().from('notification_templates').update({ body_text: tpl.body_text, is_active: true }).eq('id', existing.id);
|
||||
if (error) throw error;
|
||||
tpl.id = existing.id;
|
||||
} else {
|
||||
const { data, error } = await supabase
|
||||
.from('notification_templates')
|
||||
const { data, error } = await tenantDb().from('notification_templates')
|
||||
.insert({
|
||||
owner_id: userId.value,
|
||||
tenant_id: tenantId.value,
|
||||
channel: 'sms',
|
||||
key: tpl.key,
|
||||
domain: tpl.domain,
|
||||
@@ -271,10 +268,9 @@ async function loadLogs() {
|
||||
if (!tenantId.value) return;
|
||||
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, sent_at, failed_at, created_at')
|
||||
.eq('tenant_id', tenantId.value)
|
||||
|
||||
.eq('channel', 'sms')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(10);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useRouter, useRoute } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
const router = useRouter();
|
||||
@@ -54,20 +55,18 @@ async function loadChannel() {
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data: active } = await supabase
|
||||
.from('notification_channels')
|
||||
const { data: active } = await tenantDb().from('notification_channels')
|
||||
.select('id, provider, is_active, connection_status, twilio_phone_number, credentials, updated_at')
|
||||
.eq('tenant_id', tenantId)
|
||||
|
||||
.eq('channel', 'whatsapp')
|
||||
.is('deleted_at', null)
|
||||
.maybeSingle();
|
||||
activeChannel.value = active || null;
|
||||
|
||||
// Busca soft-deleted (pra oferecer reativação por provider)
|
||||
const { data: deletedList } = await supabase
|
||||
.from('notification_channels')
|
||||
const { data: deletedList } = await tenantDb().from('notification_channels')
|
||||
.select('id, provider, credentials, created_at')
|
||||
.eq('tenant_id', tenantId)
|
||||
|
||||
.eq('channel', 'whatsapp')
|
||||
.not('deleted_at', 'is', null)
|
||||
.order('created_at', { ascending: false });
|
||||
@@ -185,8 +184,7 @@ async function deactivateCurrent() {
|
||||
switching.value = true;
|
||||
try {
|
||||
// 1ª tentativa: UPDATE direto (funciona se owner_id = user atual)
|
||||
const { error } = await supabase
|
||||
.from('notification_channels')
|
||||
const { error } = await tenantDb().from('notification_channels')
|
||||
.update({ is_active: false, deleted_at: new Date().toISOString() })
|
||||
.eq('id', activeChannel.value.id);
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useRouter, useRoute } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
const router = useRouter();
|
||||
@@ -96,10 +97,9 @@ async function loadCredentials() {
|
||||
softDeletedRecord.value = null;
|
||||
|
||||
// 1) Tentar canal ativo por tenant_id (evolution_api)
|
||||
let { data, error } = await supabase
|
||||
.from('notification_channels')
|
||||
let { data, error } = await tenantDb().from('notification_channels')
|
||||
.select('*')
|
||||
.eq('tenant_id', tenantId.value)
|
||||
|
||||
.eq('channel', 'whatsapp')
|
||||
.eq('provider', 'evolution_api')
|
||||
.is('deleted_at', null)
|
||||
@@ -107,8 +107,7 @@ async function loadCredentials() {
|
||||
|
||||
// Fallback 1: buscar por owner_id (cenário legado ou tenant solo)
|
||||
if (!data && userId.value && userId.value !== tenantId.value) {
|
||||
const fallback = await supabase
|
||||
.from('notification_channels')
|
||||
const fallback = await tenantDb().from('notification_channels')
|
||||
.select('*')
|
||||
.eq('owner_id', userId.value)
|
||||
.eq('channel', 'whatsapp')
|
||||
@@ -136,10 +135,9 @@ async function loadCredentials() {
|
||||
}
|
||||
|
||||
// 2) Não tem ativo — verifica soft-deleted pra oferecer reativar
|
||||
const { data: deleted } = await supabase
|
||||
.from('notification_channels')
|
||||
const { data: deleted } = await tenantDb().from('notification_channels')
|
||||
.select('*')
|
||||
.eq('tenant_id', tenantId.value)
|
||||
|
||||
.eq('channel', 'whatsapp')
|
||||
.eq('provider', 'evolution_api')
|
||||
.not('deleted_at', 'is', null)
|
||||
@@ -201,8 +199,7 @@ async function checkConnectionStatus() {
|
||||
if (channelRecord.value?.id) {
|
||||
const dbStatus = rawState === 'open' ? 'connected' : rawState === 'connecting' ? 'connecting' : 'disconnected';
|
||||
if (channelRecord.value.connection_status !== dbStatus) {
|
||||
await supabase
|
||||
.from('notification_channels')
|
||||
await tenantDb().from('notification_channels')
|
||||
.update({ connection_status: dbStatus, last_health_check: new Date().toISOString() })
|
||||
.eq('id', channelRecord.value.id);
|
||||
channelRecord.value.connection_status = dbStatus;
|
||||
@@ -254,8 +251,7 @@ async function saveHeartbeatConfig() {
|
||||
heartbeat_alerts_enabled: !!heartbeatConfig.value.alerts_enabled,
|
||||
heartbeat_reconnect_enabled: !!heartbeatConfig.value.reconnect_enabled
|
||||
};
|
||||
const { error } = await supabase
|
||||
.from('notification_channels')
|
||||
const { error } = await tenantDb().from('notification_channels')
|
||||
.update({ metadata: newMeta })
|
||||
.eq('id', channelRecord.value.id);
|
||||
if (error) throw error;
|
||||
@@ -274,8 +270,7 @@ async function loadIncidents() {
|
||||
incidentsLoading.value = true;
|
||||
try {
|
||||
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 3600 * 1000).toISOString();
|
||||
const { data, error } = await supabase
|
||||
.from('whatsapp_connection_incidents')
|
||||
const { data, error } = await tenantDb().from('whatsapp_connection_incidents')
|
||||
.select('id, kind, last_state, started_at, resolved_at, duration_seconds, notified_at')
|
||||
.eq('channel_id', channelRecord.value.id)
|
||||
.gte('started_at', sevenDaysAgo)
|
||||
@@ -503,11 +498,9 @@ async function loadTemplates() {
|
||||
if (!tenantId.value) return;
|
||||
templatesLoading.value = true;
|
||||
try {
|
||||
// 1. Busca templates globais do SaaS (tenant_id IS NULL)
|
||||
const { data: globals, error: gErr } = await supabase
|
||||
.from('notification_templates')
|
||||
// 1. Busca templates default semeados no schema do tenant
|
||||
const { data: globals, error: gErr } = await tenantDb().from('notification_templates')
|
||||
.select('*')
|
||||
.is('tenant_id', null)
|
||||
.eq('channel', 'whatsapp')
|
||||
.eq('is_default', true)
|
||||
.eq('is_active', true)
|
||||
@@ -517,7 +510,7 @@ async function loadTemplates() {
|
||||
if (gErr) throw gErr;
|
||||
|
||||
// 2. Busca customizações do tenant
|
||||
const { data: customs, error: cErr } = await supabase.from('notification_templates').select('*').eq('tenant_id', tenantId.value).eq('channel', 'whatsapp').is('deleted_at', null);
|
||||
const { data: customs, error: cErr } = await tenantDb().from('notification_templates').select('*').eq('channel', 'whatsapp').is('deleted_at', null);
|
||||
if (cErr) throw cErr;
|
||||
|
||||
const customMap = {};
|
||||
@@ -576,24 +569,22 @@ async function saveTemplate(tpl) {
|
||||
try {
|
||||
if (tpl.id) {
|
||||
// Atualizar existente
|
||||
const { error } = await supabase.from('notification_templates').update({ body_text: tpl.body_text }).eq('id', tpl.id);
|
||||
const { error } = await tenantDb().from('notification_templates').update({ body_text: tpl.body_text }).eq('id', tpl.id);
|
||||
if (error) throw error;
|
||||
} else {
|
||||
// Verificar se já existe um registro ativo para esta key
|
||||
const { data: existing } = await supabase.from('notification_templates').select('id').eq('tenant_id', tenantId.value).eq('key', tpl.key).is('deleted_at', null).maybeSingle();
|
||||
const { data: existing } = await tenantDb().from('notification_templates').select('id').eq('key', tpl.key).is('deleted_at', null).maybeSingle();
|
||||
|
||||
if (existing?.id) {
|
||||
// Já existe (criado por outra sessão) — atualizar
|
||||
const { error } = await supabase.from('notification_templates').update({ body_text: tpl.body_text, is_active: true }).eq('id', existing.id);
|
||||
const { error } = await tenantDb().from('notification_templates').update({ body_text: tpl.body_text, is_active: true }).eq('id', existing.id);
|
||||
if (error) throw error;
|
||||
tpl.id = existing.id;
|
||||
} else {
|
||||
// Inserir novo
|
||||
const { data, error } = await supabase
|
||||
.from('notification_templates')
|
||||
const { data, error } = await tenantDb().from('notification_templates')
|
||||
.insert({
|
||||
owner_id: userId.value,
|
||||
tenant_id: tenantId.value,
|
||||
channel: 'whatsapp',
|
||||
key: tpl.key,
|
||||
domain: tpl.domain,
|
||||
@@ -722,7 +713,7 @@ async function loadLogs() {
|
||||
if (!tenantId.value) return;
|
||||
logsLoading.value = true;
|
||||
try {
|
||||
let query = supabase.from('notification_logs').select('*', { count: 'exact' }).eq('tenant_id', tenantId.value).eq('channel', 'whatsapp').order('created_at', { ascending: false });
|
||||
let query = tenantDb().from('notification_logs').select('*', { count: 'exact' }).eq('channel', 'whatsapp').order('created_at', { ascending: false });
|
||||
|
||||
if (logsFilter.value !== 'todos') {
|
||||
query = query.eq('status', logsFilter.value);
|
||||
|
||||
@@ -19,6 +19,7 @@ import { ref, onMounted, nextTick } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useConversationDrawerStore } from '@/stores/conversationDrawerStore';
|
||||
|
||||
@@ -77,10 +78,8 @@ async function loadTemplates() {
|
||||
if (!tenantId.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data: globals, error: gErr } = await supabase
|
||||
.from('notification_templates')
|
||||
const { data: globals, error: gErr } = await tenantDb().from('notification_templates')
|
||||
.select('*')
|
||||
.is('tenant_id', null)
|
||||
.eq('channel', 'whatsapp')
|
||||
.eq('is_default', true)
|
||||
.eq('is_active', true)
|
||||
@@ -89,10 +88,9 @@ async function loadTemplates() {
|
||||
.order('event_type');
|
||||
if (gErr) throw gErr;
|
||||
|
||||
const { data: customs, error: cErr } = await supabase
|
||||
.from('notification_templates')
|
||||
const { data: customs, error: cErr } = await tenantDb().from('notification_templates')
|
||||
.select('*')
|
||||
.eq('tenant_id', tenantId.value)
|
||||
|
||||
.eq('channel', 'whatsapp')
|
||||
.is('deleted_at', null);
|
||||
if (cErr) throw cErr;
|
||||
@@ -161,21 +159,19 @@ async function saveTemplate(tpl) {
|
||||
saving.value[tpl.key] = true;
|
||||
try {
|
||||
if (tpl.id) {
|
||||
const { error } = await supabase.from('notification_templates').update({ body_text: body }).eq('id', tpl.id);
|
||||
const { error } = await tenantDb().from('notification_templates').update({ body_text: body }).eq('id', tpl.id);
|
||||
if (error) throw error;
|
||||
} else {
|
||||
const { data: existing } = await supabase.from('notification_templates').select('id').eq('tenant_id', tenantId.value).eq('key', tpl.key).is('deleted_at', null).maybeSingle();
|
||||
const { data: existing } = await tenantDb().from('notification_templates').select('id').eq('key', tpl.key).is('deleted_at', null).maybeSingle();
|
||||
|
||||
if (existing?.id) {
|
||||
const { error } = await supabase.from('notification_templates').update({ body_text: body, is_active: true }).eq('id', existing.id);
|
||||
const { error } = await tenantDb().from('notification_templates').update({ body_text: body, is_active: true }).eq('id', existing.id);
|
||||
if (error) throw error;
|
||||
tpl.id = existing.id;
|
||||
} else {
|
||||
const { data, error } = await supabase
|
||||
.from('notification_templates')
|
||||
const { data, error } = await tenantDb().from('notification_templates')
|
||||
.insert({
|
||||
owner_id: userId.value,
|
||||
tenant_id: tenantId.value,
|
||||
channel: 'whatsapp',
|
||||
key: tpl.key,
|
||||
domain: tpl.domain,
|
||||
@@ -215,7 +211,7 @@ function confirmRevert(tpl) {
|
||||
if (reverting.value[tpl.key]) return;
|
||||
reverting.value[tpl.key] = true;
|
||||
try {
|
||||
const { error } = await supabase.from('notification_templates').update({ deleted_at: new Date().toISOString() }).eq('id', tpl.id);
|
||||
const { error } = await tenantDb().from('notification_templates').update({ deleted_at: new Date().toISOString() }).eq('id', tpl.id);
|
||||
if (error) throw error;
|
||||
tpl.id = null;
|
||||
tpl.is_custom = false;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
* trocar a área central pelo FullCalendar com tema glass.
|
||||
*/
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, inject } from 'vue';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useRouter } from 'vue-router';
|
||||
import FullCalendar from '@fullcalendar/vue3';
|
||||
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||
@@ -705,8 +706,7 @@ const historicoCardRef = ref(null);
|
||||
async function onHistoricoOpen({ id }) {
|
||||
if (!id) return;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_eventos')
|
||||
const { data, error } = await tenantDb().from('agenda_eventos')
|
||||
.select('*, patients!agenda_eventos_patient_id_fkey(nome_completo, status, avatar_url)')
|
||||
.eq('id', id)
|
||||
.maybeSingle();
|
||||
@@ -1096,8 +1096,7 @@ async function ensurePacienteCarregado(id) {
|
||||
if (!id) return;
|
||||
if (pacientesIndex.value.has(id)) return;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('patients')
|
||||
const { data, error } = await tenantDb().from('patients')
|
||||
.select('id, nome_completo, avatar_url, status, last_attended_at, created_at')
|
||||
.eq('id', id)
|
||||
.maybeSingle();
|
||||
|
||||
@@ -20,6 +20,7 @@ import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
|
||||
import MelissaConfigList from './MelissaConfigList.vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import PausasChipsEditor from '@/components/agenda/PausasChipsEditor.vue';
|
||||
// DatePicker/Select/Skeleton/Tag/ToggleSwitch: auto via PrimeVueResolver
|
||||
@@ -355,16 +356,14 @@ async function getActiveTenantId(uid) {
|
||||
}
|
||||
|
||||
async function seedConfigIfMissing(uid) {
|
||||
const { data: existing } = await supabase
|
||||
.from('agenda_configuracoes')
|
||||
const { data: existing } = await tenantDb().from('agenda_configuracoes')
|
||||
.select('owner_id')
|
||||
.eq('owner_id', uid)
|
||||
.maybeSingle();
|
||||
if (existing) return;
|
||||
const tenantId = await getActiveTenantId(uid);
|
||||
const { error } = await supabase
|
||||
.from('agenda_configuracoes')
|
||||
.insert({ owner_id: uid, tenant_id: tenantId, session_duration_min: 50, session_break_min: 10 });
|
||||
const { error } = await tenantDb().from('agenda_configuracoes')
|
||||
.insert({ owner_id: uid, session_duration_min: 50, session_break_min: 10 });
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
@@ -374,8 +373,7 @@ async function loadConfig() {
|
||||
ownerId.value = uid;
|
||||
cfg.value.owner_id = uid;
|
||||
await seedConfigIfMissing(uid);
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_configuracoes')
|
||||
const { data, error } = await tenantDb().from('agenda_configuracoes')
|
||||
.select('*')
|
||||
.eq('owner_id', uid)
|
||||
.order('created_at', { ascending: false })
|
||||
@@ -391,8 +389,7 @@ async function loadConfig() {
|
||||
async function loadRegras() {
|
||||
const uid = ownerId.value || (await getOwnerId());
|
||||
ownerId.value = uid;
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_regras_semanais')
|
||||
const { data, error } = await tenantDb().from('agenda_regras_semanais')
|
||||
.select('*')
|
||||
.eq('owner_id', uid)
|
||||
.order('dia_semana')
|
||||
@@ -411,8 +408,7 @@ async function loadOnlineSlots() {
|
||||
const uid = ownerId.value || (await getOwnerId());
|
||||
ownerId.value = uid;
|
||||
resetOnlineSlots();
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_online_slots')
|
||||
const { data, error } = await tenantDb().from('agenda_online_slots')
|
||||
.select('weekday,time,enabled')
|
||||
.eq('owner_id', uid);
|
||||
if (error) return;
|
||||
@@ -475,12 +471,10 @@ async function saveJornada() {
|
||||
cfg.value.pausas_semanais = pausasToSave;
|
||||
|
||||
const igualTodos = jornadaIgualTodos.value !== false;
|
||||
const { error: cfgErr } = await supabase
|
||||
.from('agenda_configuracoes')
|
||||
const { error: cfgErr } = await tenantDb().from('agenda_configuracoes')
|
||||
.upsert(
|
||||
{
|
||||
owner_id: uid,
|
||||
tenant_id: tenantId,
|
||||
pausas_semanais: pausasToSave,
|
||||
jornada_igual_todos: igualTodos,
|
||||
timezone: cfg.value.timezone || 'America/Sao_Paulo',
|
||||
@@ -499,7 +493,6 @@ async function saveJornada() {
|
||||
: { inicio: jornadaStart.value, fim: jornadaEnd.value };
|
||||
return {
|
||||
owner_id: uid,
|
||||
tenant_id: tenantId,
|
||||
dia_semana: d.value,
|
||||
hora_inicio: normalizeTime(t.inicio),
|
||||
hora_fim: normalizeTime(t.fim),
|
||||
@@ -508,10 +501,10 @@ async function saveJornada() {
|
||||
};
|
||||
});
|
||||
|
||||
const { error: delErr } = await supabase.from('agenda_regras_semanais').delete().eq('owner_id', uid);
|
||||
const { error: delErr } = await tenantDb().from('agenda_regras_semanais').delete().eq('owner_id', uid);
|
||||
if (delErr) throw delErr;
|
||||
if (rows.length) {
|
||||
const { error: insErr } = await supabase.from('agenda_regras_semanais').insert(rows);
|
||||
const { error: insErr } = await tenantDb().from('agenda_regras_semanais').insert(rows);
|
||||
if (insErr) throw insErr;
|
||||
}
|
||||
|
||||
@@ -519,7 +512,7 @@ async function saveJornada() {
|
||||
const activeDays = new Set(selectedDays.value.map((d) => d.value));
|
||||
const orphanDays = [0, 1, 2, 3, 4, 5, 6].filter((d) => !activeDays.has(d));
|
||||
if (orphanDays.length) {
|
||||
await supabase.from('agenda_online_slots').delete().eq('owner_id', uid).in('weekday', orphanDays);
|
||||
await tenantDb().from('agenda_online_slots').delete().eq('owner_id', uid).in('weekday', orphanDays);
|
||||
for (const d of orphanDays) _setDay(d, new Set());
|
||||
}
|
||||
|
||||
@@ -549,8 +542,7 @@ async function saveRitmo() {
|
||||
try {
|
||||
const uid = ownerId.value || (await getOwnerId());
|
||||
ownerId.value = uid;
|
||||
const { error } = await supabase
|
||||
.from('agenda_configuracoes')
|
||||
const { error } = await tenantDb().from('agenda_configuracoes')
|
||||
.upsert(
|
||||
{ owner_id: uid, session_duration_min: dur, session_break_min: gap },
|
||||
{ onConflict: 'owner_id' }
|
||||
@@ -571,12 +563,11 @@ async function saveOnline() {
|
||||
ownerId.value = uid;
|
||||
const tenantId = await getActiveTenantId(uid);
|
||||
|
||||
const { error: cfgErr } = await supabase
|
||||
.from('agenda_configuracoes')
|
||||
const { error: cfgErr } = await tenantDb().from('agenda_configuracoes')
|
||||
.upsert({ owner_id: uid, online_ativo: cfg.value.online_ativo }, { onConflict: 'owner_id' });
|
||||
if (cfgErr) throw cfgErr;
|
||||
|
||||
const { error: delErr } = await supabase.from('agenda_online_slots').delete().eq('owner_id', uid);
|
||||
const { error: delErr } = await tenantDb().from('agenda_online_slots').delete().eq('owner_id', uid);
|
||||
if (delErr) throw delErr;
|
||||
|
||||
if (cfg.value.online_ativo) {
|
||||
@@ -586,7 +577,6 @@ async function saveOnline() {
|
||||
if (isValidHHMM(hhmm)) {
|
||||
rows.push({
|
||||
owner_id: uid,
|
||||
tenant_id: tenantId,
|
||||
weekday: Number(d.value),
|
||||
time: normalizeTime(hhmm),
|
||||
enabled: true
|
||||
@@ -595,7 +585,7 @@ async function saveOnline() {
|
||||
}
|
||||
}
|
||||
if (rows.length) {
|
||||
const { error: insErr } = await supabase.from('agenda_online_slots').insert(rows);
|
||||
const { error: insErr } = await tenantDb().from('agenda_online_slots').insert(rows);
|
||||
if (insErr) throw insErr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import JoditTextEditor from '@/components/ui/JoditTextEditor.vue';
|
||||
import AgendadorPreview from '@/components/agendador/AgendadorPreview.vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useEntitlementsStore } from '@/stores/entitlementsStore';
|
||||
// InputText/Select/SelectButton/Textarea/RadioButton/Checkbox/ColorPicker/
|
||||
@@ -325,8 +326,7 @@ async function getActiveTenantId(uid) {
|
||||
// ── Load ───────────────────────────────────────────────────
|
||||
async function loadPaymentSettings(uid) {
|
||||
try {
|
||||
const { data } = await supabase
|
||||
.from('payment_settings')
|
||||
const { data } = await tenantDb().from('payment_settings')
|
||||
.select('pix_ativo, pix_chave, pix_tipo, deposito_ativo, dinheiro_ativo, cartao_ativo, convenio_ativo')
|
||||
.eq('owner_id', uid)
|
||||
.maybeSingle();
|
||||
@@ -343,7 +343,7 @@ async function load() {
|
||||
ownerId.value = uid;
|
||||
|
||||
const [{ data, error }] = await Promise.all([
|
||||
supabase.from('agendador_configuracoes').select('*').eq('owner_id', uid).maybeSingle(),
|
||||
tenantDb().from('agendador_configuracoes').select('*').eq('owner_id', uid).maybeSingle(),
|
||||
loadPaymentSettings(uid)
|
||||
]);
|
||||
if (error) throw error;
|
||||
@@ -382,10 +382,9 @@ async function toggleAtivo() {
|
||||
cfg.value.ativo = novoAtivo;
|
||||
try {
|
||||
const tenantId = await getActiveTenantId(uid);
|
||||
const { error } = await supabase
|
||||
.from('agendador_configuracoes')
|
||||
const { error } = await tenantDb().from('agendador_configuracoes')
|
||||
.upsert(
|
||||
{ owner_id: uid, tenant_id: tenantId, ativo: novoAtivo, updated_at: new Date().toISOString() },
|
||||
{ owner_id: uid, ativo: novoAtivo, updated_at: new Date().toISOString() },
|
||||
{ onConflict: 'owner_id' }
|
||||
);
|
||||
if (error) throw error;
|
||||
@@ -474,10 +473,9 @@ async function saveCard(cardKey) {
|
||||
const uid = ownerId.value;
|
||||
const tenantId = await getActiveTenantId(uid);
|
||||
const payload = buildPayload(cardKey);
|
||||
const { error } = await supabase
|
||||
.from('agendador_configuracoes')
|
||||
const { error } = await tenantDb().from('agendador_configuracoes')
|
||||
.upsert(
|
||||
{ owner_id: uid, tenant_id: tenantId, ...payload, updated_at: new Date().toISOString() },
|
||||
{ owner_id: uid, ...payload, updated_at: new Date().toISOString() },
|
||||
{ onConflict: 'owner_id' }
|
||||
);
|
||||
if (error) throw error;
|
||||
@@ -518,10 +516,9 @@ async function onFileSelected(event, field) {
|
||||
if (field === 'fundo') cfg.value.imagem_fundo_url = url;
|
||||
const uid = ownerId.value;
|
||||
const tenantId = await getActiveTenantId(uid);
|
||||
await supabase
|
||||
.from('agendador_configuracoes')
|
||||
await tenantDb().from('agendador_configuracoes')
|
||||
.upsert(
|
||||
{ owner_id: uid, tenant_id: tenantId, ...buildPayload('identidade'), updated_at: new Date().toISOString() },
|
||||
{ owner_id: uid, ...buildPayload('identidade'), updated_at: new Date().toISOString() },
|
||||
{ onConflict: 'owner_id' }
|
||||
);
|
||||
toast.add({ severity: 'success', summary: 'Imagem salva', life: 2000 });
|
||||
|
||||
@@ -21,6 +21,7 @@ import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
import AgendaEventDialog from '@/features/agenda/components/AgendaEventDialog.vue';
|
||||
@@ -221,14 +222,12 @@ async function fetchSolicitacoes() {
|
||||
if (!ownerId.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
let q = supabase
|
||||
.from('agendador_solicitacoes')
|
||||
.select('id, owner_id, tenant_id, paciente_nome, paciente_sobrenome, paciente_email, paciente_celular, paciente_cpf, tipo, modalidade, data_solicitada, hora_solicitada, reservado_ate, motivo, como_conheceu, status, recusado_motivo, autorizado_em, created_at')
|
||||
let q = tenantDb().from('agendador_solicitacoes')
|
||||
.select('id, owner_id, paciente_nome, paciente_sobrenome, paciente_email, paciente_celular, paciente_cpf, tipo, modalidade, data_solicitada, hora_solicitada, reservado_ate, motivo, como_conheceu, status, recusado_motivo, autorizado_em, created_at')
|
||||
.order('data_solicitada', { ascending: false })
|
||||
.order('hora_solicitada', { ascending: true });
|
||||
|
||||
if (isClinic.value) q = q.eq('tenant_id', tenantId.value);
|
||||
else q = q.eq('owner_id', ownerId.value);
|
||||
if (!isClinic.value) q = q.eq('owner_id', ownerId.value);
|
||||
|
||||
const { data, error } = await q;
|
||||
if (error) throw error;
|
||||
@@ -294,8 +293,7 @@ async function autorizar() {
|
||||
if (!item) return;
|
||||
dlg.value.saving = true;
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('agendador_solicitacoes')
|
||||
const { error } = await tenantDb().from('agendador_solicitacoes')
|
||||
.update({ status: 'autorizado', autorizado_em: new Date().toISOString() })
|
||||
.eq('id', item.id);
|
||||
if (error) throw error;
|
||||
@@ -325,8 +323,7 @@ async function recusar() {
|
||||
dlg.value.saving = true;
|
||||
try {
|
||||
const motivo = String(dlg.value.recusa_note || '').trim() || null;
|
||||
const { error } = await supabase
|
||||
.from('agendador_solicitacoes')
|
||||
const { error } = await tenantDb().from('agendador_solicitacoes')
|
||||
.update({ status: 'recusado', recusado_motivo: motivo })
|
||||
.eq('id', item.id);
|
||||
if (error) throw error;
|
||||
@@ -362,9 +359,8 @@ function isUuid(v) {
|
||||
async function encontrarOuCriarPaciente(s) {
|
||||
const email = s.paciente_email?.toLowerCase().trim();
|
||||
if (email) {
|
||||
const { data: found } = await supabase
|
||||
.from('patients').select('id')
|
||||
.eq('tenant_id', tenantId.value)
|
||||
const { data: found } = await tenantDb().from('patients').select('id')
|
||||
|
||||
.ilike('email_principal', email)
|
||||
.maybeSingle();
|
||||
if (found?.id) return found.id;
|
||||
@@ -378,10 +374,8 @@ async function encontrarOuCriarPaciente(s) {
|
||||
if (memberErr || !memberData?.id) throw new Error('Membro ativo não encontrado para criação do paciente.');
|
||||
const scope = isClinic.value ? 'clinic' : 'therapist';
|
||||
const nome = [s.paciente_nome, s.paciente_sobrenome].filter(Boolean).join(' ');
|
||||
const { data: novo, error: criErr } = await supabase
|
||||
.from('patients')
|
||||
const { data: novo, error: criErr } = await tenantDb().from('patients')
|
||||
.insert({
|
||||
tenant_id: tenantId.value,
|
||||
responsible_member_id: memberData.id,
|
||||
owner_id: ownerId.value,
|
||||
nome_completo: nome,
|
||||
@@ -438,19 +432,17 @@ async function onEventSaved(arg) {
|
||||
const raw = isWrapped ? arg.payload : arg;
|
||||
const normalized = { ...raw };
|
||||
if (!normalized.owner_id) normalized.owner_id = ownerId.value;
|
||||
normalized.tenant_id = tenantId.value;
|
||||
normalized.tipo = 'sessao';
|
||||
if (!normalized.status) normalized.status = 'agendado';
|
||||
if (!String(normalized.titulo || '').trim()) normalized.titulo = 'Sessão';
|
||||
if (!normalized.visibility_scope) normalized.visibility_scope = 'public';
|
||||
if (!isUuid(normalized.paciente_id)) normalized.paciente_id = null;
|
||||
if (normalized.determined_commitment_id && !isUuid(normalized.determined_commitment_id)) normalized.determined_commitment_id = null;
|
||||
const dbFields = ['tenant_id', 'owner_id', 'terapeuta_id', 'patient_id', 'tipo', 'status', 'titulo', 'observacoes', 'inicio_em', 'fim_em', 'visibility_scope', 'determined_commitment_id', 'titulo_custom', 'extra_fields', 'modalidade'];
|
||||
const dbFields = ['owner_id', 'terapeuta_id', 'patient_id', 'tipo', 'status', 'titulo', 'observacoes', 'inicio_em', 'fim_em', 'visibility_scope', 'determined_commitment_id', 'titulo_custom', 'extra_fields', 'modalidade'];
|
||||
const dbPayload = {};
|
||||
for (const k of dbFields) if (normalized[k] !== undefined) dbPayload[k] = normalized[k];
|
||||
await createEvento(dbPayload);
|
||||
const { error } = await supabase
|
||||
.from('agendador_solicitacoes')
|
||||
const { error } = await tenantDb().from('agendador_solicitacoes')
|
||||
.update({ status: 'convertido' })
|
||||
.eq('id', target.id);
|
||||
if (error) throw error;
|
||||
|
||||
@@ -19,6 +19,7 @@ import MelissaConfigList from './MelissaConfigList.vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useFeriados } from '@/composables/useFeriados';
|
||||
// DatePicker/Tag/Skeleton/Dialog: auto via PrimeVueResolver
|
||||
@@ -154,8 +155,7 @@ async function loadBloqueios() {
|
||||
if (!ownerId.value) return;
|
||||
loadingB.value = true;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_bloqueios')
|
||||
const { data, error } = await tenantDb().from('agenda_bloqueios')
|
||||
.select('*')
|
||||
.eq('owner_id', ownerId.value)
|
||||
.gte('data_inicio', `${ano.value}-01-01`)
|
||||
@@ -288,14 +288,13 @@ async function salvarBloqueio() {
|
||||
origem: 'manual'
|
||||
};
|
||||
if (dlgMode.value === 'edit') {
|
||||
const { error } = await supabase
|
||||
.from('agenda_bloqueios')
|
||||
const { error } = await tenantDb().from('agenda_bloqueios')
|
||||
.update(payload)
|
||||
.eq('id', form.value.id);
|
||||
if (error) throw error;
|
||||
toast.add({ severity: 'success', summary: 'Salvo', detail: 'Bloqueio atualizado.', life: 1800 });
|
||||
} else {
|
||||
const { error } = await supabase.from('agenda_bloqueios').insert(payload);
|
||||
const { error } = await tenantDb().from('agenda_bloqueios').insert(payload);
|
||||
if (error) throw error;
|
||||
toast.add({ severity: 'success', summary: 'Criado', detail: 'Bloqueio adicionado.', life: 1800 });
|
||||
}
|
||||
@@ -319,8 +318,7 @@ function excluirBloqueio(id) {
|
||||
acceptClass: 'p-button-danger',
|
||||
accept: async () => {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('agenda_bloqueios')
|
||||
const { error } = await tenantDb().from('agenda_bloqueios')
|
||||
.delete()
|
||||
.eq('id', id);
|
||||
if (error) throw error;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user