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,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());

View 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;

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,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'));

View 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');
*/

View 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);

View 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());