F1b: 6 tabelas anon-facing ficam em public (decisao roteamento anon)
Fluxos anon identificam tenant por token/slug e nao resolvem o schema fisico. Decisao (opcao C): manter em public com RLS por token. Volta a global: patient_intake_requests, patient_invites, patient_invite_attempts, document_share_links, agendador_configuracoes, agendador_solicitacoes. - migration 20260613000001_f1b: remove as 6 do _tenant_template (template v2, 78 tabelas). Smoke: clone gera 78, zero tabelas anon no schema, drop limpo - frontend: 38 cadeias em 14 arquivos revertidas tenantDb().from() -> supabase.from() com tenant_id/owner_id restaurado (via comparacao com main) - edge: convert-abandoned-intakes restaurada do main (SELECT global) - save-intake-progress: ja usava public, sem mudanca - doc F0 atualizado: 78 tenant + 59 global Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
-- =============================================================================
|
||||
-- F1b — Decisão de roteamento anon: 6 tabelas anon-facing FICAM em public
|
||||
--
|
||||
-- Fluxos anônimos identificam o tenant por TOKEN/SLUG (não por login), então
|
||||
-- não conseguem resolver o schema físico. Decisão (2026-06-13, opção C):
|
||||
-- manter essas tabelas em public com tenant_id + RLS por token, como hoje.
|
||||
--
|
||||
-- patient_intake_requests — intake de paciente por convite (token)
|
||||
-- patient_invites — tokens de convite
|
||||
-- patient_invite_attempts — rate-limit anon dos convites
|
||||
-- document_share_links — assinatura pública de documento (token)
|
||||
-- agendador_configuracoes — agendador público (link_slug)
|
||||
-- agendador_solicitacoes — solicitação criada por visitante anon
|
||||
--
|
||||
-- Logo, REMOVE essas 6 do _tenant_template (não viram schema do tenant).
|
||||
-- O clone_tenant_template itera as tabelas do template dinamicamente, então
|
||||
-- novos clones nascem sem elas automaticamente. Classificação final:
|
||||
-- 78 tenant-scoped + 59 globais (era 84 + 53).
|
||||
--
|
||||
-- Nota F6: public.document_share_links.documento_id tem FK -> documents, que
|
||||
-- VAI pro schema do tenant. No drop de public.documents (F6), essa FK precisa
|
||||
-- virar coluna solta (uuid sem constraint) — o RPC valida via token. Idem
|
||||
-- qualquer FK public->tenant dessas 6 (registrar no lote de FKs da F6).
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
anon_tabs text[] := ARRAY[
|
||||
'patient_intake_requests','patient_invites','patient_invite_attempts',
|
||||
'document_share_links','agendador_configuracoes','agendador_solicitacoes'
|
||||
];
|
||||
t text;
|
||||
BEGIN
|
||||
FOREACH t IN ARRAY anon_tabs LOOP
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = '_tenant_template' AND table_name = t) THEN
|
||||
EXECUTE format('DROP TABLE _tenant_template.%I CASCADE', t);
|
||||
RAISE NOTICE 'F1b: _tenant_template.% removida (fica em public)', t;
|
||||
END IF;
|
||||
-- defensivo: tira do registro de realtime do template, se estiver lá
|
||||
DELETE FROM _tenant_template._realtime_tables WHERE table_name = t;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
UPDATE _tenant_template._meta SET value = '2'::jsonb WHERE key = 'template_version';
|
||||
|
||||
COMMIT;
|
||||
@@ -9,8 +9,10 @@
|
||||
| Item | Quantidade |
|
||||
|---|---|
|
||||
| Tabelas em `public` (BASE TABLE) | 137 |
|
||||
| **Tenant-scoped** (vão pra `tenant_<x>`) — decidido Q3 | **84** |
|
||||
| **Globais** (ficam em `public`) | **53** |
|
||||
| **Tenant-scoped** (vão pra `tenant_<x>`) — Q3 + Q5 | **78** |
|
||||
| **Globais** (ficam em `public`) | **59** |
|
||||
|
||||
> **Atualização 2026-06-13 (Q5 — roteamento anon):** 6 tabelas anon-facing voltaram pra `public` (decisão opção C): `patient_intake_requests`, `patient_invites`, `patient_invite_attempts`, `document_share_links`, `agendador_configuracoes`, `agendador_solicitacoes`. Fluxos anônimos identificam o tenant por token/slug e não resolvem o schema físico — mantê-las em public com RLS por token evita um índice global de tokens. Removidas do `_tenant_template` na migration `20260613000001_f1b` (template v2, 78 tabelas). FK a observar na F6: `public.document_share_links.documento_id → documents` (tenant) vira coluna solta no drop.
|
||||
| Funções que referenciam tabelas-tenant | **66** (não 29 — o aviso do blueprint se confirmou) |
|
||||
| Views que referenciam tabelas-tenant | 6 |
|
||||
| FKs global→tenant problemáticas | **1** (`whatsapp_credits_transactions.conversation_message_id`) |
|
||||
|
||||
@@ -32,7 +32,6 @@ 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();
|
||||
|
||||
@@ -53,7 +52,7 @@ async function loadToken() {
|
||||
const { data: authData } = await supabase.auth.getUser();
|
||||
const uid = authData?.user?.id;
|
||||
if (!uid) return;
|
||||
const { data } = await tenantDb().from('patient_invites').select('token').eq('owner_id', uid).eq('active', true).order('created_at', { ascending: false }).limit(1);
|
||||
const { data } = await supabase.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;
|
||||
|
||||
@@ -59,14 +59,15 @@ async function _refresh() {
|
||||
|
||||
// 2. Cadastros recebidos (status = 'new') — RLS filtra pelo owner
|
||||
{
|
||||
const { count } = await tenantDb().from('patient_intake_requests').select('id', { count: 'exact', head: true }).eq('status', 'new');
|
||||
const { count } = await supabase.from('patient_intake_requests').select('id', { count: 'exact', head: true }).eq('status', 'new');
|
||||
cadastrosRecebidos.value = count || 0;
|
||||
}
|
||||
|
||||
// 3. Agendamentos recebidos (status = 'pendente')
|
||||
{
|
||||
let q = tenantDb().from('agendador_solicitacoes').select('id', { count: 'exact', head: true }).eq('status', 'pendente');
|
||||
if (!(isClinic && tenantId)) q = q.eq('owner_id', ownerId);
|
||||
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);
|
||||
const { count } = await q;
|
||||
agendamentosRecebidos.value = count || 0;
|
||||
}
|
||||
|
||||
@@ -595,7 +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 tenantDb().from('agendador_solicitacoes')
|
||||
const { data } = await supabase.from('agendador_solicitacoes')
|
||||
.select('id, paciente_nome, paciente_sobrenome, paciente_email')
|
||||
.eq('owner_id', props.ownerId)
|
||||
.eq('status', 'pendente')
|
||||
|
||||
@@ -981,7 +981,7 @@ async function loadAgendadorSlug() {
|
||||
const uid = ownerId.value;
|
||||
if (!uid) return;
|
||||
try {
|
||||
const { data } = await tenantDb().from('agendador_configuracoes').select('link_slug').eq('owner_id', uid).eq('ativo', true).maybeSingle();
|
||||
const { data } = await supabase.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 tenantDb().from('patient_invites').select('token').eq('owner_id', uid).eq('active', true).order('created_at', { ascending: false }).limit(1);
|
||||
const { data } = await supabase.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 = '';
|
||||
|
||||
@@ -64,12 +64,14 @@ async function load() {
|
||||
if (!ownerId.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
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')
|
||||
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')
|
||||
.order('data_solicitada', { ascending: false })
|
||||
.order('hora_solicitada', { ascending: true });
|
||||
|
||||
if (!isClinic.value) q = q.eq('owner_id', ownerId.value);
|
||||
if (isClinic.value) q = q.eq('tenant_id', tenantId.value);
|
||||
else q = q.eq('owner_id', ownerId.value);
|
||||
|
||||
if (filtroStatus.value) q = q.eq('status', filtroStatus.value);
|
||||
|
||||
@@ -78,8 +80,9 @@ async function load() {
|
||||
solicitacoes.value = data || [];
|
||||
|
||||
if (filtroStatus.value !== 'pendente') {
|
||||
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);
|
||||
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);
|
||||
const { count } = await qp;
|
||||
totalPendentes.value = count || 0;
|
||||
} else {
|
||||
@@ -88,8 +91,9 @@ async function load() {
|
||||
|
||||
// Conta autorizados (sempre, independente do filtro ativo)
|
||||
if (filtroStatus.value !== 'autorizado') {
|
||||
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);
|
||||
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);
|
||||
const { count: ca } = await qa;
|
||||
totalAutorizados.value = ca || 0;
|
||||
} else {
|
||||
@@ -155,7 +159,7 @@ const aprovando = ref(null);
|
||||
async function aprovar(s) {
|
||||
aprovando.value = s.id;
|
||||
try {
|
||||
const { error } = await tenantDb().from('agendador_solicitacoes').update({ status: 'autorizado', autorizado_em: new Date().toISOString() }).eq('id', s.id);
|
||||
const { error } = await supabase.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();
|
||||
@@ -184,7 +188,8 @@ async function confirmarRecusa() {
|
||||
if (!s) return;
|
||||
recusandoId.value = s.id;
|
||||
try {
|
||||
const { error } = await tenantDb().from('agendador_solicitacoes')
|
||||
const { error } = await supabase
|
||||
.from('agendador_solicitacoes')
|
||||
.update({ status: 'recusado', recusado_motivo: recusaMotivo.value || null })
|
||||
.eq('id', s.id);
|
||||
if (error) throw error;
|
||||
@@ -293,7 +298,7 @@ async function onEventSaved(arg) {
|
||||
if (normalized[k] !== undefined) dbPayload[k] = normalized[k];
|
||||
}
|
||||
await createEvento(dbPayload);
|
||||
const { error } = await tenantDb().from('agendador_solicitacoes').update({ status: 'convertido' }).eq('id', target.id);
|
||||
const { error } = await supabase.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();
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
-->
|
||||
<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.
|
||||
@@ -276,7 +275,7 @@ const intakeSections = computed(() => {
|
||||
async function fetchIntakes() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data, error } = await tenantDb().from('patient_intake_requests').select('*').order('created_at', { ascending: false });
|
||||
const { data, error } = await supabase.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) => {
|
||||
@@ -323,7 +322,7 @@ async function markRejected() {
|
||||
dlg.value.saving = true;
|
||||
try {
|
||||
const reason = String(dlg.value.reject_note || '').trim() || null;
|
||||
const { error } = await tenantDb().from('patient_intake_requests').update({ status: 'rejected', rejected_reason: reason, updated_at: new Date().toISOString() }).eq('id', item.id);
|
||||
const { error } = await supabase.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();
|
||||
|
||||
@@ -483,7 +483,8 @@ 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 = tenantDb().from('patient_intake_requests')
|
||||
let q = supabase
|
||||
.from('patient_intake_requests')
|
||||
.update({
|
||||
status: 'converted',
|
||||
converted_patient_id: patientId,
|
||||
@@ -491,6 +492,11 @@ 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;
|
||||
}
|
||||
|
||||
@@ -79,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 tenantDb().from('agendador_configuracoes').upsert({ owner_id: uid, ...buildPayload('identidade'), updated_at: new Date().toISOString() }, { onConflict: 'owner_id' });
|
||||
await supabase.from('agendador_configuracoes').upsert({ owner_id: uid, tenant_id: tenantId, ...buildPayload('identidade'), updated_at: new Date().toISOString() }, { onConflict: 'owner_id' });
|
||||
toast.add({ severity: 'success', summary: 'Imagem salva', life: 2000 });
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -361,7 +361,7 @@ async function load() {
|
||||
const uid = await getOwnerId();
|
||||
ownerId.value = uid;
|
||||
|
||||
const [{ data, error }] = await Promise.all([tenantDb().from('agendador_configuracoes').select('*').eq('owner_id', uid).maybeSingle(), loadPaymentSettings(uid)]);
|
||||
const [{ data, error }] = await Promise.all([supabase.from('agendador_configuracoes').select('*').eq('owner_id', uid).maybeSingle(), loadPaymentSettings(uid)]);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
@@ -400,7 +400,7 @@ async function toggleAtivo() {
|
||||
try {
|
||||
const tenantId = await getActiveTenantId(uid);
|
||||
|
||||
await tenantDb().from('agendador_configuracoes').upsert({ owner_id: uid, ativo: novoAtivo, updated_at: new Date().toISOString() }, { onConflict: 'owner_id' });
|
||||
await supabase.from('agendador_configuracoes').upsert({ owner_id: uid, tenant_id: tenantId, ativo: novoAtivo, updated_at: new Date().toISOString() }, { onConflict: 'owner_id' });
|
||||
|
||||
toast.add({
|
||||
severity: novoAtivo ? 'success' : 'info',
|
||||
@@ -422,7 +422,7 @@ async function saveCard(cardKey) {
|
||||
const tenantId = await getActiveTenantId(uid);
|
||||
const payload = buildPayload(cardKey);
|
||||
|
||||
await tenantDb().from('agendador_configuracoes').upsert({ owner_id: uid, ...payload, updated_at: new Date().toISOString() }, { onConflict: 'owner_id' });
|
||||
await supabase.from('agendador_configuracoes').upsert({ owner_id: uid, tenant_id: tenantId, ...payload, updated_at: new Date().toISOString() }, { onConflict: 'owner_id' });
|
||||
|
||||
toast.add({ severity: 'success', summary: 'Salvo', life: 2500 });
|
||||
expandedCard.value = new Set();
|
||||
|
||||
@@ -343,7 +343,7 @@ async function load() {
|
||||
ownerId.value = uid;
|
||||
|
||||
const [{ data, error }] = await Promise.all([
|
||||
tenantDb().from('agendador_configuracoes').select('*').eq('owner_id', uid).maybeSingle(),
|
||||
supabase.from('agendador_configuracoes').select('*').eq('owner_id', uid).maybeSingle(),
|
||||
loadPaymentSettings(uid)
|
||||
]);
|
||||
if (error) throw error;
|
||||
@@ -382,9 +382,10 @@ async function toggleAtivo() {
|
||||
cfg.value.ativo = novoAtivo;
|
||||
try {
|
||||
const tenantId = await getActiveTenantId(uid);
|
||||
const { error } = await tenantDb().from('agendador_configuracoes')
|
||||
const { error } = await supabase
|
||||
.from('agendador_configuracoes')
|
||||
.upsert(
|
||||
{ owner_id: uid, ativo: novoAtivo, updated_at: new Date().toISOString() },
|
||||
{ owner_id: uid, tenant_id: tenantId, ativo: novoAtivo, updated_at: new Date().toISOString() },
|
||||
{ onConflict: 'owner_id' }
|
||||
);
|
||||
if (error) throw error;
|
||||
@@ -473,9 +474,10 @@ async function saveCard(cardKey) {
|
||||
const uid = ownerId.value;
|
||||
const tenantId = await getActiveTenantId(uid);
|
||||
const payload = buildPayload(cardKey);
|
||||
const { error } = await tenantDb().from('agendador_configuracoes')
|
||||
const { error } = await supabase
|
||||
.from('agendador_configuracoes')
|
||||
.upsert(
|
||||
{ owner_id: uid, ...payload, updated_at: new Date().toISOString() },
|
||||
{ owner_id: uid, tenant_id: tenantId, ...payload, updated_at: new Date().toISOString() },
|
||||
{ onConflict: 'owner_id' }
|
||||
);
|
||||
if (error) throw error;
|
||||
@@ -516,9 +518,10 @@ async function onFileSelected(event, field) {
|
||||
if (field === 'fundo') cfg.value.imagem_fundo_url = url;
|
||||
const uid = ownerId.value;
|
||||
const tenantId = await getActiveTenantId(uid);
|
||||
await tenantDb().from('agendador_configuracoes')
|
||||
await supabase
|
||||
.from('agendador_configuracoes')
|
||||
.upsert(
|
||||
{ owner_id: uid, ...buildPayload('identidade'), updated_at: new Date().toISOString() },
|
||||
{ owner_id: uid, tenant_id: tenantId, ...buildPayload('identidade'), updated_at: new Date().toISOString() },
|
||||
{ onConflict: 'owner_id' }
|
||||
);
|
||||
toast.add({ severity: 'success', summary: 'Imagem salva', life: 2000 });
|
||||
|
||||
@@ -222,12 +222,14 @@ async function fetchSolicitacoes() {
|
||||
if (!ownerId.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
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')
|
||||
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')
|
||||
.order('data_solicitada', { ascending: false })
|
||||
.order('hora_solicitada', { ascending: true });
|
||||
|
||||
if (!isClinic.value) q = q.eq('owner_id', ownerId.value);
|
||||
if (isClinic.value) q = q.eq('tenant_id', tenantId.value);
|
||||
else q = q.eq('owner_id', ownerId.value);
|
||||
|
||||
const { data, error } = await q;
|
||||
if (error) throw error;
|
||||
@@ -293,7 +295,8 @@ async function autorizar() {
|
||||
if (!item) return;
|
||||
dlg.value.saving = true;
|
||||
try {
|
||||
const { error } = await tenantDb().from('agendador_solicitacoes')
|
||||
const { error } = await supabase
|
||||
.from('agendador_solicitacoes')
|
||||
.update({ status: 'autorizado', autorizado_em: new Date().toISOString() })
|
||||
.eq('id', item.id);
|
||||
if (error) throw error;
|
||||
@@ -323,7 +326,8 @@ async function recusar() {
|
||||
dlg.value.saving = true;
|
||||
try {
|
||||
const motivo = String(dlg.value.recusa_note || '').trim() || null;
|
||||
const { error } = await tenantDb().from('agendador_solicitacoes')
|
||||
const { error } = await supabase
|
||||
.from('agendador_solicitacoes')
|
||||
.update({ status: 'recusado', recusado_motivo: motivo })
|
||||
.eq('id', item.id);
|
||||
if (error) throw error;
|
||||
@@ -442,7 +446,8 @@ async function onEventSaved(arg) {
|
||||
const dbPayload = {};
|
||||
for (const k of dbFields) if (normalized[k] !== undefined) dbPayload[k] = normalized[k];
|
||||
await createEvento(dbPayload);
|
||||
const { error } = await tenantDb().from('agendador_solicitacoes')
|
||||
const { error } = await supabase
|
||||
.from('agendador_solicitacoes')
|
||||
.update({ status: 'convertido' })
|
||||
.eq('id', target.id);
|
||||
if (error) throw error;
|
||||
|
||||
@@ -16,7 +16,6 @@ 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';
|
||||
// Fase 2 (Graphify hotspot): convertToPatient duplicado em 2 pages — extração pro repository.
|
||||
import { createPatient, markIntakeConverted } from '@/features/patients/services/patientsRepository';
|
||||
@@ -258,7 +257,8 @@ const pagedItems = computed(() => {
|
||||
async function fetchIntakes() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data, error } = await tenantDb().from('patient_intake_requests').select('*')
|
||||
const { data, error } = await supabase
|
||||
.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);
|
||||
@@ -346,7 +346,7 @@ async function markRejected() {
|
||||
dlg.value.saving = true;
|
||||
try {
|
||||
const reason = String(dlg.value.reject_note || '').trim() || null;
|
||||
const { error } = await tenantDb().from('patient_intake_requests')
|
||||
const { error } = await supabase.from('patient_intake_requests')
|
||||
.update({ status: 'rejected', rejected_reason: reason, updated_at: new Date().toISOString() })
|
||||
.eq('id', item.id);
|
||||
if (error) throw error;
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
// ── Helpers ──────────────────────────────────────────────────
|
||||
|
||||
async function getOwnerId() {
|
||||
@@ -60,9 +58,11 @@ export async function createShareLink(documentoId, opts = {}) {
|
||||
const expiraEm = new Date();
|
||||
expiraEm.setHours(expiraEm.getHours() + expiracaoHoras);
|
||||
|
||||
const { data, error } = await tenantDb().from('document_share_links')
|
||||
const { data, error } = await supabase
|
||||
.from('document_share_links')
|
||||
.insert({
|
||||
documento_id: documentoId,
|
||||
tenant_id: tenantId,
|
||||
expira_em: expiraEm.toISOString(),
|
||||
usos_max: opts.usosMax || 5,
|
||||
criado_por: ownerId
|
||||
@@ -81,7 +81,8 @@ export async function listShareLinks(documentoId) {
|
||||
|
||||
const ownerId = await getOwnerId();
|
||||
|
||||
const { data, error } = await tenantDb().from('document_share_links')
|
||||
const { data, error } = await supabase
|
||||
.from('document_share_links')
|
||||
.select('*')
|
||||
.eq('documento_id', documentoId)
|
||||
.eq('criado_por', ownerId)
|
||||
@@ -129,7 +130,8 @@ export async function deactivateShareLink(linkId) {
|
||||
|
||||
const ownerId = await getOwnerId();
|
||||
|
||||
const { error } = await tenantDb().from('document_share_links')
|
||||
const { error } = await supabase
|
||||
.from('document_share_links')
|
||||
.update({ ativo: false })
|
||||
.eq('id', linkId)
|
||||
.eq('criado_por', ownerId);
|
||||
|
||||
@@ -432,15 +432,17 @@ async function load() {
|
||||
.eq('tenant_id', tid)
|
||||
.in('role', ['therapist', 'tenant_admin'])
|
||||
.eq('status', 'active'),
|
||||
tenantDb().from('agendador_solicitacoes')
|
||||
supabase
|
||||
.from('agendador_solicitacoes')
|
||||
.select('id, paciente_nome, paciente_sobrenome, tipo, modalidade, data_solicitada, hora_solicitada')
|
||||
|
||||
.eq('tenant_id', tid)
|
||||
.eq('status', 'pendente')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(10),
|
||||
tenantDb().from('patient_intake_requests')
|
||||
supabase
|
||||
.from('patient_intake_requests')
|
||||
.select('id, nome_completo, status, created_at')
|
||||
|
||||
.eq('tenant_id', tid)
|
||||
.eq('status', 'new')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(10)
|
||||
|
||||
@@ -562,7 +562,7 @@ const solicitacoesPendentes = computed(() => solicitacoes.value.length);
|
||||
|
||||
async function aceitarSol(id) {
|
||||
try {
|
||||
await tenantDb().from('agendador_solicitacoes').update({ status: 'autorizado' }).eq('id', id);
|
||||
await supabase.from('agendador_solicitacoes').update({ status: 'autorizado' }).eq('id', id);
|
||||
_solicitacoesBruto.value = _solicitacoesBruto.value.filter((s) => s.id !== id);
|
||||
} catch (e) {
|
||||
console.error('[TherapistDashboard] aceitarSol:', e);
|
||||
@@ -570,7 +570,7 @@ async function aceitarSol(id) {
|
||||
}
|
||||
async function recusarSol(id) {
|
||||
try {
|
||||
await tenantDb().from('agendador_solicitacoes').update({ status: 'recusado' }).eq('id', id);
|
||||
await supabase.from('agendador_solicitacoes').update({ status: 'recusado' }).eq('id', id);
|
||||
_solicitacoesBruto.value = _solicitacoesBruto.value.filter((s) => s.id !== id);
|
||||
} catch (e) {
|
||||
console.error('[TherapistDashboard] recusarSol:', e);
|
||||
@@ -900,13 +900,14 @@ async function load() {
|
||||
.order('start_date', { ascending: false });
|
||||
return q;
|
||||
})(),
|
||||
tenantDb().from('agendador_solicitacoes')
|
||||
supabase
|
||||
.from('agendador_solicitacoes')
|
||||
.select('id, paciente_nome, paciente_sobrenome, tipo, modalidade, data_solicitada, hora_solicitada')
|
||||
.eq('owner_id', ownerId.value)
|
||||
.eq('status', 'pendente')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(10),
|
||||
tenantDb().from('patient_intake_requests').select('id, nome_completo, status, created_at').eq('owner_id', ownerId.value).eq('status', 'new').order('created_at', { ascending: false }).limit(10)
|
||||
supabase.from('patient_intake_requests').select('id, nome_completo, status, created_at').eq('owner_id', ownerId.value).eq('status', 'new').order('created_at', { ascending: false }).limit(10)
|
||||
]);
|
||||
eventosDoMes.value = eventosRes.data || [];
|
||||
_solicitacoesBruto.value = solRes.data || [];
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import { adminClient, listTenantSchemas } from '../_shared/tenant.ts'
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
@@ -37,56 +37,48 @@ Deno.serve(async (req: Request) => {
|
||||
const body = await req.json().catch(() => ({})) as { idle_minutes?: number }
|
||||
const idleMinutes = Math.max(5, Math.min(1440, Number(body.idle_minutes) || DEFAULT_IDLE_MINUTES))
|
||||
|
||||
const admin = adminClient()
|
||||
const supa = createClient(
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
||||
)
|
||||
|
||||
const cutoff = new Date(Date.now() - idleMinutes * 60 * 1000).toISOString()
|
||||
|
||||
let checked = 0
|
||||
let eligibleCount = 0
|
||||
// Busca candidatos: in_progress, last_progress_at antigo, tem minimo nome OU telefone
|
||||
const { data: candidates, error: fetchErr } = await supa
|
||||
.from('patient_intake_requests')
|
||||
.select('id, nome_completo, telefone, email_principal')
|
||||
.eq('status', 'in_progress')
|
||||
.lt('last_progress_at', cutoff)
|
||||
|
||||
if (fetchErr) return json({ error: fetchErr.message }, 500)
|
||||
|
||||
const eligible = (candidates || []).filter((c) => c.nome_completo || c.telefone)
|
||||
|
||||
if (eligible.length === 0) {
|
||||
return json({ checked: candidates?.length || 0, converted: 0, errors: 0 })
|
||||
}
|
||||
|
||||
let converted = 0
|
||||
let errors = 0
|
||||
const results: Array<{ tenant_id: string; intake_id: string; ok: boolean; error?: string }> = []
|
||||
const results: Array<{ intake_id: string; ok: boolean; error?: string }> = []
|
||||
|
||||
// Varre todos os tenants; patient_intake_requests é tenant → tdb
|
||||
for (const t of await listTenantSchemas(admin)) {
|
||||
const tdb = admin.schema(t.schema)
|
||||
|
||||
// Busca candidatos: in_progress, last_progress_at antigo, tem minimo nome OU telefone
|
||||
const { data: candidates, error: fetchErr } = await tdb
|
||||
.from('patient_intake_requests')
|
||||
.select('id, nome_completo, telefone, email_principal')
|
||||
.eq('status', 'in_progress')
|
||||
.lt('last_progress_at', cutoff)
|
||||
|
||||
if (fetchErr) {
|
||||
console.error(`[convert-abandoned-intakes] fetch error (tenant ${t.tenantId}):`, fetchErr.message)
|
||||
continue
|
||||
}
|
||||
|
||||
checked += candidates?.length || 0
|
||||
|
||||
const eligible = (candidates || []).filter((c) => c.nome_completo || c.telefone)
|
||||
eligibleCount += eligible.length
|
||||
|
||||
for (const row of eligible) {
|
||||
// RPC opera no schema do tenant → tdb.rpc (assinatura só com p_intake_id).
|
||||
// TODO(F6): se a RPC passar a exigir p_tenant_id, adicionar t.tenantId aqui.
|
||||
const { error: rpcErr } = await tdb.rpc('convert_abandoned_intake_to_lead', {
|
||||
p_intake_id: row.id
|
||||
})
|
||||
if (rpcErr) {
|
||||
errors++
|
||||
results.push({ tenant_id: t.tenantId, intake_id: row.id, ok: false, error: rpcErr.message })
|
||||
} else {
|
||||
converted++
|
||||
results.push({ tenant_id: t.tenantId, intake_id: row.id, ok: true })
|
||||
}
|
||||
for (const row of eligible) {
|
||||
const { error: rpcErr } = await supa.rpc('convert_abandoned_intake_to_lead', {
|
||||
p_intake_id: row.id
|
||||
})
|
||||
if (rpcErr) {
|
||||
errors++
|
||||
results.push({ intake_id: row.id, ok: false, error: rpcErr.message })
|
||||
} else {
|
||||
converted++
|
||||
results.push({ intake_id: row.id, ok: true })
|
||||
}
|
||||
}
|
||||
|
||||
return json({
|
||||
checked,
|
||||
eligible: eligibleCount,
|
||||
checked: candidates?.length || 0,
|
||||
eligible: eligible.length,
|
||||
converted,
|
||||
errors,
|
||||
idle_minutes: idleMinutes,
|
||||
|
||||
Reference in New Issue
Block a user