Agenda, Agendador, Configurações

This commit is contained in:
Leonardo
2026-03-12 08:58:36 -03:00
parent f733db8436
commit f4b185ae17
197 changed files with 33405 additions and 6507 deletions

View File

@@ -0,0 +1,2 @@
ALTER TABLE public.agenda_configuracoes DROP COLUMN IF EXISTS
session_start_offset_min;

View File

@@ -0,0 +1 @@
select pg_get_functiondef('public.NOME_DA_FUNCAO(args_aqui)'::regprocedure);

View File

@@ -0,0 +1,45 @@
-- 1) Tabela profiles
create table if not exists public.profiles (
id uuid primary key references auth.users(id) on delete cascade,
role text not null default 'patient' check (role in ('admin','therapist','patient')),
full_name text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
-- 2) updated_at automático
create or replace function public.set_updated_at()
returns trigger
language plpgsql
as $$
begin
new.updated_at = now();
return new;
end;
$$;
drop trigger if exists trg_profiles_updated_at on public.profiles;
create trigger trg_profiles_updated_at
before update on public.profiles
for each row execute function public.set_updated_at();
-- 3) Trigger: cria profile automaticamente quando usuário nasce no auth
create or replace function public.handle_new_user()
returns trigger
language plpgsql
security definer
set search_path = public
as $$
begin
insert into public.profiles (id, role)
values (new.id, 'patient')
on conflict (id) do nothing;
return new;
end;
$$;
drop trigger if exists on_auth_user_created on auth.users;
create trigger on_auth_user_created
after insert on auth.users
for each row execute function public.handle_new_user();

View File

@@ -0,0 +1,20 @@
do $$
begin
if exists (
select 1
from information_schema.columns
where table_schema = 'public'
and table_name = 'patient_patient_tag'
and column_name = 'patient_tag_id'
)
and not exists (
select 1
from information_schema.columns
where table_schema = 'public'
and table_name = 'patient_patient_tag'
and column_name = 'tag_id'
) then
alter table public.patient_patient_tag
rename column patient_tag_id to tag_id;
end if;
end $$;

View File

@@ -0,0 +1,5 @@
insert into public.profiles (id, role)
select u.id, 'patient'
from auth.users u
left join public.profiles p on p.id = u.id
where p.id is null;

View File

@@ -0,0 +1,12 @@
select
id, name, email, phone,
birth_date, cpf, rg, gender,
marital_status, profession,
place_of_birth, education_level,
cep, address_street, address_number,
address_neighborhood, address_city, address_state,
phone_alt,
notes, consent, created_at
from public.patient_intake_requests
order by created_at desc
limit 1;

View File

@@ -0,0 +1,23 @@
create or replace function public.agenda_cfg_sync()
returns trigger
language plpgsql
as $$
begin
if new.agenda_view_mode = 'custom' then
new.usar_horario_admin_custom := true;
new.admin_inicio_visualizacao := new.agenda_custom_start;
new.admin_fim_visualizacao := new.agenda_custom_end;
else
new.usar_horario_admin_custom := false;
end if;
return new;
end;
$$;
drop trigger if exists trg_agenda_cfg_sync on public.agenda_configuracoes;
create trigger trg_agenda_cfg_sync
before insert or update on public.agenda_configuracoes
for each row
execute function public.agenda_cfg_sync();

View File

@@ -0,0 +1,2 @@
drop index if exists public.uq_subscriptions_tenant;
drop index if exists public.uq_subscriptions_personal_user;

View File

@@ -0,0 +1,6 @@
-- Limpa TUDO da agenda (eventos, regras, exceções)
-- Execute no Supabase Studio — não tem volta!
DELETE FROM recurrence_exceptions;
DELETE FROM recurrence_rules;
DELETE FROM agenda_eventos;

View File

@@ -0,0 +1,12 @@
select
t.tgname as trigger_name,
pg_get_triggerdef(t.oid) as trigger_def,
p.proname as function_name,
n.nspname as function_schema
from pg_trigger t
join pg_proc p on p.oid = t.tgfoid
join pg_namespace n on n.oid = p.pronamespace
join pg_class c on c.oid = t.tgrelid
where not t.tgisinternal
and c.relname = 'patient_intake_requests'
order by t.tgname;

View File

@@ -0,0 +1,46 @@
-- ============================================================
-- LIMPEZA DE DADOS DE TESTE — filtra por tenant/owner
-- Execute no Supabase Studio com cuidado -- ============================================================
DO $$ DECLARE
v_tenant_id uuid := 'bbbbbbbb-0002-0002-0002-000000000002';
v_owner_id uuid := 'aaaaaaaa-0002-0002-0002-000000000002';
n_exc int;
n_ev int;
n_rule int;
n_sol int;
BEGIN
-- 1. Exceções (filha de recurrence_rules — apagar primeiro)
DELETE FROM public.recurrence_exceptions
WHERE recurrence_id IN (
SELECT id FROM public.recurrence_rules
WHERE (v_tenant_id IS NULL OR tenant_id = v_tenant_id)
AND (v_owner_id IS NULL OR owner_id = v_owner_id)
);
GET DIAGNOSTICS n_exc = ROW_COUNT;
-- 2. Regras de recorrência
DELETE FROM public.recurrence_rules
WHERE (v_tenant_id IS NULL OR tenant_id = v_tenant_id)
AND (v_owner_id IS NULL OR owner_id = v_owner_id);
GET DIAGNOSTICS n_rule = ROW_COUNT;
-- 3. Eventos da agenda
DELETE FROM public.agenda_eventos
WHERE (v_tenant_id IS NULL OR tenant_id = v_tenant_id)
AND (v_owner_id IS NULL OR owner_id = v_owner_id);
GET DIAGNOSTICS n_ev = ROW_COUNT;
-- 4. Solicitações públicas (agendador online)
DELETE FROM public.agendador_solicitacoes
WHERE (v_tenant_id IS NULL OR tenant_id = v_tenant_id)
AND (v_owner_id IS NULL OR owner_id = v_owner_id);
GET DIAGNOSTICS n_sol = ROW_COUNT;
RAISE NOTICE '✅ Limpeza concluída:';
RAISE NOTICE ' recurrence_exceptions : %', n_exc;
RAISE NOTICE ' recurrence_rules : %', n_rule;
RAISE NOTICE ' agenda_eventos : %', n_ev;
RAISE NOTICE ' agendador_solicitacoes : %', n_sol;
END;
$$;

View File

@@ -0,0 +1,10 @@
select
id as tenant_member_id,
tenant_id,
user_id,
role,
status,
created_at
from public.tenant_members
where user_id = '824f125c-55bb-40f5-a8c4-7a33618b91c7'
order by created_at desc;

View File

@@ -0,0 +1,4 @@
select *
from agenda_eventos
order by created_at desc nulls last
limit 10;

View File

@@ -0,0 +1,6 @@
select
routine_name,
routine_type
from information_schema.routines
where routine_schema = 'public'
and routine_name ilike '%intake%';

View File

@@ -0,0 +1,4 @@
select id as owner_id, email, created_at
from auth.users
where email = 'admin@agendapsi.com.br'
limit 1;

View File

@@ -0,0 +1,8 @@
select
id,
owner_id,
status,
created_at,
converted_patient_id
from public.patient_intake_requests
where id = '54daa09a-b2cb-4a0b-91aa-e4cea1915efe';

View File

@@ -0,0 +1,6 @@
select f.key as feature_key
from public.plan_features pf
join public.features f on f.id = pf.feature_id
where pf.plan_id = 'fdc2813d-dfaa-4e2c-b71d-ef7b84dfd9e9'
and pf.enabled = true
order by f.key;

View File

@@ -0,0 +1,3 @@
select id, key, name
from public.plans
order by key;

View File

@@ -0,0 +1 @@
SELECT * FROM public.agendador_solicitacoes LIMIT 5;

View File

@@ -0,0 +1,5 @@
select table_schema, table_name, column_name
from information_schema.columns
where column_name in ('owner_id', 'tenant_id')
and table_schema = 'public'
order by table_name;

View File

@@ -0,0 +1,12 @@
alter table public.patient_groups enable row level security;
drop policy if exists patient_groups_select on public.patient_groups;
create policy patient_groups_select
on public.patient_groups
for select
to authenticated
using (
owner_id = auth.uid()
or owner_id is null
);

View File

@@ -0,0 +1,9 @@
select *
from public.owner_feature_entitlements
where owner_id = '816b24fe-a0c3-4409-b79b-c6c0a6935d03'::uuid
order by feature_key;
select public.has_feature('816b24fe-a0c3-4409-b79b-c6c0a6935d03'::uuid, 'online_scheduling.manage') as can_manage;

View File

@@ -0,0 +1,5 @@
create index if not exists idx_patient_group_patient_group_id
on public.patient_group_patient (patient_group_id);
create index if not exists idx_patient_groups_owner_system_nome
on public.patient_groups (owner_id, is_system, nome);

View File

@@ -0,0 +1,9 @@
-- 1) Marcar como SaaS master
insert into public.saas_admins (user_id)
values ('40a4b683-a0c9-4890-a201-20faf41fca06')
on conflict (user_id) do nothing;
-- 2) Garantir profile (seu session.js busca role em profiles)
insert into public.profiles (id, role)
values ('40a4b683-a0c9-4890-a201-20faf41fca06', 'saas_admin')
on conflict (id) do update set role = excluded.role;

View File

@@ -0,0 +1,5 @@
select *
from public.owner_feature_entitlements
where owner_id = '816b24fe-a0c3-4409-b79b-c6c0a6935d03'::uuid
order by feature_key;

View File

@@ -0,0 +1,5 @@
select column_name, data_type
from information_schema.columns
where table_schema = 'public'
and table_name = 'patient_patient_tag'
order by ordinal_position;

View File

@@ -0,0 +1,18 @@
create or replace function public.prevent_promoting_to_system()
returns trigger
language plpgsql
as $$
begin
if new.is_system = true and old.is_system is distinct from true then
raise exception 'Não é permitido transformar um grupo comum em grupo do sistema.';
end if;
return new;
end;
$$;
drop trigger if exists trg_prevent_promoting_to_system on public.patient_groups;
create trigger trg_prevent_promoting_to_system
before update on public.patient_groups
for each row
execute function public.prevent_promoting_to_system();

View File

@@ -0,0 +1,4 @@
select id, tenant_id, user_id, role, status, created_at
from tenant_members
where user_id = '1715ec83-9a30-4dce-b73a-2deb66dcfb13'
order by created_at desc;

View File

@@ -0,0 +1,2 @@
const { data, error } = await supabase.rpc('my_tenants')
console.log({ data, error })

View File

@@ -0,0 +1,6 @@
select id, tenant_id, user_id, role, status, created_at
from tenant_members
where tenant_id = '816b24fe-a0c3-4409-b79b-c6c0a6935d03'::uuid
order by
(case when role = 'clinic_admin' then 0 else 1 end),
created_at;

View File

@@ -0,0 +1,15 @@
-- Para o tenant A:
select id as responsible_member_id
from public.tenant_members
where tenant_id = '816b24fe-a0c3-4409-b79b-c6c0a6935d03'
and user_id = '824f125c-55bb-40f5-a8c4-7a33618b91c7'
and status = 'active'
limit 1;
-- Para o tenant B:
select id as responsible_member_id
from public.tenant_members
where tenant_id = 'e8b10543-fb36-4e75-9d37-6fece9745637'
and user_id = '824f125c-55bb-40f5-a8c4-7a33618b91c7'
and status = 'active'
limit 1;

View File

@@ -0,0 +1,4 @@
select id, name, kind, created_at
from public.tenants
order by created_at desc
limit 10;

View File

@@ -0,0 +1,5 @@
select id as member_id
from tenant_members
where tenant_id = '816b24fe-a0c3-4409-b79b-c6c0a6935d03'
and user_id = '824f125c-55bb-40f5-a8c4-7a33618b91c7'
limit 1;

View File

@@ -0,0 +1,2 @@
insert into public.users (id, created_at)
values ('e8b10543-fb36-4e75-9d37-6fece9745637', now());

View File

@@ -0,0 +1,4 @@
select *
from public.tenant_members
where tenant_id = 'UUID_AQUI'
order by created_at desc;

View File

@@ -0,0 +1,17 @@
create table if not exists public.subscriptions (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users(id) on delete cascade,
plan_key text not null,
interval text not null check (interval in ('month','year')),
status text not null check (status in ('active','canceled','past_due','trial')) default 'active',
started_at timestamptz not null default now(),
current_period_start timestamptz not null default now(),
current_period_end timestamptz null,
canceled_at timestamptz null,
source text not null default 'manual',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists subscriptions_user_id_idx on public.subscriptions(user_id);
create index if not exists subscriptions_status_idx on public.subscriptions(status);

View File

@@ -0,0 +1,13 @@
-- Ver todas as regras no banco
SELECT
id,
owner_id,
status,
type,
weekdays,
start_date,
end_date,
start_time,
created_at
FROM recurrence_rules
ORDER BY created_at DESC;

View File

@@ -0,0 +1,3 @@
select id, name, created_at
from public.tenants
order by created_at desc;

View File

@@ -0,0 +1,8 @@
alter table public.profiles enable row level security;
drop policy if exists "profiles_select_own" on public.profiles;
create policy "profiles_select_own"
on public.profiles
for select
to authenticated
using (id = auth.uid());

View File

@@ -0,0 +1,7 @@
-- 2) Tenho membership ativa no tenant atual?
select *
from tenant_members
where tenant_id = 'e8b10543-fb36-4e75-9d37-6fece9745637'
and user_id = auth.uid()
order by created_at desc;

View File

@@ -0,0 +1,8 @@
select
n.nspname as schema,
p.proname as function_name,
pg_get_functiondef(p.oid) as definition
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public'
and pg_get_functiondef(p.oid) ilike '%entitlements_invalidation%';

View File

@@ -0,0 +1,266 @@
-- =========================================================
-- PATCH — Completar cadastro para bater com PatientsCadastroPage.vue
-- (rode DEPOIS do seu supabase_cadastro_pacientes.sql)
-- =========================================================
create extension if not exists pgcrypto;
-- ---------------------------------------------------------
-- 1) Completar colunas que o front usa e hoje faltam em patients
-- ---------------------------------------------------------
do $$
begin
if not exists (
select 1 from information_schema.columns
where table_schema='public' and table_name='patients' and column_name='email_alt'
) then
alter table public.patients add column email_alt text;
end if;
if not exists (
select 1 from information_schema.columns
where table_schema='public' and table_name='patients' and column_name='phones'
) then
-- array de textos (Postgres). No JS você manda ["...","..."] normalmente.
alter table public.patients add column phones text[];
end if;
if not exists (
select 1 from information_schema.columns
where table_schema='public' and table_name='patients' and column_name='gender'
) then
alter table public.patients add column gender text;
end if;
if not exists (
select 1 from information_schema.columns
where table_schema='public' and table_name='patients' and column_name='marital_status'
) then
alter table public.patients add column marital_status text;
end if;
end $$;
-- (opcional) índices úteis pra busca/filtro por nome/email
create index if not exists idx_patients_owner_name on public.patients(owner_id, name);
create index if not exists idx_patients_owner_email on public.patients(owner_id, email);
-- ---------------------------------------------------------
-- 2) patient_groups
-- ---------------------------------------------------------
create table if not exists public.patient_groups (
id uuid primary key default gen_random_uuid(),
owner_id uuid not null references auth.users(id) on delete cascade,
name text not null,
color text,
is_system boolean not null default false,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
-- nome único por owner
do $$
begin
if not exists (
select 1 from pg_constraint
where conname = 'patient_groups_owner_name_uniq'
and conrelid = 'public.patient_groups'::regclass
) then
alter table public.patient_groups
add constraint patient_groups_owner_name_uniq unique(owner_id, name);
end if;
end $$;
drop trigger if exists trg_patient_groups_set_updated_at on public.patient_groups;
create trigger trg_patient_groups_set_updated_at
before update on public.patient_groups
for each row execute function public.set_updated_at();
create index if not exists idx_patient_groups_owner on public.patient_groups(owner_id);
alter table public.patient_groups enable row level security;
drop policy if exists "patient_groups_select_own" on public.patient_groups;
create policy "patient_groups_select_own"
on public.patient_groups for select
to authenticated
using (owner_id = auth.uid());
drop policy if exists "patient_groups_insert_own" on public.patient_groups;
create policy "patient_groups_insert_own"
on public.patient_groups for insert
to authenticated
with check (owner_id = auth.uid());
drop policy if exists "patient_groups_update_own" on public.patient_groups;
create policy "patient_groups_update_own"
on public.patient_groups for update
to authenticated
using (owner_id = auth.uid())
with check (owner_id = auth.uid());
drop policy if exists "patient_groups_delete_own" on public.patient_groups;
create policy "patient_groups_delete_own"
on public.patient_groups for delete
to authenticated
using (owner_id = auth.uid());
grant select, insert, update, delete on public.patient_groups to authenticated;
-- ---------------------------------------------------------
-- 3) patient_tags
-- ---------------------------------------------------------
create table if not exists public.patient_tags (
id uuid primary key default gen_random_uuid(),
owner_id uuid not null references auth.users(id) on delete cascade,
name text not null,
color text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
do $$
begin
if not exists (
select 1 from pg_constraint
where conname = 'patient_tags_owner_name_uniq'
and conrelid = 'public.patient_tags'::regclass
) then
alter table public.patient_tags
add constraint patient_tags_owner_name_uniq unique(owner_id, name);
end if;
end $$;
drop trigger if exists trg_patient_tags_set_updated_at on public.patient_tags;
create trigger trg_patient_tags_set_updated_at
before update on public.patient_tags
for each row execute function public.set_updated_at();
create index if not exists idx_patient_tags_owner on public.patient_tags(owner_id);
alter table public.patient_tags enable row level security;
drop policy if exists "patient_tags_select_own" on public.patient_tags;
create policy "patient_tags_select_own"
on public.patient_tags for select
to authenticated
using (owner_id = auth.uid());
drop policy if exists "patient_tags_insert_own" on public.patient_tags;
create policy "patient_tags_insert_own"
on public.patient_tags for insert
to authenticated
with check (owner_id = auth.uid());
drop policy if exists "patient_tags_update_own" on public.patient_tags;
create policy "patient_tags_update_own"
on public.patient_tags for update
to authenticated
using (owner_id = auth.uid())
with check (owner_id = auth.uid());
drop policy if exists "patient_tags_delete_own" on public.patient_tags;
create policy "patient_tags_delete_own"
on public.patient_tags for delete
to authenticated
using (owner_id = auth.uid());
grant select, insert, update, delete on public.patient_tags to authenticated;
-- ---------------------------------------------------------
-- 4) pivôs (patient_group_patient / patient_patient_tag)
-- ---------------------------------------------------------
create table if not exists public.patient_group_patient (
patient_id uuid not null references public.patients(id) on delete cascade,
patient_group_id uuid not null references public.patient_groups(id) on delete cascade,
created_at timestamptz not null default now(),
primary key (patient_id, patient_group_id)
);
create index if not exists idx_pgp_patient on public.patient_group_patient(patient_id);
create index if not exists idx_pgp_group on public.patient_group_patient(patient_group_id);
alter table public.patient_group_patient enable row level security;
-- a pivot “herda” tenant via join; policy usando exists pra validar owner do patient
drop policy if exists "pgp_select_own" on public.patient_group_patient;
create policy "pgp_select_own"
on public.patient_group_patient for select
to authenticated
using (
exists (
select 1 from public.patients p
where p.id = patient_group_patient.patient_id
and p.owner_id = auth.uid()
)
);
drop policy if exists "pgp_write_own" on public.patient_group_patient;
create policy "pgp_write_own"
on public.patient_group_patient for all
to authenticated
using (
exists (
select 1 from public.patients p
where p.id = patient_group_patient.patient_id
and p.owner_id = auth.uid()
)
)
with check (
exists (
select 1 from public.patients p
where p.id = patient_group_patient.patient_id
and p.owner_id = auth.uid()
)
);
grant select, insert, update, delete on public.patient_group_patient to authenticated;
-- tags pivot (ATENÇÃO: coluna é tag_id, como teu Vue usa!)
create table if not exists public.patient_patient_tag (
patient_id uuid not null references public.patients(id) on delete cascade,
tag_id uuid not null references public.patient_tags(id) on delete cascade,
created_at timestamptz not null default now(),
primary key (patient_id, tag_id)
);
create index if not exists idx_ppt_patient on public.patient_patient_tag(patient_id);
create index if not exists idx_ppt_tag on public.patient_patient_tag(tag_id);
alter table public.patient_patient_tag enable row level security;
drop policy if exists "ppt_select_own" on public.patient_patient_tag;
create policy "ppt_select_own"
on public.patient_patient_tag for select
to authenticated
using (
exists (
select 1 from public.patients p
where p.id = patient_patient_tag.patient_id
and p.owner_id = auth.uid()
)
);
drop policy if exists "ppt_write_own" on public.patient_patient_tag;
create policy "ppt_write_own"
on public.patient_patient_tag for all
to authenticated
using (
exists (
select 1 from public.patients p
where p.id = patient_patient_tag.patient_id
and p.owner_id = auth.uid()
)
)
with check (
exists (
select 1 from public.patients p
where p.id = patient_patient_tag.patient_id
and p.owner_id = auth.uid()
)
);
grant select, insert, update, delete on public.patient_patient_tag to authenticated;
-- =========================================================
-- FIM PATCH
-- =========================================================

View File

@@ -0,0 +1,42 @@
-- BUCKET avatars: RLS por pasta do usuário "<uid>/..."
-- Requer que seu path seja: `${auth.uid()}/...` (no seu código já é)
drop policy if exists "avatars_select_own" on storage.objects;
create policy "avatars_select_own"
on storage.objects for select
to authenticated
using (
bucket_id = 'avatars'
and name like auth.uid()::text || '/%'
);
drop policy if exists "avatars_insert_own" on storage.objects;
create policy "avatars_insert_own"
on storage.objects for insert
to authenticated
with check (
bucket_id = 'avatars'
and name like auth.uid()::text || '/%'
);
drop policy if exists "avatars_update_own" on storage.objects;
create policy "avatars_update_own"
on storage.objects for update
to authenticated
using (
bucket_id = 'avatars'
and name like auth.uid()::text || '/%'
)
with check (
bucket_id = 'avatars'
and name like auth.uid()::text || '/%'
);
drop policy if exists "avatars_delete_own" on storage.objects;
create policy "avatars_delete_own"
on storage.objects for delete
to authenticated
using (
bucket_id = 'avatars'
and name like auth.uid()::text || '/%'
);

View File

@@ -0,0 +1,3 @@
select *
from tenant_members
where user_id = 'SEU_USER_ID';

View File

@@ -0,0 +1,14 @@
select
tm.id as responsible_member_id,
tm.tenant_id,
tm.user_id,
tm.role,
tm.status,
tm.created_at
from public.tenant_members tm
where tm.tenant_id = (
select owner_id from public.patient_intake_requests
where id = '54daa09a-b2cb-4a0b-91aa-e4cea1915efe'
)
and tm.user_id = '824f125c-55bb-40f5-a8c4-7a33618b91c7'
limit 10;

View File

@@ -0,0 +1,17 @@
-- 1) Qual é meu uid?
select auth.uid() as my_uid;
-- 2) Tenho membership ativa no tenant atual?
select *
from tenant_members
where tenant_id = 'e8b10543-fb36-4e75-9d37-6fece9745637'
and user_id = auth.uid()
order by created_at desc;
-- 3) Se você usa status:
select *
from tenant_members
where tenant_id = 'e8b10543-fb36-4e75-9d37-6fece9745637'
and user_id = auth.uid()
and status = 'active'
order by created_at desc;

View File

@@ -0,0 +1,123 @@
-- ============================================================
-- saas_docs — Documentação dinâmica do sistema
-- Exibida nas páginas do frontend via botão "Ajuda"
-- ============================================================
-- ------------------------------------------------------------
-- 1. TABELA
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.saas_docs (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
titulo text NOT NULL,
conteudo text NOT NULL DEFAULT '',
medias jsonb NOT NULL DEFAULT '[]'::jsonb,
-- formato: [{ "tipo": "imagem"|"video", "url": "..." }, ...]
tipo_acesso text NOT NULL DEFAULT 'usuario'
CHECK (tipo_acesso IN ('usuario', 'admin')),
-- 'usuario' → todos os autenticados
-- 'admin' → clinic_admin, tenant_admin, saas_admin
pagina_path text NOT NULL,
-- path da rota do frontend, ex: '/therapist/agenda'
pagina_label text,
-- label amigável (informativo, não usado no match)
docs_relacionados uuid[] NOT NULL DEFAULT '{}',
-- IDs de outros saas_docs exibidos como "Veja também"
ativo boolean NOT NULL DEFAULT true,
ordem int NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
-- ------------------------------------------------------------
-- 2. ÍNDICE
-- ------------------------------------------------------------
-- Query principal do frontend: filtra por path + ativo
CREATE INDEX IF NOT EXISTS saas_docs_path_ativo_idx
ON public.saas_docs (pagina_path, ativo);
-- ------------------------------------------------------------
-- 3. RLS
-- ------------------------------------------------------------
ALTER TABLE public.saas_docs ENABLE ROW LEVEL SECURITY;
-- SaaS admin: acesso total (SELECT, INSERT, UPDATE, DELETE)
-- Verificado via tabela saas_admins
CREATE POLICY "saas_admin_full_access" ON public.saas_docs
FOR ALL
TO authenticated
USING (
EXISTS (
SELECT 1 FROM public.saas_admins
WHERE saas_admins.user_id = auth.uid()
)
)
WITH CHECK (
EXISTS (
SELECT 1 FROM public.saas_admins
WHERE saas_admins.user_id = auth.uid()
)
);
-- Admins de clínica: leem todos os docs ativos (usuario + admin)
CREATE POLICY "clinic_admin_read_all_docs" ON public.saas_docs
FOR SELECT
TO authenticated
USING (
ativo = true
AND EXISTS (
SELECT 1 FROM public.profiles
WHERE profiles.id = auth.uid()
AND profiles.role IN ('clinic_admin', 'tenant_admin')
)
);
-- Demais usuários autenticados: leem apenas docs do tipo 'usuario'
CREATE POLICY "users_read_usuario_docs" ON public.saas_docs
FOR SELECT
TO authenticated
USING (
ativo = true
AND tipo_acesso = 'usuario'
);
-- ------------------------------------------------------------
-- 4. STORAGE — bucket saas-docs (imagens dos documentos)
-- ------------------------------------------------------------
INSERT INTO storage.buckets (id, name, public)
VALUES ('saas-docs', 'saas-docs', true)
ON CONFLICT (id) DO NOTHING;
-- SaaS admin: pode fazer upload
CREATE POLICY "saas_admin_storage_upload" ON storage.objects
FOR INSERT
TO authenticated
WITH CHECK (
bucket_id = 'saas-docs'
AND EXISTS (
SELECT 1 FROM public.saas_admins
WHERE saas_admins.user_id = auth.uid()
)
);
-- SaaS admin: pode deletar
CREATE POLICY "saas_admin_storage_delete" ON storage.objects
FOR DELETE
TO authenticated
USING (
bucket_id = 'saas-docs'
AND EXISTS (
SELECT 1 FROM public.saas_admins
WHERE saas_admins.user_id = auth.uid()
)
);
-- Leitura pública (bucket é público, mas policy explícita para clareza)
CREATE POLICY "saas_docs_public_read" ON storage.objects
FOR SELECT
TO public
USING (bucket_id = 'saas-docs');