Agenda, Agendador, Configurações
This commit is contained in:
110
DBS/2026-03-11/src-sql-arquivos/01_profiles.sql
Normal file
110
DBS/2026-03-11/src-sql-arquivos/01_profiles.sql
Normal file
@@ -0,0 +1,110 @@
|
||||
-- =========================================================
|
||||
-- Agência PSI — Profiles (v2) + Trigger + RLS
|
||||
-- - 1 profile por auth.users.id
|
||||
-- - role base (admin|therapist|patient)
|
||||
-- - pronto para evoluir p/ multi-tenant depois
|
||||
-- =========================================================
|
||||
|
||||
-- 0) Função padrão updated_at (se já existir, mantém)
|
||||
create or replace function public.set_updated_at()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
new.updated_at = now();
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- 1) Tabela profiles
|
||||
create table if not exists public.profiles (
|
||||
id uuid primary key, -- = auth.users.id
|
||||
email text,
|
||||
full_name text,
|
||||
avatar_url text,
|
||||
|
||||
role text not null default 'patient',
|
||||
status text not null default 'active',
|
||||
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
|
||||
constraint profiles_role_check check (role in ('admin','therapist','patient')),
|
||||
constraint profiles_status_check check (status in ('active','inactive','invited'))
|
||||
);
|
||||
|
||||
-- FK opcional (em Supabase costuma ser ok)
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1
|
||||
from pg_constraint
|
||||
where conname = 'profiles_id_fkey'
|
||||
) then
|
||||
alter table public.profiles
|
||||
add constraint profiles_id_fkey
|
||||
foreign key (id) references auth.users(id)
|
||||
on delete cascade;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- Índices úteis
|
||||
create index if not exists profiles_role_idx on public.profiles(role);
|
||||
create index if not exists profiles_status_idx on public.profiles(status);
|
||||
|
||||
-- 2) Trigger updated_at
|
||||
drop trigger if exists t_profiles_set_updated_at on public.profiles;
|
||||
create trigger t_profiles_set_updated_at
|
||||
before update on public.profiles
|
||||
for each row execute function public.set_updated_at();
|
||||
|
||||
-- 3) Trigger pós-signup: cria profile automático
|
||||
-- Observação: roda como SECURITY DEFINER
|
||||
create or replace function public.handle_new_user()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
begin
|
||||
insert into public.profiles (id, email, role, status)
|
||||
values (new.id, new.email, 'patient', 'active')
|
||||
on conflict (id) do update
|
||||
set email = excluded.email;
|
||||
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists on_auth_user_created on auth.users;
|
||||
create trigger on_auth_user_created
|
||||
after insert on auth.users
|
||||
for each row execute function public.handle_new_user();
|
||||
|
||||
-- 4) RLS
|
||||
alter table public.profiles enable row level security;
|
||||
|
||||
-- Leitura do próprio profile
|
||||
drop policy if exists "profiles_select_own" on public.profiles;
|
||||
create policy "profiles_select_own"
|
||||
on public.profiles
|
||||
for select
|
||||
to authenticated
|
||||
using (id = auth.uid());
|
||||
|
||||
-- Update do próprio profile (campos não-sensíveis)
|
||||
drop policy if exists "profiles_update_own" on public.profiles;
|
||||
create policy "profiles_update_own"
|
||||
on public.profiles
|
||||
for update
|
||||
to authenticated
|
||||
using (id = auth.uid())
|
||||
with check (id = auth.uid());
|
||||
|
||||
-- Insert só do próprio (na prática quem insere é trigger, mas deixa coerente)
|
||||
drop policy if exists "profiles_insert_own" on public.profiles;
|
||||
create policy "profiles_insert_own"
|
||||
on public.profiles
|
||||
for insert
|
||||
to authenticated
|
||||
with check (id = auth.uid());
|
||||
212
DBS/2026-03-11/src-sql-arquivos/supabase_cadastro_externo.sql
Normal file
212
DBS/2026-03-11/src-sql-arquivos/supabase_cadastro_externo.sql
Normal file
@@ -0,0 +1,212 @@
|
||||
-- =========================================================
|
||||
-- Agência PSI Quasar — Cadastro Externo de Paciente (Supabase/Postgres)
|
||||
-- Objetivo:
|
||||
-- - Ter um link público com TOKEN que o terapeuta envia ao paciente
|
||||
-- - Paciente preenche um formulário público
|
||||
-- - Salva em "intake requests" (pré-cadastro)
|
||||
-- - Terapeuta revisa e converte em paciente dentro do sistema
|
||||
--
|
||||
-- Tabelas:
|
||||
-- - patient_invites
|
||||
-- - patient_intake_requests
|
||||
--
|
||||
-- Funções:
|
||||
-- - create_patient_intake_request (RPC pública - anon)
|
||||
--
|
||||
-- Segurança:
|
||||
-- - RLS habilitada
|
||||
-- - Público (anon) não lê nada, só executa RPC
|
||||
-- - Terapeuta (authenticated) lê/atualiza somente seus registros
|
||||
-- =========================================================
|
||||
|
||||
-- 0) Tabelas
|
||||
create table if not exists public.patient_invites (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
owner_id uuid not null,
|
||||
token text not null unique,
|
||||
active boolean not null default true,
|
||||
expires_at timestamptz null,
|
||||
max_uses int null,
|
||||
uses int not null default 0,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists patient_invites_owner_id_idx on public.patient_invites(owner_id);
|
||||
create index if not exists patient_invites_token_idx on public.patient_invites(token);
|
||||
|
||||
create table if not exists public.patient_intake_requests (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
owner_id uuid not null,
|
||||
token text not null,
|
||||
name text not null,
|
||||
email text null,
|
||||
phone text null,
|
||||
notes text null,
|
||||
consent boolean not null default false,
|
||||
status text not null default 'new', -- new | converted | rejected
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists patient_intake_owner_id_idx on public.patient_intake_requests(owner_id);
|
||||
create index if not exists patient_intake_token_idx on public.patient_intake_requests(token);
|
||||
create index if not exists patient_intake_status_idx on public.patient_intake_requests(status);
|
||||
|
||||
-- 1) RLS
|
||||
alter table public.patient_invites enable row level security;
|
||||
alter table public.patient_intake_requests enable row level security;
|
||||
|
||||
-- 2) Fechar acesso direto para anon (público)
|
||||
revoke all on table public.patient_invites from anon;
|
||||
revoke all on table public.patient_intake_requests from anon;
|
||||
|
||||
-- 3) Policies: terapeuta (authenticated) - somente próprios registros
|
||||
|
||||
-- patient_invites
|
||||
drop policy if exists invites_select_own on public.patient_invites;
|
||||
create policy invites_select_own
|
||||
on public.patient_invites for select
|
||||
to authenticated
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists invites_insert_own on public.patient_invites;
|
||||
create policy invites_insert_own
|
||||
on public.patient_invites for insert
|
||||
to authenticated
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists invites_update_own on public.patient_invites;
|
||||
create policy invites_update_own
|
||||
on public.patient_invites for update
|
||||
to authenticated
|
||||
using (owner_id = auth.uid())
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
-- patient_intake_requests
|
||||
drop policy if exists intake_select_own on public.patient_intake_requests;
|
||||
create policy intake_select_own
|
||||
on public.patient_intake_requests for select
|
||||
to authenticated
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists intake_update_own on public.patient_intake_requests;
|
||||
create policy intake_update_own
|
||||
on public.patient_intake_requests for update
|
||||
to authenticated
|
||||
using (owner_id = auth.uid())
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
-- 4) RPC pública para criar intake (página pública)
|
||||
-- Importantíssimo: security definer + search_path fixo
|
||||
create or replace function public.create_patient_intake_request(
|
||||
p_token text,
|
||||
p_name text,
|
||||
p_email text default null,
|
||||
p_phone text default null,
|
||||
p_notes text default null,
|
||||
p_consent boolean default false
|
||||
)
|
||||
returns uuid
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
declare
|
||||
v_owner uuid;
|
||||
v_active boolean;
|
||||
v_expires timestamptz;
|
||||
v_max_uses int;
|
||||
v_uses int;
|
||||
v_id uuid;
|
||||
begin
|
||||
select owner_id, active, expires_at, max_uses, uses
|
||||
into v_owner, v_active, v_expires, v_max_uses, v_uses
|
||||
from public.patient_invites
|
||||
where token = p_token
|
||||
limit 1;
|
||||
|
||||
if v_owner is null then
|
||||
raise exception 'Token inválido';
|
||||
end if;
|
||||
|
||||
if v_active is not true then
|
||||
raise exception 'Link desativado';
|
||||
end if;
|
||||
|
||||
if v_expires is not null and now() > v_expires then
|
||||
raise exception 'Link expirado';
|
||||
end if;
|
||||
|
||||
if v_max_uses is not null and v_uses >= v_max_uses then
|
||||
raise exception 'Limite de uso atingido';
|
||||
end if;
|
||||
|
||||
if p_name is null or length(trim(p_name)) = 0 then
|
||||
raise exception 'Nome é obrigatório';
|
||||
end if;
|
||||
|
||||
insert into public.patient_intake_requests
|
||||
(owner_id, token, name, email, phone, notes, consent, status)
|
||||
values
|
||||
(v_owner, p_token, trim(p_name),
|
||||
nullif(lower(trim(p_email)), ''),
|
||||
nullif(trim(p_phone), ''),
|
||||
nullif(trim(p_notes), ''),
|
||||
coalesce(p_consent, false),
|
||||
'new')
|
||||
returning id into v_id;
|
||||
|
||||
update public.patient_invites
|
||||
set uses = uses + 1
|
||||
where token = p_token;
|
||||
|
||||
return v_id;
|
||||
end;
|
||||
$$;
|
||||
|
||||
grant execute on function public.create_patient_intake_request(text, text, text, text, text, boolean) to anon;
|
||||
grant execute on function public.create_patient_intake_request(text, text, text, text, text, boolean) to authenticated;
|
||||
|
||||
-- 5) (Opcional) helper para rotacionar token no painel (somente authenticated)
|
||||
-- Você pode usar no front via supabase.rpc('rotate_patient_invite_token')
|
||||
create or replace function public.rotate_patient_invite_token(
|
||||
p_new_token text
|
||||
)
|
||||
returns uuid
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
declare
|
||||
v_uid uuid;
|
||||
v_id uuid;
|
||||
begin
|
||||
-- pega o usuário logado
|
||||
v_uid := auth.uid();
|
||||
if v_uid is null then
|
||||
raise exception 'Usuário não autenticado';
|
||||
end if;
|
||||
|
||||
-- desativa tokens antigos ativos do usuário
|
||||
update public.patient_invites
|
||||
set active = false
|
||||
where owner_id = v_uid
|
||||
and active = true;
|
||||
|
||||
-- cria novo token
|
||||
insert into public.patient_invites (owner_id, token, active)
|
||||
values (v_uid, p_new_token, true)
|
||||
returning id into v_id;
|
||||
|
||||
return v_id;
|
||||
end;
|
||||
$$;
|
||||
|
||||
grant execute on function public.rotate_patient_invite_token(text) to authenticated;
|
||||
|
||||
grant select, insert, update, delete on table public.patient_invites to authenticated;
|
||||
grant select, insert, update, delete on table public.patient_intake_requests to authenticated;
|
||||
|
||||
-- anon não precisa acessar tabelas diretamente
|
||||
revoke all on table public.patient_invites from anon;
|
||||
revoke all on table public.patient_intake_requests from anon;
|
||||
|
||||
266
DBS/2026-03-11/src-sql-arquivos/supabase_cadastro_pacientes.sql
Normal file
266
DBS/2026-03-11/src-sql-arquivos/supabase_cadastro_pacientes.sql
Normal file
@@ -0,0 +1,266 @@
|
||||
-- =========================================================
|
||||
-- PATCH — Completar cadastro para bater com PatientsCadastroPage.vue
|
||||
-- (rode DEPOIS do seu supabase_cadastro_pacientes.sql)
|
||||
-- =========================================================
|
||||
|
||||
create extension if not exists pgcrypto;
|
||||
|
||||
-- ---------------------------------------------------------
|
||||
-- 1) Completar colunas que o front usa e hoje faltam em patients
|
||||
-- ---------------------------------------------------------
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_schema='public' and table_name='patients' and column_name='email_alt'
|
||||
) then
|
||||
alter table public.patients add column email_alt text;
|
||||
end if;
|
||||
|
||||
if not exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_schema='public' and table_name='patients' and column_name='phones'
|
||||
) then
|
||||
-- array de textos (Postgres). No JS você manda ["...","..."] normalmente.
|
||||
alter table public.patients add column phones text[];
|
||||
end if;
|
||||
|
||||
if not exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_schema='public' and table_name='patients' and column_name='gender'
|
||||
) then
|
||||
alter table public.patients add column gender text;
|
||||
end if;
|
||||
|
||||
if not exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_schema='public' and table_name='patients' and column_name='marital_status'
|
||||
) then
|
||||
alter table public.patients add column marital_status text;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- (opcional) índices úteis pra busca/filtro por nome/email
|
||||
create index if not exists idx_patients_owner_name on public.patients(owner_id, name);
|
||||
create index if not exists idx_patients_owner_email on public.patients(owner_id, email);
|
||||
|
||||
-- ---------------------------------------------------------
|
||||
-- 2) patient_groups
|
||||
-- ---------------------------------------------------------
|
||||
create table if not exists public.patient_groups (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
owner_id uuid not null references auth.users(id) on delete cascade,
|
||||
name text not null,
|
||||
color text,
|
||||
is_system boolean not null default false,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- nome único por owner
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'patient_groups_owner_name_uniq'
|
||||
and conrelid = 'public.patient_groups'::regclass
|
||||
) then
|
||||
alter table public.patient_groups
|
||||
add constraint patient_groups_owner_name_uniq unique(owner_id, name);
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
drop trigger if exists trg_patient_groups_set_updated_at on public.patient_groups;
|
||||
create trigger trg_patient_groups_set_updated_at
|
||||
before update on public.patient_groups
|
||||
for each row execute function public.set_updated_at();
|
||||
|
||||
create index if not exists idx_patient_groups_owner on public.patient_groups(owner_id);
|
||||
|
||||
alter table public.patient_groups enable row level security;
|
||||
|
||||
drop policy if exists "patient_groups_select_own" on public.patient_groups;
|
||||
create policy "patient_groups_select_own"
|
||||
on public.patient_groups for select
|
||||
to authenticated
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists "patient_groups_insert_own" on public.patient_groups;
|
||||
create policy "patient_groups_insert_own"
|
||||
on public.patient_groups for insert
|
||||
to authenticated
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists "patient_groups_update_own" on public.patient_groups;
|
||||
create policy "patient_groups_update_own"
|
||||
on public.patient_groups for update
|
||||
to authenticated
|
||||
using (owner_id = auth.uid())
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists "patient_groups_delete_own" on public.patient_groups;
|
||||
create policy "patient_groups_delete_own"
|
||||
on public.patient_groups for delete
|
||||
to authenticated
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
grant select, insert, update, delete on public.patient_groups to authenticated;
|
||||
|
||||
-- ---------------------------------------------------------
|
||||
-- 3) patient_tags
|
||||
-- ---------------------------------------------------------
|
||||
create table if not exists public.patient_tags (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
owner_id uuid not null references auth.users(id) on delete cascade,
|
||||
name text not null,
|
||||
color text,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'patient_tags_owner_name_uniq'
|
||||
and conrelid = 'public.patient_tags'::regclass
|
||||
) then
|
||||
alter table public.patient_tags
|
||||
add constraint patient_tags_owner_name_uniq unique(owner_id, name);
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
drop trigger if exists trg_patient_tags_set_updated_at on public.patient_tags;
|
||||
create trigger trg_patient_tags_set_updated_at
|
||||
before update on public.patient_tags
|
||||
for each row execute function public.set_updated_at();
|
||||
|
||||
create index if not exists idx_patient_tags_owner on public.patient_tags(owner_id);
|
||||
|
||||
alter table public.patient_tags enable row level security;
|
||||
|
||||
drop policy if exists "patient_tags_select_own" on public.patient_tags;
|
||||
create policy "patient_tags_select_own"
|
||||
on public.patient_tags for select
|
||||
to authenticated
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists "patient_tags_insert_own" on public.patient_tags;
|
||||
create policy "patient_tags_insert_own"
|
||||
on public.patient_tags for insert
|
||||
to authenticated
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists "patient_tags_update_own" on public.patient_tags;
|
||||
create policy "patient_tags_update_own"
|
||||
on public.patient_tags for update
|
||||
to authenticated
|
||||
using (owner_id = auth.uid())
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists "patient_tags_delete_own" on public.patient_tags;
|
||||
create policy "patient_tags_delete_own"
|
||||
on public.patient_tags for delete
|
||||
to authenticated
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
grant select, insert, update, delete on public.patient_tags to authenticated;
|
||||
|
||||
-- ---------------------------------------------------------
|
||||
-- 4) pivôs (patient_group_patient / patient_patient_tag)
|
||||
-- ---------------------------------------------------------
|
||||
create table if not exists public.patient_group_patient (
|
||||
patient_id uuid not null references public.patients(id) on delete cascade,
|
||||
patient_group_id uuid not null references public.patient_groups(id) on delete cascade,
|
||||
created_at timestamptz not null default now(),
|
||||
primary key (patient_id, patient_group_id)
|
||||
);
|
||||
|
||||
create index if not exists idx_pgp_patient on public.patient_group_patient(patient_id);
|
||||
create index if not exists idx_pgp_group on public.patient_group_patient(patient_group_id);
|
||||
|
||||
alter table public.patient_group_patient enable row level security;
|
||||
|
||||
-- a pivot “herda” tenant via join; policy usando exists pra validar owner do patient
|
||||
drop policy if exists "pgp_select_own" on public.patient_group_patient;
|
||||
create policy "pgp_select_own"
|
||||
on public.patient_group_patient for select
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1 from public.patients p
|
||||
where p.id = patient_group_patient.patient_id
|
||||
and p.owner_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists "pgp_write_own" on public.patient_group_patient;
|
||||
create policy "pgp_write_own"
|
||||
on public.patient_group_patient for all
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1 from public.patients p
|
||||
where p.id = patient_group_patient.patient_id
|
||||
and p.owner_id = auth.uid()
|
||||
)
|
||||
)
|
||||
with check (
|
||||
exists (
|
||||
select 1 from public.patients p
|
||||
where p.id = patient_group_patient.patient_id
|
||||
and p.owner_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on public.patient_group_patient to authenticated;
|
||||
|
||||
-- tags pivot (ATENÇÃO: coluna é tag_id, como teu Vue usa!)
|
||||
create table if not exists public.patient_patient_tag (
|
||||
patient_id uuid not null references public.patients(id) on delete cascade,
|
||||
tag_id uuid not null references public.patient_tags(id) on delete cascade,
|
||||
created_at timestamptz not null default now(),
|
||||
primary key (patient_id, tag_id)
|
||||
);
|
||||
|
||||
create index if not exists idx_ppt_patient on public.patient_patient_tag(patient_id);
|
||||
create index if not exists idx_ppt_tag on public.patient_patient_tag(tag_id);
|
||||
|
||||
alter table public.patient_patient_tag enable row level security;
|
||||
|
||||
drop policy if exists "ppt_select_own" on public.patient_patient_tag;
|
||||
create policy "ppt_select_own"
|
||||
on public.patient_patient_tag for select
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1 from public.patients p
|
||||
where p.id = patient_patient_tag.patient_id
|
||||
and p.owner_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists "ppt_write_own" on public.patient_patient_tag;
|
||||
create policy "ppt_write_own"
|
||||
on public.patient_patient_tag for all
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1 from public.patients p
|
||||
where p.id = patient_patient_tag.patient_id
|
||||
and p.owner_id = auth.uid()
|
||||
)
|
||||
)
|
||||
with check (
|
||||
exists (
|
||||
select 1 from public.patients p
|
||||
where p.id = patient_patient_tag.patient_id
|
||||
and p.owner_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
grant select, insert, update, delete on public.patient_patient_tag to authenticated;
|
||||
|
||||
-- =========================================================
|
||||
-- FIM PATCH
|
||||
-- =========================================================
|
||||
@@ -0,0 +1,105 @@
|
||||
-- =========================================================
|
||||
-- INTakes / Cadastros Recebidos - Supabase Local
|
||||
-- =========================================================
|
||||
|
||||
-- 0) Extensões úteis (geralmente já existem no Supabase, mas é seguro)
|
||||
create extension if not exists pgcrypto;
|
||||
|
||||
-- 1) Função padrão para updated_at
|
||||
create or replace function public.set_updated_at()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
new.updated_at = now();
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- 2) Tabela patient_intake_requests (espelhando nuvem)
|
||||
create table if not exists public.patient_intake_requests (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
owner_id uuid not null,
|
||||
token text,
|
||||
name text,
|
||||
email text,
|
||||
phone text,
|
||||
notes text,
|
||||
consent boolean not null default false,
|
||||
status text not null default 'new',
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
payload jsonb
|
||||
);
|
||||
|
||||
-- 3) Índices (performance em listagem e filtros)
|
||||
create index if not exists idx_intakes_owner_created
|
||||
on public.patient_intake_requests (owner_id, created_at desc);
|
||||
|
||||
create index if not exists idx_intakes_owner_status_created
|
||||
on public.patient_intake_requests (owner_id, status, created_at desc);
|
||||
|
||||
create index if not exists idx_intakes_status_created
|
||||
on public.patient_intake_requests (status, created_at desc);
|
||||
|
||||
-- 4) Trigger updated_at
|
||||
drop trigger if exists trg_patient_intake_requests_updated_at on public.patient_intake_requests;
|
||||
|
||||
create trigger trg_patient_intake_requests_updated_at
|
||||
before update on public.patient_intake_requests
|
||||
for each row execute function public.set_updated_at();
|
||||
|
||||
-- 5) RLS
|
||||
alter table public.patient_intake_requests enable row level security;
|
||||
|
||||
-- 6) Policies (iguais às que você mostrou na nuvem)
|
||||
drop policy if exists intake_select_own on public.patient_intake_requests;
|
||||
create policy intake_select_own
|
||||
on public.patient_intake_requests
|
||||
for select
|
||||
to authenticated
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists intake_update_own on public.patient_intake_requests;
|
||||
create policy intake_update_own
|
||||
on public.patient_intake_requests
|
||||
for update
|
||||
to authenticated
|
||||
using (owner_id = auth.uid())
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists "delete own intake requests" on public.patient_intake_requests;
|
||||
create policy "delete own intake requests"
|
||||
on public.patient_intake_requests
|
||||
for delete
|
||||
to authenticated
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
-- =========================================================
|
||||
-- OPCIONAL (RECOMENDADO): registrar conversão
|
||||
-- =========================================================
|
||||
-- Se você pretende marcar intake como convertido e guardar o patient_id:
|
||||
alter table public.patient_intake_requests
|
||||
add column if not exists converted_patient_id uuid;
|
||||
|
||||
create index if not exists idx_intakes_converted_patient_id
|
||||
on public.patient_intake_requests (converted_patient_id);
|
||||
|
||||
-- Opcional: impedir delete de intakes convertidos (melhor para auditoria)
|
||||
-- (Se quiser manter delete liberado como na nuvem, comente este bloco.)
|
||||
drop policy if exists "delete own intake requests" on public.patient_intake_requests;
|
||||
create policy "delete_own_intakes_not_converted"
|
||||
on public.patient_intake_requests
|
||||
for delete
|
||||
to authenticated
|
||||
using (owner_id = auth.uid() and status <> 'converted');
|
||||
|
||||
-- =========================================================
|
||||
-- OPCIONAL: check de status (evita status inválido)
|
||||
-- =========================================================
|
||||
alter table public.patient_intake_requests
|
||||
drop constraint if exists chk_intakes_status;
|
||||
|
||||
alter table public.patient_intake_requests
|
||||
add constraint chk_intakes_status
|
||||
check (status in ('new', 'converted', 'rejected'));
|
||||
174
DBS/2026-03-11/src-sql-arquivos/supabase_patient_groups.sql
Normal file
174
DBS/2026-03-11/src-sql-arquivos/supabase_patient_groups.sql
Normal file
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
patient_groups_setup.sql
|
||||
Setup completo para:
|
||||
- public.patient_groups
|
||||
- public.patient_group_patient (tabela ponte)
|
||||
- view public.v_patient_groups_with_counts
|
||||
- índice único por owner + nome (case-insensitive)
|
||||
- 3 grupos padrão do sistema (Crianças, Adolescentes, Idosos) NÃO editáveis / NÃO removíveis
|
||||
- triggers de proteção
|
||||
|
||||
Observação (importante):
|
||||
- Os grupos padrão são criados com owner_id = '00000000-0000-0000-0000-000000000000' (SYSTEM_OWNER),
|
||||
para ficarem "globais" e não dependerem de auth.uid() em migrations.
|
||||
- Se você quiser que os grupos padrão pertençam a um owner específico (tenant),
|
||||
basta trocar o SYSTEM_OWNER abaixo por esse UUID.
|
||||
*/
|
||||
|
||||
begin;
|
||||
|
||||
-- ===========================
|
||||
-- 0) Constante de "dono do sistema"
|
||||
-- ===========================
|
||||
-- Troque aqui se você quiser que os grupos padrão pertençam a um owner específico.
|
||||
-- Ex.: '816b24fe-a0c3-4409-b79b-c6c0a6935d03'
|
||||
do $$
|
||||
begin
|
||||
-- só para documentar; não cria nada
|
||||
end $$;
|
||||
|
||||
-- ===========================
|
||||
-- 1) Tabela principal: patient_groups
|
||||
-- ===========================
|
||||
create table if not exists public.patient_groups (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
name text not null,
|
||||
description text,
|
||||
color text,
|
||||
is_active boolean not null default true,
|
||||
is_system boolean not null default false,
|
||||
owner_id uuid not null,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- (Opcional, mas recomendado) Garante que name não seja só espaços
|
||||
-- e evita nomes vazios.
|
||||
alter table public.patient_groups
|
||||
drop constraint if exists patient_groups_name_not_blank_check;
|
||||
|
||||
alter table public.patient_groups
|
||||
add constraint patient_groups_name_not_blank_check
|
||||
check (length(btrim(name)) > 0);
|
||||
|
||||
-- ===========================
|
||||
-- 2) Tabela ponte: patient_group_patient
|
||||
-- ===========================
|
||||
-- Se você já tiver essa tabela com FKs, ajuste aqui conforme seu schema.
|
||||
create table if not exists public.patient_group_patient (
|
||||
patient_group_id uuid not null references public.patient_groups(id) on delete cascade,
|
||||
patient_id uuid not null references public.patients(id) on delete cascade,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- Evita duplicar vínculo paciente<->grupo
|
||||
create unique index if not exists patient_group_patient_unique
|
||||
on public.patient_group_patient (patient_group_id, patient_id);
|
||||
|
||||
-- ===========================
|
||||
-- 3) View com contagem
|
||||
-- ===========================
|
||||
create or replace view public.v_patient_groups_with_counts as
|
||||
select
|
||||
g.*,
|
||||
coalesce(count(distinct pgp.patient_id), 0)::int as patients_count
|
||||
from public.patient_groups g
|
||||
left join public.patient_group_patient pgp
|
||||
on pgp.patient_group_id = g.id
|
||||
group by g.id;
|
||||
|
||||
-- ===========================
|
||||
-- 4) Índice único: não permitir mesmo nome por owner (case-insensitive)
|
||||
-- ===========================
|
||||
-- Atenção: se já existirem duplicados, este índice pode falhar ao criar.
|
||||
create unique index if not exists patient_groups_owner_name_unique
|
||||
on public.patient_groups (owner_id, (lower(name)));
|
||||
|
||||
-- ===========================
|
||||
-- 5) Triggers de proteção: system não edita / não remove
|
||||
-- ===========================
|
||||
create or replace function public.prevent_system_group_changes()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if old.is_system = true then
|
||||
raise exception 'Grupos padrão do sistema não podem ser alterados ou excluídos.';
|
||||
end if;
|
||||
|
||||
if tg_op = 'DELETE' then
|
||||
return old;
|
||||
end if;
|
||||
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists trg_prevent_system_group_changes on public.patient_groups;
|
||||
|
||||
create trigger trg_prevent_system_group_changes
|
||||
before update or delete on public.patient_groups
|
||||
for each row
|
||||
execute function public.prevent_system_group_changes();
|
||||
|
||||
-- Impede "promover" um grupo comum para system
|
||||
create or replace function public.prevent_promoting_to_system()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if new.is_system = true and old.is_system is distinct from true then
|
||||
raise exception 'Não é permitido transformar um grupo comum em grupo do sistema.';
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists trg_prevent_promoting_to_system on public.patient_groups;
|
||||
|
||||
create trigger trg_prevent_promoting_to_system
|
||||
before update on public.patient_groups
|
||||
for each row
|
||||
execute function public.prevent_promoting_to_system();
|
||||
|
||||
-- ===========================
|
||||
-- 6) Inserir 3 grupos padrão (imutáveis)
|
||||
-- ===========================
|
||||
-- Dono "global" do sistema (mude se quiser):
|
||||
-- 00000000-0000-0000-0000-000000000000
|
||||
with sys_owner as (
|
||||
select '00000000-0000-0000-0000-000000000000'::uuid as owner_id
|
||||
)
|
||||
insert into public.patient_groups (name, description, color, is_active, is_system, owner_id)
|
||||
select v.name, v.description, v.color, v.is_active, v.is_system, s.owner_id
|
||||
from sys_owner s
|
||||
join (values
|
||||
('Crianças', 'Grupo padrão do sistema', null, true, true),
|
||||
('Adolescentes', 'Grupo padrão do sistema', null, true, true),
|
||||
('Idosos', 'Grupo padrão do sistema', null, true, true)
|
||||
) as v(name, description, color, is_active, is_system)
|
||||
on true
|
||||
where not exists (
|
||||
select 1
|
||||
from public.patient_groups g
|
||||
where g.owner_id = s.owner_id
|
||||
and lower(g.name) = lower(v.name)
|
||||
);
|
||||
|
||||
commit;
|
||||
|
||||
/*
|
||||
Testes rápidos:
|
||||
1) Ver tudo:
|
||||
select * from public.v_patient_groups_with_counts order by is_system desc, name;
|
||||
|
||||
2) Tentar editar um system (deve falhar):
|
||||
update public.patient_groups set name='X' where name='Crianças';
|
||||
|
||||
3) Tentar deletar um system (deve falhar):
|
||||
delete from public.patient_groups where name='Crianças';
|
||||
|
||||
4) Tentar duplicar nome no mesmo owner (deve falhar por índice único):
|
||||
insert into public.patient_groups (name, is_active, is_system, owner_id)
|
||||
values ('teste22', true, false, '816b24fe-a0c3-4409-b79b-c6c0a6935d03');
|
||||
*/
|
||||
147
DBS/2026-03-11/src-sql-arquivos/supabase_patient_index_page.sql
Normal file
147
DBS/2026-03-11/src-sql-arquivos/supabase_patient_index_page.sql
Normal file
@@ -0,0 +1,147 @@
|
||||
-- =========================================================
|
||||
-- pacientesIndexPage.sql
|
||||
-- Views + índices para a tela PatientsListPage
|
||||
-- =========================================================
|
||||
|
||||
-- 0) Extensões úteis
|
||||
create extension if not exists pg_trgm;
|
||||
|
||||
-- 1) updated_at automático (se você quiser manter updated_at sempre correto)
|
||||
create or replace function public.set_updated_at()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
new.updated_at = now();
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists trg_patients_set_updated_at on public.patients;
|
||||
create trigger trg_patients_set_updated_at
|
||||
before update on public.patients
|
||||
for each row execute function public.set_updated_at();
|
||||
|
||||
|
||||
-- =========================================================
|
||||
-- 2) Views de contagem (usadas em KPIs e telas auxiliares)
|
||||
-- =========================================================
|
||||
|
||||
-- 2.1) Grupos com contagem de pacientes
|
||||
create or replace view public.v_patient_groups_with_counts as
|
||||
select
|
||||
g.id,
|
||||
g.name,
|
||||
g.color,
|
||||
coalesce(count(pgp.patient_id), 0)::int as patients_count
|
||||
from public.patient_groups g
|
||||
left join public.patient_group_patient pgp
|
||||
on pgp.patient_group_id = g.id
|
||||
group by g.id, g.name, g.color;
|
||||
|
||||
-- 2.2) Tags com contagem de pacientes
|
||||
create or replace view public.v_tag_patient_counts as
|
||||
select
|
||||
t.id,
|
||||
t.name,
|
||||
t.color,
|
||||
coalesce(count(ppt.patient_id), 0)::int as patients_count
|
||||
from public.patient_tags t
|
||||
left join public.patient_patient_tag ppt
|
||||
on ppt.tag_id = t.id
|
||||
group by t.id, t.name, t.color;
|
||||
|
||||
|
||||
-- =========================================================
|
||||
-- 3) View principal da Index (pacientes + grupos/tags agregados)
|
||||
-- =========================================================
|
||||
|
||||
create or replace view public.v_patients_index as
|
||||
select
|
||||
p.*,
|
||||
|
||||
-- array JSON com os grupos do paciente
|
||||
coalesce(gx.groups, '[]'::jsonb) as groups,
|
||||
|
||||
-- array JSON com as tags do paciente
|
||||
coalesce(tx.tags, '[]'::jsonb) as tags,
|
||||
|
||||
-- contagens para UI/KPIs
|
||||
coalesce(gx.groups_count, 0)::int as groups_count,
|
||||
coalesce(tx.tags_count, 0)::int as tags_count
|
||||
|
||||
from public.patients p
|
||||
|
||||
left join lateral (
|
||||
select
|
||||
jsonb_agg(
|
||||
distinct jsonb_build_object(
|
||||
'id', g.id,
|
||||
'name', g.name,
|
||||
'color', g.color
|
||||
)
|
||||
) filter (where g.id is not null) as groups,
|
||||
count(distinct g.id) as groups_count
|
||||
from public.patient_group_patient pgp
|
||||
join public.patient_groups g
|
||||
on g.id = pgp.patient_group_id
|
||||
where pgp.patient_id = p.id
|
||||
) gx on true
|
||||
|
||||
left join lateral (
|
||||
select
|
||||
jsonb_agg(
|
||||
distinct jsonb_build_object(
|
||||
'id', t.id,
|
||||
'name', t.name,
|
||||
'color', t.color
|
||||
)
|
||||
) filter (where t.id is not null) as tags,
|
||||
count(distinct t.id) as tags_count
|
||||
from public.patient_patient_tag ppt
|
||||
join public.patient_tags t
|
||||
on t.id = ppt.tag_id
|
||||
where ppt.patient_id = p.id
|
||||
) tx on true;
|
||||
|
||||
|
||||
-- =========================================================
|
||||
-- 4) Índices recomendados (performance real na listagem/filtros)
|
||||
-- =========================================================
|
||||
|
||||
-- Patients
|
||||
create index if not exists idx_patients_owner_id
|
||||
on public.patients (owner_id);
|
||||
|
||||
create index if not exists idx_patients_created_at
|
||||
on public.patients (created_at desc);
|
||||
|
||||
create index if not exists idx_patients_status
|
||||
on public.patients (status);
|
||||
|
||||
create index if not exists idx_patients_last_attended_at
|
||||
on public.patients (last_attended_at desc);
|
||||
|
||||
-- Busca rápida (name/email/phone)
|
||||
create index if not exists idx_patients_name_trgm
|
||||
on public.patients using gin (name gin_trgm_ops);
|
||||
|
||||
create index if not exists idx_patients_email_trgm
|
||||
on public.patients using gin (email gin_trgm_ops);
|
||||
|
||||
create index if not exists idx_patients_phone_trgm
|
||||
on public.patients using gin (phone gin_trgm_ops);
|
||||
|
||||
-- Pivot: grupos
|
||||
create index if not exists idx_pgp_patient_id
|
||||
on public.patient_group_patient (patient_id);
|
||||
|
||||
create index if not exists idx_pgp_group_id
|
||||
on public.patient_group_patient (patient_group_id);
|
||||
|
||||
-- Pivot: tags
|
||||
create index if not exists idx_ppt_patient_id
|
||||
on public.patient_patient_tag (patient_id);
|
||||
|
||||
create index if not exists idx_ppt_tag_id
|
||||
on public.patient_patient_tag (tag_id);
|
||||
134
DBS/2026-03-11/src-sql-arquivos/supabase_tags.sql
Normal file
134
DBS/2026-03-11/src-sql-arquivos/supabase_tags.sql
Normal file
@@ -0,0 +1,134 @@
|
||||
create extension if not exists pgcrypto;
|
||||
|
||||
-- ===============================
|
||||
-- TABELA: patient_tags
|
||||
-- ===============================
|
||||
create table if not exists public.patient_tags (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
owner_id uuid not null,
|
||||
name text not null,
|
||||
color text,
|
||||
is_native boolean not null default false,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz
|
||||
);
|
||||
|
||||
create unique index if not exists patient_tags_owner_name_uq
|
||||
on public.patient_tags (owner_id, lower(name));
|
||||
|
||||
-- ===============================
|
||||
-- TABELA: patient_patient_tag (pivot)
|
||||
-- ===============================
|
||||
create table if not exists public.patient_patient_tag (
|
||||
owner_id uuid not null,
|
||||
patient_id uuid not null,
|
||||
tag_id uuid not null,
|
||||
created_at timestamptz not null default now(),
|
||||
primary key (patient_id, tag_id)
|
||||
);
|
||||
|
||||
create index if not exists ppt_owner_idx on public.patient_patient_tag(owner_id);
|
||||
create index if not exists ppt_tag_idx on public.patient_patient_tag(tag_id);
|
||||
create index if not exists ppt_patient_idx on public.patient_patient_tag(patient_id);
|
||||
|
||||
-- ===============================
|
||||
-- FOREIGN KEYS (com checagem)
|
||||
-- ===============================
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'ppt_tag_fk'
|
||||
and conrelid = 'public.patient_patient_tag'::regclass
|
||||
) then
|
||||
alter table public.patient_patient_tag
|
||||
add constraint ppt_tag_fk
|
||||
foreign key (tag_id)
|
||||
references public.patient_tags(id)
|
||||
on delete cascade;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'ppt_patient_fk'
|
||||
and conrelid = 'public.patient_patient_tag'::regclass
|
||||
) then
|
||||
alter table public.patient_patient_tag
|
||||
add constraint ppt_patient_fk
|
||||
foreign key (patient_id)
|
||||
references public.patients(id)
|
||||
on delete cascade;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- ===============================
|
||||
-- VIEW: contagem por tag
|
||||
-- ===============================
|
||||
create or replace view public.v_tag_patient_counts as
|
||||
select
|
||||
t.id,
|
||||
t.owner_id,
|
||||
t.name,
|
||||
t.color,
|
||||
t.is_native,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
coalesce(count(ppt.patient_id), 0)::int as patient_count
|
||||
from public.patient_tags t
|
||||
left join public.patient_patient_tag ppt
|
||||
on ppt.tag_id = t.id
|
||||
and ppt.owner_id = t.owner_id
|
||||
group by
|
||||
t.id, t.owner_id, t.name, t.color, t.is_native, t.created_at, t.updated_at;
|
||||
|
||||
-- ===============================
|
||||
-- RLS
|
||||
-- ===============================
|
||||
alter table public.patient_tags enable row level security;
|
||||
alter table public.patient_patient_tag enable row level security;
|
||||
|
||||
drop policy if exists tags_select_own on public.patient_tags;
|
||||
create policy tags_select_own
|
||||
on public.patient_tags
|
||||
for select
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists tags_insert_own on public.patient_tags;
|
||||
create policy tags_insert_own
|
||||
on public.patient_tags
|
||||
for insert
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists tags_update_own on public.patient_tags;
|
||||
create policy tags_update_own
|
||||
on public.patient_tags
|
||||
for update
|
||||
using (owner_id = auth.uid())
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists tags_delete_own on public.patient_tags;
|
||||
create policy tags_delete_own
|
||||
on public.patient_tags
|
||||
for delete
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists ppt_select_own on public.patient_patient_tag;
|
||||
create policy ppt_select_own
|
||||
on public.patient_patient_tag
|
||||
for select
|
||||
using (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists ppt_insert_own on public.patient_patient_tag;
|
||||
create policy ppt_insert_own
|
||||
on public.patient_patient_tag
|
||||
for insert
|
||||
with check (owner_id = auth.uid());
|
||||
|
||||
drop policy if exists ppt_delete_own on public.patient_patient_tag;
|
||||
create policy ppt_delete_own
|
||||
on public.patient_patient_tag
|
||||
for delete
|
||||
using (owner_id = auth.uid());
|
||||
Reference in New Issue
Block a user