Agenda, Agendador, Configurações
This commit is contained in:
414
DBS/2026-03-11/Nova-Dev-Doc/supervisor_fase1.sql
Normal file
414
DBS/2026-03-11/Nova-Dev-Doc/supervisor_fase1.sql
Normal file
@@ -0,0 +1,414 @@
|
||||
-- ============================================================
|
||||
-- SUPERVISOR — Fase 1
|
||||
-- Aplicar no Supabase SQL Editor (em ordem)
|
||||
-- ============================================================
|
||||
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 1. tenants.kind → adiciona 'supervisor'
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
ALTER TABLE public.tenants
|
||||
DROP CONSTRAINT IF EXISTS tenants_kind_check;
|
||||
|
||||
ALTER TABLE public.tenants
|
||||
ADD CONSTRAINT tenants_kind_check
|
||||
CHECK (kind = ANY (ARRAY[
|
||||
'therapist',
|
||||
'clinic_coworking',
|
||||
'clinic_reception',
|
||||
'clinic_full',
|
||||
'clinic',
|
||||
'saas',
|
||||
'supervisor' -- ← novo
|
||||
]));
|
||||
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 2. plans.target → adiciona 'supervisor'
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
ALTER TABLE public.plans
|
||||
DROP CONSTRAINT IF EXISTS plans_target_check;
|
||||
|
||||
ALTER TABLE public.plans
|
||||
ADD CONSTRAINT plans_target_check
|
||||
CHECK (target = ANY (ARRAY[
|
||||
'patient',
|
||||
'therapist',
|
||||
'clinic',
|
||||
'supervisor' -- ← novo
|
||||
]));
|
||||
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 3. plans.max_supervisees — limite de supervisionados
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
ALTER TABLE public.plans
|
||||
ADD COLUMN IF NOT EXISTS max_supervisees integer DEFAULT NULL;
|
||||
|
||||
COMMENT ON COLUMN public.plans.max_supervisees IS
|
||||
'Limite de terapeutas que podem ser supervisionados. Apenas para planos target=supervisor. NULL = sem limite.';
|
||||
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 4. Planos supervisor_free e supervisor_pro
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
INSERT INTO public.plans (key, name, description, target, is_active, max_supervisees)
|
||||
VALUES
|
||||
(
|
||||
'supervisor_free',
|
||||
'Supervisor Free',
|
||||
'Plano gratuito de supervisão. Até 3 terapeutas supervisionados.',
|
||||
'supervisor',
|
||||
true,
|
||||
3
|
||||
),
|
||||
(
|
||||
'supervisor_pro',
|
||||
'Supervisor PRO',
|
||||
'Plano profissional de supervisão. Até 20 terapeutas supervisionados.',
|
||||
'supervisor',
|
||||
true,
|
||||
20
|
||||
)
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET
|
||||
name = EXCLUDED.name,
|
||||
description = EXCLUDED.description,
|
||||
target = EXCLUDED.target,
|
||||
is_active = EXCLUDED.is_active,
|
||||
max_supervisees = EXCLUDED.max_supervisees;
|
||||
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 5. Features de supervisor
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
INSERT INTO public.features (key, name, descricao)
|
||||
VALUES
|
||||
(
|
||||
'supervisor.access',
|
||||
'Acesso à Supervisão',
|
||||
'Acesso básico ao espaço de supervisão (sala, lista de supervisionados).'
|
||||
),
|
||||
(
|
||||
'supervisor.invite',
|
||||
'Convidar Supervisionados',
|
||||
'Permite convidar terapeutas para participar da sala de supervisão.'
|
||||
),
|
||||
(
|
||||
'supervisor.sessions',
|
||||
'Sessões de Supervisão',
|
||||
'Agendamento e registro de sessões de supervisão.'
|
||||
),
|
||||
(
|
||||
'supervisor.reports',
|
||||
'Relatórios de Supervisão',
|
||||
'Relatórios avançados de progresso e evolução dos supervisionados.'
|
||||
)
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET
|
||||
name = EXCLUDED.name,
|
||||
descricao = EXCLUDED.descricao;
|
||||
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 6. plan_features — vincula features aos planos supervisor
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
DO $$
|
||||
DECLARE
|
||||
v_free_id uuid;
|
||||
v_pro_id uuid;
|
||||
v_f_access uuid;
|
||||
v_f_invite uuid;
|
||||
v_f_sessions uuid;
|
||||
v_f_reports uuid;
|
||||
BEGIN
|
||||
SELECT id INTO v_free_id FROM public.plans WHERE key = 'supervisor_free';
|
||||
SELECT id INTO v_pro_id FROM public.plans WHERE key = 'supervisor_pro';
|
||||
|
||||
SELECT id INTO v_f_access FROM public.features WHERE key = 'supervisor.access';
|
||||
SELECT id INTO v_f_invite FROM public.features WHERE key = 'supervisor.invite';
|
||||
SELECT id INTO v_f_sessions FROM public.features WHERE key = 'supervisor.sessions';
|
||||
SELECT id INTO v_f_reports FROM public.features WHERE key = 'supervisor.reports';
|
||||
|
||||
-- supervisor_free: access + invite (limitado por max_supervisees=3)
|
||||
INSERT INTO public.plan_features (plan_id, feature_id)
|
||||
VALUES
|
||||
(v_free_id, v_f_access),
|
||||
(v_free_id, v_f_invite)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- supervisor_pro: tudo
|
||||
INSERT INTO public.plan_features (plan_id, feature_id)
|
||||
VALUES
|
||||
(v_pro_id, v_f_access),
|
||||
(v_pro_id, v_f_invite),
|
||||
(v_pro_id, v_f_sessions),
|
||||
(v_pro_id, v_f_reports)
|
||||
ON CONFLICT DO NOTHING;
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 7. activate_subscription_from_intent — suporte a supervisor
|
||||
-- Supervisor = pessoal (user_id), sem tenant_id (igual therapist)
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
CREATE OR REPLACE FUNCTION public.activate_subscription_from_intent(p_intent_id uuid)
|
||||
RETURNS public.subscriptions
|
||||
LANGUAGE plpgsql SECURITY DEFINER
|
||||
AS $$
|
||||
declare
|
||||
v_intent record;
|
||||
v_sub public.subscriptions;
|
||||
v_days int;
|
||||
v_user_id uuid;
|
||||
v_plan_id uuid;
|
||||
v_target text;
|
||||
begin
|
||||
-- lê pela VIEW unificada
|
||||
select * into v_intent
|
||||
from public.subscription_intents
|
||||
where id = p_intent_id;
|
||||
|
||||
if not found then
|
||||
raise exception 'Intent não encontrado: %', p_intent_id;
|
||||
end if;
|
||||
|
||||
if v_intent.status <> 'paid' then
|
||||
raise exception 'Intent precisa estar paid para ativar assinatura';
|
||||
end if;
|
||||
|
||||
-- resolve target e plan_id via plans.key
|
||||
select p.id, p.target
|
||||
into v_plan_id, v_target
|
||||
from public.plans p
|
||||
where p.key = v_intent.plan_key
|
||||
limit 1;
|
||||
|
||||
if v_plan_id is null then
|
||||
raise exception 'Plano não encontrado em plans.key = %', v_intent.plan_key;
|
||||
end if;
|
||||
|
||||
v_target := lower(coalesce(v_target, ''));
|
||||
|
||||
-- ✅ supervisor adicionado
|
||||
if v_target not in ('clinic', 'therapist', 'supervisor') then
|
||||
raise exception 'Target inválido em plans.target: %', v_target;
|
||||
end if;
|
||||
|
||||
-- regra por target
|
||||
if v_target = 'clinic' then
|
||||
if v_intent.tenant_id is null then
|
||||
raise exception 'Intent sem tenant_id';
|
||||
end if;
|
||||
else
|
||||
-- therapist ou supervisor: vinculado ao user
|
||||
v_user_id := v_intent.user_id;
|
||||
if v_user_id is null then
|
||||
v_user_id := v_intent.created_by_user_id;
|
||||
end if;
|
||||
end if;
|
||||
|
||||
if v_target in ('therapist', 'supervisor') and v_user_id is null then
|
||||
raise exception 'Não foi possível determinar user_id para assinatura %.', v_target;
|
||||
end if;
|
||||
|
||||
-- cancela assinatura ativa anterior
|
||||
if v_target = 'clinic' then
|
||||
update public.subscriptions
|
||||
set status = 'cancelled',
|
||||
cancelled_at = now()
|
||||
where tenant_id = v_intent.tenant_id
|
||||
and plan_id = v_plan_id
|
||||
and status = 'active';
|
||||
else
|
||||
-- therapist ou supervisor
|
||||
update public.subscriptions
|
||||
set status = 'cancelled',
|
||||
cancelled_at = now()
|
||||
where user_id = v_user_id
|
||||
and plan_id = v_plan_id
|
||||
and status = 'active'
|
||||
and tenant_id is null;
|
||||
end if;
|
||||
|
||||
-- duração do plano (30 dias para mensal)
|
||||
v_days := case
|
||||
when lower(coalesce(v_intent.interval, 'month')) = 'year' then 365
|
||||
else 30
|
||||
end;
|
||||
|
||||
-- cria nova assinatura
|
||||
insert into public.subscriptions (
|
||||
user_id,
|
||||
plan_id,
|
||||
status,
|
||||
started_at,
|
||||
expires_at,
|
||||
cancelled_at,
|
||||
activated_at,
|
||||
tenant_id,
|
||||
plan_key,
|
||||
interval,
|
||||
source,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
values (
|
||||
case when v_target = 'clinic' then null else v_user_id end,
|
||||
v_plan_id,
|
||||
'active',
|
||||
now(),
|
||||
now() + make_interval(days => v_days),
|
||||
null,
|
||||
now(),
|
||||
case when v_target = 'clinic' then v_intent.tenant_id else null end,
|
||||
v_intent.plan_key,
|
||||
v_intent.interval,
|
||||
'manual',
|
||||
now(),
|
||||
now()
|
||||
)
|
||||
returning * into v_sub;
|
||||
|
||||
-- grava vínculo intent → subscription
|
||||
if v_target = 'clinic' then
|
||||
update public.subscription_intents_tenant
|
||||
set subscription_id = v_sub.id
|
||||
where id = p_intent_id;
|
||||
else
|
||||
update public.subscription_intents_personal
|
||||
set subscription_id = v_sub.id
|
||||
where id = p_intent_id;
|
||||
end if;
|
||||
|
||||
return v_sub;
|
||||
end;
|
||||
$$;
|
||||
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 8. subscriptions_validate_scope — suporte a supervisor
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
CREATE OR REPLACE FUNCTION public.subscriptions_validate_scope()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_target text;
|
||||
BEGIN
|
||||
SELECT lower(p.target) INTO v_target
|
||||
FROM public.plans p
|
||||
WHERE p.id = NEW.plan_id;
|
||||
|
||||
IF v_target IS NULL THEN
|
||||
RAISE EXCEPTION 'Plano inválido (target nulo).';
|
||||
END IF;
|
||||
|
||||
IF v_target = 'clinic' THEN
|
||||
IF NEW.tenant_id IS NULL THEN
|
||||
RAISE EXCEPTION 'Assinatura clinic exige tenant_id.';
|
||||
END IF;
|
||||
IF NEW.user_id IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Assinatura clinic não pode ter user_id (XOR).';
|
||||
END IF;
|
||||
|
||||
ELSIF v_target IN ('therapist', 'supervisor') THEN
|
||||
-- supervisor é pessoal como therapist
|
||||
IF NEW.tenant_id IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Assinatura % não deve ter tenant_id.', v_target;
|
||||
END IF;
|
||||
IF NEW.user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'Assinatura % exige user_id.', v_target;
|
||||
END IF;
|
||||
|
||||
ELSIF v_target = 'patient' THEN
|
||||
IF NEW.tenant_id IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Assinatura patient não deve ter tenant_id.';
|
||||
END IF;
|
||||
IF NEW.user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'Assinatura patient exige user_id.';
|
||||
END IF;
|
||||
|
||||
ELSE
|
||||
RAISE EXCEPTION 'Target de plano inválido: %', v_target;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 9. subscription_intents_view_insert — suporte a supervisor
|
||||
-- supervisor é roteado como therapist (tabela personal)
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
CREATE OR REPLACE FUNCTION public.subscription_intents_view_insert()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql SECURITY DEFINER
|
||||
AS $$
|
||||
declare
|
||||
v_target text;
|
||||
v_plan_id uuid;
|
||||
begin
|
||||
select p.id, p.target into v_plan_id, v_target
|
||||
from public.plans p
|
||||
where p.key = new.plan_key;
|
||||
|
||||
if v_plan_id is null then
|
||||
raise exception 'Plano inválido: plan_key=%', new.plan_key;
|
||||
end if;
|
||||
|
||||
if lower(v_target) = 'clinic' then
|
||||
if new.tenant_id is null then
|
||||
raise exception 'Intenção clinic exige tenant_id.';
|
||||
end if;
|
||||
|
||||
insert into public.subscription_intents_tenant (
|
||||
id, tenant_id, created_by_user_id, email,
|
||||
plan_id, plan_key, interval, amount_cents, currency,
|
||||
status, source, notes, created_at, paid_at
|
||||
) values (
|
||||
coalesce(new.id, gen_random_uuid()),
|
||||
new.tenant_id, new.created_by_user_id, new.email,
|
||||
v_plan_id, new.plan_key, coalesce(new.interval,'month'),
|
||||
new.amount_cents, coalesce(new.currency,'BRL'),
|
||||
coalesce(new.status,'pending'), coalesce(new.source,'manual'),
|
||||
new.notes, coalesce(new.created_at, now()), new.paid_at
|
||||
);
|
||||
|
||||
new.plan_target := 'clinic';
|
||||
return new;
|
||||
end if;
|
||||
|
||||
-- therapist ou supervisor → tabela personal
|
||||
if lower(v_target) in ('therapist', 'supervisor') then
|
||||
insert into public.subscription_intents_personal (
|
||||
id, user_id, created_by_user_id, email,
|
||||
plan_id, plan_key, interval, amount_cents, currency,
|
||||
status, source, notes, created_at, paid_at
|
||||
) values (
|
||||
coalesce(new.id, gen_random_uuid()),
|
||||
new.user_id, new.created_by_user_id, new.email,
|
||||
v_plan_id, new.plan_key, coalesce(new.interval,'month'),
|
||||
new.amount_cents, coalesce(new.currency,'BRL'),
|
||||
coalesce(new.status,'pending'), coalesce(new.source,'manual'),
|
||||
new.notes, coalesce(new.created_at, now()), new.paid_at
|
||||
);
|
||||
|
||||
new.plan_target := lower(v_target); -- 'therapist' ou 'supervisor'
|
||||
return new;
|
||||
end if;
|
||||
|
||||
raise exception 'Target de plano não suportado: %', v_target;
|
||||
end;
|
||||
$$;
|
||||
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- FIM — verificação rápida
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
SELECT key, name, target, max_supervisees
|
||||
FROM public.plans
|
||||
WHERE target = 'supervisor'
|
||||
ORDER BY key;
|
||||
220
DBS/2026-03-11/Novo-DB/fix_missing_subscriptions.sql
Normal file
220
DBS/2026-03-11/Novo-DB/fix_missing_subscriptions.sql
Normal file
@@ -0,0 +1,220 @@
|
||||
-- =============================================================================
|
||||
-- FIX: Atribuir plano free a usuários/tenants sem assinatura ativa
|
||||
-- =============================================================================
|
||||
-- Execute no SQL Editor do Supabase (service_role)
|
||||
-- Idempotente: só insere onde não existe assinatura ativa.
|
||||
--
|
||||
-- Regras:
|
||||
-- • tenant kind = 'therapist' → therapist_free (por user_id do admin)
|
||||
-- • tenant kind IN (clinic_*) → clinic_free (por tenant_id)
|
||||
-- • profiles.account_type = 'patient' / portal_user → patient_free (por user_id)
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ──────────────────────────────────────────────────────────────────────────────
|
||||
-- DIAGNÓSTICO — mostra o estado atual antes de corrigir
|
||||
-- ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
r RECORD;
|
||||
BEGIN
|
||||
RAISE NOTICE '=== DIAGNÓSTICO DE ASSINATURAS ===';
|
||||
RAISE NOTICE '';
|
||||
|
||||
-- Terapeutas sem plano
|
||||
RAISE NOTICE '--- Terapeutas SEM assinatura ativa ---';
|
||||
FOR r IN
|
||||
SELECT
|
||||
tm.user_id,
|
||||
p.full_name,
|
||||
t.id AS tenant_id,
|
||||
t.name AS tenant_name
|
||||
FROM public.tenant_members tm
|
||||
JOIN public.tenants t ON t.id = tm.tenant_id
|
||||
JOIN public.profiles p ON p.id = tm.user_id
|
||||
WHERE t.kind = 'therapist'
|
||||
AND tm.role = 'tenant_admin'
|
||||
AND tm.status = 'active'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM public.subscriptions s
|
||||
WHERE s.user_id = tm.user_id
|
||||
AND s.status = 'active'
|
||||
)
|
||||
LOOP
|
||||
RAISE NOTICE ' FALTANDO: % (%) — tenant %', r.full_name, r.user_id, r.tenant_id;
|
||||
END LOOP;
|
||||
|
||||
-- Clínicas sem plano
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '--- Clínicas SEM assinatura ativa ---';
|
||||
FOR r IN
|
||||
SELECT t.id, t.name, t.kind
|
||||
FROM public.tenants t
|
||||
WHERE t.kind IN ('clinic_coworking', 'clinic_reception', 'clinic_full', 'clinic')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM public.subscriptions s
|
||||
WHERE s.tenant_id = t.id
|
||||
AND s.status = 'active'
|
||||
)
|
||||
LOOP
|
||||
RAISE NOTICE ' FALTANDO: % (%) — kind %', r.name, r.id, r.kind;
|
||||
END LOOP;
|
||||
|
||||
-- Pacientes sem plano
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '--- Pacientes SEM assinatura ativa ---';
|
||||
FOR r IN
|
||||
SELECT p.id, p.full_name
|
||||
FROM public.profiles p
|
||||
WHERE p.account_type = 'patient'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM public.subscriptions s
|
||||
WHERE s.user_id = p.id
|
||||
AND s.status = 'active'
|
||||
)
|
||||
LOOP
|
||||
RAISE NOTICE ' FALTANDO: % (%)', r.full_name, r.id;
|
||||
END LOOP;
|
||||
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '=== FIM DO DIAGNÓSTICO — aplicando correções... ===';
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
-- ──────────────────────────────────────────────────────────────────────────────
|
||||
-- CORREÇÃO 1: Terapeutas sem assinatura → therapist_free
|
||||
-- Escopo: user_id do tenant_admin do tenant kind='therapist'
|
||||
-- ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
INSERT INTO public.subscriptions (
|
||||
user_id, plan_id, plan_key, status, interval,
|
||||
current_period_start, current_period_end,
|
||||
source, started_at, activated_at
|
||||
)
|
||||
SELECT
|
||||
tm.user_id,
|
||||
p.id,
|
||||
p.key,
|
||||
'active',
|
||||
'month',
|
||||
now(),
|
||||
now() + interval '30 days',
|
||||
'fix_seed',
|
||||
now(),
|
||||
now()
|
||||
FROM public.tenant_members tm
|
||||
JOIN public.tenants t ON t.id = tm.tenant_id
|
||||
JOIN public.plans p ON p.key = 'therapist_free'
|
||||
WHERE t.kind = 'therapist'
|
||||
AND tm.role = 'tenant_admin'
|
||||
AND tm.status = 'active'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM public.subscriptions s
|
||||
WHERE s.user_id = tm.user_id
|
||||
AND s.status = 'active'
|
||||
);
|
||||
|
||||
|
||||
-- ──────────────────────────────────────────────────────────────────────────────
|
||||
-- CORREÇÃO 2: Clínicas sem assinatura → clinic_free
|
||||
-- Escopo: tenant_id
|
||||
-- ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
INSERT INTO public.subscriptions (
|
||||
tenant_id, plan_id, plan_key, status, interval,
|
||||
current_period_start, current_period_end,
|
||||
source, started_at, activated_at
|
||||
)
|
||||
SELECT
|
||||
t.id,
|
||||
p.id,
|
||||
p.key,
|
||||
'active',
|
||||
'month',
|
||||
now(),
|
||||
now() + interval '30 days',
|
||||
'fix_seed',
|
||||
now(),
|
||||
now()
|
||||
FROM public.tenants t
|
||||
JOIN public.plans p ON p.key = 'clinic_free'
|
||||
WHERE t.kind IN ('clinic_coworking', 'clinic_reception', 'clinic_full', 'clinic')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM public.subscriptions s
|
||||
WHERE s.tenant_id = t.id
|
||||
AND s.status = 'active'
|
||||
);
|
||||
|
||||
|
||||
-- ──────────────────────────────────────────────────────────────────────────────
|
||||
-- CORREÇÃO 3: Pacientes sem assinatura → patient_free
|
||||
-- Escopo: user_id
|
||||
-- ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
INSERT INTO public.subscriptions (
|
||||
user_id, plan_id, plan_key, status, interval,
|
||||
current_period_start, current_period_end,
|
||||
source, started_at, activated_at
|
||||
)
|
||||
SELECT
|
||||
pr.id,
|
||||
p.id,
|
||||
p.key,
|
||||
'active',
|
||||
'month',
|
||||
now(),
|
||||
now() + interval '30 days',
|
||||
'fix_seed',
|
||||
now(),
|
||||
now()
|
||||
FROM public.profiles pr
|
||||
JOIN public.plans p ON p.key = 'patient_free'
|
||||
WHERE pr.account_type = 'patient'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM public.subscriptions s
|
||||
WHERE s.user_id = pr.id
|
||||
AND s.status = 'active'
|
||||
);
|
||||
|
||||
|
||||
-- ──────────────────────────────────────────────────────────────────────────────
|
||||
-- CONFIRMAÇÃO — mostra o que foi inserido (source = 'fix_seed')
|
||||
-- ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
r RECORD;
|
||||
total INT := 0;
|
||||
BEGIN
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '=== ASSINATURAS CRIADAS NESTA EXECUÇÃO ===';
|
||||
|
||||
FOR r IN
|
||||
SELECT
|
||||
s.plan_key,
|
||||
COALESCE(pr.full_name, t.name) AS nome,
|
||||
COALESCE(s.user_id::text, s.tenant_id::text) AS owner_id
|
||||
FROM public.subscriptions s
|
||||
LEFT JOIN public.profiles pr ON pr.id = s.user_id
|
||||
LEFT JOIN public.tenants t ON t.id = s.tenant_id
|
||||
WHERE s.source = 'fix_seed'
|
||||
AND s.started_at >= now() - interval '5 seconds'
|
||||
ORDER BY s.plan_key, nome
|
||||
LOOP
|
||||
RAISE NOTICE ' ✅ % → % (%)', r.plan_key, r.nome, r.owner_id;
|
||||
total := total + 1;
|
||||
END LOOP;
|
||||
|
||||
IF total = 0 THEN
|
||||
RAISE NOTICE ' (nenhuma nova assinatura criada — todos já tinham plano ativo)';
|
||||
ELSE
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE ' Total: % assinatura(s) criada(s).', total;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMIT;
|
||||
50
DBS/2026-03-11/Novo-DB/fix_subscriptions_validate_scope.sql
Normal file
50
DBS/2026-03-11/Novo-DB/fix_subscriptions_validate_scope.sql
Normal file
@@ -0,0 +1,50 @@
|
||||
-- Fix: subscriptions_validate_scope — adiciona suporte a target='patient'
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.subscriptions_validate_scope()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_target text;
|
||||
BEGIN
|
||||
SELECT lower(p.target) INTO v_target
|
||||
FROM public.plans p
|
||||
WHERE p.id = NEW.plan_id;
|
||||
|
||||
IF v_target IS NULL THEN
|
||||
RAISE EXCEPTION 'Plano inválido (target nulo).';
|
||||
END IF;
|
||||
|
||||
IF v_target = 'clinic' THEN
|
||||
IF NEW.tenant_id IS NULL THEN
|
||||
RAISE EXCEPTION 'Assinatura clinic exige tenant_id.';
|
||||
END IF;
|
||||
IF NEW.user_id IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Assinatura clinic não pode ter user_id (XOR).';
|
||||
END IF;
|
||||
|
||||
ELSIF v_target = 'therapist' THEN
|
||||
IF NEW.tenant_id IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Assinatura therapist não deve ter tenant_id.';
|
||||
END IF;
|
||||
IF NEW.user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'Assinatura therapist exige user_id.';
|
||||
END IF;
|
||||
|
||||
ELSIF v_target = 'patient' THEN
|
||||
IF NEW.tenant_id IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Assinatura patient não deve ter tenant_id.';
|
||||
END IF;
|
||||
IF NEW.user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'Assinatura patient exige user_id.';
|
||||
END IF;
|
||||
|
||||
ELSE
|
||||
RAISE EXCEPTION 'Target de plano inválido: %', v_target;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
ALTER FUNCTION public.subscriptions_validate_scope() OWNER TO supabase_admin;
|
||||
296
DBS/2026-03-11/Novo-DB/migration_001.sql
Normal file
296
DBS/2026-03-11/Novo-DB/migration_001.sql
Normal file
@@ -0,0 +1,296 @@
|
||||
-- =============================================================================
|
||||
-- SEED 001 — Usuários fictícios para teste
|
||||
-- =============================================================================
|
||||
-- Execute APÓS migration_001.sql
|
||||
--
|
||||
-- Usuários criados:
|
||||
-- paciente@agenciapsi.com.br senha: Teste@123 → patient
|
||||
-- terapeuta@agenciapsi.com.br senha: Teste@123 → therapist
|
||||
-- clinica1@agenciapsi.com.br senha: Teste@123 → clinic_coworking
|
||||
-- clinica2@agenciapsi.com.br senha: Teste@123 → clinic_reception
|
||||
-- clinica3@agenciapsi.com.br senha: Teste@123 → clinic_full
|
||||
-- saas@agenciapsi.com.br senha: Teste@123 → saas_admin
|
||||
-- =============================================================================
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Limpeza de seeds anteriores
|
||||
-- ============================================================
|
||||
|
||||
ALTER TABLE public.patient_groups DISABLE TRIGGER ALL;
|
||||
|
||||
DELETE FROM public.tenant_members
|
||||
WHERE user_id IN (
|
||||
SELECT id FROM auth.users
|
||||
WHERE email IN (
|
||||
'paciente@agenciapsi.com.br',
|
||||
'terapeuta@agenciapsi.com.br',
|
||||
'clinica1@agenciapsi.com.br',
|
||||
'clinica2@agenciapsi.com.br',
|
||||
'clinica3@agenciapsi.com.br',
|
||||
'saas@agenciapsi.com.br'
|
||||
)
|
||||
);
|
||||
|
||||
DELETE FROM public.tenants WHERE id IN (
|
||||
'bbbbbbbb-0002-0002-0002-000000000002',
|
||||
'bbbbbbbb-0003-0003-0003-000000000003',
|
||||
'bbbbbbbb-0004-0004-0004-000000000004',
|
||||
'bbbbbbbb-0005-0005-0005-000000000005'
|
||||
);
|
||||
|
||||
DELETE FROM auth.users WHERE email IN (
|
||||
'paciente@agenciapsi.com.br',
|
||||
'terapeuta@agenciapsi.com.br',
|
||||
'clinica1@agenciapsi.com.br',
|
||||
'clinica2@agenciapsi.com.br',
|
||||
'clinica3@agenciapsi.com.br',
|
||||
'saas@agenciapsi.com.br'
|
||||
);
|
||||
|
||||
ALTER TABLE public.patient_groups ENABLE TRIGGER ALL;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 1. Usuários no auth.users
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO auth.users (
|
||||
id, email, encrypted_password, email_confirmed_at,
|
||||
created_at, updated_at, raw_user_meta_data, role, aud
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'aaaaaaaa-0001-0001-0001-000000000001',
|
||||
'paciente@agenciapsi.com.br',
|
||||
crypt('Teste@123', gen_salt('bf')),
|
||||
now(), now(), now(),
|
||||
'{"name": "Ana Paciente"}'::jsonb,
|
||||
'authenticated', 'authenticated'
|
||||
),
|
||||
(
|
||||
'aaaaaaaa-0002-0002-0002-000000000002',
|
||||
'terapeuta@agenciapsi.com.br',
|
||||
crypt('Teste@123', gen_salt('bf')),
|
||||
now(), now(), now(),
|
||||
'{"name": "Bruno Terapeuta"}'::jsonb,
|
||||
'authenticated', 'authenticated'
|
||||
),
|
||||
(
|
||||
'aaaaaaaa-0003-0003-0003-000000000003',
|
||||
'clinica1@agenciapsi.com.br',
|
||||
crypt('Teste@123', gen_salt('bf')),
|
||||
now(), now(), now(),
|
||||
'{"name": "Clinica Espaco Psi"}'::jsonb,
|
||||
'authenticated', 'authenticated'
|
||||
),
|
||||
(
|
||||
'aaaaaaaa-0004-0004-0004-000000000004',
|
||||
'clinica2@agenciapsi.com.br',
|
||||
crypt('Teste@123', gen_salt('bf')),
|
||||
now(), now(), now(),
|
||||
'{"name": "Clinica Mente Sa"}'::jsonb,
|
||||
'authenticated', 'authenticated'
|
||||
),
|
||||
(
|
||||
'aaaaaaaa-0005-0005-0005-000000000005',
|
||||
'clinica3@agenciapsi.com.br',
|
||||
crypt('Teste@123', gen_salt('bf')),
|
||||
now(), now(), now(),
|
||||
'{"name": "Clinica Bem Estar"}'::jsonb,
|
||||
'authenticated', 'authenticated'
|
||||
),
|
||||
(
|
||||
'aaaaaaaa-0006-0006-0006-000000000006',
|
||||
'saas@agenciapsi.com.br',
|
||||
crypt('Teste@123', gen_salt('bf')),
|
||||
now(), now(), now(),
|
||||
'{"name": "Admin Plataforma"}'::jsonb,
|
||||
'authenticated', 'authenticated'
|
||||
);
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 2. Profiles
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO public.profiles (id, role, account_type, full_name)
|
||||
VALUES
|
||||
('aaaaaaaa-0001-0001-0001-000000000001', 'portal_user', 'patient', 'Ana Paciente'),
|
||||
('aaaaaaaa-0002-0002-0002-000000000002', 'portal_user', 'therapist', 'Bruno Terapeuta'),
|
||||
('aaaaaaaa-0003-0003-0003-000000000003', 'portal_user', 'clinic', 'Clinica Espaco Psi'),
|
||||
('aaaaaaaa-0004-0004-0004-000000000004', 'portal_user', 'clinic', 'Clinica Mente Sa'),
|
||||
('aaaaaaaa-0005-0005-0005-000000000005', 'portal_user', 'clinic', 'Clinica Bem Estar'),
|
||||
('aaaaaaaa-0006-0006-0006-000000000006', 'saas_admin', 'free', 'Admin Plataforma')
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
role = EXCLUDED.role,
|
||||
account_type = EXCLUDED.account_type,
|
||||
full_name = EXCLUDED.full_name;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 3. SaaS Admin
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO public.saas_admins (user_id, created_at)
|
||||
VALUES ('aaaaaaaa-0006-0006-0006-000000000006', now())
|
||||
ON CONFLICT (user_id) DO NOTHING;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 4. Tenant do terapeuta
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO public.tenants (id, name, kind, created_at)
|
||||
VALUES ('bbbbbbbb-0002-0002-0002-000000000002', 'Bruno Terapeuta', 'therapist', now())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||
VALUES ('bbbbbbbb-0002-0002-0002-000000000002', 'aaaaaaaa-0002-0002-0002-000000000002', 'tenant_admin', 'active', now())
|
||||
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||
|
||||
DO $$ BEGIN
|
||||
PERFORM public.seed_determined_commitments('bbbbbbbb-0002-0002-0002-000000000002');
|
||||
END; $$;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 5. Tenant Clinica 1 — Coworking
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO public.tenants (id, name, kind, created_at)
|
||||
VALUES ('bbbbbbbb-0003-0003-0003-000000000003', 'Clinica Espaco Psi', 'clinic_coworking', now())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||
VALUES ('bbbbbbbb-0003-0003-0003-000000000003', 'aaaaaaaa-0003-0003-0003-000000000003', 'tenant_admin', 'active', now())
|
||||
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||
|
||||
DO $$ BEGIN
|
||||
PERFORM public.seed_determined_commitments('bbbbbbbb-0003-0003-0003-000000000003');
|
||||
END; $$;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 6. Tenant Clinica 2 — Recepcao
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO public.tenants (id, name, kind, created_at)
|
||||
VALUES ('bbbbbbbb-0004-0004-0004-000000000004', 'Clinica Mente Sa', 'clinic_reception', now())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||
VALUES ('bbbbbbbb-0004-0004-0004-000000000004', 'aaaaaaaa-0004-0004-0004-000000000004', 'tenant_admin', 'active', now())
|
||||
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||
|
||||
DO $$ BEGIN
|
||||
PERFORM public.seed_determined_commitments('bbbbbbbb-0004-0004-0004-000000000004');
|
||||
END; $$;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 7. Tenant Clinica 3 — Full
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO public.tenants (id, name, kind, created_at)
|
||||
VALUES ('bbbbbbbb-0005-0005-0005-000000000005', 'Clinica Bem Estar', 'clinic_full', now())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||
VALUES ('bbbbbbbb-0005-0005-0005-000000000005', 'aaaaaaaa-0005-0005-0005-000000000005', 'tenant_admin', 'active', now())
|
||||
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||
|
||||
DO $$ BEGIN
|
||||
PERFORM public.seed_determined_commitments('bbbbbbbb-0005-0005-0005-000000000005');
|
||||
END; $$;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 8. Subscriptions ativas
|
||||
-- ============================================================
|
||||
|
||||
-- Paciente → patient_free
|
||||
INSERT INTO public.subscriptions (
|
||||
user_id, plan_id, plan_key, status, interval,
|
||||
current_period_start, current_period_end, source, started_at, activated_at
|
||||
)
|
||||
SELECT
|
||||
'aaaaaaaa-0001-0001-0001-000000000001',
|
||||
p.id, p.key, 'active', 'month',
|
||||
now(), now() + interval '30 days', 'seed', now(), now()
|
||||
FROM public.plans p WHERE p.key = 'patient_free';
|
||||
|
||||
-- Terapeuta → therapist_free
|
||||
INSERT INTO public.subscriptions (
|
||||
user_id, plan_id, plan_key, status, interval,
|
||||
current_period_start, current_period_end, source, started_at, activated_at
|
||||
)
|
||||
SELECT
|
||||
'aaaaaaaa-0002-0002-0002-000000000002',
|
||||
p.id, p.key, 'active', 'month',
|
||||
now(), now() + interval '30 days', 'seed', now(), now()
|
||||
FROM public.plans p WHERE p.key = 'therapist_free';
|
||||
|
||||
-- Clinica 1 → clinic_free
|
||||
INSERT INTO public.subscriptions (
|
||||
tenant_id, plan_id, plan_key, status, interval,
|
||||
current_period_start, current_period_end, source, started_at, activated_at
|
||||
)
|
||||
SELECT
|
||||
'bbbbbbbb-0003-0003-0003-000000000003',
|
||||
p.id, p.key, 'active', 'month',
|
||||
now(), now() + interval '30 days', 'seed', now(), now()
|
||||
FROM public.plans p WHERE p.key = 'clinic_free';
|
||||
|
||||
-- Clinica 2 → clinic_free
|
||||
INSERT INTO public.subscriptions (
|
||||
tenant_id, plan_id, plan_key, status, interval,
|
||||
current_period_start, current_period_end, source, started_at, activated_at
|
||||
)
|
||||
SELECT
|
||||
'bbbbbbbb-0004-0004-0004-000000000004',
|
||||
p.id, p.key, 'active', 'month',
|
||||
now(), now() + interval '30 days', 'seed', now(), now()
|
||||
FROM public.plans p WHERE p.key = 'clinic_free';
|
||||
|
||||
-- Clinica 3 → clinic_free
|
||||
INSERT INTO public.subscriptions (
|
||||
tenant_id, plan_id, plan_key, status, interval,
|
||||
current_period_start, current_period_end, source, started_at, activated_at
|
||||
)
|
||||
SELECT
|
||||
'bbbbbbbb-0005-0005-0005-000000000005',
|
||||
p.id, p.key, 'active', 'month',
|
||||
now(), now() + interval '30 days', 'seed', now(), now()
|
||||
FROM public.plans p WHERE p.key = 'clinic_free';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 9. Vincula terapeuta à Clinica 3 (exemplo de associacao)
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||
VALUES (
|
||||
'bbbbbbbb-0005-0005-0005-000000000005',
|
||||
'aaaaaaaa-0002-0002-0002-000000000002',
|
||||
'therapist', 'active', now()
|
||||
)
|
||||
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Confirmacao
|
||||
-- ============================================================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '✅ Seed aplicado com sucesso.';
|
||||
RAISE NOTICE ' paciente@agenciapsi.com.br → patient';
|
||||
RAISE NOTICE ' terapeuta@agenciapsi.com.br → therapist';
|
||||
RAISE NOTICE ' clinica1@agenciapsi.com.br → clinic_coworking';
|
||||
RAISE NOTICE ' clinica2@agenciapsi.com.br → clinic_reception';
|
||||
RAISE NOTICE ' clinica3@agenciapsi.com.br → clinic_full';
|
||||
RAISE NOTICE ' saas@agenciapsi.com.br → saas_admin';
|
||||
RAISE NOTICE ' Senha: Teste@123';
|
||||
END;
|
||||
$$;
|
||||
13
DBS/2026-03-11/Novo-DB/migration_002_layout_variant.sql
Normal file
13
DBS/2026-03-11/Novo-DB/migration_002_layout_variant.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
-- =============================================================================
|
||||
-- Migration 002 — Adiciona layout_variant em user_settings
|
||||
-- =============================================================================
|
||||
-- Execute no SQL Editor do Supabase (ou via Docker psql).
|
||||
-- Tolerante: usa IF NOT EXISTS / DEFAULT para não quebrar dados existentes.
|
||||
-- =============================================================================
|
||||
|
||||
ALTER TABLE public.user_settings
|
||||
ADD COLUMN IF NOT EXISTS layout_variant TEXT NOT NULL DEFAULT 'classic';
|
||||
|
||||
-- =============================================================================
|
||||
RAISE NOTICE '✅ Coluna layout_variant adicionada a user_settings.';
|
||||
-- =============================================================================
|
||||
334
DBS/2026-03-11/Novo-DB/seed_001.sql
Normal file
334
DBS/2026-03-11/Novo-DB/seed_001.sql
Normal file
@@ -0,0 +1,334 @@
|
||||
-- =============================================================================
|
||||
-- SEED — Usuários fictícios para teste
|
||||
-- =============================================================================
|
||||
-- IMPORTANTE: Execute APÓS a migration_001.sql
|
||||
-- IMPORTANTE: Requer extensão pgcrypto (já ativa no Supabase)
|
||||
--
|
||||
-- Cria os seguintes usuários de teste:
|
||||
--
|
||||
-- paciente@agenciapsi.com.br senha: Teste@123 → paciente
|
||||
-- terapeuta@agenciapsi.com.br senha: Teste@123 → terapeuta solo
|
||||
-- clinica1@agenciapsi.com.br senha: Teste@123 → clínica coworking
|
||||
-- clinica2@agenciapsi.com.br senha: Teste@123 → clínica com secretaria
|
||||
-- clinica3@agenciapsi.com.br senha: Teste@123 → clínica full
|
||||
-- saas@agenciapsi.com.br senha: Teste@123 → admin da plataforma
|
||||
--
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ============================================================
|
||||
-- Helper: cria usuário no auth.users + profile
|
||||
-- (Supabase não expõe auth.users diretamente, mas em SQL Editor
|
||||
-- com acesso de service_role podemos inserir diretamente)
|
||||
-- ============================================================
|
||||
|
||||
-- Limpa seeds anteriores se existirem
|
||||
DELETE FROM auth.users
|
||||
WHERE email IN (
|
||||
'paciente@agenciapsi.com.br',
|
||||
'terapeuta@agenciapsi.com.br',
|
||||
'clinica1@agenciapsi.com.br',
|
||||
'clinica2@agenciapsi.com.br',
|
||||
'clinica3@agenciapsi.com.br',
|
||||
'saas@agenciapsi.com.br'
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 1. Cria usuários no auth.users
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO auth.users (
|
||||
instance_id,
|
||||
id,
|
||||
email,
|
||||
encrypted_password,
|
||||
email_confirmed_at,
|
||||
confirmed_at,
|
||||
created_at,
|
||||
updated_at,
|
||||
raw_user_meta_data,
|
||||
raw_app_meta_data,
|
||||
role,
|
||||
aud,
|
||||
is_sso_user,
|
||||
is_anonymous,
|
||||
confirmation_token,
|
||||
recovery_token,
|
||||
email_change_token_new,
|
||||
email_change_token_current,
|
||||
email_change
|
||||
)
|
||||
VALUES
|
||||
-- Paciente
|
||||
(
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'aaaaaaaa-0001-0001-0001-000000000001',
|
||||
'paciente@agenciapsi.com.br',
|
||||
crypt('Teste@123', gen_salt('bf')),
|
||||
now(), now(), now(), now(),
|
||||
'{"name": "Ana Paciente"}'::jsonb,
|
||||
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||
'authenticated', 'authenticated', false, false, '', '', '', '', ''
|
||||
),
|
||||
-- Terapeuta
|
||||
(
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'aaaaaaaa-0002-0002-0002-000000000002',
|
||||
'terapeuta@agenciapsi.com.br',
|
||||
crypt('Teste@123', gen_salt('bf')),
|
||||
now(), now(), now(), now(),
|
||||
'{"name": "Bruno Terapeuta"}'::jsonb,
|
||||
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||
'authenticated', 'authenticated', false, false, '', '', '', '', ''
|
||||
),
|
||||
-- Clínica 1 — Coworking
|
||||
(
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'aaaaaaaa-0003-0003-0003-000000000003',
|
||||
'clinica1@agenciapsi.com.br',
|
||||
crypt('Teste@123', gen_salt('bf')),
|
||||
now(), now(), now(), now(),
|
||||
'{"name": "Clínica Espaço Psi"}'::jsonb,
|
||||
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||
'authenticated', 'authenticated', false, false, '', '', '', '', ''
|
||||
),
|
||||
-- Clínica 2 — Recepção
|
||||
(
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'aaaaaaaa-0004-0004-0004-000000000004',
|
||||
'clinica2@agenciapsi.com.br',
|
||||
crypt('Teste@123', gen_salt('bf')),
|
||||
now(), now(), now(), now(),
|
||||
'{"name": "Clínica Mente Sã"}'::jsonb,
|
||||
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||
'authenticated', 'authenticated', false, false, '', '', '', '', ''
|
||||
),
|
||||
-- Clínica 3 — Full
|
||||
(
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'aaaaaaaa-0005-0005-0005-000000000005',
|
||||
'clinica3@agenciapsi.com.br',
|
||||
crypt('Teste@123', gen_salt('bf')),
|
||||
now(), now(), now(), now(),
|
||||
'{"name": "Clínica Bem Estar"}'::jsonb,
|
||||
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||
'authenticated', 'authenticated', false, false, '', '', '', '', ''
|
||||
),
|
||||
-- SaaS Admin
|
||||
(
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'aaaaaaaa-0006-0006-0006-000000000006',
|
||||
'saas@agenciapsi.com.br',
|
||||
crypt('Teste@123', gen_salt('bf')),
|
||||
now(), now(), now(), now(),
|
||||
'{"name": "Admin Plataforma"}'::jsonb,
|
||||
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||
'authenticated', 'authenticated', false, false, '', '', '', '', ''
|
||||
);
|
||||
|
||||
-- auth.identities (obrigatório para GoTrue reconhecer login email/senha)
|
||||
INSERT INTO auth.identities (id, user_id, provider_id, provider, identity_data, created_at, updated_at, last_sign_in_at)
|
||||
VALUES
|
||||
(gen_random_uuid(), 'aaaaaaaa-0001-0001-0001-000000000001', 'paciente@agenciapsi.com.br', 'email', '{"sub": "aaaaaaaa-0001-0001-0001-000000000001", "email": "paciente@agenciapsi.com.br", "email_verified": true}'::jsonb, now(), now(), now()),
|
||||
(gen_random_uuid(), 'aaaaaaaa-0002-0002-0002-000000000002', 'terapeuta@agenciapsi.com.br', 'email', '{"sub": "aaaaaaaa-0002-0002-0002-000000000002", "email": "terapeuta@agenciapsi.com.br", "email_verified": true}'::jsonb, now(), now(), now()),
|
||||
(gen_random_uuid(), 'aaaaaaaa-0003-0003-0003-000000000003', 'clinica1@agenciapsi.com.br', 'email', '{"sub": "aaaaaaaa-0003-0003-0003-000000000003", "email": "clinica1@agenciapsi.com.br", "email_verified": true}'::jsonb, now(), now(), now()),
|
||||
(gen_random_uuid(), 'aaaaaaaa-0004-0004-0004-000000000004', 'clinica2@agenciapsi.com.br', 'email', '{"sub": "aaaaaaaa-0004-0004-0004-000000000004", "email": "clinica2@agenciapsi.com.br", "email_verified": true}'::jsonb, now(), now(), now()),
|
||||
(gen_random_uuid(), 'aaaaaaaa-0005-0005-0005-000000000005', 'clinica3@agenciapsi.com.br', 'email', '{"sub": "aaaaaaaa-0005-0005-0005-000000000005", "email": "clinica3@agenciapsi.com.br", "email_verified": true}'::jsonb, now(), now(), now()),
|
||||
(gen_random_uuid(), 'aaaaaaaa-0006-0006-0006-000000000006', 'saas@agenciapsi.com.br', 'email', '{"sub": "aaaaaaaa-0006-0006-0006-000000000006", "email": "saas@agenciapsi.com.br", "email_verified": true}'::jsonb, now(), now(), now())
|
||||
ON CONFLICT (provider, provider_id) DO NOTHING;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 2. Profiles (o trigger handle_new_user não dispara em inserts
|
||||
-- diretos no auth.users via SQL, então criamos manualmente)
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO public.profiles (id, role, account_type, full_name)
|
||||
VALUES
|
||||
('aaaaaaaa-0001-0001-0001-000000000001', 'portal_user', 'patient', 'Ana Paciente'),
|
||||
('aaaaaaaa-0002-0002-0002-000000000002', 'tenant_member', 'therapist', 'Bruno Terapeuta'),
|
||||
('aaaaaaaa-0003-0003-0003-000000000003', 'tenant_member', 'clinic', 'Clínica Espaço Psi'),
|
||||
('aaaaaaaa-0004-0004-0004-000000000004', 'tenant_member', 'clinic', 'Clínica Mente Sã'),
|
||||
('aaaaaaaa-0005-0005-0005-000000000005', 'tenant_member', 'clinic', 'Clínica Bem Estar'),
|
||||
('aaaaaaaa-0006-0006-0006-000000000006', 'saas_admin', 'free', 'Admin Plataforma')
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
role = EXCLUDED.role,
|
||||
account_type = EXCLUDED.account_type,
|
||||
full_name = EXCLUDED.full_name;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 3. SaaS Admin na tabela saas_admins
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO public.saas_admins (user_id, created_at)
|
||||
VALUES ('aaaaaaaa-0006-0006-0006-000000000006', now())
|
||||
ON CONFLICT (user_id) DO NOTHING;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 4. Tenant do terapeuta
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO public.tenants (id, name, kind, created_at)
|
||||
VALUES ('bbbbbbbb-0002-0002-0002-000000000002', 'Bruno Terapeuta', 'therapist', now())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||
VALUES ('bbbbbbbb-0002-0002-0002-000000000002', 'aaaaaaaa-0002-0002-0002-000000000002', 'tenant_admin', 'active', now())
|
||||
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||
|
||||
DO $$ BEGIN PERFORM public.seed_determined_commitments('bbbbbbbb-0002-0002-0002-000000000002'); END $$;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 5. Tenant da Clínica 1 — Coworking
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO public.tenants (id, name, kind, created_at)
|
||||
VALUES ('bbbbbbbb-0003-0003-0003-000000000003', 'Clínica Espaço Psi', 'clinic_coworking', now())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||
VALUES ('bbbbbbbb-0003-0003-0003-000000000003', 'aaaaaaaa-0003-0003-0003-000000000003', 'tenant_admin', 'active', now())
|
||||
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||
|
||||
DO $$ BEGIN PERFORM public.seed_determined_commitments('bbbbbbbb-0003-0003-0003-000000000003'); END $$;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 6. Tenant da Clínica 2 — Recepção
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO public.tenants (id, name, kind, created_at)
|
||||
VALUES ('bbbbbbbb-0004-0004-0004-000000000004', 'Clínica Mente Sã', 'clinic_reception', now())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||
VALUES ('bbbbbbbb-0004-0004-0004-000000000004', 'aaaaaaaa-0004-0004-0004-000000000004', 'tenant_admin', 'active', now())
|
||||
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||
|
||||
DO $$ BEGIN PERFORM public.seed_determined_commitments('bbbbbbbb-0004-0004-0004-000000000004'); END $$;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 7. Tenant da Clínica 3 — Full
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO public.tenants (id, name, kind, created_at)
|
||||
VALUES ('bbbbbbbb-0005-0005-0005-000000000005', 'Clínica Bem Estar', 'clinic_full', now())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||
VALUES ('bbbbbbbb-0005-0005-0005-000000000005', 'aaaaaaaa-0005-0005-0005-000000000005', 'tenant_admin', 'active', now())
|
||||
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||
|
||||
DO $$ BEGIN PERFORM public.seed_determined_commitments('bbbbbbbb-0005-0005-0005-000000000005'); END $$;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 8. Subscriptions ativas para cada conta
|
||||
-- ============================================================
|
||||
|
||||
-- Terapeuta → plano therapist_free
|
||||
INSERT INTO public.subscriptions (
|
||||
user_id, plan_id, plan_key, status, interval,
|
||||
current_period_start, current_period_end,
|
||||
source, started_at, activated_at
|
||||
)
|
||||
SELECT
|
||||
'aaaaaaaa-0002-0002-0002-000000000002',
|
||||
p.id, p.key, 'active', 'month',
|
||||
now(), now() + interval '30 days',
|
||||
'seed', now(), now()
|
||||
FROM public.plans p WHERE p.key = 'therapist_free';
|
||||
|
||||
-- Clínica 1 → plano clinic_free
|
||||
INSERT INTO public.subscriptions (
|
||||
tenant_id, plan_id, plan_key, status, interval,
|
||||
current_period_start, current_period_end,
|
||||
source, started_at, activated_at
|
||||
)
|
||||
SELECT
|
||||
'bbbbbbbb-0003-0003-0003-000000000003',
|
||||
p.id, p.key, 'active', 'month',
|
||||
now(), now() + interval '30 days',
|
||||
'seed', now(), now()
|
||||
FROM public.plans p WHERE p.key = 'clinic_free';
|
||||
|
||||
-- Clínica 2 → plano clinic_free
|
||||
INSERT INTO public.subscriptions (
|
||||
tenant_id, plan_id, plan_key, status, interval,
|
||||
current_period_start, current_period_end,
|
||||
source, started_at, activated_at
|
||||
)
|
||||
SELECT
|
||||
'bbbbbbbb-0004-0004-0004-000000000004',
|
||||
p.id, p.key, 'active', 'month',
|
||||
now(), now() + interval '30 days',
|
||||
'seed', now(), now()
|
||||
FROM public.plans p WHERE p.key = 'clinic_free';
|
||||
|
||||
-- Clínica 3 → plano clinic_free
|
||||
INSERT INTO public.subscriptions (
|
||||
tenant_id, plan_id, plan_key, status, interval,
|
||||
current_period_start, current_period_end,
|
||||
source, started_at, activated_at
|
||||
)
|
||||
SELECT
|
||||
'bbbbbbbb-0005-0005-0005-000000000005',
|
||||
p.id, p.key, 'active', 'month',
|
||||
now(), now() + interval '30 days',
|
||||
'seed', now(), now()
|
||||
FROM public.plans p WHERE p.key = 'clinic_free';
|
||||
|
||||
-- Paciente → plano patient_free
|
||||
INSERT INTO public.subscriptions (
|
||||
user_id, plan_id, plan_key, status, interval,
|
||||
current_period_start, current_period_end,
|
||||
source, started_at, activated_at
|
||||
)
|
||||
SELECT
|
||||
'aaaaaaaa-0001-0001-0001-000000000001',
|
||||
p.id, p.key, 'active', 'month',
|
||||
now(), now() + interval '30 days',
|
||||
'seed', now(), now()
|
||||
FROM public.plans p WHERE p.key = 'patient_free';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 9. Vincula terapeuta à Clínica 3 (full) como exemplo
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||
VALUES (
|
||||
'bbbbbbbb-0005-0005-0005-000000000005',
|
||||
'aaaaaaaa-0002-0002-0002-000000000002',
|
||||
'therapist',
|
||||
'active',
|
||||
now()
|
||||
)
|
||||
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 10. Confirma
|
||||
-- ============================================================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '✅ Seed aplicado com sucesso.';
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE ' Usuários criados:';
|
||||
RAISE NOTICE ' paciente@agenciapsi.com.br → patient';
|
||||
RAISE NOTICE ' terapeuta@agenciapsi.com.br → therapist (tenant próprio + vinculado à Clínica 3)';
|
||||
RAISE NOTICE ' clinica1@agenciapsi.com.br → clinic_coworking';
|
||||
RAISE NOTICE ' clinica2@agenciapsi.com.br → clinic_reception';
|
||||
RAISE NOTICE ' clinica3@agenciapsi.com.br → clinic_full';
|
||||
RAISE NOTICE ' saas@agenciapsi.com.br → saas_admin';
|
||||
RAISE NOTICE ' Senha de todos: Teste@123';
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMIT;
|
||||
199
DBS/2026-03-11/Novo-DB/seed_002.sql
Normal file
199
DBS/2026-03-11/Novo-DB/seed_002.sql
Normal file
@@ -0,0 +1,199 @@
|
||||
-- =============================================================================
|
||||
-- SEED 002 — Supervisor e Editor
|
||||
-- =============================================================================
|
||||
-- Execute APÓS seed_001.sql
|
||||
-- Requer: pgcrypto (já ativo no Supabase)
|
||||
--
|
||||
-- Cria os seguintes usuários de teste:
|
||||
--
|
||||
-- supervisor@agenciapsi.com.br senha: Teste@123 → supervisor da Clínica 3
|
||||
-- editor@agenciapsi.com.br senha: Teste@123 → editor de conteúdo (plataforma)
|
||||
--
|
||||
-- UUIDs reservados:
|
||||
-- Supervisor → aaaaaaaa-0007-0007-0007-000000000007
|
||||
-- Editor → aaaaaaaa-0008-0008-0008-000000000008
|
||||
--
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ============================================================
|
||||
-- 0. Migration: adiciona platform_roles em profiles (se não existir)
|
||||
-- ============================================================
|
||||
|
||||
ALTER TABLE public.profiles
|
||||
ADD COLUMN IF NOT EXISTS platform_roles text[] NOT NULL DEFAULT '{}';
|
||||
|
||||
COMMENT ON COLUMN public.profiles.platform_roles IS
|
||||
'Papéis globais de plataforma, independentes de tenant. Ex: editor de microlearning. Atribuído pelo saas_admin.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 1. Remove seeds anteriores (idempotente)
|
||||
-- ============================================================
|
||||
|
||||
DELETE FROM auth.users
|
||||
WHERE email IN (
|
||||
'supervisor@agenciapsi.com.br',
|
||||
'editor@agenciapsi.com.br'
|
||||
);
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 2. Cria usuários no auth.users
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO auth.users (
|
||||
instance_id,
|
||||
id,
|
||||
email,
|
||||
encrypted_password,
|
||||
email_confirmed_at,
|
||||
created_at,
|
||||
updated_at,
|
||||
raw_user_meta_data,
|
||||
raw_app_meta_data,
|
||||
role,
|
||||
aud,
|
||||
is_sso_user,
|
||||
is_anonymous,
|
||||
confirmation_token,
|
||||
recovery_token,
|
||||
email_change_token_new,
|
||||
email_change_token_current,
|
||||
email_change
|
||||
)
|
||||
VALUES
|
||||
-- Supervisor
|
||||
(
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'aaaaaaaa-0007-0007-0007-000000000007',
|
||||
'supervisor@agenciapsi.com.br',
|
||||
crypt('Teste@123', gen_salt('bf')),
|
||||
now(), now(), now(),
|
||||
'{"name": "Carlos Supervisor"}'::jsonb,
|
||||
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||
'authenticated', 'authenticated', false, false, '', '', '', '', ''
|
||||
),
|
||||
-- Editor de Conteúdo
|
||||
(
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'aaaaaaaa-0008-0008-0008-000000000008',
|
||||
'editor@agenciapsi.com.br',
|
||||
crypt('Teste@123', gen_salt('bf')),
|
||||
now(), now(), now(),
|
||||
'{"name": "Diana Editora"}'::jsonb,
|
||||
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||
'authenticated', 'authenticated', false, false, '', '', '', '', ''
|
||||
);
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 3. auth.identities (obrigatório para GoTrue reconhecer login)
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO auth.identities (id, user_id, provider_id, provider, identity_data, created_at, updated_at, last_sign_in_at)
|
||||
VALUES
|
||||
(
|
||||
gen_random_uuid(),
|
||||
'aaaaaaaa-0007-0007-0007-000000000007',
|
||||
'supervisor@agenciapsi.com.br',
|
||||
'email',
|
||||
'{"sub": "aaaaaaaa-0007-0007-0007-000000000007", "email": "supervisor@agenciapsi.com.br", "email_verified": true}'::jsonb,
|
||||
now(), now(), now()
|
||||
),
|
||||
(
|
||||
gen_random_uuid(),
|
||||
'aaaaaaaa-0008-0008-0008-000000000008',
|
||||
'editor@agenciapsi.com.br',
|
||||
'email',
|
||||
'{"sub": "aaaaaaaa-0008-0008-0008-000000000008", "email": "editor@agenciapsi.com.br", "email_verified": true}'::jsonb,
|
||||
now(), now(), now()
|
||||
)
|
||||
ON CONFLICT (provider, provider_id) DO NOTHING;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 4. Profiles
|
||||
-- Supervisor → tenant_member (papel no tenant via tenant_members.role)
|
||||
-- Editor → tenant_member + platform_roles = '{editor}'
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO public.profiles (id, role, account_type, full_name, platform_roles)
|
||||
VALUES
|
||||
(
|
||||
'aaaaaaaa-0007-0007-0007-000000000007',
|
||||
'tenant_member',
|
||||
'therapist',
|
||||
'Carlos Supervisor',
|
||||
'{}'
|
||||
),
|
||||
(
|
||||
'aaaaaaaa-0008-0008-0008-000000000008',
|
||||
'tenant_member',
|
||||
'therapist',
|
||||
'Diana Editora',
|
||||
'{editor}' -- permissão de plataforma: acesso à área do editor
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
role = EXCLUDED.role,
|
||||
account_type = EXCLUDED.account_type,
|
||||
full_name = EXCLUDED.full_name,
|
||||
platform_roles = EXCLUDED.platform_roles;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 5. Vincula Supervisor à Clínica 3 (Full) com role 'supervisor'
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||
VALUES (
|
||||
'bbbbbbbb-0005-0005-0005-000000000005', -- Clínica Bem Estar (Full)
|
||||
'aaaaaaaa-0007-0007-0007-000000000007', -- Carlos Supervisor
|
||||
'supervisor',
|
||||
'active',
|
||||
now()
|
||||
)
|
||||
ON CONFLICT (tenant_id, user_id) DO UPDATE SET
|
||||
role = EXCLUDED.role,
|
||||
status = EXCLUDED.status;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 6. Vincula Editor à Clínica 3 como terapeuta
|
||||
-- (contexto de tenant para o editor poder usar /therapist também,
|
||||
-- se necessário. O papel de editor vem de platform_roles.)
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||
VALUES (
|
||||
'bbbbbbbb-0005-0005-0005-000000000005', -- Clínica Bem Estar (Full)
|
||||
'aaaaaaaa-0008-0008-0008-000000000008', -- Diana Editora
|
||||
'therapist',
|
||||
'active',
|
||||
now()
|
||||
)
|
||||
ON CONFLICT (tenant_id, user_id) DO UPDATE SET
|
||||
role = EXCLUDED.role,
|
||||
status = EXCLUDED.status;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 7. Confirma
|
||||
-- ============================================================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '✅ Seed 002 aplicado com sucesso.';
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE ' Migration aplicada:';
|
||||
RAISE NOTICE ' → profiles.platform_roles text[] adicionada (se não existia)';
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE ' Usuários criados:';
|
||||
RAISE NOTICE ' supervisor@agenciapsi.com.br → supervisor da Clínica Bem Estar (Full)';
|
||||
RAISE NOTICE ' editor@agenciapsi.com.br → editor de conteúdo (platform_roles = {editor})';
|
||||
RAISE NOTICE ' Senha de todos: Teste@123';
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMIT;
|
||||
283
DBS/2026-03-11/Novo-DB/seed_003.sql
Normal file
283
DBS/2026-03-11/Novo-DB/seed_003.sql
Normal file
@@ -0,0 +1,283 @@
|
||||
-- =============================================================================
|
||||
-- SEED 003 — Terapeuta 2, Terapeuta 3 e Secretária
|
||||
-- =============================================================================
|
||||
-- Execute APÓS seed_001.sql (e seed_002.sql se quiser todos os seeds)
|
||||
-- Requer: pgcrypto (já ativo no Supabase)
|
||||
--
|
||||
-- Cria os seguintes usuários de teste:
|
||||
--
|
||||
-- therapist2@agenciapsi.com.br senha: Teste@123 → terapeuta 2 (tenant próprio + Clínica 3)
|
||||
-- therapist3@agenciapsi.com.br senha: Teste@123 → terapeuta 3 (tenant próprio + Clínica 3)
|
||||
-- secretary@agenciapsi.com.br senha: Teste@123 → clinic_admin na Clínica 2 (Mente Sã)
|
||||
--
|
||||
-- UUIDs reservados:
|
||||
-- Terapeuta 2 → aaaaaaaa-0009-0009-0009-000000000009
|
||||
-- Terapeuta 3 → aaaaaaaa-0010-0010-0010-000000000010
|
||||
-- Secretária → aaaaaaaa-0011-0011-0011-000000000011
|
||||
-- Tenant Terapeuta 2 → bbbbbbbb-0009-0009-0009-000000000009
|
||||
-- Tenant Terapeuta 3 → bbbbbbbb-0010-0010-0010-000000000010
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ============================================================
|
||||
-- 1. Remove seeds anteriores (idempotente)
|
||||
-- ============================================================
|
||||
|
||||
DELETE FROM auth.users
|
||||
WHERE email IN (
|
||||
'therapist2@agenciapsi.com.br',
|
||||
'therapist3@agenciapsi.com.br',
|
||||
'secretary@agenciapsi.com.br'
|
||||
);
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 2. Cria usuários no auth.users
|
||||
-- ⚠️ confirmed_at é coluna gerada — NÃO incluir na lista
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO auth.users (
|
||||
instance_id,
|
||||
id,
|
||||
email,
|
||||
encrypted_password,
|
||||
email_confirmed_at,
|
||||
created_at,
|
||||
updated_at,
|
||||
raw_user_meta_data,
|
||||
raw_app_meta_data,
|
||||
role,
|
||||
aud,
|
||||
is_sso_user,
|
||||
is_anonymous,
|
||||
confirmation_token,
|
||||
recovery_token,
|
||||
email_change_token_new,
|
||||
email_change_token_current,
|
||||
email_change
|
||||
)
|
||||
VALUES
|
||||
-- Terapeuta 2
|
||||
(
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'aaaaaaaa-0009-0009-0009-000000000009',
|
||||
'therapist2@agenciapsi.com.br',
|
||||
crypt('Teste@123', gen_salt('bf')),
|
||||
now(), now(), now(),
|
||||
'{"name": "Eva Terapeuta"}'::jsonb,
|
||||
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||
'authenticated', 'authenticated', false, false, '', '', '', '', ''
|
||||
),
|
||||
-- Terapeuta 3
|
||||
(
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'aaaaaaaa-0010-0010-0010-000000000010',
|
||||
'therapist3@agenciapsi.com.br',
|
||||
crypt('Teste@123', gen_salt('bf')),
|
||||
now(), now(), now(),
|
||||
'{"name": "Felipe Terapeuta"}'::jsonb,
|
||||
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||
'authenticated', 'authenticated', false, false, '', '', '', '', ''
|
||||
),
|
||||
-- Secretária
|
||||
(
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'aaaaaaaa-0011-0011-0011-000000000011',
|
||||
'secretary@agenciapsi.com.br',
|
||||
crypt('Teste@123', gen_salt('bf')),
|
||||
now(), now(), now(),
|
||||
'{"name": "Gabriela Secretária"}'::jsonb,
|
||||
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||
'authenticated', 'authenticated', false, false, '', '', '', '', ''
|
||||
);
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 3. auth.identities (obrigatório para GoTrue reconhecer login)
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO auth.identities (id, user_id, provider_id, provider, identity_data, created_at, updated_at, last_sign_in_at)
|
||||
VALUES
|
||||
(
|
||||
gen_random_uuid(),
|
||||
'aaaaaaaa-0009-0009-0009-000000000009',
|
||||
'therapist2@agenciapsi.com.br',
|
||||
'email',
|
||||
'{"sub": "aaaaaaaa-0009-0009-0009-000000000009", "email": "therapist2@agenciapsi.com.br", "email_verified": true}'::jsonb,
|
||||
now(), now(), now()
|
||||
),
|
||||
(
|
||||
gen_random_uuid(),
|
||||
'aaaaaaaa-0010-0010-0010-000000000010',
|
||||
'therapist3@agenciapsi.com.br',
|
||||
'email',
|
||||
'{"sub": "aaaaaaaa-0010-0010-0010-000000000010", "email": "therapist3@agenciapsi.com.br", "email_verified": true}'::jsonb,
|
||||
now(), now(), now()
|
||||
),
|
||||
(
|
||||
gen_random_uuid(),
|
||||
'aaaaaaaa-0011-0011-0011-000000000011',
|
||||
'secretary@agenciapsi.com.br',
|
||||
'email',
|
||||
'{"sub": "aaaaaaaa-0011-0011-0011-000000000011", "email": "secretary@agenciapsi.com.br", "email_verified": true}'::jsonb,
|
||||
now(), now(), now()
|
||||
)
|
||||
ON CONFLICT (provider, provider_id) DO NOTHING;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 4. Profiles
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO public.profiles (id, role, account_type, full_name)
|
||||
VALUES
|
||||
(
|
||||
'aaaaaaaa-0009-0009-0009-000000000009',
|
||||
'tenant_member',
|
||||
'therapist',
|
||||
'Eva Terapeuta'
|
||||
),
|
||||
(
|
||||
'aaaaaaaa-0010-0010-0010-000000000010',
|
||||
'tenant_member',
|
||||
'therapist',
|
||||
'Felipe Terapeuta'
|
||||
),
|
||||
(
|
||||
'aaaaaaaa-0011-0011-0011-000000000011',
|
||||
'tenant_member',
|
||||
'therapist',
|
||||
'Gabriela Secretária'
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
role = EXCLUDED.role,
|
||||
account_type = EXCLUDED.account_type,
|
||||
full_name = EXCLUDED.full_name;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 5. Tenants pessoais dos Terapeutas 2 e 3
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO public.tenants (id, name, kind, created_at)
|
||||
VALUES
|
||||
('bbbbbbbb-0009-0009-0009-000000000009', 'Eva Terapeuta', 'therapist', now()),
|
||||
('bbbbbbbb-0010-0010-0010-000000000010', 'Felipe Terapeuta', 'therapist', now())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Terapeuta 2 → tenant_admin do próprio tenant
|
||||
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||
VALUES (
|
||||
'bbbbbbbb-0009-0009-0009-000000000009',
|
||||
'aaaaaaaa-0009-0009-0009-000000000009',
|
||||
'tenant_admin', 'active', now()
|
||||
)
|
||||
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||
|
||||
-- Terapeuta 3 → tenant_admin do próprio tenant
|
||||
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||
VALUES (
|
||||
'bbbbbbbb-0010-0010-0010-000000000010',
|
||||
'aaaaaaaa-0010-0010-0010-000000000010',
|
||||
'tenant_admin', 'active', now()
|
||||
)
|
||||
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 6. Vincula Terapeutas 2 e 3 à Clínica 3 — Full
|
||||
-- (mesmo padrão de terapeuta@agenciapsi.com.br no seed_001)
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||
VALUES
|
||||
(
|
||||
'bbbbbbbb-0005-0005-0005-000000000005', -- Clínica Bem Estar (Full)
|
||||
'aaaaaaaa-0009-0009-0009-000000000009', -- Eva Terapeuta
|
||||
'therapist', 'active', now()
|
||||
),
|
||||
(
|
||||
'bbbbbbbb-0005-0005-0005-000000000005', -- Clínica Bem Estar (Full)
|
||||
'aaaaaaaa-0010-0010-0010-000000000010', -- Felipe Terapeuta
|
||||
'therapist', 'active', now()
|
||||
)
|
||||
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 7. Vincula Secretária à Clínica 2 (Recepção) como clinic_admin
|
||||
-- A secretária gerencia a recepção/agenda da clínica.
|
||||
-- Acessa a área /admin com o mesmo contexto de clinic_admin.
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||
VALUES (
|
||||
'bbbbbbbb-0004-0004-0004-000000000004', -- Clínica Mente Sã (Recepção)
|
||||
'aaaaaaaa-0011-0011-0011-000000000011', -- Gabriela Secretária
|
||||
'clinic_admin', 'active', now()
|
||||
)
|
||||
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 8. Subscriptions
|
||||
-- Terapeutas 2 e 3 → therapist_free (escopo: user_id)
|
||||
-- Secretária → sem assinatura própria (usa o plano da Clínica 2)
|
||||
-- ============================================================
|
||||
|
||||
-- Terapeuta 2 → therapist_free
|
||||
INSERT INTO public.subscriptions (
|
||||
user_id, plan_id, plan_key, status, interval,
|
||||
current_period_start, current_period_end,
|
||||
source, started_at, activated_at
|
||||
)
|
||||
SELECT
|
||||
'aaaaaaaa-0009-0009-0009-000000000009',
|
||||
p.id, p.key, 'active', 'month',
|
||||
now(), now() + interval '30 days',
|
||||
'seed', now(), now()
|
||||
FROM public.plans p WHERE p.key = 'therapist_free'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM public.subscriptions s
|
||||
WHERE s.user_id = 'aaaaaaaa-0009-0009-0009-000000000009' AND s.status = 'active'
|
||||
);
|
||||
|
||||
-- Terapeuta 3 → therapist_free
|
||||
INSERT INTO public.subscriptions (
|
||||
user_id, plan_id, plan_key, status, interval,
|
||||
current_period_start, current_period_end,
|
||||
source, started_at, activated_at
|
||||
)
|
||||
SELECT
|
||||
'aaaaaaaa-0010-0010-0010-000000000010',
|
||||
p.id, p.key, 'active', 'month',
|
||||
now(), now() + interval '30 days',
|
||||
'seed', now(), now()
|
||||
FROM public.plans p WHERE p.key = 'therapist_free'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM public.subscriptions s
|
||||
WHERE s.user_id = 'aaaaaaaa-0010-0010-0010-000000000010' AND s.status = 'active'
|
||||
);
|
||||
|
||||
-- Nota: a Secretária não tem assinatura própria.
|
||||
-- O acesso vem do plano da Clínica 2 (tenant_id = bbbbbbbb-0004-0004-0004-000000000004).
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 9. Confirma
|
||||
-- ============================================================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '✅ Seed 003 aplicado com sucesso.';
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE ' Usuários criados:';
|
||||
RAISE NOTICE ' therapist2@agenciapsi.com.br → tenant próprio (bbbbbbbb-0009) + Clínica 3 como therapist';
|
||||
RAISE NOTICE ' therapist3@agenciapsi.com.br → tenant próprio (bbbbbbbb-0010) + Clínica 3 como therapist';
|
||||
RAISE NOTICE ' secretary@agenciapsi.com.br → clinic_admin na Clínica 2 Mente Sã (bbbbbbbb-0004)';
|
||||
RAISE NOTICE ' Senha de todos: Teste@123';
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMIT;
|
||||
36
DBS/2026-03-11/migrations/agendador_check_email.sql
Normal file
36
DBS/2026-03-11/migrations/agendador_check_email.sql
Normal file
@@ -0,0 +1,36 @@
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- agendador_check_email
|
||||
-- Verifica se um e-mail já possui solicitação anterior para este agendador
|
||||
-- SECURITY DEFINER → anon pode chamar sem burlar RLS diretamente
|
||||
-- Execute no Supabase SQL Editor
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.agendador_check_email(
|
||||
p_slug text,
|
||||
p_email text
|
||||
)
|
||||
RETURNS boolean
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_owner_id uuid;
|
||||
BEGIN
|
||||
SELECT c.owner_id INTO v_owner_id
|
||||
FROM public.agendador_configuracoes c
|
||||
WHERE c.link_slug = p_slug AND c.ativo = true
|
||||
LIMIT 1;
|
||||
|
||||
IF v_owner_id IS NULL THEN RETURN false; END IF;
|
||||
|
||||
RETURN EXISTS (
|
||||
SELECT 1 FROM public.agendador_solicitacoes s
|
||||
WHERE s.owner_id = v_owner_id
|
||||
AND lower(s.paciente_email) = lower(trim(p_email))
|
||||
LIMIT 1
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.agendador_check_email(text, text) TO anon, authenticated;
|
||||
62
DBS/2026-03-11/migrations/agendador_features.sql
Normal file
62
DBS/2026-03-11/migrations/agendador_features.sql
Normal file
@@ -0,0 +1,62 @@
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- Feature keys do Agendador Online
|
||||
-- Execute no Supabase SQL Editor
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- ── 1. Inserir as features ──────────────────────────────────────────────────
|
||||
INSERT INTO public.features (key, name, descricao)
|
||||
VALUES
|
||||
(
|
||||
'agendador.online',
|
||||
'Agendador Online',
|
||||
'Permite que pacientes solicitem agendamentos via link público. Inclui aprovação manual ou automática, controle de horários e notificações.'
|
||||
),
|
||||
(
|
||||
'agendador.link_personalizado',
|
||||
'Link Personalizado do Agendador',
|
||||
'Permite que o profissional escolha um slug de URL próprio para o agendador (ex: /agendar/dra-ana-silva) em vez de um link gerado automaticamente.'
|
||||
)
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET name = EXCLUDED.name,
|
||||
descricao = EXCLUDED.descricao;
|
||||
|
||||
-- ── 2. Vincular aos planos ──────────────────────────────────────────────────
|
||||
-- ATENÇÃO: ajuste os filtros de plan key/name conforme seus planos reais.
|
||||
-- Exemplo: agendador.online disponível para planos PRO e acima.
|
||||
-- agendador.link_personalizado apenas para planos Elite/Superior.
|
||||
|
||||
-- agendador.online → todos os planos com target 'therapist' ou 'clinic'
|
||||
-- (Adapte o WHERE conforme necessário)
|
||||
INSERT INTO public.plan_features (plan_id, feature_id, enabled)
|
||||
SELECT
|
||||
p.id,
|
||||
f.id,
|
||||
true
|
||||
FROM public.plans p
|
||||
CROSS JOIN public.features f
|
||||
WHERE f.key = 'agendador.online'
|
||||
AND p.is_active = true
|
||||
-- Comente a linha abaixo para liberar para TODOS os planos:
|
||||
-- AND p.key IN ('pro', 'elite', 'clinic_pro', 'clinic_elite')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- agendador.link_personalizado → apenas planos superiores
|
||||
-- Deixe comentado e adicione manualmente quando definir os planos:
|
||||
-- INSERT INTO public.plan_features (plan_id, feature_id, enabled)
|
||||
-- SELECT p.id, f.id, true
|
||||
-- FROM public.plans p
|
||||
-- CROSS JOIN public.features f
|
||||
-- WHERE f.key = 'agendador.link_personalizado'
|
||||
-- AND p.key IN ('elite', 'clinic_elite', 'pro_plus')
|
||||
-- ON CONFLICT DO NOTHING;
|
||||
|
||||
-- ── 3. Verificação ─────────────────────────────────────────────────────────
|
||||
SELECT
|
||||
f.key,
|
||||
f.name,
|
||||
COUNT(pf.plan_id) AS planos_vinculados
|
||||
FROM public.features f
|
||||
LEFT JOIN public.plan_features pf ON pf.feature_id = f.id AND pf.enabled = true
|
||||
WHERE f.key IN ('agendador.online', 'agendador.link_personalizado')
|
||||
GROUP BY f.key, f.name
|
||||
ORDER BY f.key;
|
||||
221
DBS/2026-03-11/migrations/agendador_fix_slots.sql
Normal file
221
DBS/2026-03-11/migrations/agendador_fix_slots.sql
Normal file
@@ -0,0 +1,221 @@
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- FIX: agendador_slots_disponiveis + agendador_dias_disponiveis
|
||||
-- Usa agenda_online_slots como fonte de slots
|
||||
-- Cruzamento com: agenda_eventos, recurrence_rules/exceptions, agendador_solicitacoes
|
||||
-- Execute no Supabase SQL Editor
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.agendador_slots_disponiveis(
|
||||
p_slug text,
|
||||
p_data date
|
||||
)
|
||||
RETURNS TABLE (hora time, disponivel boolean)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_owner_id uuid;
|
||||
v_duracao int;
|
||||
v_antecedencia int;
|
||||
v_agora timestamptz;
|
||||
v_db_dow int;
|
||||
v_slot time;
|
||||
v_slot_fim time;
|
||||
v_slot_ts timestamptz;
|
||||
v_ocupado boolean;
|
||||
-- loop de recorrências
|
||||
v_rule RECORD;
|
||||
v_rule_start_dow int;
|
||||
v_first_occ date;
|
||||
v_day_diff int;
|
||||
v_ex_type text;
|
||||
BEGIN
|
||||
SELECT c.owner_id, c.duracao_sessao_min, c.antecedencia_minima_horas
|
||||
INTO v_owner_id, v_duracao, v_antecedencia
|
||||
FROM public.agendador_configuracoes c
|
||||
WHERE c.link_slug = p_slug AND c.ativo = true
|
||||
LIMIT 1;
|
||||
|
||||
IF v_owner_id IS NULL THEN RETURN; END IF;
|
||||
|
||||
v_agora := now();
|
||||
v_db_dow := extract(dow from p_data::timestamp)::int;
|
||||
|
||||
FOR v_slot IN
|
||||
SELECT s.time
|
||||
FROM public.agenda_online_slots s
|
||||
WHERE s.owner_id = v_owner_id
|
||||
AND s.weekday = v_db_dow
|
||||
AND s.enabled = true
|
||||
ORDER BY s.time
|
||||
LOOP
|
||||
v_slot_fim := v_slot + (v_duracao || ' minutes')::interval;
|
||||
v_ocupado := false;
|
||||
|
||||
-- ── Antecedência mínima ──────────────────────────────────────────────────
|
||||
v_slot_ts := (p_data::text || ' ' || v_slot::text)::timestamp
|
||||
AT TIME ZONE 'America/Sao_Paulo';
|
||||
IF v_slot_ts < v_agora + (v_antecedencia || ' hours')::interval THEN
|
||||
v_ocupado := true;
|
||||
END IF;
|
||||
|
||||
-- ── Eventos avulsos internos (agenda_eventos) ────────────────────────────
|
||||
IF NOT v_ocupado THEN
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM public.agenda_eventos e
|
||||
WHERE e.owner_id = v_owner_id
|
||||
AND e.status::text NOT IN ('cancelado', 'faltou')
|
||||
AND (e.inicio_em AT TIME ZONE 'America/Sao_Paulo')::date = p_data
|
||||
AND (e.inicio_em AT TIME ZONE 'America/Sao_Paulo')::time < v_slot_fim
|
||||
AND (e.fim_em AT TIME ZONE 'America/Sao_Paulo')::time > v_slot
|
||||
) INTO v_ocupado;
|
||||
END IF;
|
||||
|
||||
-- ── Recorrências ativas (recurrence_rules) ───────────────────────────────
|
||||
-- Loop explícito para evitar erros de tipo no cálculo do ciclo semanal
|
||||
IF NOT v_ocupado THEN
|
||||
FOR v_rule IN
|
||||
SELECT
|
||||
r.id,
|
||||
r.start_date::date AS start_date,
|
||||
r.end_date::date AS end_date,
|
||||
r.start_time::time AS start_time,
|
||||
r.end_time::time AS end_time,
|
||||
COALESCE(r.interval, 1)::int AS interval
|
||||
FROM public.recurrence_rules r
|
||||
WHERE r.owner_id = v_owner_id
|
||||
AND r.status = 'ativo'
|
||||
AND p_data >= r.start_date::date
|
||||
AND (r.end_date IS NULL OR p_data <= r.end_date::date)
|
||||
AND v_db_dow = ANY(r.weekdays)
|
||||
AND r.start_time::time < v_slot_fim
|
||||
AND r.end_time::time > v_slot
|
||||
LOOP
|
||||
-- Calcula a primeira ocorrência do dia-da-semana a partir do start_date
|
||||
v_rule_start_dow := extract(dow from v_rule.start_date)::int;
|
||||
v_first_occ := v_rule.start_date
|
||||
+ (((v_db_dow - v_rule_start_dow + 7) % 7))::int;
|
||||
v_day_diff := (p_data - v_first_occ)::int;
|
||||
|
||||
-- Ocorrência válida: diff >= 0 e divisível pelo ciclo semanal
|
||||
IF v_day_diff >= 0 AND v_day_diff % (7 * v_rule.interval) = 0 THEN
|
||||
|
||||
-- Verifica se há exceção para esta data
|
||||
v_ex_type := NULL;
|
||||
SELECT ex.type INTO v_ex_type
|
||||
FROM public.recurrence_exceptions ex
|
||||
WHERE ex.recurrence_id = v_rule.id
|
||||
AND ex.original_date = p_data
|
||||
LIMIT 1;
|
||||
|
||||
-- Sem exceção, ou exceção que não cancela → bloqueia o slot
|
||||
IF v_ex_type IS NULL OR v_ex_type NOT IN (
|
||||
'cancel_session', 'patient_missed',
|
||||
'therapist_canceled', 'holiday_block',
|
||||
'reschedule_session'
|
||||
) THEN
|
||||
v_ocupado := true;
|
||||
EXIT; -- já basta uma regra que conflite
|
||||
END IF;
|
||||
|
||||
END IF;
|
||||
END LOOP;
|
||||
END IF;
|
||||
|
||||
-- ── Recorrências remarcadas para este dia (reschedule → new_date = p_data) ─
|
||||
IF NOT v_ocupado THEN
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM public.recurrence_exceptions ex
|
||||
JOIN public.recurrence_rules r ON r.id = ex.recurrence_id
|
||||
WHERE r.owner_id = v_owner_id
|
||||
AND r.status = 'ativo'
|
||||
AND ex.type = 'reschedule_session'
|
||||
AND ex.new_date = p_data
|
||||
AND COALESCE(ex.new_start_time, r.start_time)::time < v_slot_fim
|
||||
AND COALESCE(ex.new_end_time, r.end_time)::time > v_slot
|
||||
) INTO v_ocupado;
|
||||
END IF;
|
||||
|
||||
-- ── Solicitações públicas pendentes ──────────────────────────────────────
|
||||
IF NOT v_ocupado THEN
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM public.agendador_solicitacoes sol
|
||||
WHERE sol.owner_id = v_owner_id
|
||||
AND sol.status = 'pendente'
|
||||
AND sol.data_solicitada = p_data
|
||||
AND sol.hora_solicitada = v_slot
|
||||
AND (sol.reservado_ate IS NULL OR sol.reservado_ate > v_agora)
|
||||
) INTO v_ocupado;
|
||||
END IF;
|
||||
|
||||
hora := v_slot;
|
||||
disponivel := NOT v_ocupado;
|
||||
RETURN NEXT;
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.agendador_slots_disponiveis(text, date) TO anon, authenticated;
|
||||
|
||||
|
||||
-- ── agendador_dias_disponiveis ───────────────────────────────────────────────
|
||||
CREATE OR REPLACE FUNCTION public.agendador_dias_disponiveis(
|
||||
p_slug text,
|
||||
p_ano int,
|
||||
p_mes int
|
||||
)
|
||||
RETURNS TABLE (data date, tem_slots boolean)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_owner_id uuid;
|
||||
v_antecedencia int;
|
||||
v_agora timestamptz;
|
||||
v_data date;
|
||||
v_data_inicio date;
|
||||
v_data_fim date;
|
||||
v_db_dow int;
|
||||
v_tem_slot boolean;
|
||||
BEGIN
|
||||
SELECT c.owner_id, c.antecedencia_minima_horas
|
||||
INTO v_owner_id, v_antecedencia
|
||||
FROM public.agendador_configuracoes c
|
||||
WHERE c.link_slug = p_slug AND c.ativo = true
|
||||
LIMIT 1;
|
||||
|
||||
IF v_owner_id IS NULL THEN RETURN; END IF;
|
||||
|
||||
v_agora := now();
|
||||
v_data_inicio := make_date(p_ano, p_mes, 1);
|
||||
v_data_fim := (v_data_inicio + interval '1 month' - interval '1 day')::date;
|
||||
|
||||
v_data := v_data_inicio;
|
||||
WHILE v_data <= v_data_fim LOOP
|
||||
v_db_dow := extract(dow from v_data::timestamp)::int;
|
||||
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM public.agenda_online_slots s
|
||||
WHERE s.owner_id = v_owner_id
|
||||
AND s.weekday = v_db_dow
|
||||
AND s.enabled = true
|
||||
AND (v_data::text || ' ' || s.time::text)::timestamp
|
||||
AT TIME ZONE 'America/Sao_Paulo'
|
||||
>= v_agora + (v_antecedencia || ' hours')::interval
|
||||
) INTO v_tem_slot;
|
||||
|
||||
IF v_tem_slot THEN
|
||||
data := v_data;
|
||||
tem_slots := true;
|
||||
RETURN NEXT;
|
||||
END IF;
|
||||
|
||||
v_data := v_data + 1;
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.agendador_dias_disponiveis(text, int, int) TO anon, authenticated;
|
||||
170
DBS/2026-03-11/migrations/agendador_online.sql
Normal file
170
DBS/2026-03-11/migrations/agendador_online.sql
Normal file
@@ -0,0 +1,170 @@
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- Agendador Online — tabelas de configuração e solicitações
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- ── 1. agendador_configuracoes ──────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS "public"."agendador_configuracoes" (
|
||||
"owner_id" "uuid" NOT NULL,
|
||||
"tenant_id" "uuid",
|
||||
|
||||
-- PRO / Ativação
|
||||
"ativo" boolean DEFAULT false NOT NULL,
|
||||
"link_slug" "text",
|
||||
|
||||
-- Identidade Visual
|
||||
"imagem_fundo_url" "text",
|
||||
"imagem_header_url" "text",
|
||||
"logomarca_url" "text",
|
||||
"cor_primaria" "text" DEFAULT '#4b6bff',
|
||||
|
||||
-- Perfil Público
|
||||
"nome_exibicao" "text",
|
||||
"endereco" "text",
|
||||
"botao_como_chegar_ativo" boolean DEFAULT true NOT NULL,
|
||||
"maps_url" "text",
|
||||
|
||||
-- Fluxo de Agendamento
|
||||
"modo_aprovacao" "text" DEFAULT 'aprovacao' NOT NULL,
|
||||
"modalidade" "text" DEFAULT 'presencial' NOT NULL,
|
||||
"tipos_habilitados" "jsonb" DEFAULT '["primeira","retorno"]'::jsonb NOT NULL,
|
||||
"duracao_sessao_min" integer DEFAULT 50 NOT NULL,
|
||||
"antecedencia_minima_horas" integer DEFAULT 24 NOT NULL,
|
||||
"prazo_resposta_horas" integer DEFAULT 2 NOT NULL,
|
||||
"reserva_horas" integer DEFAULT 2 NOT NULL,
|
||||
|
||||
-- Pagamento
|
||||
"pagamento_obrigatorio" boolean DEFAULT false NOT NULL,
|
||||
"pix_chave" "text",
|
||||
"pix_countdown_minutos" integer DEFAULT 20 NOT NULL,
|
||||
|
||||
-- Triagem & Conformidade
|
||||
"triagem_motivo" boolean DEFAULT true NOT NULL,
|
||||
"triagem_como_conheceu" boolean DEFAULT false NOT NULL,
|
||||
"verificacao_email" boolean DEFAULT false NOT NULL,
|
||||
"exigir_aceite_lgpd" boolean DEFAULT true NOT NULL,
|
||||
|
||||
-- Textos
|
||||
"mensagem_boas_vindas" "text",
|
||||
"texto_como_se_preparar" "text",
|
||||
"texto_termos_lgpd" "text",
|
||||
|
||||
-- Timestamps
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
|
||||
CONSTRAINT "agendador_configuracoes_pkey" PRIMARY KEY ("owner_id"),
|
||||
CONSTRAINT "agendador_configuracoes_owner_fk"
|
||||
FOREIGN KEY ("owner_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE,
|
||||
CONSTRAINT "agendador_configuracoes_tenant_fk"
|
||||
FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE,
|
||||
CONSTRAINT "agendador_configuracoes_modo_check"
|
||||
CHECK ("modo_aprovacao" = ANY (ARRAY['automatico','aprovacao'])),
|
||||
CONSTRAINT "agendador_configuracoes_modalidade_check"
|
||||
CHECK ("modalidade" = ANY (ARRAY['presencial','online','ambos'])),
|
||||
CONSTRAINT "agendador_configuracoes_duracao_check"
|
||||
CHECK ("duracao_sessao_min" >= 10 AND "duracao_sessao_min" <= 240),
|
||||
CONSTRAINT "agendador_configuracoes_antecedencia_check"
|
||||
CHECK ("antecedencia_minima_horas" >= 0 AND "antecedencia_minima_horas" <= 720),
|
||||
CONSTRAINT "agendador_configuracoes_reserva_check"
|
||||
CHECK ("reserva_horas" >= 1 AND "reserva_horas" <= 48),
|
||||
CONSTRAINT "agendador_configuracoes_pix_countdown_check"
|
||||
CHECK ("pix_countdown_minutos" >= 5 AND "pix_countdown_minutos" <= 120),
|
||||
CONSTRAINT "agendador_configuracoes_prazo_check"
|
||||
CHECK ("prazo_resposta_horas" >= 1 AND "prazo_resposta_horas" <= 72)
|
||||
);
|
||||
|
||||
ALTER TABLE "public"."agendador_configuracoes" ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS "agendador_cfg_select" ON "public"."agendador_configuracoes";
|
||||
CREATE POLICY "agendador_cfg_select" ON "public"."agendador_configuracoes"
|
||||
FOR SELECT USING (auth.uid() = owner_id);
|
||||
|
||||
DROP POLICY IF EXISTS "agendador_cfg_write" ON "public"."agendador_configuracoes";
|
||||
CREATE POLICY "agendador_cfg_write" ON "public"."agendador_configuracoes"
|
||||
USING (auth.uid() = owner_id)
|
||||
WITH CHECK (auth.uid() = owner_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "agendador_cfg_tenant_idx"
|
||||
ON "public"."agendador_configuracoes" ("tenant_id");
|
||||
|
||||
-- ── 2. agendador_solicitacoes ───────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS "public"."agendador_solicitacoes" (
|
||||
"id" "uuid" DEFAULT gen_random_uuid() NOT NULL,
|
||||
"owner_id" "uuid" NOT NULL,
|
||||
"tenant_id" "uuid",
|
||||
|
||||
-- Dados do paciente
|
||||
"paciente_nome" "text" NOT NULL,
|
||||
"paciente_sobrenome" "text",
|
||||
"paciente_email" "text" NOT NULL,
|
||||
"paciente_celular" "text",
|
||||
"paciente_cpf" "text",
|
||||
|
||||
-- Agendamento solicitado
|
||||
"tipo" "text" NOT NULL,
|
||||
"modalidade" "text" NOT NULL,
|
||||
"data_solicitada" date NOT NULL,
|
||||
"hora_solicitada" time NOT NULL,
|
||||
|
||||
-- Reserva temporária
|
||||
"reservado_ate" timestamp with time zone,
|
||||
|
||||
-- Triagem
|
||||
"motivo" "text",
|
||||
"como_conheceu" "text",
|
||||
|
||||
-- Pagamento
|
||||
"pix_status" "text" DEFAULT 'pendente',
|
||||
"pix_pago_em" timestamp with time zone,
|
||||
|
||||
-- Status geral
|
||||
"status" "text" DEFAULT 'pendente' NOT NULL,
|
||||
"recusado_motivo" "text",
|
||||
|
||||
-- Autorização
|
||||
"autorizado_em" timestamp with time zone,
|
||||
"autorizado_por" "uuid",
|
||||
|
||||
-- Vínculos internos
|
||||
"user_id" "uuid",
|
||||
"patient_id" "uuid",
|
||||
"evento_id" "uuid",
|
||||
|
||||
-- Timestamps
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
|
||||
CONSTRAINT "agendador_solicitacoes_pkey" PRIMARY KEY ("id"),
|
||||
CONSTRAINT "agendador_sol_owner_fk"
|
||||
FOREIGN KEY ("owner_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE,
|
||||
CONSTRAINT "agendador_sol_tenant_fk"
|
||||
FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE,
|
||||
CONSTRAINT "agendador_sol_status_check"
|
||||
CHECK ("status" = ANY (ARRAY['pendente','autorizado','recusado','expirado'])),
|
||||
CONSTRAINT "agendador_sol_tipo_check"
|
||||
CHECK ("tipo" = ANY (ARRAY['primeira','retorno','reagendar'])),
|
||||
CONSTRAINT "agendador_sol_modalidade_check"
|
||||
CHECK ("modalidade" = ANY (ARRAY['presencial','online'])),
|
||||
CONSTRAINT "agendador_sol_pix_check"
|
||||
CHECK ("pix_status" IS NULL OR "pix_status" = ANY (ARRAY['pendente','pago','expirado']))
|
||||
);
|
||||
|
||||
ALTER TABLE "public"."agendador_solicitacoes" ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS "agendador_sol_owner_select" ON "public"."agendador_solicitacoes";
|
||||
CREATE POLICY "agendador_sol_owner_select" ON "public"."agendador_solicitacoes"
|
||||
FOR SELECT USING (auth.uid() = owner_id);
|
||||
|
||||
DROP POLICY IF EXISTS "agendador_sol_owner_write" ON "public"."agendador_solicitacoes";
|
||||
CREATE POLICY "agendador_sol_owner_write" ON "public"."agendador_solicitacoes"
|
||||
USING (auth.uid() = owner_id)
|
||||
WITH CHECK (auth.uid() = owner_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "agendador_sol_owner_idx"
|
||||
ON "public"."agendador_solicitacoes" ("owner_id", "status");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "agendador_sol_tenant_idx"
|
||||
ON "public"."agendador_solicitacoes" ("tenant_id");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "agendador_sol_data_idx"
|
||||
ON "public"."agendador_solicitacoes" ("data_solicitada", "hora_solicitada");
|
||||
219
DBS/2026-03-11/migrations/agendador_publico.sql
Normal file
219
DBS/2026-03-11/migrations/agendador_publico.sql
Normal file
@@ -0,0 +1,219 @@
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- Agendador Online — acesso público (anon) + função de slots disponíveis
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- ── 1. Geração automática de slug ──────────────────────────────────────────
|
||||
-- Cria slug único de 8 chars quando o profissional ativa sem link_personalizado
|
||||
CREATE OR REPLACE FUNCTION public.agendador_gerar_slug()
|
||||
RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
v_slug text;
|
||||
v_exists boolean;
|
||||
BEGIN
|
||||
-- só gera se ativou e não tem slug ainda
|
||||
IF NEW.ativo = true AND (NEW.link_slug IS NULL OR NEW.link_slug = '') THEN
|
||||
LOOP
|
||||
v_slug := lower(substring(replace(gen_random_uuid()::text, '-', ''), 1, 8));
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM public.agendador_configuracoes
|
||||
WHERE link_slug = v_slug AND owner_id <> NEW.owner_id
|
||||
) INTO v_exists;
|
||||
EXIT WHEN NOT v_exists;
|
||||
END LOOP;
|
||||
NEW.link_slug := v_slug;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS agendador_slug_trigger ON public.agendador_configuracoes;
|
||||
CREATE TRIGGER agendador_slug_trigger
|
||||
BEFORE INSERT OR UPDATE ON public.agendador_configuracoes
|
||||
FOR EACH ROW EXECUTE FUNCTION public.agendador_gerar_slug();
|
||||
|
||||
-- ── 2. Políticas públicas (anon) ────────────────────────────────────────────
|
||||
|
||||
-- Leitura pública da config pelo slug (só ativo)
|
||||
DROP POLICY IF EXISTS "agendador_cfg_public_read" ON public.agendador_configuracoes;
|
||||
CREATE POLICY "agendador_cfg_public_read" ON public.agendador_configuracoes
|
||||
FOR SELECT TO anon
|
||||
USING (ativo = true AND link_slug IS NOT NULL);
|
||||
|
||||
-- Inserção pública de solicitações (qualquer pessoa pode solicitar)
|
||||
DROP POLICY IF EXISTS "agendador_sol_public_insert" ON public.agendador_solicitacoes;
|
||||
CREATE POLICY "agendador_sol_public_insert" ON public.agendador_solicitacoes
|
||||
FOR INSERT TO anon
|
||||
WITH CHECK (true);
|
||||
|
||||
-- Leitura da própria solicitação (pelo paciente logado)
|
||||
DROP POLICY IF EXISTS "agendador_sol_patient_read" ON public.agendador_solicitacoes;
|
||||
CREATE POLICY "agendador_sol_patient_read" ON public.agendador_solicitacoes
|
||||
FOR SELECT TO authenticated
|
||||
USING (auth.uid() = user_id OR auth.uid() = owner_id);
|
||||
|
||||
-- ── 3. Função: retorna slots disponíveis para uma data ──────────────────────
|
||||
-- Roda como SECURITY DEFINER (acessa agenda_regras e agenda_eventos sem RLS)
|
||||
CREATE OR REPLACE FUNCTION public.agendador_slots_disponiveis(
|
||||
p_slug text,
|
||||
p_data date
|
||||
)
|
||||
RETURNS TABLE (hora time, disponivel boolean)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_owner_id uuid;
|
||||
v_duracao int;
|
||||
v_reserva int;
|
||||
v_antecedencia int;
|
||||
v_dia_semana int; -- 0=dom..6=sab (JS) → convertemos
|
||||
v_db_dow int; -- 0=dom..6=sab no Postgres (extract dow)
|
||||
v_inicio time;
|
||||
v_fim time;
|
||||
v_slot time;
|
||||
v_slot_fim time;
|
||||
v_agora timestamptz;
|
||||
BEGIN
|
||||
-- carrega config do agendador
|
||||
SELECT
|
||||
c.owner_id,
|
||||
c.duracao_sessao_min,
|
||||
c.reserva_horas,
|
||||
c.antecedencia_minima_horas
|
||||
INTO v_owner_id, v_duracao, v_reserva, v_antecedencia
|
||||
FROM public.agendador_configuracoes c
|
||||
WHERE c.link_slug = p_slug AND c.ativo = true
|
||||
LIMIT 1;
|
||||
|
||||
IF v_owner_id IS NULL THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
v_agora := now();
|
||||
v_db_dow := extract(dow from p_data::timestamp)::int; -- 0=dom..6=sab
|
||||
|
||||
-- regra semanal para o dia da semana
|
||||
SELECT hora_inicio, hora_fim
|
||||
INTO v_inicio, v_fim
|
||||
FROM public.agenda_regras_semanais
|
||||
WHERE owner_id = v_owner_id
|
||||
AND dia_semana = v_db_dow
|
||||
AND ativo = true
|
||||
LIMIT 1;
|
||||
|
||||
IF v_inicio IS NULL THEN
|
||||
RETURN; -- profissional não atende nesse dia
|
||||
END IF;
|
||||
|
||||
-- itera slots de v_duracao em v_duracao dentro da jornada
|
||||
v_slot := v_inicio;
|
||||
WHILE v_slot + (v_duracao || ' minutes')::interval <= v_fim LOOP
|
||||
v_slot_fim := v_slot + (v_duracao || ' minutes')::interval;
|
||||
|
||||
-- bloco temporário para verificar conflitos
|
||||
DECLARE
|
||||
v_ocupado boolean := false;
|
||||
v_slot_ts timestamptz;
|
||||
BEGIN
|
||||
-- antecedência mínima (compara em horário de Brasília)
|
||||
v_slot_ts := (p_data::text || ' ' || v_slot::text)::timestamp AT TIME ZONE 'America/Sao_Paulo';
|
||||
|
||||
IF v_slot_ts < v_agora + (v_antecedencia || ' hours')::interval THEN
|
||||
v_ocupado := true;
|
||||
END IF;
|
||||
|
||||
-- conflito com eventos existentes na agenda
|
||||
IF NOT v_ocupado THEN
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM public.agenda_eventos
|
||||
WHERE owner_id = v_owner_id
|
||||
AND status::text NOT IN ('cancelado', 'faltou')
|
||||
AND inicio_em AT TIME ZONE 'America/Sao_Paulo' >= p_data::timestamp
|
||||
AND inicio_em AT TIME ZONE 'America/Sao_Paulo' < p_data::timestamp + interval '1 day'
|
||||
AND (inicio_em AT TIME ZONE 'America/Sao_Paulo')::time < v_slot_fim
|
||||
AND (fim_em AT TIME ZONE 'America/Sao_Paulo')::time > v_slot
|
||||
) INTO v_ocupado;
|
||||
END IF;
|
||||
|
||||
-- conflito com solicitações pendentes (reservadas)
|
||||
IF NOT v_ocupado THEN
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM public.agendador_solicitacoes
|
||||
WHERE owner_id = v_owner_id
|
||||
AND status = 'pendente'
|
||||
AND data_solicitada = p_data
|
||||
AND hora_solicitada = v_slot
|
||||
AND (reservado_ate IS NULL OR reservado_ate > v_agora)
|
||||
) INTO v_ocupado;
|
||||
END IF;
|
||||
|
||||
hora := v_slot;
|
||||
disponivel := NOT v_ocupado;
|
||||
RETURN NEXT;
|
||||
END;
|
||||
|
||||
v_slot := v_slot + (v_duracao || ' minutes')::interval;
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.agendador_slots_disponiveis(text, date) TO anon, authenticated;
|
||||
|
||||
-- ── 4. Função: retorna dias com disponibilidade no mês ─────────────────────
|
||||
CREATE OR REPLACE FUNCTION public.agendador_dias_disponiveis(
|
||||
p_slug text,
|
||||
p_ano int,
|
||||
p_mes int -- 1-12
|
||||
)
|
||||
RETURNS TABLE (data date, tem_slots boolean)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_owner_id uuid;
|
||||
v_antecedencia int;
|
||||
v_data date;
|
||||
v_data_inicio date;
|
||||
v_data_fim date;
|
||||
v_agora timestamptz;
|
||||
v_db_dow int;
|
||||
v_tem_regra boolean;
|
||||
BEGIN
|
||||
SELECT c.owner_id, c.antecedencia_minima_horas
|
||||
INTO v_owner_id, v_antecedencia
|
||||
FROM public.agendador_configuracoes c
|
||||
WHERE c.link_slug = p_slug AND c.ativo = true
|
||||
LIMIT 1;
|
||||
|
||||
IF v_owner_id IS NULL THEN RETURN; END IF;
|
||||
|
||||
v_agora := now();
|
||||
v_data_inicio := make_date(p_ano, p_mes, 1);
|
||||
v_data_fim := (v_data_inicio + interval '1 month' - interval '1 day')::date;
|
||||
|
||||
v_data := v_data_inicio;
|
||||
WHILE v_data <= v_data_fim LOOP
|
||||
-- não oferece dias no passado ou dentro da antecedência mínima
|
||||
IF v_data::timestamptz + '23:59:59'::interval > v_agora + (v_antecedencia || ' hours')::interval THEN
|
||||
v_db_dow := extract(dow from v_data::timestamp)::int;
|
||||
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM public.agenda_regras_semanais
|
||||
WHERE owner_id = v_owner_id AND dia_semana = v_db_dow AND ativo = true
|
||||
) INTO v_tem_regra;
|
||||
|
||||
IF v_tem_regra THEN
|
||||
data := v_data;
|
||||
tem_slots := true;
|
||||
RETURN NEXT;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
v_data := v_data + 1;
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.agendador_dias_disponiveis(text, int, int) TO anon, authenticated;
|
||||
19
DBS/2026-03-11/migrations/agendador_status_convertido.sql
Normal file
19
DBS/2026-03-11/migrations/agendador_status_convertido.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- FIX: adiciona status 'convertido' na constraint de agendador_solicitacoes
|
||||
-- e adiciona coluna motivo_recusa (alias amigável de recusado_motivo)
|
||||
-- Execute no Supabase SQL Editor
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- 1. Remove o CHECK existente e recria com os novos valores
|
||||
ALTER TABLE public.agendador_solicitacoes
|
||||
DROP CONSTRAINT IF EXISTS "agendador_sol_status_check";
|
||||
|
||||
ALTER TABLE public.agendador_solicitacoes
|
||||
ADD CONSTRAINT "agendador_sol_status_check"
|
||||
CHECK (status = ANY (ARRAY[
|
||||
'pendente',
|
||||
'autorizado',
|
||||
'recusado',
|
||||
'expirado',
|
||||
'convertido'
|
||||
]));
|
||||
56
DBS/2026-03-11/migrations/agendador_storage.sql
Normal file
56
DBS/2026-03-11/migrations/agendador_storage.sql
Normal file
@@ -0,0 +1,56 @@
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- Storage bucket para imagens do Agendador Online
|
||||
-- Execute no Supabase SQL Editor
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- ── 1. Criar o bucket ──────────────────────────────────────────────────────
|
||||
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
||||
VALUES (
|
||||
'agendador',
|
||||
'agendador',
|
||||
true, -- público (URLs diretas sem assinar)
|
||||
5242880, -- 5 MB
|
||||
ARRAY['image/jpeg','image/png','image/webp','image/gif']
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE
|
||||
SET public = true,
|
||||
file_size_limit = 5242880,
|
||||
allowed_mime_types = ARRAY['image/jpeg','image/png','image/webp','image/gif'];
|
||||
|
||||
-- ── 2. Políticas ───────────────────────────────────────────────────────────
|
||||
|
||||
-- Leitura pública (anon e authenticated)
|
||||
DROP POLICY IF EXISTS "agendador_storage_public_read" ON storage.objects;
|
||||
CREATE POLICY "agendador_storage_public_read"
|
||||
ON storage.objects FOR SELECT
|
||||
USING (bucket_id = 'agendador');
|
||||
|
||||
-- Upload: apenas o dono da pasta (owner_id é o primeiro segmento do path)
|
||||
DROP POLICY IF EXISTS "agendador_storage_owner_insert" ON storage.objects;
|
||||
CREATE POLICY "agendador_storage_owner_insert"
|
||||
ON storage.objects FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (
|
||||
bucket_id = 'agendador'
|
||||
AND (storage.foldername(name))[1] = auth.uid()::text
|
||||
);
|
||||
|
||||
-- Update/upsert pelo dono
|
||||
DROP POLICY IF EXISTS "agendador_storage_owner_update" ON storage.objects;
|
||||
CREATE POLICY "agendador_storage_owner_update"
|
||||
ON storage.objects FOR UPDATE
|
||||
TO authenticated
|
||||
USING (
|
||||
bucket_id = 'agendador'
|
||||
AND (storage.foldername(name))[1] = auth.uid()::text
|
||||
);
|
||||
|
||||
-- Delete pelo dono
|
||||
DROP POLICY IF EXISTS "agendador_storage_owner_delete" ON storage.objects;
|
||||
CREATE POLICY "agendador_storage_owner_delete"
|
||||
ON storage.objects FOR DELETE
|
||||
TO authenticated
|
||||
USING (
|
||||
bucket_id = 'agendador'
|
||||
AND (storage.foldername(name))[1] = auth.uid()::text
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Migration: remove session_start_offset_min from agenda_configuracoes
|
||||
-- This field is replaced by hora_inicio in agenda_regras_semanais (work schedule per day)
|
||||
-- The first session slot is now derived directly from hora_inicio of the work rule.
|
||||
|
||||
ALTER TABLE public.agenda_configuracoes
|
||||
DROP COLUMN IF EXISTS session_start_offset_min;
|
||||
BIN
DBS/2026-03-11/root/backup.sql
Normal file
BIN
DBS/2026-03-11/root/backup.sql
Normal file
Binary file not shown.
3195
DBS/2026-03-11/root/data_dump.sql
Normal file
3195
DBS/2026-03-11/root/data_dump.sql
Normal file
File diff suppressed because it is too large
Load Diff
10622
DBS/2026-03-11/root/full_dump.sql
Normal file
10622
DBS/2026-03-11/root/full_dump.sql
Normal file
File diff suppressed because it is too large
Load Diff
BIN
DBS/2026-03-11/root/schema.sql
Normal file
BIN
DBS/2026-03-11/root/schema.sql
Normal file
Binary file not shown.
110
DBS/2026-03-11/src-sql-arquivos/01_profiles.sql
Normal file
110
DBS/2026-03-11/src-sql-arquivos/01_profiles.sql
Normal file
@@ -0,0 +1,110 @@
|
||||
-- =========================================================
|
||||
-- Agência PSI — Profiles (v2) + Trigger + RLS
|
||||
-- - 1 profile por auth.users.id
|
||||
-- - role base (admin|therapist|patient)
|
||||
-- - pronto para evoluir p/ multi-tenant depois
|
||||
-- =========================================================
|
||||
|
||||
-- 0) Função padrão updated_at (se já existir, mantém)
|
||||
create or replace function public.set_updated_at()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
new.updated_at = now();
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- 1) Tabela profiles
|
||||
create table if not exists public.profiles (
|
||||
id uuid primary key, -- = auth.users.id
|
||||
email text,
|
||||
full_name text,
|
||||
avatar_url text,
|
||||
|
||||
role text not null default 'patient',
|
||||
status text not null default 'active',
|
||||
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
|
||||
constraint profiles_role_check check (role in ('admin','therapist','patient')),
|
||||
constraint profiles_status_check check (status in ('active','inactive','invited'))
|
||||
);
|
||||
|
||||
-- FK opcional (em Supabase costuma ser ok)
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1
|
||||
from pg_constraint
|
||||
where conname = 'profiles_id_fkey'
|
||||
) then
|
||||
alter table public.profiles
|
||||
add constraint profiles_id_fkey
|
||||
foreign key (id) references auth.users(id)
|
||||
on delete cascade;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- Índices úteis
|
||||
create index if not exists profiles_role_idx on public.profiles(role);
|
||||
create index if not exists profiles_status_idx on public.profiles(status);
|
||||
|
||||
-- 2) Trigger updated_at
|
||||
drop trigger if exists t_profiles_set_updated_at on public.profiles;
|
||||
create trigger t_profiles_set_updated_at
|
||||
before update on public.profiles
|
||||
for each row execute function public.set_updated_at();
|
||||
|
||||
-- 3) Trigger pós-signup: cria profile automático
|
||||
-- Observação: roda como SECURITY DEFINER
|
||||
create or replace function public.handle_new_user()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
begin
|
||||
insert into public.profiles (id, email, role, status)
|
||||
values (new.id, new.email, 'patient', 'active')
|
||||
on conflict (id) do update
|
||||
set email = excluded.email;
|
||||
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists on_auth_user_created on auth.users;
|
||||
create trigger on_auth_user_created
|
||||
after insert on auth.users
|
||||
for each row execute function public.handle_new_user();
|
||||
|
||||
-- 4) RLS
|
||||
alter table public.profiles enable row level security;
|
||||
|
||||
-- Leitura do próprio profile
|
||||
drop policy if exists "profiles_select_own" on public.profiles;
|
||||
create policy "profiles_select_own"
|
||||
on public.profiles
|
||||
for select
|
||||
to authenticated
|
||||
using (id = auth.uid());
|
||||
|
||||
-- Update do próprio profile (campos não-sensíveis)
|
||||
drop policy if exists "profiles_update_own" on public.profiles;
|
||||
create policy "profiles_update_own"
|
||||
on public.profiles
|
||||
for update
|
||||
to authenticated
|
||||
using (id = auth.uid())
|
||||
with check (id = auth.uid());
|
||||
|
||||
-- Insert só do próprio (na prática quem insere é trigger, mas deixa coerente)
|
||||
drop policy if exists "profiles_insert_own" on public.profiles;
|
||||
create policy "profiles_insert_own"
|
||||
on public.profiles
|
||||
for insert
|
||||
to authenticated
|
||||
with check (id = auth.uid());
|
||||
212
DBS/2026-03-11/src-sql-arquivos/supabase_cadastro_externo.sql
Normal file
212
DBS/2026-03-11/src-sql-arquivos/supabase_cadastro_externo.sql
Normal file
@@ -0,0 +1,212 @@
|
||||
-- =========================================================
|
||||
-- Agência PSI Quasar — Cadastro Externo de Paciente (Supabase/Postgres)
|
||||
-- Objetivo:
|
||||
-- - Ter um link público com TOKEN que o terapeuta envia ao paciente
|
||||
-- - Paciente preenche um formulário público
|
||||
-- - Salva em "intake requests" (pré-cadastro)
|
||||
-- - Terapeuta revisa e converte em paciente dentro do sistema
|
||||
--
|
||||
-- Tabelas:
|
||||
-- - patient_invites
|
||||
-- - patient_intake_requests
|
||||
--
|
||||
-- Funções:
|
||||
-- - create_patient_intake_request (RPC pública - anon)
|
||||
--
|
||||
-- Segurança:
|
||||
-- - RLS habilitada
|
||||
-- - Público (anon) não lê nada, só executa RPC
|
||||
-- - Terapeuta (authenticated) lê/atualiza somente seus registros
|
||||
-- =========================================================
|
||||
|
||||
-- 0) Tabelas
|
||||
create table if not exists public.patient_invites (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
owner_id uuid not null,
|
||||
token text not null unique,
|
||||
active boolean not null default true,
|
||||
expires_at timestamptz null,
|
||||
max_uses int null,
|
||||
uses int not null default 0,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists patient_invites_owner_id_idx on public.patient_invites(owner_id);
|
||||
create index if not exists patient_invites_token_idx on public.patient_invites(token);
|
||||
|
||||
create table if not exists public.patient_intake_requests (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
owner_id uuid not null,
|
||||
token text not null,
|
||||
name text not null,
|
||||
email text null,
|
||||
phone text null,
|
||||
notes text null,
|
||||
consent boolean not null default false,
|
||||
status text not null default 'new', -- new | converted | rejected
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists patient_intake_owner_id_idx on public.patient_intake_requests(owner_id);
|
||||
create index if not exists patient_intake_token_idx on public.patient_intake_requests(token);
|
||||
create index if not exists patient_intake_status_idx on public.patient_intake_requests(status);
|
||||
|
||||
-- 1) RLS
|
||||
alter table public.patient_invites enable row level security;
|
||||
alter table public.patient_intake_requests enable row level security;
|
||||
|
||||
-- 2) Fechar acesso direto para anon (público)
|
||||
revoke all on table public.patient_invites from anon;
|
||||
revoke all on table public.patient_intake_requests from anon;
|
||||
|
||||
-- 3) Policies: terapeuta (authenticated) - somente próprios registros
|
||||
|
||||
-- patient_invites
|
||||
drop policy if exists invites_select_own on public.patient_invites;
|
||||
create policy invites_select_own
|
||||
on public.patient_invites for select
|
||||
to authenticated
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists invites_insert_own on public.patient_invites;
|
||||
create policy invites_insert_own
|
||||
on public.patient_invites for insert
|
||||
to authenticated
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists invites_update_own on public.patient_invites;
|
||||
create policy invites_update_own
|
||||
on public.patient_invites for update
|
||||
to authenticated
|
||||
using (owner_id = auth.uid())
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
-- patient_intake_requests
|
||||
drop policy if exists intake_select_own on public.patient_intake_requests;
|
||||
create policy intake_select_own
|
||||
on public.patient_intake_requests for select
|
||||
to authenticated
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists intake_update_own on public.patient_intake_requests;
|
||||
create policy intake_update_own
|
||||
on public.patient_intake_requests for update
|
||||
to authenticated
|
||||
using (owner_id = auth.uid())
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
-- 4) RPC pública para criar intake (página pública)
|
||||
-- Importantíssimo: security definer + search_path fixo
|
||||
create or replace function public.create_patient_intake_request(
|
||||
p_token text,
|
||||
p_name text,
|
||||
p_email text default null,
|
||||
p_phone text default null,
|
||||
p_notes text default null,
|
||||
p_consent boolean default false
|
||||
)
|
||||
returns uuid
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
declare
|
||||
v_owner uuid;
|
||||
v_active boolean;
|
||||
v_expires timestamptz;
|
||||
v_max_uses int;
|
||||
v_uses int;
|
||||
v_id uuid;
|
||||
begin
|
||||
select owner_id, active, expires_at, max_uses, uses
|
||||
into v_owner, v_active, v_expires, v_max_uses, v_uses
|
||||
from public.patient_invites
|
||||
where token = p_token
|
||||
limit 1;
|
||||
|
||||
if v_owner is null then
|
||||
raise exception 'Token inválido';
|
||||
end if;
|
||||
|
||||
if v_active is not true then
|
||||
raise exception 'Link desativado';
|
||||
end if;
|
||||
|
||||
if v_expires is not null and now() > v_expires then
|
||||
raise exception 'Link expirado';
|
||||
end if;
|
||||
|
||||
if v_max_uses is not null and v_uses >= v_max_uses then
|
||||
raise exception 'Limite de uso atingido';
|
||||
end if;
|
||||
|
||||
if p_name is null or length(trim(p_name)) = 0 then
|
||||
raise exception 'Nome é obrigatório';
|
||||
end if;
|
||||
|
||||
insert into public.patient_intake_requests
|
||||
(owner_id, token, name, email, phone, notes, consent, status)
|
||||
values
|
||||
(v_owner, p_token, trim(p_name),
|
||||
nullif(lower(trim(p_email)), ''),
|
||||
nullif(trim(p_phone), ''),
|
||||
nullif(trim(p_notes), ''),
|
||||
coalesce(p_consent, false),
|
||||
'new')
|
||||
returning id into v_id;
|
||||
|
||||
update public.patient_invites
|
||||
set uses = uses + 1
|
||||
where token = p_token;
|
||||
|
||||
return v_id;
|
||||
end;
|
||||
$$;
|
||||
|
||||
grant execute on function public.create_patient_intake_request(text, text, text, text, text, boolean) to anon;
|
||||
grant execute on function public.create_patient_intake_request(text, text, text, text, text, boolean) to authenticated;
|
||||
|
||||
-- 5) (Opcional) helper para rotacionar token no painel (somente authenticated)
|
||||
-- Você pode usar no front via supabase.rpc('rotate_patient_invite_token')
|
||||
create or replace function public.rotate_patient_invite_token(
|
||||
p_new_token text
|
||||
)
|
||||
returns uuid
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
declare
|
||||
v_uid uuid;
|
||||
v_id uuid;
|
||||
begin
|
||||
-- pega o usuário logado
|
||||
v_uid := auth.uid();
|
||||
if v_uid is null then
|
||||
raise exception 'Usuário não autenticado';
|
||||
end if;
|
||||
|
||||
-- desativa tokens antigos ativos do usuário
|
||||
update public.patient_invites
|
||||
set active = false
|
||||
where owner_id = v_uid
|
||||
and active = true;
|
||||
|
||||
-- cria novo token
|
||||
insert into public.patient_invites (owner_id, token, active)
|
||||
values (v_uid, p_new_token, true)
|
||||
returning id into v_id;
|
||||
|
||||
return v_id;
|
||||
end;
|
||||
$$;
|
||||
|
||||
grant execute on function public.rotate_patient_invite_token(text) to authenticated;
|
||||
|
||||
grant select, insert, update, delete on table public.patient_invites to authenticated;
|
||||
grant select, insert, update, delete on table public.patient_intake_requests to authenticated;
|
||||
|
||||
-- anon não precisa acessar tabelas diretamente
|
||||
revoke all on table public.patient_invites from anon;
|
||||
revoke all on table public.patient_intake_requests from anon;
|
||||
|
||||
266
DBS/2026-03-11/src-sql-arquivos/supabase_cadastro_pacientes.sql
Normal file
266
DBS/2026-03-11/src-sql-arquivos/supabase_cadastro_pacientes.sql
Normal file
@@ -0,0 +1,266 @@
|
||||
-- =========================================================
|
||||
-- PATCH — Completar cadastro para bater com PatientsCadastroPage.vue
|
||||
-- (rode DEPOIS do seu supabase_cadastro_pacientes.sql)
|
||||
-- =========================================================
|
||||
|
||||
create extension if not exists pgcrypto;
|
||||
|
||||
-- ---------------------------------------------------------
|
||||
-- 1) Completar colunas que o front usa e hoje faltam em patients
|
||||
-- ---------------------------------------------------------
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_schema='public' and table_name='patients' and column_name='email_alt'
|
||||
) then
|
||||
alter table public.patients add column email_alt text;
|
||||
end if;
|
||||
|
||||
if not exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_schema='public' and table_name='patients' and column_name='phones'
|
||||
) then
|
||||
-- array de textos (Postgres). No JS você manda ["...","..."] normalmente.
|
||||
alter table public.patients add column phones text[];
|
||||
end if;
|
||||
|
||||
if not exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_schema='public' and table_name='patients' and column_name='gender'
|
||||
) then
|
||||
alter table public.patients add column gender text;
|
||||
end if;
|
||||
|
||||
if not exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_schema='public' and table_name='patients' and column_name='marital_status'
|
||||
) then
|
||||
alter table public.patients add column marital_status text;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- (opcional) índices úteis pra busca/filtro por nome/email
|
||||
create index if not exists idx_patients_owner_name on public.patients(owner_id, name);
|
||||
create index if not exists idx_patients_owner_email on public.patients(owner_id, email);
|
||||
|
||||
-- ---------------------------------------------------------
|
||||
-- 2) patient_groups
|
||||
-- ---------------------------------------------------------
|
||||
create table if not exists public.patient_groups (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
owner_id uuid not null references auth.users(id) on delete cascade,
|
||||
name text not null,
|
||||
color text,
|
||||
is_system boolean not null default false,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- nome único por owner
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'patient_groups_owner_name_uniq'
|
||||
and conrelid = 'public.patient_groups'::regclass
|
||||
) then
|
||||
alter table public.patient_groups
|
||||
add constraint patient_groups_owner_name_uniq unique(owner_id, name);
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
drop trigger if exists trg_patient_groups_set_updated_at on public.patient_groups;
|
||||
create trigger trg_patient_groups_set_updated_at
|
||||
before update on public.patient_groups
|
||||
for each row execute function public.set_updated_at();
|
||||
|
||||
create index if not exists idx_patient_groups_owner on public.patient_groups(owner_id);
|
||||
|
||||
alter table public.patient_groups enable row level security;
|
||||
|
||||
drop policy if exists "patient_groups_select_own" on public.patient_groups;
|
||||
create policy "patient_groups_select_own"
|
||||
on public.patient_groups for select
|
||||
to authenticated
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists "patient_groups_insert_own" on public.patient_groups;
|
||||
create policy "patient_groups_insert_own"
|
||||
on public.patient_groups for insert
|
||||
to authenticated
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists "patient_groups_update_own" on public.patient_groups;
|
||||
create policy "patient_groups_update_own"
|
||||
on public.patient_groups for update
|
||||
to authenticated
|
||||
using (owner_id = auth.uid())
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists "patient_groups_delete_own" on public.patient_groups;
|
||||
create policy "patient_groups_delete_own"
|
||||
on public.patient_groups for delete
|
||||
to authenticated
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
grant select, insert, update, delete on public.patient_groups to authenticated;
|
||||
|
||||
-- ---------------------------------------------------------
|
||||
-- 3) patient_tags
|
||||
-- ---------------------------------------------------------
|
||||
create table if not exists public.patient_tags (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
owner_id uuid not null references auth.users(id) on delete cascade,
|
||||
name text not null,
|
||||
color text,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'patient_tags_owner_name_uniq'
|
||||
and conrelid = 'public.patient_tags'::regclass
|
||||
) then
|
||||
alter table public.patient_tags
|
||||
add constraint patient_tags_owner_name_uniq unique(owner_id, name);
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
drop trigger if exists trg_patient_tags_set_updated_at on public.patient_tags;
|
||||
create trigger trg_patient_tags_set_updated_at
|
||||
before update on public.patient_tags
|
||||
for each row execute function public.set_updated_at();
|
||||
|
||||
create index if not exists idx_patient_tags_owner on public.patient_tags(owner_id);
|
||||
|
||||
alter table public.patient_tags enable row level security;
|
||||
|
||||
drop policy if exists "patient_tags_select_own" on public.patient_tags;
|
||||
create policy "patient_tags_select_own"
|
||||
on public.patient_tags for select
|
||||
to authenticated
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists "patient_tags_insert_own" on public.patient_tags;
|
||||
create policy "patient_tags_insert_own"
|
||||
on public.patient_tags for insert
|
||||
to authenticated
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists "patient_tags_update_own" on public.patient_tags;
|
||||
create policy "patient_tags_update_own"
|
||||
on public.patient_tags for update
|
||||
to authenticated
|
||||
using (owner_id = auth.uid())
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists "patient_tags_delete_own" on public.patient_tags;
|
||||
create policy "patient_tags_delete_own"
|
||||
on public.patient_tags for delete
|
||||
to authenticated
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
grant select, insert, update, delete on public.patient_tags to authenticated;
|
||||
|
||||
-- ---------------------------------------------------------
|
||||
-- 4) pivôs (patient_group_patient / patient_patient_tag)
|
||||
-- ---------------------------------------------------------
|
||||
create table if not exists public.patient_group_patient (
|
||||
patient_id uuid not null references public.patients(id) on delete cascade,
|
||||
patient_group_id uuid not null references public.patient_groups(id) on delete cascade,
|
||||
created_at timestamptz not null default now(),
|
||||
primary key (patient_id, patient_group_id)
|
||||
);
|
||||
|
||||
create index if not exists idx_pgp_patient on public.patient_group_patient(patient_id);
|
||||
create index if not exists idx_pgp_group on public.patient_group_patient(patient_group_id);
|
||||
|
||||
alter table public.patient_group_patient enable row level security;
|
||||
|
||||
-- a pivot “herda” tenant via join; policy usando exists pra validar owner do patient
|
||||
drop policy if exists "pgp_select_own" on public.patient_group_patient;
|
||||
create policy "pgp_select_own"
|
||||
on public.patient_group_patient for select
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1 from public.patients p
|
||||
where p.id = patient_group_patient.patient_id
|
||||
and p.owner_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists "pgp_write_own" on public.patient_group_patient;
|
||||
create policy "pgp_write_own"
|
||||
on public.patient_group_patient for all
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1 from public.patients p
|
||||
where p.id = patient_group_patient.patient_id
|
||||
and p.owner_id = auth.uid()
|
||||
)
|
||||
)
|
||||
with check (
|
||||
exists (
|
||||
select 1 from public.patients p
|
||||
where p.id = patient_group_patient.patient_id
|
||||
and p.owner_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on public.patient_group_patient to authenticated;
|
||||
|
||||
-- tags pivot (ATENÇÃO: coluna é tag_id, como teu Vue usa!)
|
||||
create table if not exists public.patient_patient_tag (
|
||||
patient_id uuid not null references public.patients(id) on delete cascade,
|
||||
tag_id uuid not null references public.patient_tags(id) on delete cascade,
|
||||
created_at timestamptz not null default now(),
|
||||
primary key (patient_id, tag_id)
|
||||
);
|
||||
|
||||
create index if not exists idx_ppt_patient on public.patient_patient_tag(patient_id);
|
||||
create index if not exists idx_ppt_tag on public.patient_patient_tag(tag_id);
|
||||
|
||||
alter table public.patient_patient_tag enable row level security;
|
||||
|
||||
drop policy if exists "ppt_select_own" on public.patient_patient_tag;
|
||||
create policy "ppt_select_own"
|
||||
on public.patient_patient_tag for select
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1 from public.patients p
|
||||
where p.id = patient_patient_tag.patient_id
|
||||
and p.owner_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists "ppt_write_own" on public.patient_patient_tag;
|
||||
create policy "ppt_write_own"
|
||||
on public.patient_patient_tag for all
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1 from public.patients p
|
||||
where p.id = patient_patient_tag.patient_id
|
||||
and p.owner_id = auth.uid()
|
||||
)
|
||||
)
|
||||
with check (
|
||||
exists (
|
||||
select 1 from public.patients p
|
||||
where p.id = patient_patient_tag.patient_id
|
||||
and p.owner_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on public.patient_patient_tag to authenticated;
|
||||
|
||||
-- =========================================================
|
||||
-- FIM PATCH
|
||||
-- =========================================================
|
||||
@@ -0,0 +1,105 @@
|
||||
-- =========================================================
|
||||
-- INTakes / Cadastros Recebidos - Supabase Local
|
||||
-- =========================================================
|
||||
|
||||
-- 0) Extensões úteis (geralmente já existem no Supabase, mas é seguro)
|
||||
create extension if not exists pgcrypto;
|
||||
|
||||
-- 1) Função padrão para updated_at
|
||||
create or replace function public.set_updated_at()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
new.updated_at = now();
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- 2) Tabela patient_intake_requests (espelhando nuvem)
|
||||
create table if not exists public.patient_intake_requests (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
owner_id uuid not null,
|
||||
token text,
|
||||
name text,
|
||||
email text,
|
||||
phone text,
|
||||
notes text,
|
||||
consent boolean not null default false,
|
||||
status text not null default 'new',
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
payload jsonb
|
||||
);
|
||||
|
||||
-- 3) Índices (performance em listagem e filtros)
|
||||
create index if not exists idx_intakes_owner_created
|
||||
on public.patient_intake_requests (owner_id, created_at desc);
|
||||
|
||||
create index if not exists idx_intakes_owner_status_created
|
||||
on public.patient_intake_requests (owner_id, status, created_at desc);
|
||||
|
||||
create index if not exists idx_intakes_status_created
|
||||
on public.patient_intake_requests (status, created_at desc);
|
||||
|
||||
-- 4) Trigger updated_at
|
||||
drop trigger if exists trg_patient_intake_requests_updated_at on public.patient_intake_requests;
|
||||
|
||||
create trigger trg_patient_intake_requests_updated_at
|
||||
before update on public.patient_intake_requests
|
||||
for each row execute function public.set_updated_at();
|
||||
|
||||
-- 5) RLS
|
||||
alter table public.patient_intake_requests enable row level security;
|
||||
|
||||
-- 6) Policies (iguais às que você mostrou na nuvem)
|
||||
drop policy if exists intake_select_own on public.patient_intake_requests;
|
||||
create policy intake_select_own
|
||||
on public.patient_intake_requests
|
||||
for select
|
||||
to authenticated
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists intake_update_own on public.patient_intake_requests;
|
||||
create policy intake_update_own
|
||||
on public.patient_intake_requests
|
||||
for update
|
||||
to authenticated
|
||||
using (owner_id = auth.uid())
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists "delete own intake requests" on public.patient_intake_requests;
|
||||
create policy "delete own intake requests"
|
||||
on public.patient_intake_requests
|
||||
for delete
|
||||
to authenticated
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
-- =========================================================
|
||||
-- OPCIONAL (RECOMENDADO): registrar conversão
|
||||
-- =========================================================
|
||||
-- Se você pretende marcar intake como convertido e guardar o patient_id:
|
||||
alter table public.patient_intake_requests
|
||||
add column if not exists converted_patient_id uuid;
|
||||
|
||||
create index if not exists idx_intakes_converted_patient_id
|
||||
on public.patient_intake_requests (converted_patient_id);
|
||||
|
||||
-- Opcional: impedir delete de intakes convertidos (melhor para auditoria)
|
||||
-- (Se quiser manter delete liberado como na nuvem, comente este bloco.)
|
||||
drop policy if exists "delete own intake requests" on public.patient_intake_requests;
|
||||
create policy "delete_own_intakes_not_converted"
|
||||
on public.patient_intake_requests
|
||||
for delete
|
||||
to authenticated
|
||||
using (owner_id = auth.uid() and status <> 'converted');
|
||||
|
||||
-- =========================================================
|
||||
-- OPCIONAL: check de status (evita status inválido)
|
||||
-- =========================================================
|
||||
alter table public.patient_intake_requests
|
||||
drop constraint if exists chk_intakes_status;
|
||||
|
||||
alter table public.patient_intake_requests
|
||||
add constraint chk_intakes_status
|
||||
check (status in ('new', 'converted', 'rejected'));
|
||||
174
DBS/2026-03-11/src-sql-arquivos/supabase_patient_groups.sql
Normal file
174
DBS/2026-03-11/src-sql-arquivos/supabase_patient_groups.sql
Normal file
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
patient_groups_setup.sql
|
||||
Setup completo para:
|
||||
- public.patient_groups
|
||||
- public.patient_group_patient (tabela ponte)
|
||||
- view public.v_patient_groups_with_counts
|
||||
- índice único por owner + nome (case-insensitive)
|
||||
- 3 grupos padrão do sistema (Crianças, Adolescentes, Idosos) NÃO editáveis / NÃO removíveis
|
||||
- triggers de proteção
|
||||
|
||||
Observação (importante):
|
||||
- Os grupos padrão são criados com owner_id = '00000000-0000-0000-0000-000000000000' (SYSTEM_OWNER),
|
||||
para ficarem "globais" e não dependerem de auth.uid() em migrations.
|
||||
- Se você quiser que os grupos padrão pertençam a um owner específico (tenant),
|
||||
basta trocar o SYSTEM_OWNER abaixo por esse UUID.
|
||||
*/
|
||||
|
||||
begin;
|
||||
|
||||
-- ===========================
|
||||
-- 0) Constante de "dono do sistema"
|
||||
-- ===========================
|
||||
-- Troque aqui se você quiser que os grupos padrão pertençam a um owner específico.
|
||||
-- Ex.: '816b24fe-a0c3-4409-b79b-c6c0a6935d03'
|
||||
do $$
|
||||
begin
|
||||
-- só para documentar; não cria nada
|
||||
end $$;
|
||||
|
||||
-- ===========================
|
||||
-- 1) Tabela principal: patient_groups
|
||||
-- ===========================
|
||||
create table if not exists public.patient_groups (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
name text not null,
|
||||
description text,
|
||||
color text,
|
||||
is_active boolean not null default true,
|
||||
is_system boolean not null default false,
|
||||
owner_id uuid not null,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- (Opcional, mas recomendado) Garante que name não seja só espaços
|
||||
-- e evita nomes vazios.
|
||||
alter table public.patient_groups
|
||||
drop constraint if exists patient_groups_name_not_blank_check;
|
||||
|
||||
alter table public.patient_groups
|
||||
add constraint patient_groups_name_not_blank_check
|
||||
check (length(btrim(name)) > 0);
|
||||
|
||||
-- ===========================
|
||||
-- 2) Tabela ponte: patient_group_patient
|
||||
-- ===========================
|
||||
-- Se você já tiver essa tabela com FKs, ajuste aqui conforme seu schema.
|
||||
create table if not exists public.patient_group_patient (
|
||||
patient_group_id uuid not null references public.patient_groups(id) on delete cascade,
|
||||
patient_id uuid not null references public.patients(id) on delete cascade,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- Evita duplicar vínculo paciente<->grupo
|
||||
create unique index if not exists patient_group_patient_unique
|
||||
on public.patient_group_patient (patient_group_id, patient_id);
|
||||
|
||||
-- ===========================
|
||||
-- 3) View com contagem
|
||||
-- ===========================
|
||||
create or replace view public.v_patient_groups_with_counts as
|
||||
select
|
||||
g.*,
|
||||
coalesce(count(distinct pgp.patient_id), 0)::int as patients_count
|
||||
from public.patient_groups g
|
||||
left join public.patient_group_patient pgp
|
||||
on pgp.patient_group_id = g.id
|
||||
group by g.id;
|
||||
|
||||
-- ===========================
|
||||
-- 4) Índice único: não permitir mesmo nome por owner (case-insensitive)
|
||||
-- ===========================
|
||||
-- Atenção: se já existirem duplicados, este índice pode falhar ao criar.
|
||||
create unique index if not exists patient_groups_owner_name_unique
|
||||
on public.patient_groups (owner_id, (lower(name)));
|
||||
|
||||
-- ===========================
|
||||
-- 5) Triggers de proteção: system não edita / não remove
|
||||
-- ===========================
|
||||
create or replace function public.prevent_system_group_changes()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if old.is_system = true then
|
||||
raise exception 'Grupos padrão do sistema não podem ser alterados ou excluídos.';
|
||||
end if;
|
||||
|
||||
if tg_op = 'DELETE' then
|
||||
return old;
|
||||
end if;
|
||||
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists trg_prevent_system_group_changes on public.patient_groups;
|
||||
|
||||
create trigger trg_prevent_system_group_changes
|
||||
before update or delete on public.patient_groups
|
||||
for each row
|
||||
execute function public.prevent_system_group_changes();
|
||||
|
||||
-- Impede "promover" um grupo comum para system
|
||||
create or replace function public.prevent_promoting_to_system()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if new.is_system = true and old.is_system is distinct from true then
|
||||
raise exception 'Não é permitido transformar um grupo comum em grupo do sistema.';
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists trg_prevent_promoting_to_system on public.patient_groups;
|
||||
|
||||
create trigger trg_prevent_promoting_to_system
|
||||
before update on public.patient_groups
|
||||
for each row
|
||||
execute function public.prevent_promoting_to_system();
|
||||
|
||||
-- ===========================
|
||||
-- 6) Inserir 3 grupos padrão (imutáveis)
|
||||
-- ===========================
|
||||
-- Dono "global" do sistema (mude se quiser):
|
||||
-- 00000000-0000-0000-0000-000000000000
|
||||
with sys_owner as (
|
||||
select '00000000-0000-0000-0000-000000000000'::uuid as owner_id
|
||||
)
|
||||
insert into public.patient_groups (name, description, color, is_active, is_system, owner_id)
|
||||
select v.name, v.description, v.color, v.is_active, v.is_system, s.owner_id
|
||||
from sys_owner s
|
||||
join (values
|
||||
('Crianças', 'Grupo padrão do sistema', null, true, true),
|
||||
('Adolescentes', 'Grupo padrão do sistema', null, true, true),
|
||||
('Idosos', 'Grupo padrão do sistema', null, true, true)
|
||||
) as v(name, description, color, is_active, is_system)
|
||||
on true
|
||||
where not exists (
|
||||
select 1
|
||||
from public.patient_groups g
|
||||
where g.owner_id = s.owner_id
|
||||
and lower(g.name) = lower(v.name)
|
||||
);
|
||||
|
||||
commit;
|
||||
|
||||
/*
|
||||
Testes rápidos:
|
||||
1) Ver tudo:
|
||||
select * from public.v_patient_groups_with_counts order by is_system desc, name;
|
||||
|
||||
2) Tentar editar um system (deve falhar):
|
||||
update public.patient_groups set name='X' where name='Crianças';
|
||||
|
||||
3) Tentar deletar um system (deve falhar):
|
||||
delete from public.patient_groups where name='Crianças';
|
||||
|
||||
4) Tentar duplicar nome no mesmo owner (deve falhar por índice único):
|
||||
insert into public.patient_groups (name, is_active, is_system, owner_id)
|
||||
values ('teste22', true, false, '816b24fe-a0c3-4409-b79b-c6c0a6935d03');
|
||||
*/
|
||||
147
DBS/2026-03-11/src-sql-arquivos/supabase_patient_index_page.sql
Normal file
147
DBS/2026-03-11/src-sql-arquivos/supabase_patient_index_page.sql
Normal file
@@ -0,0 +1,147 @@
|
||||
-- =========================================================
|
||||
-- pacientesIndexPage.sql
|
||||
-- Views + índices para a tela PatientsListPage
|
||||
-- =========================================================
|
||||
|
||||
-- 0) Extensões úteis
|
||||
create extension if not exists pg_trgm;
|
||||
|
||||
-- 1) updated_at automático (se você quiser manter updated_at sempre correto)
|
||||
create or replace function public.set_updated_at()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
new.updated_at = now();
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists trg_patients_set_updated_at on public.patients;
|
||||
create trigger trg_patients_set_updated_at
|
||||
before update on public.patients
|
||||
for each row execute function public.set_updated_at();
|
||||
|
||||
|
||||
-- =========================================================
|
||||
-- 2) Views de contagem (usadas em KPIs e telas auxiliares)
|
||||
-- =========================================================
|
||||
|
||||
-- 2.1) Grupos com contagem de pacientes
|
||||
create or replace view public.v_patient_groups_with_counts as
|
||||
select
|
||||
g.id,
|
||||
g.name,
|
||||
g.color,
|
||||
coalesce(count(pgp.patient_id), 0)::int as patients_count
|
||||
from public.patient_groups g
|
||||
left join public.patient_group_patient pgp
|
||||
on pgp.patient_group_id = g.id
|
||||
group by g.id, g.name, g.color;
|
||||
|
||||
-- 2.2) Tags com contagem de pacientes
|
||||
create or replace view public.v_tag_patient_counts as
|
||||
select
|
||||
t.id,
|
||||
t.name,
|
||||
t.color,
|
||||
coalesce(count(ppt.patient_id), 0)::int as patients_count
|
||||
from public.patient_tags t
|
||||
left join public.patient_patient_tag ppt
|
||||
on ppt.tag_id = t.id
|
||||
group by t.id, t.name, t.color;
|
||||
|
||||
|
||||
-- =========================================================
|
||||
-- 3) View principal da Index (pacientes + grupos/tags agregados)
|
||||
-- =========================================================
|
||||
|
||||
create or replace view public.v_patients_index as
|
||||
select
|
||||
p.*,
|
||||
|
||||
-- array JSON com os grupos do paciente
|
||||
coalesce(gx.groups, '[]'::jsonb) as groups,
|
||||
|
||||
-- array JSON com as tags do paciente
|
||||
coalesce(tx.tags, '[]'::jsonb) as tags,
|
||||
|
||||
-- contagens para UI/KPIs
|
||||
coalesce(gx.groups_count, 0)::int as groups_count,
|
||||
coalesce(tx.tags_count, 0)::int as tags_count
|
||||
|
||||
from public.patients p
|
||||
|
||||
left join lateral (
|
||||
select
|
||||
jsonb_agg(
|
||||
distinct jsonb_build_object(
|
||||
'id', g.id,
|
||||
'name', g.name,
|
||||
'color', g.color
|
||||
)
|
||||
) filter (where g.id is not null) as groups,
|
||||
count(distinct g.id) as groups_count
|
||||
from public.patient_group_patient pgp
|
||||
join public.patient_groups g
|
||||
on g.id = pgp.patient_group_id
|
||||
where pgp.patient_id = p.id
|
||||
) gx on true
|
||||
|
||||
left join lateral (
|
||||
select
|
||||
jsonb_agg(
|
||||
distinct jsonb_build_object(
|
||||
'id', t.id,
|
||||
'name', t.name,
|
||||
'color', t.color
|
||||
)
|
||||
) filter (where t.id is not null) as tags,
|
||||
count(distinct t.id) as tags_count
|
||||
from public.patient_patient_tag ppt
|
||||
join public.patient_tags t
|
||||
on t.id = ppt.tag_id
|
||||
where ppt.patient_id = p.id
|
||||
) tx on true;
|
||||
|
||||
|
||||
-- =========================================================
|
||||
-- 4) Índices recomendados (performance real na listagem/filtros)
|
||||
-- =========================================================
|
||||
|
||||
-- Patients
|
||||
create index if not exists idx_patients_owner_id
|
||||
on public.patients (owner_id);
|
||||
|
||||
create index if not exists idx_patients_created_at
|
||||
on public.patients (created_at desc);
|
||||
|
||||
create index if not exists idx_patients_status
|
||||
on public.patients (status);
|
||||
|
||||
create index if not exists idx_patients_last_attended_at
|
||||
on public.patients (last_attended_at desc);
|
||||
|
||||
-- Busca rápida (name/email/phone)
|
||||
create index if not exists idx_patients_name_trgm
|
||||
on public.patients using gin (name gin_trgm_ops);
|
||||
|
||||
create index if not exists idx_patients_email_trgm
|
||||
on public.patients using gin (email gin_trgm_ops);
|
||||
|
||||
create index if not exists idx_patients_phone_trgm
|
||||
on public.patients using gin (phone gin_trgm_ops);
|
||||
|
||||
-- Pivot: grupos
|
||||
create index if not exists idx_pgp_patient_id
|
||||
on public.patient_group_patient (patient_id);
|
||||
|
||||
create index if not exists idx_pgp_group_id
|
||||
on public.patient_group_patient (patient_group_id);
|
||||
|
||||
-- Pivot: tags
|
||||
create index if not exists idx_ppt_patient_id
|
||||
on public.patient_patient_tag (patient_id);
|
||||
|
||||
create index if not exists idx_ppt_tag_id
|
||||
on public.patient_patient_tag (tag_id);
|
||||
134
DBS/2026-03-11/src-sql-arquivos/supabase_tags.sql
Normal file
134
DBS/2026-03-11/src-sql-arquivos/supabase_tags.sql
Normal file
@@ -0,0 +1,134 @@
|
||||
create extension if not exists pgcrypto;
|
||||
|
||||
-- ===============================
|
||||
-- TABELA: patient_tags
|
||||
-- ===============================
|
||||
create table if not exists public.patient_tags (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
owner_id uuid not null,
|
||||
name text not null,
|
||||
color text,
|
||||
is_native boolean not null default false,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz
|
||||
);
|
||||
|
||||
create unique index if not exists patient_tags_owner_name_uq
|
||||
on public.patient_tags (owner_id, lower(name));
|
||||
|
||||
-- ===============================
|
||||
-- TABELA: patient_patient_tag (pivot)
|
||||
-- ===============================
|
||||
create table if not exists public.patient_patient_tag (
|
||||
owner_id uuid not null,
|
||||
patient_id uuid not null,
|
||||
tag_id uuid not null,
|
||||
created_at timestamptz not null default now(),
|
||||
primary key (patient_id, tag_id)
|
||||
);
|
||||
|
||||
create index if not exists ppt_owner_idx on public.patient_patient_tag(owner_id);
|
||||
create index if not exists ppt_tag_idx on public.patient_patient_tag(tag_id);
|
||||
create index if not exists ppt_patient_idx on public.patient_patient_tag(patient_id);
|
||||
|
||||
-- ===============================
|
||||
-- FOREIGN KEYS (com checagem)
|
||||
-- ===============================
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'ppt_tag_fk'
|
||||
and conrelid = 'public.patient_patient_tag'::regclass
|
||||
) then
|
||||
alter table public.patient_patient_tag
|
||||
add constraint ppt_tag_fk
|
||||
foreign key (tag_id)
|
||||
references public.patient_tags(id)
|
||||
on delete cascade;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'ppt_patient_fk'
|
||||
and conrelid = 'public.patient_patient_tag'::regclass
|
||||
) then
|
||||
alter table public.patient_patient_tag
|
||||
add constraint ppt_patient_fk
|
||||
foreign key (patient_id)
|
||||
references public.patients(id)
|
||||
on delete cascade;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- ===============================
|
||||
-- VIEW: contagem por tag
|
||||
-- ===============================
|
||||
create or replace view public.v_tag_patient_counts as
|
||||
select
|
||||
t.id,
|
||||
t.owner_id,
|
||||
t.name,
|
||||
t.color,
|
||||
t.is_native,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
coalesce(count(ppt.patient_id), 0)::int as patient_count
|
||||
from public.patient_tags t
|
||||
left join public.patient_patient_tag ppt
|
||||
on ppt.tag_id = t.id
|
||||
and ppt.owner_id = t.owner_id
|
||||
group by
|
||||
t.id, t.owner_id, t.name, t.color, t.is_native, t.created_at, t.updated_at;
|
||||
|
||||
-- ===============================
|
||||
-- RLS
|
||||
-- ===============================
|
||||
alter table public.patient_tags enable row level security;
|
||||
alter table public.patient_patient_tag enable row level security;
|
||||
|
||||
drop policy if exists tags_select_own on public.patient_tags;
|
||||
create policy tags_select_own
|
||||
on public.patient_tags
|
||||
for select
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists tags_insert_own on public.patient_tags;
|
||||
create policy tags_insert_own
|
||||
on public.patient_tags
|
||||
for insert
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists tags_update_own on public.patient_tags;
|
||||
create policy tags_update_own
|
||||
on public.patient_tags
|
||||
for update
|
||||
using (owner_id = auth.uid())
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists tags_delete_own on public.patient_tags;
|
||||
create policy tags_delete_own
|
||||
on public.patient_tags
|
||||
for delete
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists ppt_select_own on public.patient_patient_tag;
|
||||
create policy ppt_select_own
|
||||
on public.patient_patient_tag
|
||||
for select
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists ppt_insert_own on public.patient_patient_tag;
|
||||
create policy ppt_insert_own
|
||||
on public.patient_patient_tag
|
||||
for insert
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists ppt_delete_own on public.patient_patient_tag;
|
||||
create policy ppt_delete_own
|
||||
on public.patient_patient_tag
|
||||
for delete
|
||||
using (owner_id = auth.uid());
|
||||
2
DBS/2026-03-11/supabase-snippets/Untitled query 116.sql
Normal file
2
DBS/2026-03-11/supabase-snippets/Untitled query 116.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE public.agenda_configuracoes DROP COLUMN IF EXISTS
|
||||
session_start_offset_min;
|
||||
1
DBS/2026-03-11/supabase-snippets/Untitled query 130.sql
Normal file
1
DBS/2026-03-11/supabase-snippets/Untitled query 130.sql
Normal file
@@ -0,0 +1 @@
|
||||
select pg_get_functiondef('public.NOME_DA_FUNCAO(args_aqui)'::regprocedure);
|
||||
45
DBS/2026-03-11/supabase-snippets/Untitled query 132.sql
Normal file
45
DBS/2026-03-11/supabase-snippets/Untitled query 132.sql
Normal file
@@ -0,0 +1,45 @@
|
||||
-- 1) Tabela profiles
|
||||
create table if not exists public.profiles (
|
||||
id uuid primary key references auth.users(id) on delete cascade,
|
||||
role text not null default 'patient' check (role in ('admin','therapist','patient')),
|
||||
full_name text,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- 2) updated_at automático
|
||||
create or replace function public.set_updated_at()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
new.updated_at = now();
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists trg_profiles_updated_at on public.profiles;
|
||||
create trigger trg_profiles_updated_at
|
||||
before update on public.profiles
|
||||
for each row execute function public.set_updated_at();
|
||||
|
||||
-- 3) Trigger: cria profile automaticamente quando usuário nasce no auth
|
||||
create or replace function public.handle_new_user()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
begin
|
||||
insert into public.profiles (id, role)
|
||||
values (new.id, 'patient')
|
||||
on conflict (id) do nothing;
|
||||
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists on_auth_user_created on auth.users;
|
||||
create trigger on_auth_user_created
|
||||
after insert on auth.users
|
||||
for each row execute function public.handle_new_user();
|
||||
20
DBS/2026-03-11/supabase-snippets/Untitled query 157.sql
Normal file
20
DBS/2026-03-11/supabase-snippets/Untitled query 157.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
do $$
|
||||
begin
|
||||
if exists (
|
||||
select 1
|
||||
from information_schema.columns
|
||||
where table_schema = 'public'
|
||||
and table_name = 'patient_patient_tag'
|
||||
and column_name = 'patient_tag_id'
|
||||
)
|
||||
and not exists (
|
||||
select 1
|
||||
from information_schema.columns
|
||||
where table_schema = 'public'
|
||||
and table_name = 'patient_patient_tag'
|
||||
and column_name = 'tag_id'
|
||||
) then
|
||||
alter table public.patient_patient_tag
|
||||
rename column patient_tag_id to tag_id;
|
||||
end if;
|
||||
end $$;
|
||||
5
DBS/2026-03-11/supabase-snippets/Untitled query 159.sql
Normal file
5
DBS/2026-03-11/supabase-snippets/Untitled query 159.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
insert into public.profiles (id, role)
|
||||
select u.id, 'patient'
|
||||
from auth.users u
|
||||
left join public.profiles p on p.id = u.id
|
||||
where p.id is null;
|
||||
12
DBS/2026-03-11/supabase-snippets/Untitled query 174.sql
Normal file
12
DBS/2026-03-11/supabase-snippets/Untitled query 174.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
select
|
||||
id, name, email, phone,
|
||||
birth_date, cpf, rg, gender,
|
||||
marital_status, profession,
|
||||
place_of_birth, education_level,
|
||||
cep, address_street, address_number,
|
||||
address_neighborhood, address_city, address_state,
|
||||
phone_alt,
|
||||
notes, consent, created_at
|
||||
from public.patient_intake_requests
|
||||
order by created_at desc
|
||||
limit 1;
|
||||
23
DBS/2026-03-11/supabase-snippets/Untitled query 209.sql
Normal file
23
DBS/2026-03-11/supabase-snippets/Untitled query 209.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
create or replace function public.agenda_cfg_sync()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if new.agenda_view_mode = 'custom' then
|
||||
new.usar_horario_admin_custom := true;
|
||||
new.admin_inicio_visualizacao := new.agenda_custom_start;
|
||||
new.admin_fim_visualizacao := new.agenda_custom_end;
|
||||
else
|
||||
new.usar_horario_admin_custom := false;
|
||||
end if;
|
||||
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists trg_agenda_cfg_sync on public.agenda_configuracoes;
|
||||
|
||||
create trigger trg_agenda_cfg_sync
|
||||
before insert or update on public.agenda_configuracoes
|
||||
for each row
|
||||
execute function public.agenda_cfg_sync();
|
||||
2
DBS/2026-03-11/supabase-snippets/Untitled query 216.sql
Normal file
2
DBS/2026-03-11/supabase-snippets/Untitled query 216.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
drop index if exists public.uq_subscriptions_tenant;
|
||||
drop index if exists public.uq_subscriptions_personal_user;
|
||||
6
DBS/2026-03-11/supabase-snippets/Untitled query 219.sql
Normal file
6
DBS/2026-03-11/supabase-snippets/Untitled query 219.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
-- Limpa TUDO da agenda (eventos, regras, exceções)
|
||||
-- Execute no Supabase Studio — não tem volta!
|
||||
|
||||
DELETE FROM recurrence_exceptions;
|
||||
DELETE FROM recurrence_rules;
|
||||
DELETE FROM agenda_eventos;
|
||||
12
DBS/2026-03-11/supabase-snippets/Untitled query 221.sql
Normal file
12
DBS/2026-03-11/supabase-snippets/Untitled query 221.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
select
|
||||
t.tgname as trigger_name,
|
||||
pg_get_triggerdef(t.oid) as trigger_def,
|
||||
p.proname as function_name,
|
||||
n.nspname as function_schema
|
||||
from pg_trigger t
|
||||
join pg_proc p on p.oid = t.tgfoid
|
||||
join pg_namespace n on n.oid = p.pronamespace
|
||||
join pg_class c on c.oid = t.tgrelid
|
||||
where not t.tgisinternal
|
||||
and c.relname = 'patient_intake_requests'
|
||||
order by t.tgname;
|
||||
46
DBS/2026-03-11/supabase-snippets/Untitled query 235.sql
Normal file
46
DBS/2026-03-11/supabase-snippets/Untitled query 235.sql
Normal file
@@ -0,0 +1,46 @@
|
||||
-- ============================================================
|
||||
-- LIMPEZA DE DADOS DE TESTE — filtra por tenant/owner
|
||||
-- Execute no Supabase Studio com cuidado -- ============================================================
|
||||
DO $$ DECLARE
|
||||
v_tenant_id uuid := 'bbbbbbbb-0002-0002-0002-000000000002';
|
||||
v_owner_id uuid := 'aaaaaaaa-0002-0002-0002-000000000002';
|
||||
n_exc int;
|
||||
n_ev int;
|
||||
n_rule int;
|
||||
n_sol int;
|
||||
BEGIN
|
||||
|
||||
-- 1. Exceções (filha de recurrence_rules — apagar primeiro)
|
||||
DELETE FROM public.recurrence_exceptions
|
||||
WHERE recurrence_id IN (
|
||||
SELECT id FROM public.recurrence_rules
|
||||
WHERE (v_tenant_id IS NULL OR tenant_id = v_tenant_id)
|
||||
AND (v_owner_id IS NULL OR owner_id = v_owner_id)
|
||||
);
|
||||
GET DIAGNOSTICS n_exc = ROW_COUNT;
|
||||
|
||||
-- 2. Regras de recorrência
|
||||
DELETE FROM public.recurrence_rules
|
||||
WHERE (v_tenant_id IS NULL OR tenant_id = v_tenant_id)
|
||||
AND (v_owner_id IS NULL OR owner_id = v_owner_id);
|
||||
GET DIAGNOSTICS n_rule = ROW_COUNT;
|
||||
|
||||
-- 3. Eventos da agenda
|
||||
DELETE FROM public.agenda_eventos
|
||||
WHERE (v_tenant_id IS NULL OR tenant_id = v_tenant_id)
|
||||
AND (v_owner_id IS NULL OR owner_id = v_owner_id);
|
||||
GET DIAGNOSTICS n_ev = ROW_COUNT;
|
||||
|
||||
-- 4. Solicitações públicas (agendador online)
|
||||
DELETE FROM public.agendador_solicitacoes
|
||||
WHERE (v_tenant_id IS NULL OR tenant_id = v_tenant_id)
|
||||
AND (v_owner_id IS NULL OR owner_id = v_owner_id);
|
||||
GET DIAGNOSTICS n_sol = ROW_COUNT;
|
||||
|
||||
RAISE NOTICE '✅ Limpeza concluída:';
|
||||
RAISE NOTICE ' recurrence_exceptions : %', n_exc;
|
||||
RAISE NOTICE ' recurrence_rules : %', n_rule;
|
||||
RAISE NOTICE ' agenda_eventos : %', n_ev;
|
||||
RAISE NOTICE ' agendador_solicitacoes : %', n_sol;
|
||||
END;
|
||||
$$;
|
||||
10
DBS/2026-03-11/supabase-snippets/Untitled query 271.sql
Normal file
10
DBS/2026-03-11/supabase-snippets/Untitled query 271.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
select
|
||||
id as tenant_member_id,
|
||||
tenant_id,
|
||||
user_id,
|
||||
role,
|
||||
status,
|
||||
created_at
|
||||
from public.tenant_members
|
||||
where user_id = '824f125c-55bb-40f5-a8c4-7a33618b91c7'
|
||||
order by created_at desc;
|
||||
4
DBS/2026-03-11/supabase-snippets/Untitled query 277.sql
Normal file
4
DBS/2026-03-11/supabase-snippets/Untitled query 277.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
select *
|
||||
from agenda_eventos
|
||||
order by created_at desc nulls last
|
||||
limit 10;
|
||||
6
DBS/2026-03-11/supabase-snippets/Untitled query 319.sql
Normal file
6
DBS/2026-03-11/supabase-snippets/Untitled query 319.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
select
|
||||
routine_name,
|
||||
routine_type
|
||||
from information_schema.routines
|
||||
where routine_schema = 'public'
|
||||
and routine_name ilike '%intake%';
|
||||
4
DBS/2026-03-11/supabase-snippets/Untitled query 323.sql
Normal file
4
DBS/2026-03-11/supabase-snippets/Untitled query 323.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
select id as owner_id, email, created_at
|
||||
from auth.users
|
||||
where email = 'admin@agendapsi.com.br'
|
||||
limit 1;
|
||||
8
DBS/2026-03-11/supabase-snippets/Untitled query 324.sql
Normal file
8
DBS/2026-03-11/supabase-snippets/Untitled query 324.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
select
|
||||
id,
|
||||
owner_id,
|
||||
status,
|
||||
created_at,
|
||||
converted_patient_id
|
||||
from public.patient_intake_requests
|
||||
where id = '54daa09a-b2cb-4a0b-91aa-e4cea1915efe';
|
||||
6
DBS/2026-03-11/supabase-snippets/Untitled query 330.sql
Normal file
6
DBS/2026-03-11/supabase-snippets/Untitled query 330.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
select f.key as feature_key
|
||||
from public.plan_features pf
|
||||
join public.features f on f.id = pf.feature_id
|
||||
where pf.plan_id = 'fdc2813d-dfaa-4e2c-b71d-ef7b84dfd9e9'
|
||||
and pf.enabled = true
|
||||
order by f.key;
|
||||
3
DBS/2026-03-11/supabase-snippets/Untitled query 361.sql
Normal file
3
DBS/2026-03-11/supabase-snippets/Untitled query 361.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
select id, key, name
|
||||
from public.plans
|
||||
order by key;
|
||||
1
DBS/2026-03-11/supabase-snippets/Untitled query 376.sql
Normal file
1
DBS/2026-03-11/supabase-snippets/Untitled query 376.sql
Normal file
@@ -0,0 +1 @@
|
||||
SELECT * FROM public.agendador_solicitacoes LIMIT 5;
|
||||
5
DBS/2026-03-11/supabase-snippets/Untitled query 431.sql
Normal file
5
DBS/2026-03-11/supabase-snippets/Untitled query 431.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
select table_schema, table_name, column_name
|
||||
from information_schema.columns
|
||||
where column_name in ('owner_id', 'tenant_id')
|
||||
and table_schema = 'public'
|
||||
order by table_name;
|
||||
12
DBS/2026-03-11/supabase-snippets/Untitled query 437.sql
Normal file
12
DBS/2026-03-11/supabase-snippets/Untitled query 437.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
alter table public.patient_groups enable row level security;
|
||||
|
||||
drop policy if exists patient_groups_select on public.patient_groups;
|
||||
|
||||
create policy patient_groups_select
|
||||
on public.patient_groups
|
||||
for select
|
||||
to authenticated
|
||||
using (
|
||||
owner_id = auth.uid()
|
||||
or owner_id is null
|
||||
);
|
||||
9
DBS/2026-03-11/supabase-snippets/Untitled query 439.sql
Normal file
9
DBS/2026-03-11/supabase-snippets/Untitled query 439.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
|
||||
select *
|
||||
from public.owner_feature_entitlements
|
||||
where owner_id = '816b24fe-a0c3-4409-b79b-c6c0a6935d03'::uuid
|
||||
order by feature_key;
|
||||
|
||||
select public.has_feature('816b24fe-a0c3-4409-b79b-c6c0a6935d03'::uuid, 'online_scheduling.manage') as can_manage;
|
||||
|
||||
5
DBS/2026-03-11/supabase-snippets/Untitled query 449.sql
Normal file
5
DBS/2026-03-11/supabase-snippets/Untitled query 449.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
create index if not exists idx_patient_group_patient_group_id
|
||||
on public.patient_group_patient (patient_group_id);
|
||||
|
||||
create index if not exists idx_patient_groups_owner_system_nome
|
||||
on public.patient_groups (owner_id, is_system, nome);
|
||||
9
DBS/2026-03-11/supabase-snippets/Untitled query 457.sql
Normal file
9
DBS/2026-03-11/supabase-snippets/Untitled query 457.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
-- 1) Marcar como SaaS master
|
||||
insert into public.saas_admins (user_id)
|
||||
values ('40a4b683-a0c9-4890-a201-20faf41fca06')
|
||||
on conflict (user_id) do nothing;
|
||||
|
||||
-- 2) Garantir profile (seu session.js busca role em profiles)
|
||||
insert into public.profiles (id, role)
|
||||
values ('40a4b683-a0c9-4890-a201-20faf41fca06', 'saas_admin')
|
||||
on conflict (id) do update set role = excluded.role;
|
||||
5
DBS/2026-03-11/supabase-snippets/Untitled query 468.sql
Normal file
5
DBS/2026-03-11/supabase-snippets/Untitled query 468.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
select *
|
||||
from public.owner_feature_entitlements
|
||||
where owner_id = '816b24fe-a0c3-4409-b79b-c6c0a6935d03'::uuid
|
||||
order by feature_key;
|
||||
|
||||
5
DBS/2026-03-11/supabase-snippets/Untitled query 476.sql
Normal file
5
DBS/2026-03-11/supabase-snippets/Untitled query 476.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
select column_name, data_type
|
||||
from information_schema.columns
|
||||
where table_schema = 'public'
|
||||
and table_name = 'patient_patient_tag'
|
||||
order by ordinal_position;
|
||||
18
DBS/2026-03-11/supabase-snippets/Untitled query 508.sql
Normal file
18
DBS/2026-03-11/supabase-snippets/Untitled query 508.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
create or replace function public.prevent_promoting_to_system()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if new.is_system = true and old.is_system is distinct from true then
|
||||
raise exception 'Não é permitido transformar um grupo comum em grupo do sistema.';
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists trg_prevent_promoting_to_system on public.patient_groups;
|
||||
|
||||
create trigger trg_prevent_promoting_to_system
|
||||
before update on public.patient_groups
|
||||
for each row
|
||||
execute function public.prevent_promoting_to_system();
|
||||
4
DBS/2026-03-11/supabase-snippets/Untitled query 521.sql
Normal file
4
DBS/2026-03-11/supabase-snippets/Untitled query 521.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
select id, tenant_id, user_id, role, status, created_at
|
||||
from tenant_members
|
||||
where user_id = '1715ec83-9a30-4dce-b73a-2deb66dcfb13'
|
||||
order by created_at desc;
|
||||
2
DBS/2026-03-11/supabase-snippets/Untitled query 566.sql
Normal file
2
DBS/2026-03-11/supabase-snippets/Untitled query 566.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
const { data, error } = await supabase.rpc('my_tenants')
|
||||
console.log({ data, error })
|
||||
6
DBS/2026-03-11/supabase-snippets/Untitled query 633.sql
Normal file
6
DBS/2026-03-11/supabase-snippets/Untitled query 633.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
select id, tenant_id, user_id, role, status, created_at
|
||||
from tenant_members
|
||||
where tenant_id = '816b24fe-a0c3-4409-b79b-c6c0a6935d03'::uuid
|
||||
order by
|
||||
(case when role = 'clinic_admin' then 0 else 1 end),
|
||||
created_at;
|
||||
15
DBS/2026-03-11/supabase-snippets/Untitled query 641.sql
Normal file
15
DBS/2026-03-11/supabase-snippets/Untitled query 641.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
-- Para o tenant A:
|
||||
select id as responsible_member_id
|
||||
from public.tenant_members
|
||||
where tenant_id = '816b24fe-a0c3-4409-b79b-c6c0a6935d03'
|
||||
and user_id = '824f125c-55bb-40f5-a8c4-7a33618b91c7'
|
||||
and status = 'active'
|
||||
limit 1;
|
||||
|
||||
-- Para o tenant B:
|
||||
select id as responsible_member_id
|
||||
from public.tenant_members
|
||||
where tenant_id = 'e8b10543-fb36-4e75-9d37-6fece9745637'
|
||||
and user_id = '824f125c-55bb-40f5-a8c4-7a33618b91c7'
|
||||
and status = 'active'
|
||||
limit 1;
|
||||
4
DBS/2026-03-11/supabase-snippets/Untitled query 649.sql
Normal file
4
DBS/2026-03-11/supabase-snippets/Untitled query 649.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
select id, name, kind, created_at
|
||||
from public.tenants
|
||||
order by created_at desc
|
||||
limit 10;
|
||||
5
DBS/2026-03-11/supabase-snippets/Untitled query 677.sql
Normal file
5
DBS/2026-03-11/supabase-snippets/Untitled query 677.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
select id as member_id
|
||||
from tenant_members
|
||||
where tenant_id = '816b24fe-a0c3-4409-b79b-c6c0a6935d03'
|
||||
and user_id = '824f125c-55bb-40f5-a8c4-7a33618b91c7'
|
||||
limit 1;
|
||||
2
DBS/2026-03-11/supabase-snippets/Untitled query 744.sql
Normal file
2
DBS/2026-03-11/supabase-snippets/Untitled query 744.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
insert into public.users (id, created_at)
|
||||
values ('e8b10543-fb36-4e75-9d37-6fece9745637', now());
|
||||
4
DBS/2026-03-11/supabase-snippets/Untitled query 781.sql
Normal file
4
DBS/2026-03-11/supabase-snippets/Untitled query 781.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
select *
|
||||
from public.tenant_members
|
||||
where tenant_id = 'UUID_AQUI'
|
||||
order by created_at desc;
|
||||
17
DBS/2026-03-11/supabase-snippets/Untitled query 790.sql
Normal file
17
DBS/2026-03-11/supabase-snippets/Untitled query 790.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
create table if not exists public.subscriptions (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
user_id uuid not null references auth.users(id) on delete cascade,
|
||||
plan_key text not null,
|
||||
interval text not null check (interval in ('month','year')),
|
||||
status text not null check (status in ('active','canceled','past_due','trial')) default 'active',
|
||||
started_at timestamptz not null default now(),
|
||||
current_period_start timestamptz not null default now(),
|
||||
current_period_end timestamptz null,
|
||||
canceled_at timestamptz null,
|
||||
source text not null default 'manual',
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists subscriptions_user_id_idx on public.subscriptions(user_id);
|
||||
create index if not exists subscriptions_status_idx on public.subscriptions(status);
|
||||
13
DBS/2026-03-11/supabase-snippets/Untitled query 830.sql
Normal file
13
DBS/2026-03-11/supabase-snippets/Untitled query 830.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
-- Ver todas as regras no banco
|
||||
SELECT
|
||||
id,
|
||||
owner_id,
|
||||
status,
|
||||
type,
|
||||
weekdays,
|
||||
start_date,
|
||||
end_date,
|
||||
start_time,
|
||||
created_at
|
||||
FROM recurrence_rules
|
||||
ORDER BY created_at DESC;
|
||||
3
DBS/2026-03-11/supabase-snippets/Untitled query 843.sql
Normal file
3
DBS/2026-03-11/supabase-snippets/Untitled query 843.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
select id, name, created_at
|
||||
from public.tenants
|
||||
order by created_at desc;
|
||||
8
DBS/2026-03-11/supabase-snippets/Untitled query 856.sql
Normal file
8
DBS/2026-03-11/supabase-snippets/Untitled query 856.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
alter table public.profiles enable row level security;
|
||||
|
||||
drop policy if exists "profiles_select_own" on public.profiles;
|
||||
create policy "profiles_select_own"
|
||||
on public.profiles
|
||||
for select
|
||||
to authenticated
|
||||
using (id = auth.uid());
|
||||
7
DBS/2026-03-11/supabase-snippets/Untitled query 869.sql
Normal file
7
DBS/2026-03-11/supabase-snippets/Untitled query 869.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- 2) Tenho membership ativa no tenant atual?
|
||||
select *
|
||||
from tenant_members
|
||||
where tenant_id = 'e8b10543-fb36-4e75-9d37-6fece9745637'
|
||||
and user_id = auth.uid()
|
||||
order by created_at desc;
|
||||
|
||||
8
DBS/2026-03-11/supabase-snippets/Untitled query 880.sql
Normal file
8
DBS/2026-03-11/supabase-snippets/Untitled query 880.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
select
|
||||
n.nspname as schema,
|
||||
p.proname as function_name,
|
||||
pg_get_functiondef(p.oid) as definition
|
||||
from pg_proc p
|
||||
join pg_namespace n on n.oid = p.pronamespace
|
||||
where n.nspname = 'public'
|
||||
and pg_get_functiondef(p.oid) ilike '%entitlements_invalidation%';
|
||||
266
DBS/2026-03-11/supabase-snippets/Untitled query 886.sql
Normal file
266
DBS/2026-03-11/supabase-snippets/Untitled query 886.sql
Normal file
@@ -0,0 +1,266 @@
|
||||
-- =========================================================
|
||||
-- PATCH — Completar cadastro para bater com PatientsCadastroPage.vue
|
||||
-- (rode DEPOIS do seu supabase_cadastro_pacientes.sql)
|
||||
-- =========================================================
|
||||
|
||||
create extension if not exists pgcrypto;
|
||||
|
||||
-- ---------------------------------------------------------
|
||||
-- 1) Completar colunas que o front usa e hoje faltam em patients
|
||||
-- ---------------------------------------------------------
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_schema='public' and table_name='patients' and column_name='email_alt'
|
||||
) then
|
||||
alter table public.patients add column email_alt text;
|
||||
end if;
|
||||
|
||||
if not exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_schema='public' and table_name='patients' and column_name='phones'
|
||||
) then
|
||||
-- array de textos (Postgres). No JS você manda ["...","..."] normalmente.
|
||||
alter table public.patients add column phones text[];
|
||||
end if;
|
||||
|
||||
if not exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_schema='public' and table_name='patients' and column_name='gender'
|
||||
) then
|
||||
alter table public.patients add column gender text;
|
||||
end if;
|
||||
|
||||
if not exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_schema='public' and table_name='patients' and column_name='marital_status'
|
||||
) then
|
||||
alter table public.patients add column marital_status text;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- (opcional) índices úteis pra busca/filtro por nome/email
|
||||
create index if not exists idx_patients_owner_name on public.patients(owner_id, name);
|
||||
create index if not exists idx_patients_owner_email on public.patients(owner_id, email);
|
||||
|
||||
-- ---------------------------------------------------------
|
||||
-- 2) patient_groups
|
||||
-- ---------------------------------------------------------
|
||||
create table if not exists public.patient_groups (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
owner_id uuid not null references auth.users(id) on delete cascade,
|
||||
name text not null,
|
||||
color text,
|
||||
is_system boolean not null default false,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- nome único por owner
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'patient_groups_owner_name_uniq'
|
||||
and conrelid = 'public.patient_groups'::regclass
|
||||
) then
|
||||
alter table public.patient_groups
|
||||
add constraint patient_groups_owner_name_uniq unique(owner_id, name);
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
drop trigger if exists trg_patient_groups_set_updated_at on public.patient_groups;
|
||||
create trigger trg_patient_groups_set_updated_at
|
||||
before update on public.patient_groups
|
||||
for each row execute function public.set_updated_at();
|
||||
|
||||
create index if not exists idx_patient_groups_owner on public.patient_groups(owner_id);
|
||||
|
||||
alter table public.patient_groups enable row level security;
|
||||
|
||||
drop policy if exists "patient_groups_select_own" on public.patient_groups;
|
||||
create policy "patient_groups_select_own"
|
||||
on public.patient_groups for select
|
||||
to authenticated
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists "patient_groups_insert_own" on public.patient_groups;
|
||||
create policy "patient_groups_insert_own"
|
||||
on public.patient_groups for insert
|
||||
to authenticated
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists "patient_groups_update_own" on public.patient_groups;
|
||||
create policy "patient_groups_update_own"
|
||||
on public.patient_groups for update
|
||||
to authenticated
|
||||
using (owner_id = auth.uid())
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists "patient_groups_delete_own" on public.patient_groups;
|
||||
create policy "patient_groups_delete_own"
|
||||
on public.patient_groups for delete
|
||||
to authenticated
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
grant select, insert, update, delete on public.patient_groups to authenticated;
|
||||
|
||||
-- ---------------------------------------------------------
|
||||
-- 3) patient_tags
|
||||
-- ---------------------------------------------------------
|
||||
create table if not exists public.patient_tags (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
owner_id uuid not null references auth.users(id) on delete cascade,
|
||||
name text not null,
|
||||
color text,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'patient_tags_owner_name_uniq'
|
||||
and conrelid = 'public.patient_tags'::regclass
|
||||
) then
|
||||
alter table public.patient_tags
|
||||
add constraint patient_tags_owner_name_uniq unique(owner_id, name);
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
drop trigger if exists trg_patient_tags_set_updated_at on public.patient_tags;
|
||||
create trigger trg_patient_tags_set_updated_at
|
||||
before update on public.patient_tags
|
||||
for each row execute function public.set_updated_at();
|
||||
|
||||
create index if not exists idx_patient_tags_owner on public.patient_tags(owner_id);
|
||||
|
||||
alter table public.patient_tags enable row level security;
|
||||
|
||||
drop policy if exists "patient_tags_select_own" on public.patient_tags;
|
||||
create policy "patient_tags_select_own"
|
||||
on public.patient_tags for select
|
||||
to authenticated
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists "patient_tags_insert_own" on public.patient_tags;
|
||||
create policy "patient_tags_insert_own"
|
||||
on public.patient_tags for insert
|
||||
to authenticated
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists "patient_tags_update_own" on public.patient_tags;
|
||||
create policy "patient_tags_update_own"
|
||||
on public.patient_tags for update
|
||||
to authenticated
|
||||
using (owner_id = auth.uid())
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists "patient_tags_delete_own" on public.patient_tags;
|
||||
create policy "patient_tags_delete_own"
|
||||
on public.patient_tags for delete
|
||||
to authenticated
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
grant select, insert, update, delete on public.patient_tags to authenticated;
|
||||
|
||||
-- ---------------------------------------------------------
|
||||
-- 4) pivôs (patient_group_patient / patient_patient_tag)
|
||||
-- ---------------------------------------------------------
|
||||
create table if not exists public.patient_group_patient (
|
||||
patient_id uuid not null references public.patients(id) on delete cascade,
|
||||
patient_group_id uuid not null references public.patient_groups(id) on delete cascade,
|
||||
created_at timestamptz not null default now(),
|
||||
primary key (patient_id, patient_group_id)
|
||||
);
|
||||
|
||||
create index if not exists idx_pgp_patient on public.patient_group_patient(patient_id);
|
||||
create index if not exists idx_pgp_group on public.patient_group_patient(patient_group_id);
|
||||
|
||||
alter table public.patient_group_patient enable row level security;
|
||||
|
||||
-- a pivot “herda” tenant via join; policy usando exists pra validar owner do patient
|
||||
drop policy if exists "pgp_select_own" on public.patient_group_patient;
|
||||
create policy "pgp_select_own"
|
||||
on public.patient_group_patient for select
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1 from public.patients p
|
||||
where p.id = patient_group_patient.patient_id
|
||||
and p.owner_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists "pgp_write_own" on public.patient_group_patient;
|
||||
create policy "pgp_write_own"
|
||||
on public.patient_group_patient for all
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1 from public.patients p
|
||||
where p.id = patient_group_patient.patient_id
|
||||
and p.owner_id = auth.uid()
|
||||
)
|
||||
)
|
||||
with check (
|
||||
exists (
|
||||
select 1 from public.patients p
|
||||
where p.id = patient_group_patient.patient_id
|
||||
and p.owner_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on public.patient_group_patient to authenticated;
|
||||
|
||||
-- tags pivot (ATENÇÃO: coluna é tag_id, como teu Vue usa!)
|
||||
create table if not exists public.patient_patient_tag (
|
||||
patient_id uuid not null references public.patients(id) on delete cascade,
|
||||
tag_id uuid not null references public.patient_tags(id) on delete cascade,
|
||||
created_at timestamptz not null default now(),
|
||||
primary key (patient_id, tag_id)
|
||||
);
|
||||
|
||||
create index if not exists idx_ppt_patient on public.patient_patient_tag(patient_id);
|
||||
create index if not exists idx_ppt_tag on public.patient_patient_tag(tag_id);
|
||||
|
||||
alter table public.patient_patient_tag enable row level security;
|
||||
|
||||
drop policy if exists "ppt_select_own" on public.patient_patient_tag;
|
||||
create policy "ppt_select_own"
|
||||
on public.patient_patient_tag for select
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1 from public.patients p
|
||||
where p.id = patient_patient_tag.patient_id
|
||||
and p.owner_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists "ppt_write_own" on public.patient_patient_tag;
|
||||
create policy "ppt_write_own"
|
||||
on public.patient_patient_tag for all
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1 from public.patients p
|
||||
where p.id = patient_patient_tag.patient_id
|
||||
and p.owner_id = auth.uid()
|
||||
)
|
||||
)
|
||||
with check (
|
||||
exists (
|
||||
select 1 from public.patients p
|
||||
where p.id = patient_patient_tag.patient_id
|
||||
and p.owner_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on public.patient_patient_tag to authenticated;
|
||||
|
||||
-- =========================================================
|
||||
-- FIM PATCH
|
||||
-- =========================================================
|
||||
42
DBS/2026-03-11/supabase-snippets/Untitled query 899.sql
Normal file
42
DBS/2026-03-11/supabase-snippets/Untitled query 899.sql
Normal file
@@ -0,0 +1,42 @@
|
||||
-- BUCKET avatars: RLS por pasta do usuário "<uid>/..."
|
||||
-- Requer que seu path seja: `${auth.uid()}/...` (no seu código já é)
|
||||
|
||||
drop policy if exists "avatars_select_own" on storage.objects;
|
||||
create policy "avatars_select_own"
|
||||
on storage.objects for select
|
||||
to authenticated
|
||||
using (
|
||||
bucket_id = 'avatars'
|
||||
and name like auth.uid()::text || '/%'
|
||||
);
|
||||
|
||||
drop policy if exists "avatars_insert_own" on storage.objects;
|
||||
create policy "avatars_insert_own"
|
||||
on storage.objects for insert
|
||||
to authenticated
|
||||
with check (
|
||||
bucket_id = 'avatars'
|
||||
and name like auth.uid()::text || '/%'
|
||||
);
|
||||
|
||||
drop policy if exists "avatars_update_own" on storage.objects;
|
||||
create policy "avatars_update_own"
|
||||
on storage.objects for update
|
||||
to authenticated
|
||||
using (
|
||||
bucket_id = 'avatars'
|
||||
and name like auth.uid()::text || '/%'
|
||||
)
|
||||
with check (
|
||||
bucket_id = 'avatars'
|
||||
and name like auth.uid()::text || '/%'
|
||||
);
|
||||
|
||||
drop policy if exists "avatars_delete_own" on storage.objects;
|
||||
create policy "avatars_delete_own"
|
||||
on storage.objects for delete
|
||||
to authenticated
|
||||
using (
|
||||
bucket_id = 'avatars'
|
||||
and name like auth.uid()::text || '/%'
|
||||
);
|
||||
3
DBS/2026-03-11/supabase-snippets/Untitled query 934.sql
Normal file
3
DBS/2026-03-11/supabase-snippets/Untitled query 934.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
select *
|
||||
from tenant_members
|
||||
where user_id = 'SEU_USER_ID';
|
||||
14
DBS/2026-03-11/supabase-snippets/Untitled query 938.sql
Normal file
14
DBS/2026-03-11/supabase-snippets/Untitled query 938.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
select
|
||||
tm.id as responsible_member_id,
|
||||
tm.tenant_id,
|
||||
tm.user_id,
|
||||
tm.role,
|
||||
tm.status,
|
||||
tm.created_at
|
||||
from public.tenant_members tm
|
||||
where tm.tenant_id = (
|
||||
select owner_id from public.patient_intake_requests
|
||||
where id = '54daa09a-b2cb-4a0b-91aa-e4cea1915efe'
|
||||
)
|
||||
and tm.user_id = '824f125c-55bb-40f5-a8c4-7a33618b91c7'
|
||||
limit 10;
|
||||
17
DBS/2026-03-11/supabase-snippets/Untitled query 975.sql
Normal file
17
DBS/2026-03-11/supabase-snippets/Untitled query 975.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
-- 1) Qual é meu uid?
|
||||
select auth.uid() as my_uid;
|
||||
|
||||
-- 2) Tenho membership ativa no tenant atual?
|
||||
select *
|
||||
from tenant_members
|
||||
where tenant_id = 'e8b10543-fb36-4e75-9d37-6fece9745637'
|
||||
and user_id = auth.uid()
|
||||
order by created_at desc;
|
||||
|
||||
-- 3) Se você usa status:
|
||||
select *
|
||||
from tenant_members
|
||||
where tenant_id = 'e8b10543-fb36-4e75-9d37-6fece9745637'
|
||||
and user_id = auth.uid()
|
||||
and status = 'active'
|
||||
order by created_at desc;
|
||||
123
DBS/2026-03-11/supabase-snippets/saas_docs.sql
Normal file
123
DBS/2026-03-11/supabase-snippets/saas_docs.sql
Normal file
@@ -0,0 +1,123 @@
|
||||
-- ============================================================
|
||||
-- saas_docs — Documentação dinâmica do sistema
|
||||
-- Exibida nas páginas do frontend via botão "Ajuda"
|
||||
-- ============================================================
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 1. TABELA
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS public.saas_docs (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
titulo text NOT NULL,
|
||||
conteudo text NOT NULL DEFAULT '',
|
||||
medias jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||
-- formato: [{ "tipo": "imagem"|"video", "url": "..." }, ...]
|
||||
|
||||
tipo_acesso text NOT NULL DEFAULT 'usuario'
|
||||
CHECK (tipo_acesso IN ('usuario', 'admin')),
|
||||
-- 'usuario' → todos os autenticados
|
||||
-- 'admin' → clinic_admin, tenant_admin, saas_admin
|
||||
|
||||
pagina_path text NOT NULL,
|
||||
-- path da rota do frontend, ex: '/therapist/agenda'
|
||||
|
||||
pagina_label text,
|
||||
-- label amigável (informativo, não usado no match)
|
||||
|
||||
docs_relacionados uuid[] NOT NULL DEFAULT '{}',
|
||||
-- IDs de outros saas_docs exibidos como "Veja também"
|
||||
|
||||
ativo boolean NOT NULL DEFAULT true,
|
||||
ordem int NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 2. ÍNDICE
|
||||
-- ------------------------------------------------------------
|
||||
-- Query principal do frontend: filtra por path + ativo
|
||||
CREATE INDEX IF NOT EXISTS saas_docs_path_ativo_idx
|
||||
ON public.saas_docs (pagina_path, ativo);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 3. RLS
|
||||
-- ------------------------------------------------------------
|
||||
ALTER TABLE public.saas_docs ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- SaaS admin: acesso total (SELECT, INSERT, UPDATE, DELETE)
|
||||
-- Verificado via tabela saas_admins
|
||||
CREATE POLICY "saas_admin_full_access" ON public.saas_docs
|
||||
FOR ALL
|
||||
TO authenticated
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.saas_admins
|
||||
WHERE saas_admins.user_id = auth.uid()
|
||||
)
|
||||
)
|
||||
WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.saas_admins
|
||||
WHERE saas_admins.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- Admins de clínica: leem todos os docs ativos (usuario + admin)
|
||||
CREATE POLICY "clinic_admin_read_all_docs" ON public.saas_docs
|
||||
FOR SELECT
|
||||
TO authenticated
|
||||
USING (
|
||||
ativo = true
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM public.profiles
|
||||
WHERE profiles.id = auth.uid()
|
||||
AND profiles.role IN ('clinic_admin', 'tenant_admin')
|
||||
)
|
||||
);
|
||||
|
||||
-- Demais usuários autenticados: leem apenas docs do tipo 'usuario'
|
||||
CREATE POLICY "users_read_usuario_docs" ON public.saas_docs
|
||||
FOR SELECT
|
||||
TO authenticated
|
||||
USING (
|
||||
ativo = true
|
||||
AND tipo_acesso = 'usuario'
|
||||
);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 4. STORAGE — bucket saas-docs (imagens dos documentos)
|
||||
-- ------------------------------------------------------------
|
||||
INSERT INTO storage.buckets (id, name, public)
|
||||
VALUES ('saas-docs', 'saas-docs', true)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- SaaS admin: pode fazer upload
|
||||
CREATE POLICY "saas_admin_storage_upload" ON storage.objects
|
||||
FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (
|
||||
bucket_id = 'saas-docs'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM public.saas_admins
|
||||
WHERE saas_admins.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- SaaS admin: pode deletar
|
||||
CREATE POLICY "saas_admin_storage_delete" ON storage.objects
|
||||
FOR DELETE
|
||||
TO authenticated
|
||||
USING (
|
||||
bucket_id = 'saas-docs'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM public.saas_admins
|
||||
WHERE saas_admins.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- Leitura pública (bucket é público, mas policy explícita para clareza)
|
||||
CREATE POLICY "saas_docs_public_read" ON storage.objects
|
||||
FOR SELECT
|
||||
TO public
|
||||
USING (bucket_id = 'saas-docs');
|
||||
Reference in New Issue
Block a user