Correcao Sidebar Classico e Rail, Correcao Layout, Ajuste de Breakpoint para Tailwind, Ajuste AppTopbar, Ajuste Menu PopOver, Recriado Paleta de Cores, Inserido algumas animações leves, Reajuste Cor items NOVOS da tabela, Drawer Ajuda Corrigido no Logout, Whatsapp, sms, email, recursos extras
This commit is contained in:
@@ -20,7 +20,30 @@
|
|||||||
"Bash(find:*)",
|
"Bash(find:*)",
|
||||||
"Bash(ls:*)",
|
"Bash(ls:*)",
|
||||||
"Bash(npx vite:*)",
|
"Bash(npx vite:*)",
|
||||||
"Bash(powershell -Command \"$content = [System.IO.File]::ReadAllText\\(''src/views/pages/clinic/clinic/ClinicFeaturesPage.vue'', [System.Text.Encoding]::UTF8\\); $content = $content -replace [char]0x201C, ''\"\"'' -replace [char]0x201D, ''\"\"''; [System.IO.File]::WriteAllText\\(''src/views/pages/clinic/clinic/ClinicFeaturesPage.vue'', $content, [System.Text.Encoding]::UTF8\\)\")"
|
"Bash(powershell -Command \"$content = [System.IO.File]::ReadAllText\\(''src/views/pages/clinic/clinic/ClinicFeaturesPage.vue'', [System.Text.Encoding]::UTF8\\); $content = $content -replace [char]0x201C, ''\"\"'' -replace [char]0x201D, ''\"\"''; [System.IO.File]::WriteAllText\\(''src/views/pages/clinic/clinic/ClinicFeaturesPage.vue'', $content, [System.Text.Encoding]::UTF8\\)\")",
|
||||||
|
"Bash(xargs cat:*)",
|
||||||
|
"Bash(xxd \"D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/DBS/2026-03-21/schema.sql\")",
|
||||||
|
"Bash(iconv -f UTF-16LE -t UTF-8 \"D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/DBS/2026-03-21/schema.sql\")",
|
||||||
|
"Bash(mkdir -p docs/billing docs/planos docs/subscription-health docs/estrategia docs/specs)",
|
||||||
|
"Bash(mkdir -p database/backups database/migrations database/seeds database/fixes database/snippets)",
|
||||||
|
"Bash(cd:*)",
|
||||||
|
"Bash(mv disparando-whatsapp-local.md docs/whatsapp.md)",
|
||||||
|
"Bash(mv comandos.txt docs/)",
|
||||||
|
"Bash(mv dados-padrões-da-agenda.txt docs/)",
|
||||||
|
"Bash(mv USER_ARCHETYPES.html docs/)",
|
||||||
|
"Bash(mv Novo-DB/migration_*.sql database/migrations/)",
|
||||||
|
"Bash(mv Novo-DB/seed_*.sql database/seeds/)",
|
||||||
|
"Bash(mv Novo-DB/fix_*.sql database/fixes/)",
|
||||||
|
"Bash(npm install:*)",
|
||||||
|
"Bash(cp:*)",
|
||||||
|
"Bash(npm uninstall:*)",
|
||||||
|
"Bash(rm:*)",
|
||||||
|
"Bash(docker ps:*)",
|
||||||
|
"Bash(docker exec:*)",
|
||||||
|
"Bash(\"D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/database-novo/backups/2026-03-23/schema.sql\")",
|
||||||
|
"Bash(\"D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/database-novo/backups/2026-03-23/data.sql\")",
|
||||||
|
"Bash(\"D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/database-novo/backups/2026-03-23/full_dump.sql\")",
|
||||||
|
"Bash(wc:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,3 +14,4 @@ api-generator/typedoc.json
|
|||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
Dev-documentacao/
|
Dev-documentacao/
|
||||||
supabase/
|
supabase/
|
||||||
|
evolution-api/
|
||||||
15
.hintrc
Normal file
15
.hintrc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"development"
|
||||||
|
],
|
||||||
|
"hints": {
|
||||||
|
"compat-api/css": [
|
||||||
|
"default",
|
||||||
|
{
|
||||||
|
"ignore": [
|
||||||
|
"background-color: color-mix(in srgb, var(--primary-color) 50%, transparent)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,414 +0,0 @@
|
|||||||
-- ============================================================
|
|
||||||
-- 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;
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
-- =============================================================================
|
|
||||||
-- 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;
|
|
||||||
$$;
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
-- =============================================================================
|
|
||||||
-- 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.';
|
|
||||||
-- =============================================================================
|
|
||||||
@@ -1,334 +0,0 @@
|
|||||||
-- =============================================================================
|
|
||||||
-- 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;
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
-- =============================================================================
|
|
||||||
-- 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;
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
-- ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
-- 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;
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
-- ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
-- 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;
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
-- ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
-- 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;
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
-- ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
-- 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");
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
-- ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
-- 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;
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
-- ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
-- 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'
|
|
||||||
]));
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
-- ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
-- 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
|
|
||||||
);
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
-- 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;
|
|
||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,110 +0,0 @@
|
|||||||
-- =========================================================
|
|
||||||
-- 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());
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
-- =========================================================
|
|
||||||
-- 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;
|
|
||||||
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
-- =========================================================
|
|
||||||
-- 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
|
|
||||||
-- =========================================================
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
-- =========================================================
|
|
||||||
-- 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'));
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
/*
|
|
||||||
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');
|
|
||||||
*/
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
-- =========================================================
|
|
||||||
-- 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);
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
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());
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE public.agenda_configuracoes DROP COLUMN IF EXISTS
|
|
||||||
session_start_offset_min;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
select pg_get_functiondef('public.NOME_DA_FUNCAO(args_aqui)'::regprocedure);
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
-- 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();
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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 $$;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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();
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
drop index if exists public.uq_subscriptions_tenant;
|
|
||||||
drop index if exists public.uq_subscriptions_personal_user;
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
-- 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;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
-- ============================================================
|
|
||||||
-- 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;
|
|
||||||
$$;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
select *
|
|
||||||
from agenda_eventos
|
|
||||||
order by created_at desc nulls last
|
|
||||||
limit 10;
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
select
|
|
||||||
routine_name,
|
|
||||||
routine_type
|
|
||||||
from information_schema.routines
|
|
||||||
where routine_schema = 'public'
|
|
||||||
and routine_name ilike '%intake%';
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
select id as owner_id, email, created_at
|
|
||||||
from auth.users
|
|
||||||
where email = 'admin@agendapsi.com.br'
|
|
||||||
limit 1;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
select
|
|
||||||
id,
|
|
||||||
owner_id,
|
|
||||||
status,
|
|
||||||
created_at,
|
|
||||||
converted_patient_id
|
|
||||||
from public.patient_intake_requests
|
|
||||||
where id = '54daa09a-b2cb-4a0b-91aa-e4cea1915efe';
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
select id, key, name
|
|
||||||
from public.plans
|
|
||||||
order by key;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
SELECT * FROM public.agendador_solicitacoes LIMIT 5;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
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
|
|
||||||
);
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
-- 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;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
select *
|
|
||||||
from public.owner_feature_entitlements
|
|
||||||
where owner_id = '816b24fe-a0c3-4409-b79b-c6c0a6935d03'::uuid
|
|
||||||
order by feature_key;
|
|
||||||
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
select column_name, data_type
|
|
||||||
from information_schema.columns
|
|
||||||
where table_schema = 'public'
|
|
||||||
and table_name = 'patient_patient_tag'
|
|
||||||
order by ordinal_position;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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();
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
const { data, error } = await supabase.rpc('my_tenants')
|
|
||||||
console.log({ data, error })
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
-- 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;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
select id, name, kind, created_at
|
|
||||||
from public.tenants
|
|
||||||
order by created_at desc
|
|
||||||
limit 10;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
insert into public.users (id, created_at)
|
|
||||||
values ('e8b10543-fb36-4e75-9d37-6fece9745637', now());
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
select *
|
|
||||||
from public.tenant_members
|
|
||||||
where tenant_id = 'UUID_AQUI'
|
|
||||||
order by created_at desc;
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
-- 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;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
select id, name, created_at
|
|
||||||
from public.tenants
|
|
||||||
order by created_at desc;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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());
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
-- 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;
|
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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%';
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
-- =========================================================
|
|
||||||
-- 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
|
|
||||||
-- =========================================================
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
-- 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 || '/%'
|
|
||||||
);
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
select *
|
|
||||||
from tenant_members
|
|
||||||
where user_id = 'SEU_USER_ID';
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
-- 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;
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
-- ============================================================
|
|
||||||
-- 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');
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,414 +0,0 @@
|
|||||||
-- ============================================================
|
|
||||||
-- 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;
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
-- =============================================================================
|
|
||||||
-- 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;
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
-- 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;
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
-- =============================================================================
|
|
||||||
-- 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;
|
|
||||||
$$;
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
-- =============================================================================
|
|
||||||
-- 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.';
|
|
||||||
-- =============================================================================
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
-- =============================================================================
|
|
||||||
-- 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;
|
|
||||||
159
blueprints/DialogConfirmation-blueprint.md
Normal file
159
blueprints/DialogConfirmation-blueprint.md
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
# DialogConfirmation — Padrão de Componente
|
||||||
|
|
||||||
|
> **Stack**: Vue 3 + PrimeVue 4 + Tailwind CSS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Regras gerais
|
||||||
|
|
||||||
|
| Propriedade | Valor obrigatório |
|
||||||
|
|---|---|
|
||||||
|
| `group` | sempre `"headless"` — desacopla o template do trigger |
|
||||||
|
| `ConfirmDialog` | declarado **uma única vez**, no componente pai (página) |
|
||||||
|
| Filhos | disparam via `useConfirm()` com `group: 'headless'` — sem declarar `ConfirmDialog` próprio |
|
||||||
|
| `icon` | passado em `confirm.require({ icon })` — classe PrimeIcons sem o prefixo `pi` (ex: `'pi-trash'`) |
|
||||||
|
| `color` | passado em `confirm.require({ color })` — hex; define o fundo do círculo e a cor do botão Confirmar |
|
||||||
|
| `ConfirmationService` | obrigatório em `main.js` — `app.use(ConfirmationService)` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arquitetura pai / filho
|
||||||
|
|
||||||
|
```
|
||||||
|
Pai (página)
|
||||||
|
└── <ConfirmDialog group="headless" /> ← único, renderiza aqui
|
||||||
|
├── Filho A (componente qualquer) → confirm.require({ group: 'headless', ... })
|
||||||
|
└── Filho B (Dialog interno) → confirm.require({ group: 'headless', ... })
|
||||||
|
```
|
||||||
|
|
||||||
|
> O `ConfirmDialog` **não** deve ser colocado dentro de um `<Dialog>` filho — isso causaria dois popups simultâneos. Sempre no pai.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup obrigatório — `main.js`
|
||||||
|
|
||||||
|
```js
|
||||||
|
import ConfirmationService from 'primevue/confirmationservice'
|
||||||
|
|
||||||
|
app.use(ConfirmationService) // sem isso, useConfirm() não funciona
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Template do `ConfirmDialog` (somente no pai)
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- Declarado uma única vez, antes do conteúdo principal -->
|
||||||
|
<ConfirmDialog group="headless">
|
||||||
|
<template #container="{ message, acceptCallback, rejectCallback }">
|
||||||
|
<div class="flex flex-col items-center p-8 bg-surface-0 dark:bg-surface-900 rounded-xl shadow-xl">
|
||||||
|
|
||||||
|
<!-- Círculo central: cor e ícone vindos de message -->
|
||||||
|
<div
|
||||||
|
class="rounded-full inline-flex justify-center items-center h-24 w-24 -mt-20"
|
||||||
|
:style="{ background: message.color || 'var(--p-primary-color)', color: '#fff' }"
|
||||||
|
>
|
||||||
|
<i :class="`pi ${message.icon || 'pi-question'} !text-4xl`"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="font-bold text-2xl block mb-2 mt-6">{{ message.header }}</span>
|
||||||
|
<p class="mb-0 text-center text-[var(--text-color-secondary)]">{{ message.message }}</p>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 mt-6">
|
||||||
|
<!-- Confirmar: cor dinâmica via message.color -->
|
||||||
|
<Button
|
||||||
|
label="Confirmar"
|
||||||
|
class="rounded-full"
|
||||||
|
:style="{
|
||||||
|
background: message.color || 'var(--p-primary-color)',
|
||||||
|
borderColor: message.color || 'var(--p-primary-color)'
|
||||||
|
}"
|
||||||
|
@click="acceptCallback"
|
||||||
|
/>
|
||||||
|
<!-- Cancelar: sempre outlined, neutro -->
|
||||||
|
<Button
|
||||||
|
label="Cancelar"
|
||||||
|
variant="outlined"
|
||||||
|
class="rounded-full"
|
||||||
|
@click="rejectCallback"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ConfirmDialog>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Uso nos componentes (pai ou filhos)
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { useConfirm } from 'primevue/useconfirm'
|
||||||
|
const confirm = useConfirm()
|
||||||
|
|
||||||
|
function confirmDelete(item) {
|
||||||
|
confirm.require({
|
||||||
|
group: 'headless',
|
||||||
|
header: 'Excluir item?',
|
||||||
|
message: `"${item.name}" será removido permanentemente. Essa ação não pode ser desfeita.`,
|
||||||
|
icon: 'pi-trash',
|
||||||
|
color: '#ef4444',
|
||||||
|
accept: () => onDelete(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Paleta de ícones e cores por ação
|
||||||
|
|
||||||
|
| Ação | `icon` | `color` | Observação |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Excluir / Remover | `pi-trash` | `#ef4444` | Vermelho — ação destrutiva |
|
||||||
|
| Salvar / Confirmar | `pi-save` | `var(--p-primary-color)` | Cor primária do tema |
|
||||||
|
| Editar / Atualizar | `pi-pencil` | `#f97316` | Laranja — mudança de estado |
|
||||||
|
| Aviso / Atenção | `pi-exclamation-triangle` | `#eab308` | Amarelo — ação reversível |
|
||||||
|
| Info / Neutro | `pi-info-circle` | `#3b82f6` | Azul — informativo |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Referência completa de `confirm.require`
|
||||||
|
|
||||||
|
```js
|
||||||
|
confirm.require({
|
||||||
|
group: 'headless', // obrigatório — aponta para o ConfirmDialog correto
|
||||||
|
header: 'Título do popup', // linha em negrito
|
||||||
|
message: 'Descrição clara.', // linha secundária
|
||||||
|
icon: 'pi-trash', // sufixo PrimeIcons sem o "pi " inicial
|
||||||
|
color: '#ef4444', // hex — fundo do círculo + cor do botão Confirmar
|
||||||
|
accept: () => { /* ação confirmada */ },
|
||||||
|
reject: () => { /* opcional — ação cancelada */ }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checklist antes de usar
|
||||||
|
|
||||||
|
- [ ] `ConfirmationService` registrado no `main.js`
|
||||||
|
- [ ] `<ConfirmDialog group="headless">` declarado **apenas no pai**, antes do conteúdo
|
||||||
|
- [ ] Filhos usam `useConfirm()` com `group: 'headless'` — sem `ConfirmDialog` próprio
|
||||||
|
- [ ] `icon` passado como sufixo PrimeIcons: `'pi-trash'`, não `'pi pi-trash'`
|
||||||
|
- [ ] `color` em hex para ações com semântica de cor (delete = `#ef4444`)
|
||||||
|
- [ ] `header` curto e direto | `message` com contexto suficiente para o usuário decidir
|
||||||
|
- [ ] `accept` contém a ação real — `reject` é opcional
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Variações de confirmação
|
||||||
|
|
||||||
|
| Contexto | `header` | `icon` | `color` |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Excluir registro | `'Excluir <entidade>?'` | `pi-trash` | `#ef4444` |
|
||||||
|
| Remover item de lista | `'Remover campo?'` | `pi-trash` | `#ef4444` |
|
||||||
|
| Salvar com impacto | `'Confirmar alterações?'` | `pi-save` | primária |
|
||||||
|
| Atualizar com risco | `'Atualizar <entidade>?'` | `pi-pencil` | `#f97316` |
|
||||||
|
| Ação irreversível genérica | `'Tem certeza?'` | `pi-exclamation-triangle` | `#eab308` |
|
||||||
183
blueprints/dialog-blueprint.md
Normal file
183
blueprints/dialog-blueprint.md
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
# Dialog — Padrão de Componente
|
||||||
|
|
||||||
|
> **Stack**: Vue 3 + PrimeVue 4 + Tailwind CSS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Regras gerais
|
||||||
|
|
||||||
|
| Propriedade | Valor obrigatório |
|
||||||
|
|---|---|
|
||||||
|
| `modal` | sempre `true` |
|
||||||
|
| `maximizable` | sempre presente — botão nativo do PrimeVue, sem estado manual |
|
||||||
|
| `:draggable` | sempre `false` |
|
||||||
|
| `:closable` | `!saving` — desabilita o X durante operações assíncronas |
|
||||||
|
| `:dismissableMask` | `!saving` — impede fechar clicando fora durante saving |
|
||||||
|
| `pt:mask:class` | `backdrop-blur-xs` |
|
||||||
|
| Largura | `w-[50rem]` (padrão); responsivo via `:breakpoints` |
|
||||||
|
| Breakpoints | `{ '1199px': '90vw', '768px': '94vw' }` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estrutura obrigatória
|
||||||
|
|
||||||
|
```
|
||||||
|
<Dialog>
|
||||||
|
├── #header ← dot de cor (se aplicável), título/subtítulo, btn Excluir
|
||||||
|
├── Banner ← preview visual (opcional — apenas quando há cor/identidade visual)
|
||||||
|
├── Corpo ← campos do formulário
|
||||||
|
└── #footer ← Cancelar (flat) | Salvar (primary)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuração completa do `<Dialog>`
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<Dialog
|
||||||
|
v-model:visible="visible"
|
||||||
|
modal
|
||||||
|
:draggable="false"
|
||||||
|
:closable="!saving"
|
||||||
|
:dismissableMask="!saving"
|
||||||
|
maximizable
|
||||||
|
class="dc-dialog w-[50rem]"
|
||||||
|
:breakpoints="{ '1199px': '90vw', '768px': '94vw' }"
|
||||||
|
:pt="{
|
||||||
|
header: { class: '!p-3 !rounded-t-[12px] border-b border-[var(--surface-border)] shadow-[0_1px_0_0_rgba(255,255,255,0.06)] bg-gray-100' },
|
||||||
|
content: { class: '!p-3' },
|
||||||
|
footer: { class: '!p-0 !rounded-b-[12px] border-t border-[var(--surface-border)] shadow-[0_1px_0_0_rgba(255,255,255,0.06)] bg-gray-100' },
|
||||||
|
pcCloseButton: { root: { class: '!rounded-md hover:!text-red-500' } },
|
||||||
|
pcMaximizeButton: { root: { class: '!rounded-md hover:!text-primary' } },
|
||||||
|
}"
|
||||||
|
pt:mask:class="backdrop-blur-xs"
|
||||||
|
>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Detalhes do `pt`
|
||||||
|
|
||||||
|
| Chave | O que faz |
|
||||||
|
|---|---|
|
||||||
|
| `header` | `!p-3` padding uniforme; `!rounded-t-[12px]` borda top arredondada; `border-b` + `shadow` separador com profundidade; `bg-gray-100` fundo levemente cinza |
|
||||||
|
| `content` | `!p-3` padding interno do corpo |
|
||||||
|
| `footer` | `!p-0` remove padding nativo (controlado pelo wrapper interno); `!rounded-b-[12px]` borda bottom arredondada; `border-t` + `shadow` separador; `bg-gray-100` fundo levemente cinza |
|
||||||
|
| `pcCloseButton` | `!rounded-md` remove o círculo nativo; `hover:!text-red-500` feedback de danger no hover |
|
||||||
|
| `pcMaximizeButton` | `!rounded-md` remove o círculo nativo; `hover:!text-primary` feedback de cor primária no hover |
|
||||||
|
|
||||||
|
> O `!` (important) é necessário porque o PrimeVue injeta estilos inline nos botões e no root do Dialog — sem ele o Tailwind perde a disputa de especificidade.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Header — slot `#header`
|
||||||
|
|
||||||
|
```
|
||||||
|
[dot-cor] [título / subtítulo] [btn-excluir] ← Close e Maximize nativos vêm após
|
||||||
|
```
|
||||||
|
|
||||||
|
- O PrimeVue injeta **Maximize** e **Close** automaticamente à direita do slot `#header`.
|
||||||
|
- O botão **Excluir** fica **sempre no header**, nunca no footer.
|
||||||
|
- Excluir desabilitado quando o registro é nativo/padrão: `:disabled="saving || isNativeRecord"`.
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template #header>
|
||||||
|
<div class="flex w-full items-center justify-between gap-3 px-1">
|
||||||
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
|
<!-- Dot de cor (omitir se não houver cor associada) -->
|
||||||
|
<span
|
||||||
|
class="shrink-0 w-3.5 h-3.5 rounded-full border-2 border-white/30
|
||||||
|
shadow-[0_0_0_3px_rgba(0,0,0,0.08)] transition-colors duration-200"
|
||||||
|
:style="{ backgroundColor: previewBgColor }"
|
||||||
|
/>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<div class="text-base font-semibold truncate">
|
||||||
|
{{ form.name || (mode === 'create' ? 'Novo item' : 'Editar item') }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs opacity-50">
|
||||||
|
{{ mode === 'create' ? 'Criando novo registro' : 'Editando registro' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
|
<!-- Excluir — visível apenas em edit, desabilitado se nativo -->
|
||||||
|
<Button
|
||||||
|
v-if="mode === 'edit' && canDelete !== undefined"
|
||||||
|
icon="pi pi-trash"
|
||||||
|
severity="danger"
|
||||||
|
text
|
||||||
|
rounded
|
||||||
|
:disabled="saving || isNativeRecord"
|
||||||
|
v-tooltip.top="'Excluir'"
|
||||||
|
@click="emitDelete"
|
||||||
|
/>
|
||||||
|
<!-- Maximize e Close nativos do PrimeVue são injetados aqui automaticamente -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Footer — slot `#footer`
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex items-center justify-end gap-2 px-3 py-3">
|
||||||
|
<!-- Cancelar: sempre flat, hover vermelho suave -->
|
||||||
|
<Button
|
||||||
|
label="Cancelar"
|
||||||
|
severity="secondary"
|
||||||
|
text
|
||||||
|
class="rounded-full hover:!text-red-500"
|
||||||
|
:disabled="saving"
|
||||||
|
@click="close"
|
||||||
|
/>
|
||||||
|
<!-- Salvar: sempre primary -->
|
||||||
|
<Button
|
||||||
|
label="Salvar"
|
||||||
|
icon="pi pi-check"
|
||||||
|
class="rounded-full"
|
||||||
|
:loading="saving"
|
||||||
|
:disabled="!canSubmit"
|
||||||
|
@click="submit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Regra**: Cancelar = `severity="secondary" text` + `hover:!text-red-500`. Salvar = primary (sem severity, usa o padrão do tema). Padding controlado pelo `div` interno (`px-3 py-3`), não pelo `pt.footer`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Maximizar
|
||||||
|
|
||||||
|
Use a prop nativa `maximizable`. O PrimeVue injeta e gerencia o botão automaticamente — sem `ref`, sem `isMaximized`, sem `<Button>` manual.
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<Dialog maximizable ...>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checklist antes de publicar um Dialog
|
||||||
|
|
||||||
|
- [ ] `modal`, `:draggable="false"`, `:closable="!saving"`, `:dismissableMask="!saving"` presentes
|
||||||
|
- [ ] `maximizable` na prop (botão nativo, sem estado manual)
|
||||||
|
- [ ] `class="dc-dialog w-[50rem]"` + `:breakpoints="{ '1199px': '90vw', '768px': '94vw' }"`
|
||||||
|
- [ ] `pt` completo: header, content, footer, pcCloseButton, pcMaximizeButton
|
||||||
|
- [ ] Header com `bg-gray-100`, `border-b`, shadow e `!rounded-t-[12px]`
|
||||||
|
- [ ] Footer com `bg-gray-100`, `border-t`, shadow e `!rounded-b-[12px]`
|
||||||
|
- [ ] Botão **Excluir** no header (nunca no footer), desabilitado se nativo
|
||||||
|
- [ ] Cancelar = `text` + `hover:!text-red-500` | Salvar = primary
|
||||||
|
- [ ] Padding do footer via `px-3 py-3` no `div` interno
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Variações de largura
|
||||||
|
|
||||||
|
| Uso | Classe |
|
||||||
|
|---|---|
|
||||||
|
| Formulário simples | `w-[36rem]` |
|
||||||
|
| Formulário padrão | `w-[50rem]` ← **padrão** |
|
||||||
|
| Formulário complexo | `w-[70rem]` |
|
||||||
|
| Tela cheia | `maximizable` — usuário controla |
|
||||||
119
database-novo/README.md
Normal file
119
database-novo/README.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# database-novo
|
||||||
|
|
||||||
|
Banco de dados do AgenciaPsi — organizado, documentado e com CLI para gerenciamento.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd database-novo
|
||||||
|
|
||||||
|
# Instalação do zero (schema + fixes + seeds + backup)
|
||||||
|
node db.cjs setup
|
||||||
|
|
||||||
|
# Ver estado do banco
|
||||||
|
node db.cjs status
|
||||||
|
|
||||||
|
# Backup
|
||||||
|
node db.cjs backup
|
||||||
|
|
||||||
|
# Restaurar (perdi o banco!)
|
||||||
|
node db.cjs restore
|
||||||
|
```
|
||||||
|
|
||||||
|
Para o guia completo, veja **`docs/setup_guide.md`**.
|
||||||
|
|
||||||
|
## Comandos do CLI
|
||||||
|
|
||||||
|
| Comando | O que faz |
|
||||||
|
|---------|-----------|
|
||||||
|
| `node db.cjs setup` | Instala do zero (schema + fixes + seeds) |
|
||||||
|
| `node db.cjs backup` | Exporta backup com data para `backups/` |
|
||||||
|
| `node db.cjs restore [data]` | Restaura de um backup |
|
||||||
|
| `node db.cjs migrate` | Aplica migrations pendentes |
|
||||||
|
| `node db.cjs seed [grupo]` | Roda seeds (all, users, system, test_data) |
|
||||||
|
| `node db.cjs status` | Estado do banco, backups, migrations |
|
||||||
|
| `node db.cjs diff` | Compara schema atual vs último backup |
|
||||||
|
| `node db.cjs reset` | Reseta e reinstala tudo |
|
||||||
|
| `node db.cjs verify` | Verifica integridade dos dados |
|
||||||
|
|
||||||
|
## Estrutura
|
||||||
|
|
||||||
|
```
|
||||||
|
database-novo/
|
||||||
|
│
|
||||||
|
├── db.cjs # CLI de gerenciamento do banco
|
||||||
|
├── db.config.json # Configuração (container, seeds, fixes)
|
||||||
|
│
|
||||||
|
├── schema/ # Schema SQL separado por seção
|
||||||
|
│ ├── 00_full/schema.sql # Schema completo (referência)
|
||||||
|
│ ├── 01_extensions/ # Schemas + extensões PostgreSQL
|
||||||
|
│ ├── 02_types/ # Enums (auth, public, infra)
|
||||||
|
│ ├── 03_functions/ # 11 arquivos por domínio
|
||||||
|
│ ├── 04_tables/ # 10 arquivos por domínio
|
||||||
|
│ ├── 05_views/ # 24 views
|
||||||
|
│ ├── 06_indexes/ # Índices
|
||||||
|
│ ├── 07_foreign_keys/ # PKs, FKs, constraints
|
||||||
|
│ ├── 08_triggers/ # Triggers
|
||||||
|
│ ├── 09_policies/ # 217 RLS policies
|
||||||
|
│ └── 10_grants/ # Grants
|
||||||
|
│
|
||||||
|
├── seeds/ # Seeds de dados
|
||||||
|
│ ├── seed_001_fixed.sql # 6 usuários base + tenants
|
||||||
|
│ ├── seed_002.sql # Supervisor + Editor
|
||||||
|
│ ├── seed_003.sql # Therapist2, Therapist3, Secretary
|
||||||
|
│ ├── seed_010_plans.sql # 7 planos + 4 preços
|
||||||
|
│ ├── seed_011_features.sql # 26 features
|
||||||
|
│ ├── seed_012_plan_features.sql # 85 vínculos plano↔feature
|
||||||
|
│ ├── seed_013_subscriptions.sql # 9 subscriptions + compromissos
|
||||||
|
│ ├── seed_014_global_data.sql # 11 email + 16 notif templates + 3 slides
|
||||||
|
│ ├── seed_020_test_data.sql # Dados de teste (50 pacientes, eventos, etc.)
|
||||||
|
│ ├── seed_020_test_data_cleanup.sql # Limpeza dos dados de teste
|
||||||
|
│ └── run_all_seeds.sh # Script bash alternativo
|
||||||
|
│
|
||||||
|
├── migrations/ # Migrations incrementais
|
||||||
|
│
|
||||||
|
├── fixes/ # 7 correções aplicadas
|
||||||
|
│
|
||||||
|
├── backups/ # Backups com data (auto-gerenciados)
|
||||||
|
│ └── 2026-03-23/ # schema.sql + data.sql + full_dump.sql
|
||||||
|
│
|
||||||
|
└── docs/ # Documentação
|
||||||
|
├── setup_guide.md # Guia completo de instalação e uso
|
||||||
|
├── schema_map.md # Mapa das 84 tabelas
|
||||||
|
├── business_rules.md # Regras de negócio
|
||||||
|
└── users_test.md # 11 usuários de teste (UUIDs + vínculos)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Planos
|
||||||
|
|
||||||
|
| Key | Target | Preço | Limites |
|
||||||
|
|-----|--------|-------|---------|
|
||||||
|
| `patient_free` | patient | R$0 | — |
|
||||||
|
| `therapist_free` | therapist | R$0 | 40 agendamentos/mês, 50 lembretes/mês |
|
||||||
|
| `therapist_pro` | therapist | R$49/mês · R$490/ano | Ilimitado |
|
||||||
|
| `clinic_free` | clinic | R$0 | 30 pacientes, 5 terapeutas, 40 agend/mês |
|
||||||
|
| `clinic_pro` | clinic | R$149/mês · R$1490/ano | Ilimitado |
|
||||||
|
| `supervisor_free` | supervisor | R$0 | Até 3 supervisionados |
|
||||||
|
| `supervisor_pro` | supervisor | R$0 | Até 20 supervisionados |
|
||||||
|
|
||||||
|
## Usuários de Teste
|
||||||
|
|
||||||
|
Senha de todos: `Teste@123`
|
||||||
|
|
||||||
|
| Email | Plano | Tipo |
|
||||||
|
|-------|-------|------|
|
||||||
|
| paciente@agenciapsi.com.br | patient_free | Paciente |
|
||||||
|
| terapeuta@agenciapsi.com.br | therapist_free | Terapeuta solo + Clínica 3 |
|
||||||
|
| clinica1@agenciapsi.com.br | clinic_free | Clínica coworking |
|
||||||
|
| clinica2@agenciapsi.com.br | clinic_free | Clínica recepção |
|
||||||
|
| clinica3@agenciapsi.com.br | clinic_free | Clínica full |
|
||||||
|
| saas@agenciapsi.com.br | — | Admin plataforma |
|
||||||
|
| supervisor@agenciapsi.com.br | supervisor_free | Supervisor |
|
||||||
|
| editor@agenciapsi.com.br | therapist_free | Editor |
|
||||||
|
| therapist2@agenciapsi.com.br | therapist_free | Terapeuta |
|
||||||
|
| therapist3@agenciapsi.com.br | therapist_free | Terapeuta |
|
||||||
|
| secretary@agenciapsi.com.br | — | Secretária (Clínica 2) |
|
||||||
|
|
||||||
|
## Idempotência
|
||||||
|
|
||||||
|
Todos os seeds são idempotentes (ON CONFLICT DO UPDATE ou DELETE + INSERT). Podem ser re-executados quantas vezes necessário.
|
||||||
1347
database-novo/backups/2026-03-23/data.sql
Normal file
1347
database-novo/backups/2026-03-23/data.sql
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
733
database-novo/db.cjs
Normal file
733
database-novo/db.cjs
Normal file
@@ -0,0 +1,733 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// =============================================================================
|
||||||
|
// AgenciaPsi — Database CLI
|
||||||
|
// =============================================================================
|
||||||
|
// Uso: node db.cjs <comando> [opcoes]
|
||||||
|
//
|
||||||
|
// Comandos:
|
||||||
|
// setup Instalação do zero (schema + seeds)
|
||||||
|
// backup Exporta backup com data atual
|
||||||
|
// restore [data] Restaura de um backup (ex: 2026-03-23)
|
||||||
|
// migrate Aplica migrations pendentes
|
||||||
|
// seed [grupo] Roda seeds (all|users|system|test_data)
|
||||||
|
// status Mostra estado atual do banco
|
||||||
|
// diff Compara schema atual vs último backup
|
||||||
|
// reset Reseta o banco e reinstala tudo
|
||||||
|
// verify Verifica integridade dos dados essenciais
|
||||||
|
// help Mostra ajuda
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const { execSync, spawnSync } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Config
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const ROOT = __dirname;
|
||||||
|
const CONFIG = JSON.parse(fs.readFileSync(path.join(ROOT, 'db.config.json'), 'utf8'));
|
||||||
|
const CONTAINER = CONFIG.container;
|
||||||
|
const DB = CONFIG.database;
|
||||||
|
const USER = CONFIG.user;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Colors (sem dependências externas)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const c = {
|
||||||
|
reset: '\x1b[0m',
|
||||||
|
bold: '\x1b[1m',
|
||||||
|
red: '\x1b[31m',
|
||||||
|
green: '\x1b[32m',
|
||||||
|
yellow: '\x1b[33m',
|
||||||
|
blue: '\x1b[34m',
|
||||||
|
cyan: '\x1b[36m',
|
||||||
|
gray: '\x1b[90m'
|
||||||
|
};
|
||||||
|
|
||||||
|
function log(msg) {
|
||||||
|
console.log(msg);
|
||||||
|
}
|
||||||
|
function info(msg) {
|
||||||
|
log(`${c.cyan}ℹ${c.reset} ${msg}`);
|
||||||
|
}
|
||||||
|
function ok(msg) {
|
||||||
|
log(`${c.green}✔${c.reset} ${msg}`);
|
||||||
|
}
|
||||||
|
function warn(msg) {
|
||||||
|
log(`${c.yellow}⚠${c.reset} ${msg}`);
|
||||||
|
}
|
||||||
|
function err(msg) {
|
||||||
|
log(`${c.red}✖${c.reset} ${msg}`);
|
||||||
|
}
|
||||||
|
function title(msg) {
|
||||||
|
log(`\n${c.bold}${c.blue}═══ ${msg} ═══${c.reset}\n`);
|
||||||
|
}
|
||||||
|
function step(msg) {
|
||||||
|
log(`${c.gray} →${c.reset} ${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function dockerRunning() {
|
||||||
|
try {
|
||||||
|
const result = spawnSync('docker', ['inspect', '-f', '{{.State.Running}}', CONTAINER], {
|
||||||
|
encoding: 'utf8',
|
||||||
|
timeout: 10000,
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
return result.stdout.trim() === 'true';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function psql(sql, opts = {}) {
|
||||||
|
const cmd = `docker exec -i -e PGCLIENTENCODING=UTF8 ${CONTAINER} psql -U ${USER} -d ${DB} ${opts.tuples ? '-t' : ''} ${opts.quiet ? '-q' : ''} -c "${sql.replace(/"/g, '\\"')}"`;
|
||||||
|
return execSync(cmd, { encoding: 'utf8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, PYTHONIOENCODING: 'utf-8', LANG: 'C.UTF-8' } }).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function psqlFile(filePath) {
|
||||||
|
const absPath = path.resolve(filePath);
|
||||||
|
const content = fs.readFileSync(absPath, 'utf8');
|
||||||
|
// Prepend SET client_encoding to ensure UTF-8 inside the session
|
||||||
|
const utf8Content = "SET client_encoding TO 'UTF8';\n" + content;
|
||||||
|
const cmd = `docker exec -i -e PGCLIENTENCODING=UTF8 ${CONTAINER} psql -U ${USER} -d ${DB} -q`;
|
||||||
|
return execSync(cmd, { input: utf8Content, encoding: 'utf8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, PYTHONIOENCODING: 'utf-8', LANG: 'C.UTF-8' } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function pgDump(args) {
|
||||||
|
const cmd = `docker exec -e PGCLIENTENCODING=UTF8 ${CONTAINER} pg_dump -U ${USER} -d ${DB} ${args}`;
|
||||||
|
return execSync(cmd, { encoding: 'utf8', timeout: 120000, maxBuffer: 50 * 1024 * 1024 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function today() {
|
||||||
|
return new Date().toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileHash(filePath) {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureDir(dir) {
|
||||||
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireDocker() {
|
||||||
|
if (!dockerRunning()) {
|
||||||
|
err(`Container "${CONTAINER}" não está rodando.`);
|
||||||
|
log(`\n Inicie o Supabase primeiro:`);
|
||||||
|
log(` ${c.cyan}npx supabase start${c.reset}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function listBackups() {
|
||||||
|
const dir = path.join(ROOT, 'backups');
|
||||||
|
if (!fs.existsSync(dir)) return [];
|
||||||
|
return fs
|
||||||
|
.readdirSync(dir)
|
||||||
|
.filter((f) => /^\d{4}-\d{2}-\d{2}$/.test(f))
|
||||||
|
.sort()
|
||||||
|
.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Migration tracking table
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function ensureMigrationTable() {
|
||||||
|
psql(`
|
||||||
|
CREATE TABLE IF NOT EXISTS _db_migrations (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
filename TEXT NOT NULL UNIQUE,
|
||||||
|
hash TEXT NOT NULL,
|
||||||
|
category TEXT NOT NULL DEFAULT 'migration',
|
||||||
|
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAppliedMigrations() {
|
||||||
|
ensureMigrationTable();
|
||||||
|
const result = psql('SELECT filename, hash, category, applied_at::text FROM _db_migrations ORDER BY id;', { tuples: true });
|
||||||
|
if (!result) return [];
|
||||||
|
return result
|
||||||
|
.split('\n')
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((line) => {
|
||||||
|
const [filename, hash, category, applied_at] = line.split('|').map((s) => s.trim());
|
||||||
|
return { filename, hash, category, applied_at };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function recordMigration(filename, hash, category) {
|
||||||
|
psql(`INSERT INTO _db_migrations (filename, hash, category) VALUES ('${filename}', '${hash}', '${category}') ON CONFLICT (filename) DO UPDATE SET hash = EXCLUDED.hash, applied_at = now();`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Commands
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const commands = {};
|
||||||
|
|
||||||
|
// ---- SETUP ----
|
||||||
|
commands.setup = function () {
|
||||||
|
title('Setup — Instalação do zero');
|
||||||
|
requireDocker();
|
||||||
|
|
||||||
|
// 1. Schema
|
||||||
|
const schemaFile = path.join(ROOT, CONFIG.schema);
|
||||||
|
if (!fs.existsSync(schemaFile)) {
|
||||||
|
err(`Schema não encontrado: ${schemaFile}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
info('Aplicando schema...');
|
||||||
|
psqlFile(schemaFile);
|
||||||
|
ok('Schema aplicado');
|
||||||
|
|
||||||
|
// 2. Fixes
|
||||||
|
info('Aplicando fixes...');
|
||||||
|
for (const fix of CONFIG.fixes) {
|
||||||
|
const fixPath = path.join(ROOT, 'fixes', fix);
|
||||||
|
if (fs.existsSync(fixPath)) {
|
||||||
|
step(fix);
|
||||||
|
psqlFile(fixPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ok(`${CONFIG.fixes.length} fixes aplicados`);
|
||||||
|
|
||||||
|
// 3. Seeds
|
||||||
|
commands.seed('all');
|
||||||
|
|
||||||
|
// 4. Migration table
|
||||||
|
ensureMigrationTable();
|
||||||
|
|
||||||
|
// 5. Record seeds as applied
|
||||||
|
const allSeeds = [...CONFIG.seeds.users, ...CONFIG.seeds.system];
|
||||||
|
for (const seed of allSeeds) {
|
||||||
|
const seedPath = path.join(ROOT, 'seeds', seed);
|
||||||
|
if (fs.existsSync(seedPath)) {
|
||||||
|
recordMigration(seed, fileHash(seedPath), 'seed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const fix of CONFIG.fixes) {
|
||||||
|
const fixPath = path.join(ROOT, 'fixes', fix);
|
||||||
|
if (fs.existsSync(fixPath)) {
|
||||||
|
recordMigration(fix, fileHash(fixPath), 'fix');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ok('Setup completo!');
|
||||||
|
log('');
|
||||||
|
|
||||||
|
// 6. Auto-backup
|
||||||
|
info('Criando backup pós-setup...');
|
||||||
|
commands.backup();
|
||||||
|
|
||||||
|
// 7. Verify
|
||||||
|
commands.verify();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- BACKUP ----
|
||||||
|
commands.backup = function () {
|
||||||
|
title('Backup');
|
||||||
|
requireDocker();
|
||||||
|
|
||||||
|
const date = today();
|
||||||
|
const dir = path.join(ROOT, 'backups', date);
|
||||||
|
ensureDir(dir);
|
||||||
|
|
||||||
|
const infraSchemas = ['storage', 'realtime', '_realtime', 'supabase_functions', 'extensions', 'graphql', 'graphql_public', 'pgsodium', 'vault', 'net', '_analytics'];
|
||||||
|
const excludeFlags = infraSchemas.map((s) => `--exclude-schema=${s}`).join(' ');
|
||||||
|
|
||||||
|
step('Exportando schema...');
|
||||||
|
const schema = pgDump('--schema-only --no-owner --no-privileges');
|
||||||
|
fs.writeFileSync(path.join(dir, 'schema.sql'), schema);
|
||||||
|
|
||||||
|
step('Exportando dados...');
|
||||||
|
const data = pgDump(`--data-only --no-owner --no-privileges ${excludeFlags}`);
|
||||||
|
fs.writeFileSync(path.join(dir, 'data.sql'), data);
|
||||||
|
|
||||||
|
step('Exportando dump completo...');
|
||||||
|
const full = pgDump('--no-owner --no-privileges');
|
||||||
|
fs.writeFileSync(path.join(dir, 'full_dump.sql'), full);
|
||||||
|
|
||||||
|
const sizes = ['schema.sql', 'data.sql', 'full_dump.sql'].map((f) => {
|
||||||
|
const stat = fs.statSync(path.join(dir, f));
|
||||||
|
return `${f}: ${(stat.size / 1024).toFixed(0)}KB`;
|
||||||
|
});
|
||||||
|
|
||||||
|
ok(`Backup salvo em backups/${date}/`);
|
||||||
|
sizes.forEach((s) => step(s));
|
||||||
|
|
||||||
|
// Cleanup old backups
|
||||||
|
cleanupBackups();
|
||||||
|
};
|
||||||
|
|
||||||
|
function cleanupBackups() {
|
||||||
|
const backups = listBackups();
|
||||||
|
const cutoff = new Date();
|
||||||
|
cutoff.setDate(cutoff.getDate() - CONFIG.backupRetentionDays);
|
||||||
|
const cutoffStr = cutoff.toISOString().slice(0, 10);
|
||||||
|
|
||||||
|
let removed = 0;
|
||||||
|
for (const b of backups) {
|
||||||
|
if (b < cutoffStr) {
|
||||||
|
const dir = path.join(ROOT, 'backups', b);
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (removed > 0) {
|
||||||
|
info(`${removed} backup(s) antigo(s) removido(s) (>${CONFIG.backupRetentionDays} dias)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- RESTORE ----
|
||||||
|
commands.restore = function (dateArg) {
|
||||||
|
title('Restore');
|
||||||
|
requireDocker();
|
||||||
|
|
||||||
|
const backups = listBackups();
|
||||||
|
if (backups.length === 0) {
|
||||||
|
err('Nenhum backup encontrado.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = dateArg || backups[0];
|
||||||
|
const dir = path.join(ROOT, 'backups', date);
|
||||||
|
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
err(`Backup não encontrado: ${date}`);
|
||||||
|
log(`\n Backups disponíveis:`);
|
||||||
|
backups.forEach((b) => log(` ${c.cyan}${b}${c.reset}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullDump = path.join(dir, 'full_dump.sql');
|
||||||
|
const schemaFile = path.join(dir, 'schema.sql');
|
||||||
|
const dataFile = path.join(dir, 'data.sql');
|
||||||
|
|
||||||
|
// Safety backup before restore
|
||||||
|
info('Criando backup de segurança antes do restore...');
|
||||||
|
try {
|
||||||
|
commands.backup();
|
||||||
|
} catch {
|
||||||
|
warn('Não foi possível criar backup de segurança (banco pode estar vazio)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(fullDump)) {
|
||||||
|
info(`Restaurando de backups/${date}/full_dump.sql ...`);
|
||||||
|
|
||||||
|
// Drop and recreate public schema
|
||||||
|
step('Limpando schema public...');
|
||||||
|
psql('DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO postgres; GRANT ALL ON SCHEMA public TO public;');
|
||||||
|
|
||||||
|
step('Aplicando full dump...');
|
||||||
|
psqlFile(fullDump);
|
||||||
|
} else if (fs.existsSync(schemaFile) && fs.existsSync(dataFile)) {
|
||||||
|
info(`Restaurando de backups/${date}/ (schema + data)...`);
|
||||||
|
|
||||||
|
step('Limpando schema public...');
|
||||||
|
psql('DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO postgres; GRANT ALL ON SCHEMA public TO public;');
|
||||||
|
|
||||||
|
step('Aplicando schema...');
|
||||||
|
psqlFile(schemaFile);
|
||||||
|
|
||||||
|
step('Aplicando dados...');
|
||||||
|
psqlFile(dataFile);
|
||||||
|
} else {
|
||||||
|
err(`Backup incompleto em ${date}/`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ok(`Banco restaurado de backups/${date}/`);
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
commands.verify();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- MIGRATE ----
|
||||||
|
commands.migrate = function () {
|
||||||
|
title('Migrate');
|
||||||
|
requireDocker();
|
||||||
|
ensureMigrationTable();
|
||||||
|
|
||||||
|
const migrationsDir = path.join(ROOT, 'migrations');
|
||||||
|
if (!fs.existsSync(migrationsDir)) {
|
||||||
|
info('Nenhuma pasta migrations/ encontrada.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs
|
||||||
|
.readdirSync(migrationsDir)
|
||||||
|
.filter((f) => f.endsWith('.sql'))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
info('Nenhuma migration encontrada.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const applied = getAppliedMigrations().map((m) => m.filename);
|
||||||
|
const pending = files.filter((f) => !applied.includes(f));
|
||||||
|
|
||||||
|
if (pending.length === 0) {
|
||||||
|
ok('Todas as migrations já foram aplicadas.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-backup before migrating
|
||||||
|
info('Criando backup antes de migrar...');
|
||||||
|
commands.backup();
|
||||||
|
|
||||||
|
info(`${pending.length} migration(s) pendente(s):`);
|
||||||
|
for (const file of pending) {
|
||||||
|
step(`Aplicando ${file}...`);
|
||||||
|
const filePath = path.join(migrationsDir, file);
|
||||||
|
try {
|
||||||
|
psqlFile(filePath);
|
||||||
|
recordMigration(file, fileHash(filePath), 'migration');
|
||||||
|
ok(` ${file}`);
|
||||||
|
} catch (e) {
|
||||||
|
err(` FALHA em ${file}: ${e.message}`);
|
||||||
|
err('Migration abortada. Banco pode estar em estado parcial.');
|
||||||
|
err('Use "node db.cjs restore" para voltar ao backup.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ok(`${pending.length} migration(s) aplicada(s)`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- SEED ----
|
||||||
|
commands.seed = function (group) {
|
||||||
|
const validGroups = ['all', 'users', 'system', 'test_data'];
|
||||||
|
if (!group) group = 'all';
|
||||||
|
|
||||||
|
if (!validGroups.includes(group)) {
|
||||||
|
err(`Grupo inválido: ${group}`);
|
||||||
|
log(` Grupos válidos: ${validGroups.join(', ')}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
title(`Seeds — ${group}`);
|
||||||
|
requireDocker();
|
||||||
|
|
||||||
|
const groups = group === 'all' ? ['users', 'system'] : [group];
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
for (const g of groups) {
|
||||||
|
const seeds = CONFIG.seeds[g];
|
||||||
|
if (!seeds || seeds.length === 0) continue;
|
||||||
|
|
||||||
|
info(`Grupo: ${g}`);
|
||||||
|
for (const seed of seeds) {
|
||||||
|
const seedPath = path.join(ROOT, 'seeds', seed);
|
||||||
|
if (!fs.existsSync(seedPath)) {
|
||||||
|
warn(` Arquivo não encontrado: ${seed}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
step(seed);
|
||||||
|
try {
|
||||||
|
psqlFile(seedPath);
|
||||||
|
total++;
|
||||||
|
} catch (e) {
|
||||||
|
err(` FALHA em ${seed}: ${e.stderr || e.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ok(`${total} seed(s) aplicado(s)`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- STATUS ----
|
||||||
|
commands.status = function () {
|
||||||
|
title('Status');
|
||||||
|
requireDocker();
|
||||||
|
|
||||||
|
// Docker
|
||||||
|
ok(`Container: ${CONTAINER} (rodando)`);
|
||||||
|
|
||||||
|
// Backups
|
||||||
|
const backups = listBackups();
|
||||||
|
if (backups.length > 0) {
|
||||||
|
ok(`Último backup: ${backups[0]}`);
|
||||||
|
info(`Total de backups: ${backups.length}`);
|
||||||
|
} else {
|
||||||
|
warn('Nenhum backup encontrado');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrations
|
||||||
|
try {
|
||||||
|
const applied = getAppliedMigrations();
|
||||||
|
if (applied.length > 0) {
|
||||||
|
info(`Migrations aplicadas: ${applied.length}`);
|
||||||
|
applied.slice(-5).forEach((m) => {
|
||||||
|
step(`${m.filename} ${c.gray}(${m.category}, ${m.applied_at})${c.reset}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pending
|
||||||
|
const migrationsDir = path.join(ROOT, 'migrations');
|
||||||
|
if (fs.existsSync(migrationsDir)) {
|
||||||
|
const files = fs.readdirSync(migrationsDir).filter((f) => f.endsWith('.sql'));
|
||||||
|
const pending = files.filter((f) => !applied.map((m) => m.filename).includes(f));
|
||||||
|
if (pending.length > 0) {
|
||||||
|
warn(`${pending.length} migration(s) pendente(s):`);
|
||||||
|
pending.forEach((f) => step(`${c.yellow}${f}${c.reset}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
info('Tabela _db_migrations não existe (rode setup primeiro)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// DB counts
|
||||||
|
log('');
|
||||||
|
info('Dados no banco:');
|
||||||
|
const counts = [
|
||||||
|
['auth.users', 'SELECT count(*) FROM auth.users'],
|
||||||
|
['profiles', 'SELECT count(*) FROM profiles'],
|
||||||
|
['tenants', 'SELECT count(*) FROM tenants'],
|
||||||
|
['plans', 'SELECT count(*) FROM plans'],
|
||||||
|
['features', 'SELECT count(*) FROM features'],
|
||||||
|
['plan_features', 'SELECT count(*) FROM plan_features'],
|
||||||
|
['subscriptions', 'SELECT count(*) FROM subscriptions'],
|
||||||
|
['email_templates_global', 'SELECT count(*) FROM email_templates_global'],
|
||||||
|
['notification_templates', 'SELECT count(*) FROM notification_templates']
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [label, sql] of counts) {
|
||||||
|
try {
|
||||||
|
const count = psql(sql, { tuples: true }).trim();
|
||||||
|
const color = parseInt(count) > 0 ? c.green : c.red;
|
||||||
|
step(`${label}: ${color}${count}${c.reset}`);
|
||||||
|
} catch {
|
||||||
|
step(`${label}: ${c.gray}(tabela não existe)${c.reset}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- DIFF ----
|
||||||
|
commands.diff = function () {
|
||||||
|
title('Diff — Schema');
|
||||||
|
requireDocker();
|
||||||
|
|
||||||
|
const backups = listBackups();
|
||||||
|
if (backups.length === 0) {
|
||||||
|
err('Nenhum backup para comparar. Rode "node db.cjs backup" primeiro.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastBackup = backups[0];
|
||||||
|
const lastSchemaPath = path.join(ROOT, 'backups', lastBackup, 'schema.sql');
|
||||||
|
if (!fs.existsSync(lastSchemaPath)) {
|
||||||
|
err(`Schema não encontrado no backup ${lastBackup}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
info('Exportando schema atual...');
|
||||||
|
const currentSchema = pgDump('--schema-only --no-owner --no-privileges');
|
||||||
|
|
||||||
|
const lastSchema = fs.readFileSync(lastSchemaPath, 'utf8');
|
||||||
|
|
||||||
|
// Extract table definitions for comparison
|
||||||
|
const extractTables = (sql) => {
|
||||||
|
const tables = {};
|
||||||
|
const regex = /CREATE TABLE (?:IF NOT EXISTS )?(\S+)\s*\(([\s\S]*?)\);/g;
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(sql)) !== null) {
|
||||||
|
tables[match[1]] = match[2].trim();
|
||||||
|
}
|
||||||
|
return tables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentTables = extractTables(currentSchema);
|
||||||
|
const lastTables = extractTables(lastSchema);
|
||||||
|
|
||||||
|
const allTables = new Set([...Object.keys(currentTables), ...Object.keys(lastTables)]);
|
||||||
|
|
||||||
|
let added = 0,
|
||||||
|
removed = 0,
|
||||||
|
changed = 0,
|
||||||
|
unchanged = 0;
|
||||||
|
|
||||||
|
for (const table of [...allTables].sort()) {
|
||||||
|
if (!lastTables[table]) {
|
||||||
|
log(` ${c.green}+ ${table}${c.reset} (nova)`);
|
||||||
|
added++;
|
||||||
|
} else if (!currentTables[table]) {
|
||||||
|
log(` ${c.red}- ${table}${c.reset} (removida)`);
|
||||||
|
removed++;
|
||||||
|
} else if (currentTables[table] !== lastTables[table]) {
|
||||||
|
log(` ${c.yellow}~ ${table}${c.reset} (alterada)`);
|
||||||
|
changed++;
|
||||||
|
} else {
|
||||||
|
unchanged++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log('');
|
||||||
|
ok(`Comparado com backup de ${lastBackup}:`);
|
||||||
|
step(`${added} nova(s), ${changed} alterada(s), ${removed} removida(s), ${unchanged} sem mudança`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- RESET ----
|
||||||
|
commands.reset = function () {
|
||||||
|
title('Reset — CUIDADO');
|
||||||
|
requireDocker();
|
||||||
|
|
||||||
|
// Safety backup
|
||||||
|
info('Criando backup antes do reset...');
|
||||||
|
try {
|
||||||
|
commands.backup();
|
||||||
|
} catch {
|
||||||
|
warn('Não foi possível criar backup');
|
||||||
|
}
|
||||||
|
|
||||||
|
warn('Resetando schema public...');
|
||||||
|
psql('DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO postgres; GRANT ALL ON SCHEMA public TO public;');
|
||||||
|
ok('Schema public resetado');
|
||||||
|
|
||||||
|
// Re-run setup
|
||||||
|
commands.setup();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- VERIFY ----
|
||||||
|
commands.verify = function () {
|
||||||
|
title('Verificação de integridade');
|
||||||
|
requireDocker();
|
||||||
|
|
||||||
|
const checks = [
|
||||||
|
{ name: 'auth.users', sql: 'SELECT count(*) FROM auth.users', min: 1 },
|
||||||
|
{ name: 'profiles', sql: 'SELECT count(*) FROM profiles', min: 1 },
|
||||||
|
{ name: 'tenants', sql: 'SELECT count(*) FROM tenants', min: 1 },
|
||||||
|
{ name: 'plans', sql: 'SELECT count(*) FROM plans', min: 7 },
|
||||||
|
{ name: 'features', sql: 'SELECT count(*) FROM features', min: 20 },
|
||||||
|
{ name: 'plan_features', sql: 'SELECT count(*) FROM plan_features', min: 50 },
|
||||||
|
{ name: 'subscriptions', sql: 'SELECT count(*) FROM subscriptions', min: 1 },
|
||||||
|
{ name: 'email_templates', sql: 'SELECT count(*) FROM email_templates_global', min: 10 }
|
||||||
|
];
|
||||||
|
|
||||||
|
let pass = 0,
|
||||||
|
fail = 0;
|
||||||
|
|
||||||
|
for (const check of checks) {
|
||||||
|
try {
|
||||||
|
const count = parseInt(psql(check.sql, { tuples: true }).trim());
|
||||||
|
if (count >= check.min) {
|
||||||
|
ok(`${check.name}: ${count} (mín: ${check.min})`);
|
||||||
|
pass++;
|
||||||
|
} else {
|
||||||
|
err(`${check.name}: ${count} (esperado ≥ ${check.min})`);
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
err(`${check.name}: tabela não existe`);
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check entitlements view
|
||||||
|
try {
|
||||||
|
const ent = psql('SELECT count(*) FROM v_tenant_entitlements;', { tuples: true }).trim();
|
||||||
|
ok(`v_tenant_entitlements: ${ent} registros`);
|
||||||
|
pass++;
|
||||||
|
} catch {
|
||||||
|
err('v_tenant_entitlements: view não existe');
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
|
||||||
|
log('');
|
||||||
|
if (fail === 0) {
|
||||||
|
ok(`${c.bold}Todos os ${pass} checks passaram!${c.reset}`);
|
||||||
|
} else {
|
||||||
|
err(`${fail} check(s) falharam, ${pass} passaram`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- HELP ----
|
||||||
|
commands.help = function () {
|
||||||
|
log(`
|
||||||
|
${c.bold}AgenciaPsi — Database CLI${c.reset}
|
||||||
|
|
||||||
|
${c.cyan}Uso:${c.reset} node db.cjs <comando> [opções]
|
||||||
|
|
||||||
|
${c.cyan}Comandos:${c.reset}
|
||||||
|
|
||||||
|
${c.bold}setup${c.reset} Instalação do zero (schema + fixes + seeds)
|
||||||
|
Cria backup automático após concluir
|
||||||
|
|
||||||
|
${c.bold}backup${c.reset} Exporta banco para backups/YYYY-MM-DD/
|
||||||
|
Gera: schema.sql, data.sql, full_dump.sql
|
||||||
|
|
||||||
|
${c.bold}restore [data]${c.reset} Restaura de um backup
|
||||||
|
Sem data = último backup disponível
|
||||||
|
Ex: node db.cjs restore 2026-03-23
|
||||||
|
|
||||||
|
${c.bold}migrate${c.reset} Aplica migrations pendentes (pasta migrations/)
|
||||||
|
Backup automático antes de aplicar
|
||||||
|
|
||||||
|
${c.bold}seed [grupo]${c.reset} Roda seeds (all, users, system, test_data)
|
||||||
|
Ex: node db.cjs seed system
|
||||||
|
|
||||||
|
${c.bold}status${c.reset} Mostra estado do banco, backups, migrations
|
||||||
|
|
||||||
|
${c.bold}diff${c.reset} Compara schema atual vs último backup
|
||||||
|
|
||||||
|
${c.bold}reset${c.reset} Reseta o banco e reinstala tudo do zero
|
||||||
|
${c.yellow}⚠ Cria backup antes de resetar${c.reset}
|
||||||
|
|
||||||
|
${c.bold}verify${c.reset} Verifica integridade dos dados essenciais
|
||||||
|
|
||||||
|
${c.bold}help${c.reset} Mostra esta ajuda
|
||||||
|
|
||||||
|
${c.cyan}Exemplos:${c.reset}
|
||||||
|
|
||||||
|
${c.gray}# Primeira vez — instala tudo${c.reset}
|
||||||
|
node db.cjs setup
|
||||||
|
|
||||||
|
${c.gray}# Backup diário${c.reset}
|
||||||
|
node db.cjs backup
|
||||||
|
|
||||||
|
${c.gray}# Perdi o banco — restaurar${c.reset}
|
||||||
|
node db.cjs restore
|
||||||
|
|
||||||
|
${c.gray}# Nova migration${c.reset}
|
||||||
|
node db.cjs migrate
|
||||||
|
|
||||||
|
${c.gray}# Ver o que tem no banco${c.reset}
|
||||||
|
node db.cjs status
|
||||||
|
`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Main
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const [, , cmd, ...args] = process.argv;
|
||||||
|
|
||||||
|
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
||||||
|
commands.help();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!commands[cmd]) {
|
||||||
|
err(`Comando desconhecido: ${cmd}`);
|
||||||
|
log(` Use ${c.cyan}node db.cjs help${c.reset} para ver os comandos disponíveis.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
commands[cmd](...args);
|
||||||
|
} catch (e) {
|
||||||
|
err(`Erro: ${e.message}`);
|
||||||
|
if (process.env.DEBUG) console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
34
database-novo/db.config.json
Normal file
34
database-novo/db.config.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"container": "supabase_db_agenciapsi-primesakai",
|
||||||
|
"database": "postgres",
|
||||||
|
"user": "postgres",
|
||||||
|
"backupRetentionDays": 30,
|
||||||
|
"schema": "schema/00_full/schema.sql",
|
||||||
|
"seeds": {
|
||||||
|
"users": [
|
||||||
|
"seed_001_fixed.sql",
|
||||||
|
"seed_002.sql",
|
||||||
|
"seed_003.sql"
|
||||||
|
],
|
||||||
|
"system": [
|
||||||
|
"seed_010_plans.sql",
|
||||||
|
"seed_011_features.sql",
|
||||||
|
"seed_012_plan_features.sql",
|
||||||
|
"seed_013_subscriptions.sql",
|
||||||
|
"seed_014_global_data.sql"
|
||||||
|
],
|
||||||
|
"test_data": [
|
||||||
|
"seed_020_test_data.sql"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fixes": [
|
||||||
|
"fix_addon_credits_fk.sql",
|
||||||
|
"fix_addon_rls_saas_admin.sql",
|
||||||
|
"fix_missing_subscriptions.sql",
|
||||||
|
"fix_notification_templates_rls_admin.sql",
|
||||||
|
"fix_seed_patient_groups.sql",
|
||||||
|
"fix_subscriptions_validate_scope.sql",
|
||||||
|
"fix_template_keys_match_populate.sql",
|
||||||
|
"fix_encoding_accents.sql"
|
||||||
|
]
|
||||||
|
}
|
||||||
176
database-novo/docs/business_rules.md
Normal file
176
database-novo/docs/business_rules.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# Regras de Negócio — Banco de Dados AgenciaPsi
|
||||||
|
|
||||||
|
## 1. Planos e Targets
|
||||||
|
|
||||||
|
| Target | Planos | Escopo da Subscription |
|
||||||
|
|--------|--------|----------------------|
|
||||||
|
| `patient` | patient_free | `user_id` (sem tenant_id) |
|
||||||
|
| `therapist` | therapist_free, therapist_pro | `user_id` (sem tenant_id) |
|
||||||
|
| `clinic` | clinic_free, clinic_pro | `tenant_id` (sem user_id) |
|
||||||
|
| `supervisor` | supervisor_free, supervisor_pro | `user_id` (sem tenant_id) |
|
||||||
|
|
||||||
|
**Constraint `subscriptions_owner_xor`**: Uma subscription DEVE ter `tenant_id` XOR `user_id`, nunca ambos.
|
||||||
|
|
||||||
|
**Trigger `subscriptions_validate_scope`**: Valida que o target do plano casa com o escopo:
|
||||||
|
- `clinic` → exige `tenant_id`, rejeita `user_id`
|
||||||
|
- `therapist`, `supervisor`, `patient` → exige `user_id`, rejeita `tenant_id`
|
||||||
|
|
||||||
|
## 2. Planos Core (protegidos)
|
||||||
|
|
||||||
|
Os planos `clinic_free`, `clinic_pro`, `therapist_free`, `therapist_pro` são **core**:
|
||||||
|
- **Não podem ter `key` alterada** (trigger `trg_no_change_core_plan_key`)
|
||||||
|
- **Não podem ter `target` alterado** (trigger `trg_no_change_plan_target`)
|
||||||
|
- **Não podem ser deletados** (trigger `trg_no_delete_core_plans`)
|
||||||
|
|
||||||
|
Para bypass (migração): `SET LOCAL app.plan_migration_bypass = '1'`
|
||||||
|
|
||||||
|
## 3. Entitlements (Features)
|
||||||
|
|
||||||
|
### Resolução de features para TENANTS (clínicas)
|
||||||
|
```
|
||||||
|
tenant_has_feature(tenant_id, feature_key) =
|
||||||
|
EXISTS em v_tenant_entitlements (via plano)
|
||||||
|
OR
|
||||||
|
EXISTS em tenant_features (override direto)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Resolução de features para USERS (terapeutas, supervisores)
|
||||||
|
```
|
||||||
|
user_has_feature(user_id, feature_key) =
|
||||||
|
EXISTS em v_user_entitlements (via plano pessoal)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cadeia de resolução
|
||||||
|
```
|
||||||
|
subscription → plan → plan_features → features
|
||||||
|
↓
|
||||||
|
plan_features.limits (jsonb) → limites quantitativos
|
||||||
|
```
|
||||||
|
|
||||||
|
### Views de entitlements
|
||||||
|
- `v_tenant_active_subscription` → subscription ativa do tenant
|
||||||
|
- `v_user_active_subscription` → subscription ativa do user
|
||||||
|
- `v_tenant_entitlements` → feature_key + allowed
|
||||||
|
- `v_tenant_entitlements_full` → + limits + plan_id + plan_key
|
||||||
|
- `v_user_entitlements` → feature_key + allowed (para planos pessoais)
|
||||||
|
|
||||||
|
## 4. Tipos de Tenant
|
||||||
|
|
||||||
|
| kind | Descrição | Criação |
|
||||||
|
|------|-----------|---------|
|
||||||
|
| `therapist` | Terapeuta solo | Automático ao criar conta de terapeuta |
|
||||||
|
| `clinic_coworking` | Clínica coworking | Manual |
|
||||||
|
| `clinic_reception` | Clínica com recepção | Manual |
|
||||||
|
| `clinic_full` | Clínica completa | Manual |
|
||||||
|
| `supervisor` | Supervisor | Automático |
|
||||||
|
| `saas` | Sistema (legado) | — |
|
||||||
|
| `clinic` | Legado | — |
|
||||||
|
|
||||||
|
**O `kind` é imutável após criação** (trigger `trg_tenant_kind_immutable`).
|
||||||
|
|
||||||
|
## 5. Roles e Permissões
|
||||||
|
|
||||||
|
### Profile roles
|
||||||
|
| Role | Descrição |
|
||||||
|
|------|-----------|
|
||||||
|
| `saas_admin` | Administrador da plataforma |
|
||||||
|
| `tenant_member` | Membro de um ou mais tenants |
|
||||||
|
| `portal_user` | Paciente (acesso ao portal) |
|
||||||
|
| `patient` | Paciente (legado) |
|
||||||
|
|
||||||
|
### Tenant member roles
|
||||||
|
| Role | Descrição |
|
||||||
|
|------|-----------|
|
||||||
|
| `tenant_admin` | Admin do tenant (dono) |
|
||||||
|
| `therapist` | Terapeuta membro |
|
||||||
|
| `clinic_admin` | Admin da clínica (secretária com poderes) |
|
||||||
|
| `secretary` | Secretária |
|
||||||
|
| `supervisor` | Supervisor |
|
||||||
|
| `patient` | Paciente do tenant |
|
||||||
|
|
||||||
|
### Platform roles (array em profiles)
|
||||||
|
| Role | Descrição |
|
||||||
|
|------|-----------|
|
||||||
|
| `editor` | Editor de conteúdo da plataforma |
|
||||||
|
|
||||||
|
## 6. Compromissos Determinados
|
||||||
|
|
||||||
|
A função `seed_determined_commitments(tenant_id)` cria 5 tipos nativos:
|
||||||
|
|
||||||
|
| native_key | Nome | locked | active |
|
||||||
|
|------------|------|--------|--------|
|
||||||
|
| `session` | Sessão | true | true |
|
||||||
|
| `reading` | Leitura | false | true |
|
||||||
|
| `supervision` | Supervisão | false | true |
|
||||||
|
| `class` | Aula | false | **false** |
|
||||||
|
| `analysis` | Análise Pessoal | false | true |
|
||||||
|
|
||||||
|
- `session` é **locked** (não pode ser editado/deletado)
|
||||||
|
- O `native_key = 'session'` é usado pelo agendador online para identificar o compromisso padrão
|
||||||
|
|
||||||
|
## 7. Grupos de Pacientes Padrão
|
||||||
|
|
||||||
|
A função `seed_default_patient_groups(tenant_id)` cria 3 grupos sistema:
|
||||||
|
|
||||||
|
| Nome | Cor | is_system |
|
||||||
|
|------|-----|-----------|
|
||||||
|
| Crianças | #60a5fa | true |
|
||||||
|
| Adolescentes | #a78bfa | true |
|
||||||
|
| Idosos | #34d399 | true |
|
||||||
|
|
||||||
|
Grupos sistema não podem ser editados/deletados (trigger `prevent_system_group_changes`).
|
||||||
|
|
||||||
|
## 8. Subscriptions — Status
|
||||||
|
|
||||||
|
| Status | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `pending` | Aguardando ativação |
|
||||||
|
| `active` | Ativa |
|
||||||
|
| `past_due` | Pagamento atrasado |
|
||||||
|
| `suspended` | Suspensa |
|
||||||
|
| `cancelled` | Cancelada |
|
||||||
|
| `expired` | Expirada |
|
||||||
|
|
||||||
|
## 9. Templates de Email
|
||||||
|
|
||||||
|
**Globais** (`email_templates_global`): templates padrão da plataforma, gerenciados pelo saas_admin.
|
||||||
|
|
||||||
|
**Tenant** (`email_templates_tenant`): overrides por tenant. Se existir, usa o do tenant; se não, usa o global.
|
||||||
|
|
||||||
|
### Keys de template
|
||||||
|
| Domínio | Templates |
|
||||||
|
|---------|-----------|
|
||||||
|
| session | reminder, confirmation, cancellation, rescheduled |
|
||||||
|
| intake | received, approved, rejected |
|
||||||
|
| scheduler | request_accepted, request_rejected |
|
||||||
|
| system | welcome, password_reset |
|
||||||
|
|
||||||
|
Canais: `email`, `whatsapp`, `sms`
|
||||||
|
|
||||||
|
## 10. Notificações — Sistema
|
||||||
|
|
||||||
|
| Canal | Tipos |
|
||||||
|
|-------|-------|
|
||||||
|
| WhatsApp | lembrete_sessao, confirmacao_sessao, cancelamento_sessao |
|
||||||
|
| SMS | lembrete_sessao |
|
||||||
|
|
||||||
|
### Schedule keys
|
||||||
|
| Key | Descrição |
|
||||||
|
|-----|-----------|
|
||||||
|
| `lembrete_24h` | 24 horas antes |
|
||||||
|
| `lembrete_2h` | 2 horas antes |
|
||||||
|
| `lembrete_30min` | 30 minutos antes |
|
||||||
|
| `confirmacao_imediata` | Imediato após confirmar |
|
||||||
|
| `cancelamento_imediato` | Imediato após cancelar |
|
||||||
|
|
||||||
|
## 11. RLS (Row Level Security)
|
||||||
|
|
||||||
|
Todas as tabelas do schema `public` têm RLS habilitado. As policies usam:
|
||||||
|
- `auth.uid()` — ID do usuário autenticado
|
||||||
|
- `is_saas_admin()` — verifica se é admin da plataforma
|
||||||
|
- `is_tenant_member(tenant_id)` — verifica se pertence ao tenant
|
||||||
|
- `is_tenant_admin(tenant_id)` — verifica se é admin do tenant
|
||||||
|
- `current_member_role(tenant_id)` — role do membro no tenant
|
||||||
|
- `tenant_has_feature(tenant_id, feature_key)` — verifica feature
|
||||||
|
|
||||||
|
**Se as features/plan_features não existirem no banco, as policies de RLS bloqueiam o acesso.**
|
||||||
191
database-novo/docs/schema_map.md
Normal file
191
database-novo/docs/schema_map.md
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
# Schema Map — AgenciaPsi
|
||||||
|
|
||||||
|
Mapa completo do banco de dados PostgreSQL 17, extraído de `schema.sql` (2026-03-23).
|
||||||
|
**84 tabelas** no schema `public` + tabelas de infraestrutura (auth, storage, realtime).
|
||||||
|
|
||||||
|
## Domínios
|
||||||
|
|
||||||
|
### Core (11 tabelas)
|
||||||
|
| Tabela | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `profiles` | Perfil do usuário (role, account_type, full_name, platform_roles) |
|
||||||
|
| `tenants` | Organizações (clínicas, terapeutas solo, supervisores) |
|
||||||
|
| `tenant_members` | Vínculo usuário↔tenant com role (tenant_admin, therapist, secretary, etc.) |
|
||||||
|
| `tenant_invites` | Convites pendentes para ingressar em um tenant |
|
||||||
|
| `tenant_features` | Overrides de features por tenant (exceções comerciais) |
|
||||||
|
| `tenant_feature_exceptions_log` | Log de alterações em tenant_features |
|
||||||
|
| `saas_admins` | Administradores da plataforma |
|
||||||
|
| `owner_users` | Mapeamento owner_id→user_id para RLS |
|
||||||
|
| `user_settings` | Configurações pessoais do usuário |
|
||||||
|
| `company_profiles` | Perfil da empresa/clínica (logo, endereço, etc.) |
|
||||||
|
| `dev_user_credentials` | Credenciais de teste (apenas dev) |
|
||||||
|
|
||||||
|
### Plans & Billing (20 tabelas)
|
||||||
|
| Tabela | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `plans` | Planos disponíveis (key, target, price_cents, max_supervisees) |
|
||||||
|
| `plan_prices` | Preços por intervalo (month/year) com versionamento |
|
||||||
|
| `plan_features` | Vínculo plano↔feature com limites (limits jsonb) |
|
||||||
|
| `plan_public` | Info pública dos planos (para página de preços) |
|
||||||
|
| `plan_public_bullets` | Bullets de marketing dos planos |
|
||||||
|
| `features` | Features do sistema (key, name, descricao) |
|
||||||
|
| `entitlements_invalidation` | Cache invalidation de entitlements |
|
||||||
|
| `subscriptions` | Assinaturas ativas (user_id XOR tenant_id) |
|
||||||
|
| `subscription_events` | Histórico de eventos de assinatura |
|
||||||
|
| `subscription_intents_personal` | Intenções de assinatura pessoal |
|
||||||
|
| `subscription_intents_tenant` | Intenções de assinatura de tenant |
|
||||||
|
| `subscription_intents_legacy` | Intenções legadas |
|
||||||
|
| `billing_contracts` | Contratos de cobrança |
|
||||||
|
| `addon_credits` | Créditos de add-ons por tenant |
|
||||||
|
| `addon_products` | Produtos add-on disponíveis |
|
||||||
|
| `addon_transactions` | Transações de add-ons |
|
||||||
|
| `modules` | Módulos do sistema |
|
||||||
|
| `module_features` | Features por módulo |
|
||||||
|
| `tenant_modules` | Módulos ativos por tenant |
|
||||||
|
|
||||||
|
### Agenda (11 tabelas)
|
||||||
|
| Tabela | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `agenda_bloqueios` | Bloqueios de horário |
|
||||||
|
| `agenda_configuracoes` | Configurações da agenda por tenant_member |
|
||||||
|
| `agenda_eventos` | Eventos da agenda (sessões, bloqueios) |
|
||||||
|
| `agenda_excecoes` | Exceções na agenda (horários extras, bloqueios pontuais) |
|
||||||
|
| `agenda_online_slots` | Slots de agendamento online |
|
||||||
|
| `agenda_regras_semanais` | Regras semanais de disponibilidade |
|
||||||
|
| `agenda_slots_bloqueados_semanais` | Slots bloqueados na semana |
|
||||||
|
| `agenda_slots_regras` | Regras de slots |
|
||||||
|
| `recurrence_rules` | Regras de recorrência de sessões |
|
||||||
|
| `recurrence_exceptions` | Exceções a recorrências |
|
||||||
|
| `recurrence_rule_services` | Serviços vinculados a recorrências |
|
||||||
|
|
||||||
|
### Agendador Online (2 tabelas)
|
||||||
|
| Tabela | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `agendador_configuracoes` | Configurações do agendador online público |
|
||||||
|
| `agendador_solicitacoes` | Solicitações de agendamento recebidas |
|
||||||
|
|
||||||
|
### Pacientes (8 tabelas)
|
||||||
|
| Tabela | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `patients` | Pacientes vinculados a um tenant |
|
||||||
|
| `patient_groups` | Grupos de pacientes (sistema + customizados) |
|
||||||
|
| `patient_group_patient` | Vínculo paciente↔grupo |
|
||||||
|
| `patient_tags` | Tags personalizadas |
|
||||||
|
| `patient_patient_tag` | Vínculo paciente↔tag |
|
||||||
|
| `patient_intake_requests` | Solicitações de cadastro (triagem) |
|
||||||
|
| `patient_invites` | Convites para portal do paciente |
|
||||||
|
| `patient_discounts` | Descontos por paciente |
|
||||||
|
|
||||||
|
### Compromissos Determinados (4 tabelas)
|
||||||
|
| Tabela | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `determined_commitments` | Tipos de compromisso (sessão, leitura, supervisão, etc.) |
|
||||||
|
| `determined_commitment_fields` | Campos customizados por tipo de compromisso |
|
||||||
|
| `commitment_services` | Serviços vinculados a compromissos |
|
||||||
|
| `commitment_time_logs` | Logs de tempo por compromisso |
|
||||||
|
|
||||||
|
### Financeiro (9 tabelas)
|
||||||
|
| Tabela | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `financial_records` | Lançamentos financeiros (receita/despesa) |
|
||||||
|
| `financial_categories` | Categorias de lançamento |
|
||||||
|
| `financial_exceptions` | Exceções financeiras |
|
||||||
|
| `payment_settings` | Configurações de pagamento por tenant |
|
||||||
|
| `professional_pricing` | Precificação por profissional |
|
||||||
|
| `therapist_payouts` | Repasses a terapeutas |
|
||||||
|
| `therapist_payout_records` | Registros de repasse |
|
||||||
|
| `services` | Serviços oferecidos |
|
||||||
|
| `insurance_plans` + `insurance_plan_services` | Convênios e serviços por convênio |
|
||||||
|
|
||||||
|
### Notificações (10 tabelas)
|
||||||
|
| Tabela | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `notification_channels` | Canais de notificação por tenant |
|
||||||
|
| `notification_logs` | Logs de envio |
|
||||||
|
| `notification_preferences` | Preferências do paciente (opt-in/out) |
|
||||||
|
| `notification_queue` | Fila de envio |
|
||||||
|
| `notification_schedules` | Agendamentos de notificação |
|
||||||
|
| `notification_templates` | Templates WhatsApp/SMS (default + tenant) |
|
||||||
|
| `notifications` | Notificações in-app |
|
||||||
|
| `email_templates_global` | Templates de email globais (plataforma) |
|
||||||
|
| `email_templates_tenant` | Overrides de templates por tenant |
|
||||||
|
| `email_layout_config` | Configuração de layout de email |
|
||||||
|
|
||||||
|
### SaaS Admin / UI (8 tabelas)
|
||||||
|
| Tabela | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `saas_docs` | Documentação da plataforma |
|
||||||
|
| `saas_doc_votos` | Votos em docs |
|
||||||
|
| `saas_faq` | Categorias de FAQ |
|
||||||
|
| `saas_faq_itens` | Itens de FAQ |
|
||||||
|
| `feriados` | Feriados nacionais/regionais |
|
||||||
|
| `global_notices` | Avisos globais da plataforma |
|
||||||
|
| `login_carousel_slides` | Slides do carrossel de login |
|
||||||
|
| `notice_dismissals` | Dismissals de avisos por usuário |
|
||||||
|
|
||||||
|
### Suporte (1 tabela)
|
||||||
|
| Tabela | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `support_sessions` | Sessões de suporte técnico |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Views Principais
|
||||||
|
|
||||||
|
| View | Descrição |
|
||||||
|
|------|-----------|
|
||||||
|
| `v_tenant_active_subscription` | Subscription ativa por tenant |
|
||||||
|
| `v_user_active_subscription` | Subscription ativa por user |
|
||||||
|
| `v_tenant_entitlements` | Features habilitadas por tenant (via plano) |
|
||||||
|
| `v_tenant_entitlements_full` | Entitlements + limits + plan info |
|
||||||
|
| `v_tenant_entitlements_json` | Entitlements agregados como JSON |
|
||||||
|
| `v_user_entitlements` | Features habilitadas por user (via plano) |
|
||||||
|
| `v_tenant_members_with_profiles` | Membros do tenant com dados do perfil |
|
||||||
|
| `v_tenant_staff` | Staff do tenant (membros + convites) |
|
||||||
|
| `v_tenant_people` | Todas as pessoas do tenant |
|
||||||
|
| `v_plan_active_prices` | Preços ativos dos planos |
|
||||||
|
| `v_public_pricing` | Preços públicos para página de marketing |
|
||||||
|
| `v_subscription_health` | Saúde das subscriptions |
|
||||||
|
| `v_cashflow_projection` | Projeção de fluxo de caixa |
|
||||||
|
| `v_commitment_totals` | Totais de compromissos |
|
||||||
|
| `v_patient_groups_with_counts` | Grupos com contagem de pacientes |
|
||||||
|
| `v_tag_patient_counts` | Tags com contagem de pacientes |
|
||||||
|
| `subscription_intents` | View unificada de intenções (com INSTEAD OF trigger) |
|
||||||
|
| `owner_feature_entitlements` | Entitlements por owner |
|
||||||
|
| `current_tenant_id` | Tenant ativo do usuário corrente |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Funções Críticas
|
||||||
|
|
||||||
|
| Função | Tipo | Descrição |
|
||||||
|
|--------|------|-----------|
|
||||||
|
| `tenant_has_feature(uuid, text)` | Query | Verifica se tenant tem feature (plano + override) |
|
||||||
|
| `user_has_feature(uuid, text)` | Query | Verifica se user tem feature via plano pessoal |
|
||||||
|
| `has_feature(uuid, text)` | Query | Alias genérico |
|
||||||
|
| `seed_determined_commitments(uuid)` | Seed | Cria 5 tipos de compromisso nativos por tenant |
|
||||||
|
| `seed_default_patient_groups(uuid)` | Seed | Cria 3 grupos de pacientes padrão |
|
||||||
|
| `seed_default_financial_categories(uuid)` | Seed | Cria categorias financeiras padrão |
|
||||||
|
| `subscriptions_validate_scope()` | Trigger | Valida XOR (user_id vs tenant_id) por target |
|
||||||
|
| `activate_subscription_from_intent(uuid)` | RPC | Ativa subscription a partir de intent |
|
||||||
|
| `handle_new_user()` | Trigger | Cria profile + tenant pessoal ao cadastrar |
|
||||||
|
| `ensure_personal_tenant()` | RPC | Garante que o user tem um tenant pessoal |
|
||||||
|
| `populate_notification_queue()` | Cron | Popula fila de notificações |
|
||||||
|
| `agendador_slots_disponiveis(text, date)` | RPC | Retorna slots disponíveis para agendamento |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Enums (public schema)
|
||||||
|
|
||||||
|
| Tipo | Valores |
|
||||||
|
|------|---------|
|
||||||
|
| `commitment_log_source` | manual, auto |
|
||||||
|
| `determined_field_type` | text, textarea, number, date, select, boolean |
|
||||||
|
| `financial_record_type` | receita, despesa |
|
||||||
|
| `recurrence_exception_type` | cancel_session, reschedule_session, patient_missed, therapist_canceled, holiday_block |
|
||||||
|
| `recurrence_type` | weekly, biweekly, monthly, yearly, custom_weekdays |
|
||||||
|
| `status_agenda_serie` | ativo, pausado, cancelado |
|
||||||
|
| `status_evento_agenda` | agendado, realizado, faltou, cancelado, remarcar |
|
||||||
|
| `status_excecao_agenda` | pendente, ativo, arquivado |
|
||||||
|
| `tipo_evento_agenda` | sessao, bloqueio |
|
||||||
|
| `tipo_excecao_agenda` | bloqueio, horario_extra |
|
||||||
297
database-novo/docs/setup_guide.md
Normal file
297
database-novo/docs/setup_guide.md
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
# Guia de Instalação e Uso — AgenciaPsi Database
|
||||||
|
|
||||||
|
## Pré-requisitos
|
||||||
|
|
||||||
|
1. **Docker Desktop** instalado e rodando
|
||||||
|
2. **Node.js** 18+ instalado
|
||||||
|
3. **Supabase CLI** instalado (`npm install -g supabase`)
|
||||||
|
|
||||||
|
## Instalação do Zero (banco vazio)
|
||||||
|
|
||||||
|
### 1. Iniciar o Supabase
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Na raiz do projeto (agenciapsi-primesakai/)
|
||||||
|
npx supabase start
|
||||||
|
```
|
||||||
|
|
||||||
|
Aguarde até o container `supabase_db_agenciapsi-primesakai` estar rodando.
|
||||||
|
|
||||||
|
### 2. Verificar se o container está ok
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker ps | grep supabase_db
|
||||||
|
```
|
||||||
|
|
||||||
|
Deve mostrar o container com status `Up`.
|
||||||
|
|
||||||
|
### 3. Instalar o banco completo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd database-novo
|
||||||
|
node db.cjs setup
|
||||||
|
```
|
||||||
|
|
||||||
|
Isso faz tudo automaticamente:
|
||||||
|
- Aplica o schema completo (84 tabelas, funções, triggers, policies)
|
||||||
|
- Aplica os 7 fixes conhecidos
|
||||||
|
- Cria os 11 usuários de teste
|
||||||
|
- Cria os 7 planos + 4 preços
|
||||||
|
- Cria as 26 features + 85 vínculos plano↔feature
|
||||||
|
- Cria as 9 subscriptions + compromissos determinados
|
||||||
|
- Cria os templates de email, notificação e carousel
|
||||||
|
- Cria backup automático pós-instalação
|
||||||
|
- Verifica integridade no final
|
||||||
|
|
||||||
|
### 4. Verificar
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs status
|
||||||
|
```
|
||||||
|
|
||||||
|
Deve mostrar todos os counts verdes.
|
||||||
|
|
||||||
|
## Backup
|
||||||
|
|
||||||
|
### Criar backup manual
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs backup
|
||||||
|
```
|
||||||
|
|
||||||
|
Salva em `backups/YYYY-MM-DD/` com 3 arquivos:
|
||||||
|
- `schema.sql` — estrutura do banco
|
||||||
|
- `data.sql` — dados (sem schemas de infra)
|
||||||
|
- `full_dump.sql` — tudo junto
|
||||||
|
|
||||||
|
### Backup automático
|
||||||
|
|
||||||
|
O backup é feito automaticamente:
|
||||||
|
- Após o `setup`
|
||||||
|
- Antes de cada `migrate`
|
||||||
|
- Antes de cada `restore`
|
||||||
|
- Antes de cada `reset`
|
||||||
|
|
||||||
|
### Retenção
|
||||||
|
|
||||||
|
Backups com mais de 30 dias são removidos automaticamente. Para alterar, edite `backupRetentionDays` no `db.config.json`.
|
||||||
|
|
||||||
|
## Restaurar o Banco
|
||||||
|
|
||||||
|
### Restaurar do último backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs restore
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restaurar de uma data específica
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs restore 2026-03-23
|
||||||
|
```
|
||||||
|
|
||||||
|
O restore:
|
||||||
|
1. Cria backup de segurança do estado atual
|
||||||
|
2. Limpa o schema public
|
||||||
|
3. Aplica o full_dump.sql do backup
|
||||||
|
4. Verifica integridade
|
||||||
|
|
||||||
|
## Migrations (alterações no banco)
|
||||||
|
|
||||||
|
### Criar uma migration
|
||||||
|
|
||||||
|
Crie um arquivo SQL na pasta `migrations/` com nome sequencial:
|
||||||
|
|
||||||
|
```
|
||||||
|
migrations/
|
||||||
|
├── 001_add_column_x.sql
|
||||||
|
├── 002_create_table_y.sql
|
||||||
|
└── 003_fix_something.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
O nome deve começar com número para garantir a ordem.
|
||||||
|
|
||||||
|
### Aplicar migrations pendentes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
O CLI:
|
||||||
|
1. Cria backup automático
|
||||||
|
2. Compara com a tabela `_db_migrations` no banco
|
||||||
|
3. Aplica apenas as que ainda não foram executadas
|
||||||
|
4. Registra cada migration aplicada
|
||||||
|
5. Se uma falhar, para imediatamente (use `restore` para voltar)
|
||||||
|
|
||||||
|
### Ver migrations aplicadas
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs status
|
||||||
|
```
|
||||||
|
|
||||||
|
## Seeds (dados de teste)
|
||||||
|
|
||||||
|
### Rodar todos os seeds
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs seed all # ou simplesmente: node db.cjs seed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rodar grupo específico
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs seed users # Apenas usuários (seed_001 a 003)
|
||||||
|
node db.cjs seed system # Apenas sistema (seed_010 a 014)
|
||||||
|
node db.cjs seed test_data # Dados de teste (seed_020)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ordem dos seeds
|
||||||
|
|
||||||
|
| # | Arquivo | O que faz |
|
||||||
|
|---|---------|-----------|
|
||||||
|
| 1 | `seed_001_fixed.sql` | 6 usuários base + tenants |
|
||||||
|
| 2 | `seed_002.sql` | Supervisor + Editor |
|
||||||
|
| 3 | `seed_003.sql` | Therapist2, Therapist3, Secretary |
|
||||||
|
| 4 | `seed_010_plans.sql` | 7 planos + 4 preços |
|
||||||
|
| 5 | `seed_011_features.sql` | 26 features |
|
||||||
|
| 6 | `seed_012_plan_features.sql` | 85 vínculos plano↔feature |
|
||||||
|
| 7 | `seed_013_subscriptions.sql` | 9 subscriptions + compromissos |
|
||||||
|
| 8 | `seed_014_global_data.sql` | Templates + carousel |
|
||||||
|
|
||||||
|
## Outros Comandos
|
||||||
|
|
||||||
|
### Ver status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs status
|
||||||
|
```
|
||||||
|
|
||||||
|
Mostra: container, backups, migrations aplicadas/pendentes, counts de todas as tabelas.
|
||||||
|
|
||||||
|
### Comparar mudanças
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs diff
|
||||||
|
```
|
||||||
|
|
||||||
|
Compara o schema atual no banco com o último backup. Mostra tabelas adicionadas, removidas ou alteradas.
|
||||||
|
|
||||||
|
### Verificar integridade
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs verify
|
||||||
|
```
|
||||||
|
|
||||||
|
Checa se os dados essenciais existem (plans, features, subscriptions, etc).
|
||||||
|
|
||||||
|
### Reset completo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs reset
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠ CUIDADO**: Apaga tudo e reinstala do zero. Cria backup antes.
|
||||||
|
|
||||||
|
## Estrutura de Pastas
|
||||||
|
|
||||||
|
```
|
||||||
|
database-novo/
|
||||||
|
├── db.js ← CLI principal
|
||||||
|
├── db.config.json ← Configuração (container, seeds, fixes)
|
||||||
|
│
|
||||||
|
├── schema/ ← Schema SQL separado por seção
|
||||||
|
│ ├── 00_full/ ← Schema completo (referência)
|
||||||
|
│ ├── 01_extensions/ ← Extensões PostgreSQL
|
||||||
|
│ ├── 02_types/ ← Enums e tipos
|
||||||
|
│ ├── 03_functions/ ← Funções (11 arquivos por domínio)
|
||||||
|
│ ├── 04_tables/ ← Tabelas (10 arquivos por domínio)
|
||||||
|
│ ├── 05_views/ ← 24 views
|
||||||
|
│ ├── 06_indexes/ ← Índices
|
||||||
|
│ ├── 07_foreign_keys/ ← PKs, FKs, constraints
|
||||||
|
│ ├── 08_triggers/ ← Triggers
|
||||||
|
│ ├── 09_policies/ ← 217 RLS policies
|
||||||
|
│ └── 10_grants/ ← Grants
|
||||||
|
│
|
||||||
|
├── seeds/ ← Seeds de dados
|
||||||
|
│ ├── seed_001_fixed.sql
|
||||||
|
│ ├── ...
|
||||||
|
│ └── run_all_seeds.sh
|
||||||
|
│
|
||||||
|
├── migrations/ ← Migrations (alterações incrementais)
|
||||||
|
│
|
||||||
|
├── fixes/ ← Correções aplicadas
|
||||||
|
│
|
||||||
|
├── backups/ ← Backups com data
|
||||||
|
│ ├── 2026-03-23/
|
||||||
|
│ └── ...
|
||||||
|
│
|
||||||
|
└── docs/ ← Documentação
|
||||||
|
├── setup_guide.md ← Este arquivo
|
||||||
|
├── schema_map.md ← Mapa de 84 tabelas
|
||||||
|
├── business_rules.md ← Regras de negócio
|
||||||
|
└── users_test.md ← Usuários de teste
|
||||||
|
```
|
||||||
|
|
||||||
|
## Credenciais de Teste
|
||||||
|
|
||||||
|
| Email | Senha | Tipo |
|
||||||
|
|-------|-------|------|
|
||||||
|
| paciente@agenciapsi.com.br | Teste@123 | Paciente |
|
||||||
|
| terapeuta@agenciapsi.com.br | Teste@123 | Terapeuta solo |
|
||||||
|
| clinica1@agenciapsi.com.br | Teste@123 | Clínica coworking |
|
||||||
|
| clinica2@agenciapsi.com.br | Teste@123 | Clínica recepção |
|
||||||
|
| clinica3@agenciapsi.com.br | Teste@123 | Clínica full |
|
||||||
|
| saas@agenciapsi.com.br | Teste@123 | Admin plataforma |
|
||||||
|
| supervisor@agenciapsi.com.br | Teste@123 | Supervisor |
|
||||||
|
| editor@agenciapsi.com.br | Teste@123 | Editor |
|
||||||
|
| therapist2@agenciapsi.com.br | Teste@123 | Terapeuta |
|
||||||
|
| therapist3@agenciapsi.com.br | Teste@123 | Terapeuta |
|
||||||
|
| secretary@agenciapsi.com.br | Teste@123 | Secretária |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Container não está rodando"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verificar
|
||||||
|
docker ps | grep supabase
|
||||||
|
|
||||||
|
# Reiniciar
|
||||||
|
npx supabase stop
|
||||||
|
npx supabase start
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Tabela não existe" após setup
|
||||||
|
|
||||||
|
O schema pode não ter sido aplicado corretamente. Rode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs reset
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Permission denied" / RLS bloqueando
|
||||||
|
|
||||||
|
Se features/plan_features estiverem vazios, o RLS bloqueia tudo. Rode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs seed system
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration falhou no meio
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Voltar ao estado anterior
|
||||||
|
node db.cjs restore
|
||||||
|
|
||||||
|
# Corrigir o SQL da migration, depois tentar de novo
|
||||||
|
node db.cjs migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quero começar do zero
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs reset
|
||||||
|
```
|
||||||
|
|
||||||
|
Isso apaga tudo, reaplica schema, fixes, seeds, e verifica.
|
||||||
90
database-novo/docs/users_test.md
Normal file
90
database-novo/docs/users_test.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# Usuários de Teste — AgenciaPsi
|
||||||
|
|
||||||
|
Senha de todos: `Teste@123`
|
||||||
|
|
||||||
|
## Mapa de UUIDs
|
||||||
|
|
||||||
|
### Users (auth.users.id = profiles.id)
|
||||||
|
| Email | UUID | Nome |
|
||||||
|
|-------|------|------|
|
||||||
|
| paciente@agenciapsi.com.br | `aaaaaaaa-0001-0001-0001-000000000001` | Ana Paciente |
|
||||||
|
| terapeuta@agenciapsi.com.br | `aaaaaaaa-0002-0002-0002-000000000002` | Bruno Terapeuta |
|
||||||
|
| clinica1@agenciapsi.com.br | `aaaaaaaa-0003-0003-0003-000000000003` | Clínica Espaço Psi |
|
||||||
|
| clinica2@agenciapsi.com.br | `aaaaaaaa-0004-0004-0004-000000000004` | Clínica Mente sã |
|
||||||
|
| clinica3@agenciapsi.com.br | `aaaaaaaa-0005-0005-0005-000000000005` | Clínica Bem Estar |
|
||||||
|
| saas@agenciapsi.com.br | `aaaaaaaa-0006-0006-0006-000000000006` | Admin Plataforma |
|
||||||
|
| supervisor@agenciapsi.com.br | `aaaaaaaa-0007-0007-0007-000000000007` | Carlos Supervisor |
|
||||||
|
| editor@agenciapsi.com.br | `aaaaaaaa-0008-0008-0008-000000000008` | Diana Editora |
|
||||||
|
| therapist2@agenciapsi.com.br | `aaaaaaaa-0009-0009-0009-000000000009` | Eva Terapeuta |
|
||||||
|
| therapist3@agenciapsi.com.br | `aaaaaaaa-0010-0010-0010-000000000010` | Felipe Terapeuta |
|
||||||
|
| secretary@agenciapsi.com.br | `aaaaaaaa-0011-0011-0011-000000000011` | Gabriela Secretária |
|
||||||
|
|
||||||
|
### Tenants
|
||||||
|
| Nome | UUID | Kind |
|
||||||
|
|------|------|------|
|
||||||
|
| Bruno Terapeuta | `bbbbbbbb-0002-0002-0002-000000000002` | therapist |
|
||||||
|
| Clínica Espaço Psi | `bbbbbbbb-0003-0003-0003-000000000003` | clinic_coworking |
|
||||||
|
| Clínica Mente sã | `bbbbbbbb-0004-0004-0004-000000000004` | clinic_reception |
|
||||||
|
| Clínica Bem Estar | `bbbbbbbb-0005-0005-0005-000000000005` | clinic_full |
|
||||||
|
| Eva Terapeuta | `bbbbbbbb-0009-0009-0009-000000000009` | therapist |
|
||||||
|
| Felipe Terapeuta | `bbbbbbbb-0010-0010-0010-000000000010` | therapist |
|
||||||
|
|
||||||
|
## Mapa de Vínculos
|
||||||
|
|
||||||
|
```
|
||||||
|
paciente@ → portal_user / patient_free (user_id)
|
||||||
|
Sem tenant próprio
|
||||||
|
|
||||||
|
terapeuta@ → tenant_member / therapist
|
||||||
|
Tenant: bbbbbbbb-0002 (therapist) → tenant_admin
|
||||||
|
Clínica 3: bbbbbbbb-0005 → therapist
|
||||||
|
Subscription: therapist_free (user_id)
|
||||||
|
|
||||||
|
clinica1@ → tenant_member / clinic
|
||||||
|
Tenant: bbbbbbbb-0003 (clinic_coworking) → tenant_admin
|
||||||
|
Subscription: clinic_free (tenant_id)
|
||||||
|
|
||||||
|
clinica2@ → tenant_member / clinic
|
||||||
|
Tenant: bbbbbbbb-0004 (clinic_reception) → tenant_admin
|
||||||
|
Subscription: clinic_free (tenant_id)
|
||||||
|
|
||||||
|
clinica3@ → tenant_member / clinic
|
||||||
|
Tenant: bbbbbbbb-0005 (clinic_full) → tenant_admin
|
||||||
|
Subscription: clinic_free (tenant_id)
|
||||||
|
|
||||||
|
saas@ → saas_admin
|
||||||
|
Sem tenant, sem subscription
|
||||||
|
|
||||||
|
supervisor@ → tenant_member / therapist
|
||||||
|
Clínica 3: bbbbbbbb-0005 → supervisor
|
||||||
|
Subscription: supervisor_free (user_id)
|
||||||
|
|
||||||
|
editor@ → tenant_member / therapist + platform_roles: {editor}
|
||||||
|
Clínica 3: bbbbbbbb-0005 → therapist
|
||||||
|
Subscription: therapist_free (user_id)
|
||||||
|
|
||||||
|
therapist2@ → tenant_member / therapist
|
||||||
|
Tenant: bbbbbbbb-0009 (therapist) → tenant_admin
|
||||||
|
Clínica 3: bbbbbbbb-0005 → therapist
|
||||||
|
Subscription: therapist_free (user_id)
|
||||||
|
|
||||||
|
therapist3@ → tenant_member / therapist
|
||||||
|
Tenant: bbbbbbbb-0010 (therapist) → tenant_admin
|
||||||
|
Clínica 3: bbbbbbbb-0005 → therapist
|
||||||
|
Subscription: therapist_free (user_id)
|
||||||
|
|
||||||
|
secretary@ → tenant_member / therapist (profile)
|
||||||
|
Clínica 2: bbbbbbbb-0004 → clinic_admin
|
||||||
|
Sem subscription própria (usa plano da Clínica 2)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Clínica 3 — Bem Estar (Full) — Membros
|
||||||
|
|
||||||
|
| Membro | Role |
|
||||||
|
|--------|------|
|
||||||
|
| clinica3@ | tenant_admin |
|
||||||
|
| terapeuta@ | therapist |
|
||||||
|
| supervisor@ | supervisor |
|
||||||
|
| editor@ | therapist |
|
||||||
|
| therapist2@ | therapist |
|
||||||
|
| therapist3@ | therapist |
|
||||||
11
database-novo/fixes/fix_addon_credits_fk.sql
Normal file
11
database-novo/fixes/fix_addon_credits_fk.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Fix: addon_credits e addon_transactions tenant_id FK
|
||||||
|
-- Corrige FK que apontava para auth.users → agora aponta para public.tenants
|
||||||
|
-- Agência PSI — 2026-03-22
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
ALTER TABLE public.addon_credits DROP CONSTRAINT IF EXISTS addon_credits_tenant_id_fkey;
|
||||||
|
ALTER TABLE public.addon_credits ADD CONSTRAINT addon_credits_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id);
|
||||||
|
|
||||||
|
ALTER TABLE public.addon_transactions DROP CONSTRAINT IF EXISTS addon_transactions_tenant_id_fkey;
|
||||||
|
ALTER TABLE public.addon_transactions ADD CONSTRAINT addon_transactions_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id);
|
||||||
83
database-novo/fixes/fix_addon_rls_saas_admin.sql
Normal file
83
database-novo/fixes/fix_addon_rls_saas_admin.sql
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Fix: RLS addon_credits e addon_transactions
|
||||||
|
-- 1. SaaS Admin: acesso total
|
||||||
|
-- 2. Tenant members: SELECT nos seus créditos/transações
|
||||||
|
-- Agência PSI — 2026-03-22
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- ── addon_products: admin pode tudo (CRUD) ────────────────────
|
||||||
|
DROP POLICY IF EXISTS "addon_products_admin_all" ON public.addon_products;
|
||||||
|
CREATE POLICY "addon_products_admin_all"
|
||||||
|
ON public.addon_products FOR ALL
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
EXISTS (SELECT 1 FROM public.saas_admins WHERE user_id = auth.uid())
|
||||||
|
)
|
||||||
|
WITH CHECK (
|
||||||
|
EXISTS (SELECT 1 FROM public.saas_admins WHERE user_id = auth.uid())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ── addon_credits: admin pode ver todos ───────────────────────
|
||||||
|
DROP POLICY IF EXISTS "addon_credits_admin_select" ON public.addon_credits;
|
||||||
|
CREATE POLICY "addon_credits_admin_select"
|
||||||
|
ON public.addon_credits FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
EXISTS (SELECT 1 FROM public.saas_admins WHERE user_id = auth.uid())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ── addon_credits: admin pode inserir/atualizar ───────────────
|
||||||
|
DROP POLICY IF EXISTS "addon_credits_admin_write" ON public.addon_credits;
|
||||||
|
CREATE POLICY "addon_credits_admin_write"
|
||||||
|
ON public.addon_credits FOR ALL
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
EXISTS (SELECT 1 FROM public.saas_admins WHERE user_id = auth.uid())
|
||||||
|
)
|
||||||
|
WITH CHECK (
|
||||||
|
EXISTS (SELECT 1 FROM public.saas_admins WHERE user_id = auth.uid())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ── addon_transactions: admin pode ver todas ──────────────────
|
||||||
|
DROP POLICY IF EXISTS "addon_transactions_admin_select" ON public.addon_transactions;
|
||||||
|
CREATE POLICY "addon_transactions_admin_select"
|
||||||
|
ON public.addon_transactions FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
EXISTS (SELECT 1 FROM public.saas_admins WHERE user_id = auth.uid())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ── addon_transactions: admin pode inserir ────────────────────
|
||||||
|
DROP POLICY IF EXISTS "addon_transactions_admin_insert" ON public.addon_transactions;
|
||||||
|
CREATE POLICY "addon_transactions_admin_insert"
|
||||||
|
ON public.addon_transactions FOR INSERT
|
||||||
|
TO authenticated
|
||||||
|
WITH CHECK (
|
||||||
|
EXISTS (SELECT 1 FROM public.saas_admins WHERE user_id = auth.uid())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ══════════════════════════════════════════════════════════════
|
||||||
|
-- Corrige policies de tenant members (SELECT)
|
||||||
|
-- A policy original usava tenant_id = auth.uid(), mas o auth.uid()
|
||||||
|
-- é o user_id, não o tenant_id. Usa is_tenant_member() em vez disso.
|
||||||
|
-- ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- addon_credits: membro do tenant vê os créditos do seu tenant
|
||||||
|
DROP POLICY IF EXISTS "addon_credits_select_own" ON public.addon_credits;
|
||||||
|
CREATE POLICY "addon_credits_select_own"
|
||||||
|
ON public.addon_credits FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
public.is_tenant_member(tenant_id)
|
||||||
|
OR owner_id = auth.uid()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- addon_transactions: membro do tenant vê as transações do seu tenant
|
||||||
|
DROP POLICY IF EXISTS "addon_transactions_select_own" ON public.addon_transactions;
|
||||||
|
CREATE POLICY "addon_transactions_select_own"
|
||||||
|
ON public.addon_transactions FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
public.is_tenant_member(tenant_id)
|
||||||
|
OR owner_id = auth.uid()
|
||||||
|
);
|
||||||
179
database-novo/fixes/fix_encoding_accents.sql
Normal file
179
database-novo/fixes/fix_encoding_accents.sql
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- FIX: Corrige acentuação perdida (caracteres ?? no banco)
|
||||||
|
-- =============================================================================
|
||||||
|
-- Causa: Seeds aplicados originalmente sem encoding UTF-8 correto.
|
||||||
|
-- Os ?? são bytes literais 0x3F (ASCII ?) onde deveria haver UTF-8.
|
||||||
|
-- Este fix faz UPDATE direto nos valores conhecidos.
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
SET client_encoding TO 'UTF8';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. PROFILES — full_name
|
||||||
|
-- ============================================================
|
||||||
|
UPDATE profiles SET full_name = 'Clínica Espaço Psi' WHERE id = 'aaaaaaaa-0003-0003-0003-000000000003' AND full_name != 'Clínica Espaço Psi';
|
||||||
|
UPDATE profiles SET full_name = 'Clínica Mente Sã' WHERE id = 'aaaaaaaa-0004-0004-0004-000000000004' AND full_name != 'Clínica Mente Sã';
|
||||||
|
UPDATE profiles SET full_name = 'Clínica Bem Estar' WHERE id = 'aaaaaaaa-0005-0005-0005-000000000005' AND full_name != 'Clínica Bem Estar';
|
||||||
|
UPDATE profiles SET full_name = 'Gabriela Secretária' WHERE id = 'aaaaaaaa-0011-0011-0011-000000000011' AND full_name != 'Gabriela Secretária';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. TENANTS — name
|
||||||
|
-- ============================================================
|
||||||
|
UPDATE tenants SET name = 'Clínica Espaço Psi' WHERE id = 'bbbbbbbb-0003-0003-0003-000000000003';
|
||||||
|
UPDATE tenants SET name = 'Clínica Mente Sã' WHERE id = 'bbbbbbbb-0004-0004-0004-000000000004';
|
||||||
|
UPDATE tenants SET name = 'Clínica Bem Estar' WHERE id = 'bbbbbbbb-0005-0005-0005-000000000005';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. DETERMINED_COMMITMENTS — name
|
||||||
|
-- ============================================================
|
||||||
|
UPDATE determined_commitments SET name = 'Sessão' WHERE native_key = 'session';
|
||||||
|
UPDATE determined_commitments SET name = 'Supervisão' WHERE native_key = 'supervision';
|
||||||
|
UPDATE determined_commitments SET name = 'Análise Pessoal' WHERE native_key = 'analysis';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. PLANS — name, description
|
||||||
|
-- ============================================================
|
||||||
|
UPDATE plans SET name = 'THERAPIST PRO', description = 'Plano profissional para terapeutas' WHERE key = 'therapist_pro' AND description LIKE '%??%';
|
||||||
|
UPDATE plans SET name = 'CLINIC PRO', description = 'Plano profissional para clínicas' WHERE key = 'clinic_pro' AND description LIKE '%??%';
|
||||||
|
UPDATE plans SET name = 'THERAPIST FREE', description = 'Plano gratuito para terapeutas' WHERE key = 'therapist_free' AND description LIKE '%??%';
|
||||||
|
UPDATE plans SET name = 'CLINIC FREE', description = 'Plano gratuito para clínicas' WHERE key = 'clinic_free' AND description LIKE '%??%';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 5. FEATURES — name, description
|
||||||
|
-- ============================================================
|
||||||
|
UPDATE features SET name = 'Agenda - Visualizar', description = 'Visualização da agenda' WHERE key = 'agenda.view';
|
||||||
|
UPDATE features SET name = 'Agenda - Gerenciar', description = 'Gerenciamento completo da agenda' WHERE key = 'agenda.manage';
|
||||||
|
UPDATE features SET name = 'Pacientes', description = 'Módulo de pacientes' WHERE key = 'patients';
|
||||||
|
UPDATE features SET name = 'Pacientes - Visualizar', description = 'Visualização de pacientes' WHERE key = 'patients.view';
|
||||||
|
UPDATE features SET name = 'Pacientes - Gerenciar', description = 'Gerenciamento completo de pacientes' WHERE key = 'patients.manage';
|
||||||
|
UPDATE features SET name = 'Agendamento Online', description = 'Sistema de agendamento online' WHERE key = 'online_scheduling';
|
||||||
|
UPDATE features SET name = 'Agendamento Online - Gerenciar', description = 'Gerenciamento do agendamento online' WHERE key = 'online_scheduling.manage';
|
||||||
|
UPDATE features SET name = 'Agendamento Online - Público', description = 'Página pública do agendador' WHERE key = 'online_scheduling.public';
|
||||||
|
UPDATE features SET name = 'Lembretes', description = 'Sistema de lembretes automáticos' WHERE key = 'reminders';
|
||||||
|
UPDATE features SET name = 'Relatórios Básicos', description = 'Relatórios básicos' WHERE key = 'reports_basic';
|
||||||
|
UPDATE features SET name = 'Relatórios Avançados', description = 'Relatórios avançados com exportação' WHERE key = 'reports_advanced';
|
||||||
|
UPDATE features SET name = 'Secretária', description = 'Funcionalidade de secretária' WHERE key = 'secretary';
|
||||||
|
UPDATE features SET name = 'Recepção Compartilhada', description = 'Recepção compartilhada entre terapeutas' WHERE key = 'shared_reception';
|
||||||
|
UPDATE features SET name = 'Salas', description = 'Gerenciamento de salas' WHERE key = 'rooms';
|
||||||
|
UPDATE features SET name = 'Intake Público', description = 'Formulário de intake público' WHERE key = 'intake_public';
|
||||||
|
UPDATE features SET name = 'Intakes PRO', description = 'Funcionalidades avançadas de intake' WHERE key = 'intakes_pro';
|
||||||
|
UPDATE features SET name = 'Branding Personalizado', description = 'Personalização de marca' WHERE key = 'custom_branding';
|
||||||
|
UPDATE features SET name = 'Acesso API', description = 'Acesso via API' WHERE key = 'api_access';
|
||||||
|
UPDATE features SET name = 'Log de Auditoria', description = 'Log de auditoria completo' WHERE key = 'audit_log';
|
||||||
|
UPDATE features SET name = 'Lembrete SMS', description = 'Lembretes via SMS' WHERE key = 'sms_reminder';
|
||||||
|
UPDATE features SET name = 'Calendário da Clínica', description = 'Visão consolidada do calendário' WHERE key = 'clinic_calendar';
|
||||||
|
UPDATE features SET name = 'Relatórios Avançados (Clínica)', description = 'Relatórios avançados da clínica' WHERE key = 'advanced_reports';
|
||||||
|
UPDATE features SET name = 'Supervisor - Acesso', description = 'Acesso ao módulo de supervisão' WHERE key = 'supervisor.access';
|
||||||
|
UPDATE features SET name = 'Supervisor - Convidar', description = 'Convidar supervisionados' WHERE key = 'supervisor.invite';
|
||||||
|
UPDATE features SET name = 'Supervisor - Sessões', description = 'Gerenciar sessões de supervisão' WHERE key = 'supervisor.sessions';
|
||||||
|
UPDATE features SET name = 'Supervisor - Relatórios', description = 'Relatórios de supervisão' WHERE key = 'supervisor.reports';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 6. EMAIL_TEMPLATES_GLOBAL — subject, body_html, body_text
|
||||||
|
-- ============================================================
|
||||||
|
UPDATE email_templates_global SET
|
||||||
|
subject = 'Lembrete: sua sessão amanhã às {{session_time}}',
|
||||||
|
body_text = 'Olá {{patient_name}}, lembrete da sua sessão amanhã às {{session_time}} com {{therapist_name}}.'
|
||||||
|
WHERE key = 'session.reminder';
|
||||||
|
|
||||||
|
UPDATE email_templates_global SET
|
||||||
|
subject = 'Sessão confirmada — {{session_date}} às {{session_time}}',
|
||||||
|
body_text = 'Sua sessão com {{therapist_name}} em {{session_date}} às {{session_time}} foi confirmada.'
|
||||||
|
WHERE key = 'session.confirmation';
|
||||||
|
|
||||||
|
UPDATE email_templates_global SET
|
||||||
|
subject = 'Sessão cancelada — {{session_date}}',
|
||||||
|
body_text = 'A sessão de {{session_date}} às {{session_time}} com {{therapist_name}} foi cancelada.'
|
||||||
|
WHERE key = 'session.cancellation';
|
||||||
|
|
||||||
|
UPDATE email_templates_global SET
|
||||||
|
subject = 'Sessão reagendada — novo horário: {{session_date}} às {{session_time}}',
|
||||||
|
body_text = 'Sua sessão foi reagendada para {{session_date}} às {{session_time}} com {{therapist_name}}.'
|
||||||
|
WHERE key = 'session.rescheduled';
|
||||||
|
|
||||||
|
UPDATE email_templates_global SET
|
||||||
|
subject = 'Recebemos seu cadastro — {{patient_name}}',
|
||||||
|
body_text = 'Olá {{patient_name}}, recebemos seu formulário de cadastro. Entraremos em contato em breve.'
|
||||||
|
WHERE key = 'intake.received';
|
||||||
|
|
||||||
|
UPDATE email_templates_global SET
|
||||||
|
subject = 'Cadastro aprovado — Bem-vindo(a)!',
|
||||||
|
body_text = 'Olá {{patient_name}}, seu cadastro foi aprovado. Você já pode acessar a plataforma.'
|
||||||
|
WHERE key = 'intake.approved';
|
||||||
|
|
||||||
|
UPDATE email_templates_global SET
|
||||||
|
subject = 'Cadastro não aprovado',
|
||||||
|
body_text = 'Olá {{patient_name}}, infelizmente seu cadastro não foi aprovado no momento.'
|
||||||
|
WHERE key = 'intake.rejected';
|
||||||
|
|
||||||
|
UPDATE email_templates_global SET
|
||||||
|
subject = 'Solicitação aceita — {{session_date}} às {{session_time}}',
|
||||||
|
body_text = 'Sua solicitação de agendamento para {{session_date}} às {{session_time}} foi aceita.'
|
||||||
|
WHERE key = 'scheduler.request_accepted';
|
||||||
|
|
||||||
|
UPDATE email_templates_global SET
|
||||||
|
subject = 'Solicitação não disponível',
|
||||||
|
body_text = 'Infelizmente o horário solicitado não está disponível. Por favor, escolha outro horário.'
|
||||||
|
WHERE key = 'scheduler.request_rejected';
|
||||||
|
|
||||||
|
UPDATE email_templates_global SET
|
||||||
|
subject = 'Bem-vindo(a) à AgenciaPsi!',
|
||||||
|
body_text = 'Olá {{user_name}}, sua conta foi criada com sucesso. Acesse a plataforma para começar.'
|
||||||
|
WHERE key = 'system.welcome';
|
||||||
|
|
||||||
|
UPDATE email_templates_global SET
|
||||||
|
subject = 'Redefinição de senha — AgenciaPsi',
|
||||||
|
body_text = 'Clique no link abaixo para redefinir sua senha: {{reset_link}}'
|
||||||
|
WHERE key = 'system.password_reset';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 7. LOGIN_CAROUSEL_SLIDES — title, description
|
||||||
|
-- ============================================================
|
||||||
|
UPDATE login_carousel_slides SET
|
||||||
|
title = '<strong>Gestão clínica simplificada</strong>',
|
||||||
|
body = 'Gerencie agenda, pacientes e financeiro em um só lugar. Simples, rápido e seguro.'
|
||||||
|
WHERE ordem = 1;
|
||||||
|
|
||||||
|
UPDATE login_carousel_slides SET
|
||||||
|
title = '<strong>Múltiplos profissionais, uma só plataforma</strong>',
|
||||||
|
body = 'Ideal para clínicas com vários terapeutas. Cada profissional com sua agenda e seus pacientes.'
|
||||||
|
WHERE ordem = 2;
|
||||||
|
|
||||||
|
UPDATE login_carousel_slides SET
|
||||||
|
title = '<strong>Seguro, privado e sempre disponível</strong>',
|
||||||
|
body = 'Seus dados protegidos com criptografia. Acesse de qualquer lugar, a qualquer hora.'
|
||||||
|
WHERE ordem = 3;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 8. PATIENT_GROUPS (default groups) — name
|
||||||
|
-- ============================================================
|
||||||
|
UPDATE patient_groups SET nome = 'Crianças' WHERE nome LIKE 'Crian%' AND is_system = true;
|
||||||
|
UPDATE patient_groups SET nome = 'Adolescentes' WHERE nome LIKE 'Adolescen%' AND is_system = true;
|
||||||
|
UPDATE patient_groups SET nome = 'Idosos' WHERE nome LIKE 'Idoso%' AND is_system = true;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 9. AUTH.USERS — raw_user_meta_data (name field)
|
||||||
|
-- ============================================================
|
||||||
|
UPDATE auth.users SET raw_user_meta_data = jsonb_set(raw_user_meta_data, '{name}', '"Clínica Espaço Psi"') WHERE id = 'aaaaaaaa-0003-0003-0003-000000000003';
|
||||||
|
UPDATE auth.users SET raw_user_meta_data = jsonb_set(raw_user_meta_data, '{name}', '"Clínica Mente Sã"') WHERE id = 'aaaaaaaa-0004-0004-0004-000000000004';
|
||||||
|
UPDATE auth.users SET raw_user_meta_data = jsonb_set(raw_user_meta_data, '{name}', '"Clínica Bem Estar"') WHERE id = 'aaaaaaaa-0005-0005-0005-000000000005';
|
||||||
|
UPDATE auth.users SET raw_user_meta_data = jsonb_set(raw_user_meta_data, '{name}', '"Gabriela Secretária"') WHERE id = 'aaaaaaaa-0011-0011-0011-000000000011';
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
broken_count int;
|
||||||
|
BEGIN
|
||||||
|
SELECT count(*) INTO broken_count
|
||||||
|
FROM profiles WHERE full_name LIKE '%??%';
|
||||||
|
|
||||||
|
IF broken_count = 0 THEN
|
||||||
|
RAISE NOTICE 'fix_encoding_accents: Todos os acentos corrigidos com sucesso.';
|
||||||
|
ELSE
|
||||||
|
RAISE WARNING 'fix_encoding_accents: Ainda restam % registros com ?? em profiles.full_name', broken_count;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user