Documentos Pacientes, Template Documentos Pacientes Saas, Documentos prontuários, Documentos Externos, Visualização Externa, Permissão de Visualização, Render Otimização
This commit is contained in:
57
database-novo/migrations/002_setup_wizard1_fields.sql
Normal file
57
database-novo/migrations/002_setup_wizard1_fields.sql
Normal file
@@ -0,0 +1,57 @@
|
||||
-- ============================================================
|
||||
-- Migration 002 — SetupWizard1: campos Negócio e Atendimento
|
||||
-- ============================================================
|
||||
-- Tabela: tenants (Step 2 — Negócio)
|
||||
-- Tabela: agenda_configuracoes (Step 3 — Atendimento)
|
||||
-- ============================================================
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- tenants: dados do negócio
|
||||
-- ----------------------------------------------------------
|
||||
|
||||
ALTER TABLE public.tenants
|
||||
ADD COLUMN IF NOT EXISTS business_type text,
|
||||
ADD COLUMN IF NOT EXISTS logo_url text,
|
||||
ADD COLUMN IF NOT EXISTS address text,
|
||||
ADD COLUMN IF NOT EXISTS phone text,
|
||||
ADD COLUMN IF NOT EXISTS contact_email text,
|
||||
ADD COLUMN IF NOT EXISTS site_url text,
|
||||
ADD COLUMN IF NOT EXISTS social_instagram text;
|
||||
|
||||
-- Valores aceitos: consultorio | clinica | instituto | grupo
|
||||
ALTER TABLE public.tenants
|
||||
ADD CONSTRAINT tenants_business_type_check
|
||||
CHECK (business_type IS NULL OR business_type = ANY (ARRAY[
|
||||
'consultorio'::text,
|
||||
'clinica'::text,
|
||||
'instituto'::text,
|
||||
'grupo'::text
|
||||
]));
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- agenda_configuracoes: modo de atendimento
|
||||
-- ----------------------------------------------------------
|
||||
|
||||
ALTER TABLE public.agenda_configuracoes
|
||||
ADD COLUMN IF NOT EXISTS atendimento_mode text DEFAULT 'particular'::text;
|
||||
|
||||
ALTER TABLE public.agenda_configuracoes
|
||||
ADD CONSTRAINT agenda_configuracoes_atendimento_mode_check
|
||||
CHECK (atendimento_mode IS NULL OR atendimento_mode = ANY (ARRAY[
|
||||
'particular'::text,
|
||||
'convenio'::text,
|
||||
'ambos'::text
|
||||
]));
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- Comments
|
||||
-- ----------------------------------------------------------
|
||||
|
||||
COMMENT ON COLUMN public.tenants.business_type IS 'Tipo de negócio: consultorio, clinica, instituto, grupo';
|
||||
COMMENT ON COLUMN public.tenants.logo_url IS 'URL da logo do negócio (Storage bucket)';
|
||||
COMMENT ON COLUMN public.tenants.address IS 'Endereço do negócio (texto livre)';
|
||||
COMMENT ON COLUMN public.tenants.phone IS 'Telefone/WhatsApp do negócio';
|
||||
COMMENT ON COLUMN public.tenants.contact_email IS 'E-mail público de contato do negócio';
|
||||
COMMENT ON COLUMN public.tenants.site_url IS 'Site do negócio';
|
||||
COMMENT ON COLUMN public.tenants.social_instagram IS 'Instagram do negócio (sem @)';
|
||||
COMMENT ON COLUMN public.agenda_configuracoes.atendimento_mode IS 'Modo de atendimento: particular | convenio | ambos';
|
||||
33
database-novo/migrations/003_tenants_address_fields.sql
Normal file
33
database-novo/migrations/003_tenants_address_fields.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
-- ============================================================
|
||||
-- Migration 003 — Tenants: campos de endereço detalhado
|
||||
-- ============================================================
|
||||
-- Substitui o campo address (texto livre) por campos estruturados
|
||||
-- preenchidos via consulta de CEP (ViaCEP)
|
||||
-- ============================================================
|
||||
|
||||
ALTER TABLE public.tenants
|
||||
ADD COLUMN IF NOT EXISTS cep text,
|
||||
ADD COLUMN IF NOT EXISTS logradouro text,
|
||||
ADD COLUMN IF NOT EXISTS numero text,
|
||||
ADD COLUMN IF NOT EXISTS complemento text,
|
||||
ADD COLUMN IF NOT EXISTS bairro text,
|
||||
ADD COLUMN IF NOT EXISTS cidade text,
|
||||
ADD COLUMN IF NOT EXISTS estado text;
|
||||
|
||||
-- Migra dados existentes do campo address para logradouro
|
||||
UPDATE public.tenants
|
||||
SET logradouro = address
|
||||
WHERE address IS NOT NULL
|
||||
AND logradouro IS NULL;
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- Comments
|
||||
-- ----------------------------------------------------------
|
||||
|
||||
COMMENT ON COLUMN public.tenants.cep IS 'CEP do endereço do negócio';
|
||||
COMMENT ON COLUMN public.tenants.logradouro IS 'Logradouro (rua, avenida, etc.)';
|
||||
COMMENT ON COLUMN public.tenants.numero IS 'Número do endereço';
|
||||
COMMENT ON COLUMN public.tenants.complemento IS 'Complemento (sala, andar, etc.)';
|
||||
COMMENT ON COLUMN public.tenants.bairro IS 'Bairro';
|
||||
COMMENT ON COLUMN public.tenants.cidade IS 'Cidade';
|
||||
COMMENT ON COLUMN public.tenants.estado IS 'UF (2 letras)';
|
||||
147
database-novo/migrations/20260328000001_create_medicos.sql
Normal file
147
database-novo/migrations/20260328000001_create_medicos.sql
Normal file
@@ -0,0 +1,147 @@
|
||||
-- ==========================================================================
|
||||
-- Agência PSI — Migração: tabela `medicos`
|
||||
-- ==========================================================================
|
||||
-- Criado por: Leonardo Nohama
|
||||
-- Data: 2026 · São Carlos/SP — Brasil
|
||||
--
|
||||
-- Propósito:
|
||||
-- Armazena médicos e profissionais de referência (psiquiatras, neurologistas,
|
||||
-- clínicos gerais, etc.) que encaminham pacientes ou fazem parte da rede de
|
||||
-- suporte clínico do terapeuta.
|
||||
--
|
||||
-- Usado em:
|
||||
-- - PatientsCadastroPage: campo "Encaminhado por" (FK medico_id)
|
||||
-- - CadastroRapidoMedico.vue: cadastro rápido dentro do formulário
|
||||
-- - MedicosCadastroPage.vue: página completa de gestão de médicos
|
||||
--
|
||||
-- Relacionamentos:
|
||||
-- medicos.owner_id → auth.users(id)
|
||||
-- medicos.tenant_id → tenants(id)
|
||||
-- patients.medico_encaminhador_id → medicos(id) (opcional, ver abaixo)
|
||||
--
|
||||
-- RLS: owner_id = auth.uid() — cada profissional vê apenas seus médicos.
|
||||
-- ==========================================================================
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 1. Tabela principal
|
||||
-- --------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS public.medicos (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
|
||||
-- Contexto de acesso
|
||||
owner_id uuid NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
|
||||
-- Identidade profissional
|
||||
nome text NOT NULL,
|
||||
crm text, -- Ex: "123456/SP"
|
||||
especialidade text, -- Ex: "Psiquiatria"
|
||||
|
||||
-- Contatos — telefone_pessoal é sensível (exibido com ícone de olho)
|
||||
telefone_profissional text, -- Consultório / clínica
|
||||
telefone_pessoal text, -- WhatsApp / pessoal
|
||||
email text,
|
||||
|
||||
-- Local de atuação
|
||||
clinica text, -- Nome da clínica/hospital
|
||||
cidade text,
|
||||
estado text DEFAULT 'SP',
|
||||
|
||||
-- Notas internas do terapeuta
|
||||
observacoes text,
|
||||
|
||||
-- Controle
|
||||
ativo boolean DEFAULT true NOT NULL,
|
||||
created_at timestamptz DEFAULT now(),
|
||||
updated_at timestamptz DEFAULT now(),
|
||||
|
||||
CONSTRAINT medicos_pkey PRIMARY KEY (id),
|
||||
|
||||
-- CRM único por owner (mesmo terapeuta não cadastra o mesmo CRM duas vezes)
|
||||
CONSTRAINT medicos_crm_owner_unique UNIQUE NULLS NOT DISTINCT (owner_id, crm)
|
||||
);
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 2. Índices de performance
|
||||
-- --------------------------------------------------------------------------
|
||||
CREATE INDEX IF NOT EXISTS medicos_owner_idx
|
||||
ON public.medicos USING btree (owner_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS medicos_tenant_idx
|
||||
ON public.medicos USING btree (tenant_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS medicos_nome_idx
|
||||
ON public.medicos USING btree (nome);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS medicos_especialidade_idx
|
||||
ON public.medicos USING btree (especialidade);
|
||||
|
||||
-- Busca textual por nome e especialidade
|
||||
CREATE INDEX IF NOT EXISTS medicos_nome_trgm_idx
|
||||
ON public.medicos USING gin (nome gin_trgm_ops);
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 3. Trigger de updated_at
|
||||
-- --------------------------------------------------------------------------
|
||||
CREATE OR REPLACE FUNCTION public.set_medicos_updated_at()
|
||||
RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER trg_medicos_updated_at
|
||||
BEFORE UPDATE ON public.medicos
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.set_medicos_updated_at();
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 4. Row Level Security
|
||||
-- --------------------------------------------------------------------------
|
||||
ALTER TABLE public.medicos ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Owner tem acesso total aos seus próprios médicos
|
||||
CREATE POLICY "medicos: owner full access"
|
||||
ON public.medicos
|
||||
USING (owner_id = auth.uid())
|
||||
WITH CHECK (owner_id = auth.uid());
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 5. Comentários de documentação
|
||||
-- --------------------------------------------------------------------------
|
||||
COMMENT ON TABLE public.medicos IS 'Médicos e profissionais de referência cadastrados pelo terapeuta.';
|
||||
COMMENT ON COLUMN public.medicos.owner_id IS 'Terapeuta dono do cadastro (auth.uid()).';
|
||||
COMMENT ON COLUMN public.medicos.tenant_id IS 'Tenant do terapeuta.';
|
||||
COMMENT ON COLUMN public.medicos.nome IS 'Nome completo do médico/profissional.';
|
||||
COMMENT ON COLUMN public.medicos.crm IS 'CRM com UF. Ex: 123456/SP. Único por owner_id.';
|
||||
COMMENT ON COLUMN public.medicos.especialidade IS 'Especialidade médica. Ex: Psiquiatria, Neurologia.';
|
||||
COMMENT ON COLUMN public.medicos.telefone_profissional IS 'Telefone do consultório ou clínica.';
|
||||
COMMENT ON COLUMN public.medicos.telefone_pessoal IS 'Telefone pessoal / WhatsApp. Campo sensível.';
|
||||
COMMENT ON COLUMN public.medicos.email IS 'E-mail profissional.';
|
||||
COMMENT ON COLUMN public.medicos.clinica IS 'Nome da clínica ou hospital onde atua.';
|
||||
COMMENT ON COLUMN public.medicos.cidade IS 'Cidade de atuação.';
|
||||
COMMENT ON COLUMN public.medicos.estado IS 'UF de atuação. Default SP.';
|
||||
COMMENT ON COLUMN public.medicos.observacoes IS 'Notas internas do terapeuta sobre o médico.';
|
||||
COMMENT ON COLUMN public.medicos.ativo IS 'Soft delete: false oculta da listagem.';
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 6. Coluna FK opcional em patients
|
||||
-- (Conecta "Encaminhado por" ao cadastro de médico)
|
||||
-- Execute apenas se quiser a FK estruturada; caso contrário,
|
||||
-- o campo encaminhado_por (text) no PatientsCadastroPage já funciona.
|
||||
-- --------------------------------------------------------------------------
|
||||
|
||||
-- ALTER TABLE public.patients
|
||||
-- ADD COLUMN IF NOT EXISTS medico_encaminhador_id uuid
|
||||
-- REFERENCES public.medicos(id) ON DELETE SET NULL;
|
||||
|
||||
-- CREATE INDEX IF NOT EXISTS patients_medico_encaminhador_idx
|
||||
-- ON public.patients USING btree (medico_encaminhador_id);
|
||||
|
||||
-- COMMENT ON COLUMN public.patients.medico_encaminhador_id
|
||||
-- IS 'FK para medicos.id — quem encaminhou o paciente.';
|
||||
|
||||
-- ==========================================================================
|
||||
-- FIM DA MIGRAÇÃO
|
||||
-- ==========================================================================
|
||||
119
database-novo/migrations/20260328000002_patients_new_columns.sql
Normal file
119
database-novo/migrations/20260328000002_patients_new_columns.sql
Normal file
@@ -0,0 +1,119 @@
|
||||
-- ==========================================================================
|
||||
-- Agência PSI — Migração: novos campos em `patients`
|
||||
-- ==========================================================================
|
||||
-- Arquivo: supabase/migrations/20260328000002_patients_new_columns.sql
|
||||
-- Criado por: Leonardo Nohama · 2026 · São Carlos/SP
|
||||
--
|
||||
-- Adiciona as colunas identificadas na engenharia reversa da tela de detalhe
|
||||
-- (PatientsDetailPage) que ainda não existiam na tabela `patients`.
|
||||
--
|
||||
-- Também ajusta os CHECK constraints de `status` e `patient_scope` para
|
||||
-- aceitar os valores usados no novo formulário de cadastro.
|
||||
-- ==========================================================================
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 1. Colunas novas
|
||||
-- --------------------------------------------------------------------------
|
||||
|
||||
-- Identidade
|
||||
ALTER TABLE public.patients
|
||||
ADD COLUMN IF NOT EXISTS pronomes text,
|
||||
ADD COLUMN IF NOT EXISTS nome_social text,
|
||||
ADD COLUMN IF NOT EXISTS etnia text;
|
||||
|
||||
-- Contato
|
||||
ALTER TABLE public.patients
|
||||
ADD COLUMN IF NOT EXISTS canal_preferido text,
|
||||
ADD COLUMN IF NOT EXISTS horario_contato text;
|
||||
|
||||
-- Clínico / convênio
|
||||
-- convenio: nome de exibição (badge azul no header)
|
||||
-- convenio_id: FK para insurance_plans (opcional — permite vincular ao cadastro)
|
||||
ALTER TABLE public.patients
|
||||
ADD COLUMN IF NOT EXISTS convenio text,
|
||||
ADD COLUMN IF NOT EXISTS convenio_id uuid REFERENCES public.insurance_plans(id) ON DELETE SET NULL;
|
||||
|
||||
-- Origem
|
||||
ALTER TABLE public.patients
|
||||
ADD COLUMN IF NOT EXISTS metodo_pagamento_preferido text,
|
||||
ADD COLUMN IF NOT EXISTS motivo_saida text;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 2. Ajuste do CHECK constraint de `status`
|
||||
-- Valores originais: Ativo | Inativo | Alta | Encaminhado | Arquivado
|
||||
-- Valores novos: + Em espera
|
||||
-- --------------------------------------------------------------------------
|
||||
ALTER TABLE public.patients
|
||||
DROP CONSTRAINT IF EXISTS patients_status_check;
|
||||
|
||||
ALTER TABLE public.patients
|
||||
ADD CONSTRAINT patients_status_check CHECK (
|
||||
status = ANY (ARRAY[
|
||||
'Ativo'::text,
|
||||
'Em espera'::text,
|
||||
'Inativo'::text,
|
||||
'Alta'::text,
|
||||
'Encaminhado'::text,
|
||||
'Arquivado'::text
|
||||
])
|
||||
);
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 3. Ajuste do CHECK constraint de `patient_scope`
|
||||
-- Valores originais: clinic | therapist (valores técnicos internos)
|
||||
-- Valores novos: + Clínica | Particular | Online | Híbrido
|
||||
-- Estratégia: remover o constraint restritivo e deixar livre (text),
|
||||
-- pois o controle já é feito no frontend via Select com opções fixas.
|
||||
-- --------------------------------------------------------------------------
|
||||
ALTER TABLE public.patients
|
||||
DROP CONSTRAINT IF EXISTS patients_patient_scope_check;
|
||||
|
||||
-- Também remove a constraint de consistência que dependia do scope antigo
|
||||
ALTER TABLE public.patients
|
||||
DROP CONSTRAINT IF EXISTS patients_therapist_scope_consistency;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 4. Índices de performance
|
||||
-- --------------------------------------------------------------------------
|
||||
CREATE INDEX IF NOT EXISTS patients_convenio_id_idx
|
||||
ON public.patients USING btree (convenio_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS patients_pronomes_idx
|
||||
ON public.patients USING btree (pronomes);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS patients_etnia_idx
|
||||
ON public.patients USING btree (etnia);
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 5. Comentários
|
||||
-- --------------------------------------------------------------------------
|
||||
COMMENT ON COLUMN public.patients.pronomes
|
||||
IS 'Pronomes de tratamento. Ex: ela/dela, ele/dele. Exibido no header do perfil.';
|
||||
|
||||
COMMENT ON COLUMN public.patients.nome_social
|
||||
IS 'Nome social / como prefere ser chamado(a) no atendimento.';
|
||||
|
||||
COMMENT ON COLUMN public.patients.etnia
|
||||
IS 'Etnia / raça autodeclarada. Exibida no card "Dados pessoais".';
|
||||
|
||||
COMMENT ON COLUMN public.patients.canal_preferido
|
||||
IS 'Canal preferido de contato. Ex: WhatsApp, Telefone, E-mail.';
|
||||
|
||||
COMMENT ON COLUMN public.patients.horario_contato
|
||||
IS 'Horário preferido para contato. Ex: 08h–18h.';
|
||||
|
||||
COMMENT ON COLUMN public.patients.convenio
|
||||
IS 'Nome do convênio para exibição (badge azul no header). Derivado de convenio_id.';
|
||||
|
||||
COMMENT ON COLUMN public.patients.convenio_id
|
||||
IS 'FK para insurance_plans.id. Vincula o paciente ao convênio cadastrado.';
|
||||
|
||||
COMMENT ON COLUMN public.patients.metodo_pagamento_preferido
|
||||
IS 'Método de pagamento preferido. Ex: PIX, Cartão crédito. Exibido no card Origem.';
|
||||
|
||||
COMMENT ON COLUMN public.patients.motivo_saida
|
||||
IS 'Motivo de encerramento do acompanhamento. Exibido no card Origem quando preenchido.';
|
||||
|
||||
-- ==========================================================================
|
||||
-- FIM DA MIGRAÇÃO
|
||||
-- ==========================================================================
|
||||
@@ -0,0 +1,70 @@
|
||||
-- ==========================================================================
|
||||
-- Agência PSI — Migração: remove check constraints dos novos campos
|
||||
-- ==========================================================================
|
||||
-- Arquivo: supabase/migrations/20260328000003_patients_drop_check_constraints.sql
|
||||
-- Criado por: Leonardo Nohama · 2026 · São Carlos/SP
|
||||
--
|
||||
-- O banco tinha CHECK constraints nos novos campos que foram adicionados
|
||||
-- pela migration anterior (ou que já existiam no schema ao vivo).
|
||||
-- O frontend já controla os valores via Select com opções fixas,
|
||||
-- então os constraints são desnecessários e serão removidos.
|
||||
-- ==========================================================================
|
||||
|
||||
-- canal_preferido
|
||||
ALTER TABLE public.patients
|
||||
DROP CONSTRAINT IF EXISTS patients_canal_preferido_check;
|
||||
|
||||
-- horario_contato
|
||||
ALTER TABLE public.patients
|
||||
DROP CONSTRAINT IF EXISTS patients_horario_contato_check;
|
||||
|
||||
-- pronomes
|
||||
ALTER TABLE public.patients
|
||||
DROP CONSTRAINT IF EXISTS patients_pronomes_check;
|
||||
|
||||
-- nome_social
|
||||
ALTER TABLE public.patients
|
||||
DROP CONSTRAINT IF EXISTS patients_nome_social_check;
|
||||
|
||||
-- etnia
|
||||
ALTER TABLE public.patients
|
||||
DROP CONSTRAINT IF EXISTS patients_etnia_check;
|
||||
|
||||
-- convenio
|
||||
ALTER TABLE public.patients
|
||||
DROP CONSTRAINT IF EXISTS patients_convenio_check;
|
||||
|
||||
-- metodo_pagamento_preferido
|
||||
ALTER TABLE public.patients
|
||||
DROP CONSTRAINT IF EXISTS patients_metodo_pagamento_preferido_check;
|
||||
|
||||
-- motivo_saida
|
||||
ALTER TABLE public.patients
|
||||
DROP CONSTRAINT IF EXISTS patients_motivo_saida_check;
|
||||
|
||||
-- status (já ajustado na migration anterior, mas garante)
|
||||
ALTER TABLE public.patients
|
||||
DROP CONSTRAINT IF EXISTS patients_status_check;
|
||||
|
||||
ALTER TABLE public.patients
|
||||
ADD CONSTRAINT patients_status_check CHECK (
|
||||
status = ANY (ARRAY[
|
||||
'Ativo'::text,
|
||||
'Em espera'::text,
|
||||
'Inativo'::text,
|
||||
'Alta'::text,
|
||||
'Encaminhado'::text,
|
||||
'Arquivado'::text
|
||||
])
|
||||
);
|
||||
|
||||
-- patient_scope (já ajustado na migration anterior, mas garante)
|
||||
ALTER TABLE public.patients
|
||||
DROP CONSTRAINT IF EXISTS patients_patient_scope_check;
|
||||
|
||||
ALTER TABLE public.patients
|
||||
DROP CONSTRAINT IF EXISTS patients_therapist_scope_consistency;
|
||||
|
||||
-- ==========================================================================
|
||||
-- FIM DA MIGRAÇÃO
|
||||
-- ==========================================================================
|
||||
@@ -0,0 +1,56 @@
|
||||
-- ==========================================================================
|
||||
-- Agência PSI — Migração: tabela `patient_support_contacts`
|
||||
-- ==========================================================================
|
||||
-- Arquivo: supabase/migrations/20260328000004_create_patient_support_contacts.sql
|
||||
-- Criado por: Leonardo Nohama · 2026 · São Carlos/SP
|
||||
--
|
||||
-- Contatos da rede de suporte do paciente.
|
||||
-- Alimenta o card "Contatos & rede de suporte" na tela de detalhe.
|
||||
-- is_primario = true → badge vermelho "emergência" no perfil.
|
||||
-- ==========================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.patient_support_contacts (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
|
||||
patient_id uuid NOT NULL REFERENCES public.patients(id) ON DELETE CASCADE,
|
||||
owner_id uuid NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
|
||||
nome text,
|
||||
relacao text, -- Ex: mãe, psiquiatra, cônjuge
|
||||
tipo text, -- emergencia | familiar | profissional_saude | amigo | outro
|
||||
telefone text,
|
||||
email text,
|
||||
is_primario boolean DEFAULT false NOT NULL,
|
||||
|
||||
created_at timestamptz DEFAULT now(),
|
||||
updated_at timestamptz DEFAULT now(),
|
||||
|
||||
CONSTRAINT patient_support_contacts_pkey PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
-- Índices
|
||||
CREATE INDEX IF NOT EXISTS psc_patient_idx ON public.patient_support_contacts USING btree (patient_id);
|
||||
CREATE INDEX IF NOT EXISTS psc_owner_idx ON public.patient_support_contacts USING btree (owner_id);
|
||||
|
||||
-- Trigger updated_at
|
||||
CREATE TRIGGER trg_psc_updated_at
|
||||
BEFORE UPDATE ON public.patient_support_contacts
|
||||
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
-- RLS
|
||||
ALTER TABLE public.patient_support_contacts ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "psc: owner full access"
|
||||
ON public.patient_support_contacts
|
||||
USING (owner_id = auth.uid())
|
||||
WITH CHECK (owner_id = auth.uid());
|
||||
|
||||
-- Comentários
|
||||
COMMENT ON TABLE public.patient_support_contacts IS 'Rede de suporte do paciente. Exibida no card "Contatos & rede de suporte" do perfil.';
|
||||
COMMENT ON COLUMN public.patient_support_contacts.is_primario IS 'true = badge vermelho "emergência" no perfil do paciente.';
|
||||
COMMENT ON COLUMN public.patient_support_contacts.tipo IS 'emergencia | familiar | profissional_saude | amigo | outro';
|
||||
|
||||
-- ==========================================================================
|
||||
-- FIM DA MIGRAÇÃO
|
||||
-- ==========================================================================
|
||||
@@ -0,0 +1,454 @@
|
||||
-- ==========================================================================
|
||||
-- Agencia PSI — Migracao: tabelas de Documentos & Arquivos
|
||||
-- ==========================================================================
|
||||
-- Criado por: Leonardo Nohama
|
||||
-- Data: 2026-03-29 · Sao Carlos/SP — Brasil
|
||||
--
|
||||
-- Proposito:
|
||||
-- Modulo completo de documentos do paciente.
|
||||
-- Tabelas: documents, document_access_logs, document_signatures,
|
||||
-- document_share_links.
|
||||
--
|
||||
-- Relacionamentos:
|
||||
-- documents.patient_id → patients(id)
|
||||
-- documents.owner_id → auth.users(id)
|
||||
-- documents.tenant_id → tenants(id)
|
||||
-- documents.agenda_evento_id → agenda_eventos(id) (opcional)
|
||||
-- document_access_logs.documento_id → documents(id)
|
||||
-- document_signatures.documento_id → documents(id)
|
||||
-- document_share_links.documento_id → documents(id)
|
||||
--
|
||||
-- RLS: owner_id = auth.uid() para documents, signatures e share_links.
|
||||
-- access_logs: somente INSERT (imutavel) + SELECT por tenant.
|
||||
-- ==========================================================================
|
||||
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 1. Tabela principal: documents
|
||||
-- --------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS public.documents (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
|
||||
-- Contexto de acesso
|
||||
owner_id uuid NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
|
||||
-- Vinculo com paciente
|
||||
patient_id uuid NOT NULL REFERENCES public.patients(id) ON DELETE CASCADE,
|
||||
|
||||
-- Arquivo no Storage
|
||||
bucket_path text NOT NULL,
|
||||
storage_bucket text NOT NULL DEFAULT 'documents',
|
||||
nome_original text NOT NULL,
|
||||
mime_type text,
|
||||
tamanho_bytes bigint,
|
||||
|
||||
-- Classificacao
|
||||
tipo_documento text NOT NULL DEFAULT 'outro',
|
||||
-- laudo | receita | exame | termo_assinado | relatorio_externo
|
||||
-- identidade | convenio | declaracao | atestado | recibo | outro
|
||||
categoria text,
|
||||
descricao text,
|
||||
tags text[] DEFAULT '{}',
|
||||
|
||||
-- Vinculo opcional com sessao/nota
|
||||
agenda_evento_id uuid REFERENCES public.agenda_eventos(id) ON DELETE SET NULL,
|
||||
session_note_id uuid,
|
||||
|
||||
-- Visibilidade & controle de acesso
|
||||
visibilidade text NOT NULL DEFAULT 'privado',
|
||||
-- privado | compartilhado_supervisor | compartilhado_portal
|
||||
compartilhado_portal boolean DEFAULT false NOT NULL,
|
||||
compartilhado_supervisor boolean DEFAULT false NOT NULL,
|
||||
compartilhado_em timestamptz,
|
||||
expira_compartilhamento timestamptz,
|
||||
|
||||
-- Upload pelo paciente (portal)
|
||||
enviado_pelo_paciente boolean DEFAULT false NOT NULL,
|
||||
status_revisao text DEFAULT 'aprovado',
|
||||
-- pendente | aprovado | rejeitado
|
||||
revisado_por uuid,
|
||||
revisado_em timestamptz,
|
||||
|
||||
-- Quem fez upload
|
||||
uploaded_by uuid NOT NULL,
|
||||
uploaded_at timestamptz DEFAULT now() NOT NULL,
|
||||
|
||||
-- Soft delete com retencao (LGPD / CFP)
|
||||
deleted_at timestamptz,
|
||||
deleted_by uuid,
|
||||
retencao_ate timestamptz,
|
||||
|
||||
-- Controle
|
||||
created_at timestamptz DEFAULT now(),
|
||||
updated_at timestamptz DEFAULT now(),
|
||||
|
||||
CONSTRAINT documents_pkey PRIMARY KEY (id),
|
||||
|
||||
-- Validacoes
|
||||
CONSTRAINT documents_tipo_check CHECK (
|
||||
tipo_documento = ANY (ARRAY[
|
||||
'laudo', 'receita', 'exame', 'termo_assinado', 'relatorio_externo',
|
||||
'identidade', 'convenio', 'declaracao', 'atestado', 'recibo', 'outro'
|
||||
])
|
||||
),
|
||||
CONSTRAINT documents_visibilidade_check CHECK (
|
||||
visibilidade = ANY (ARRAY['privado', 'compartilhado_supervisor', 'compartilhado_portal'])
|
||||
),
|
||||
CONSTRAINT documents_status_revisao_check CHECK (
|
||||
status_revisao = ANY (ARRAY['pendente', 'aprovado', 'rejeitado'])
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 2. Indices — documents
|
||||
-- --------------------------------------------------------------------------
|
||||
CREATE INDEX IF NOT EXISTS docs_patient_idx
|
||||
ON public.documents USING btree (patient_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS docs_owner_idx
|
||||
ON public.documents USING btree (owner_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS docs_tenant_idx
|
||||
ON public.documents USING btree (tenant_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS docs_tipo_idx
|
||||
ON public.documents USING btree (patient_id, tipo_documento);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS docs_tags_idx
|
||||
ON public.documents USING gin (tags);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS docs_uploaded_at_idx
|
||||
ON public.documents USING btree (patient_id, uploaded_at DESC);
|
||||
|
||||
-- Excluir soft-deleted da listagem padrao
|
||||
CREATE INDEX IF NOT EXISTS docs_active_idx
|
||||
ON public.documents USING btree (patient_id, uploaded_at DESC)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- Busca textual no nome do arquivo
|
||||
CREATE INDEX IF NOT EXISTS docs_nome_trgm_idx
|
||||
ON public.documents USING gin (nome_original gin_trgm_ops);
|
||||
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 3. Trigger updated_at — documents
|
||||
-- --------------------------------------------------------------------------
|
||||
CREATE TRIGGER trg_documents_updated_at
|
||||
BEFORE UPDATE ON public.documents
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 4. Trigger: registrar na patient_timeline ao adicionar documento
|
||||
-- --------------------------------------------------------------------------
|
||||
CREATE OR REPLACE FUNCTION public.fn_documents_timeline_insert()
|
||||
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER AS $$
|
||||
BEGIN
|
||||
INSERT INTO public.patient_timeline (
|
||||
patient_id, tenant_id, evento_tipo,
|
||||
titulo, descricao, icone_cor,
|
||||
link_ref_tipo, link_ref_id,
|
||||
gerado_por, ocorrido_em
|
||||
) VALUES (
|
||||
NEW.patient_id,
|
||||
NEW.tenant_id,
|
||||
'documento_adicionado',
|
||||
'Documento adicionado: ' || COALESCE(NEW.nome_original, 'arquivo'),
|
||||
'Tipo: ' || COALESCE(NEW.tipo_documento, 'outro'),
|
||||
'blue',
|
||||
'documento',
|
||||
NEW.id,
|
||||
NEW.uploaded_by,
|
||||
NEW.uploaded_at
|
||||
);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER trg_documents_timeline_insert
|
||||
AFTER INSERT ON public.documents
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.fn_documents_timeline_insert();
|
||||
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 5. RLS — documents
|
||||
-- --------------------------------------------------------------------------
|
||||
ALTER TABLE public.documents ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "documents: owner full access"
|
||||
ON public.documents
|
||||
USING (owner_id = auth.uid())
|
||||
WITH CHECK (owner_id = auth.uid());
|
||||
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 6. Comentarios — documents
|
||||
-- --------------------------------------------------------------------------
|
||||
COMMENT ON TABLE public.documents IS 'Documentos e arquivos vinculados a pacientes. Armazenados no Supabase Storage.';
|
||||
COMMENT ON COLUMN public.documents.owner_id IS 'Terapeuta dono do documento (auth.uid()).';
|
||||
COMMENT ON COLUMN public.documents.tenant_id IS 'Tenant do terapeuta.';
|
||||
COMMENT ON COLUMN public.documents.patient_id IS 'Paciente ao qual o documento pertence.';
|
||||
COMMENT ON COLUMN public.documents.bucket_path IS 'Caminho do arquivo no Supabase Storage bucket.';
|
||||
COMMENT ON COLUMN public.documents.storage_bucket IS 'Nome do bucket no Storage. Default: documents.';
|
||||
COMMENT ON COLUMN public.documents.nome_original IS 'Nome original do arquivo enviado.';
|
||||
COMMENT ON COLUMN public.documents.mime_type IS 'MIME type do arquivo. Ex: application/pdf, image/jpeg.';
|
||||
COMMENT ON COLUMN public.documents.tamanho_bytes IS 'Tamanho do arquivo em bytes.';
|
||||
COMMENT ON COLUMN public.documents.tipo_documento IS 'Tipo: laudo|receita|exame|termo_assinado|relatorio_externo|identidade|convenio|declaracao|atestado|recibo|outro.';
|
||||
COMMENT ON COLUMN public.documents.categoria IS 'Categoria livre para organizacao adicional.';
|
||||
COMMENT ON COLUMN public.documents.tags IS 'Tags livres para busca e filtro. Array de text.';
|
||||
COMMENT ON COLUMN public.documents.visibilidade IS 'privado|compartilhado_supervisor|compartilhado_portal.';
|
||||
COMMENT ON COLUMN public.documents.compartilhado_portal IS 'true = visivel para o paciente no portal.';
|
||||
COMMENT ON COLUMN public.documents.compartilhado_supervisor IS 'true = visivel para o supervisor.';
|
||||
COMMENT ON COLUMN public.documents.enviado_pelo_paciente IS 'true = upload feito pelo paciente via portal.';
|
||||
COMMENT ON COLUMN public.documents.status_revisao IS 'pendente|aprovado|rejeitado — para uploads do paciente.';
|
||||
COMMENT ON COLUMN public.documents.deleted_at IS 'Soft delete: data da exclusao. NULL = ativo.';
|
||||
COMMENT ON COLUMN public.documents.retencao_ate IS 'LGPD/CFP: arquivo retido ate esta data mesmo apos soft delete.';
|
||||
|
||||
|
||||
-- ==========================================================================
|
||||
-- 7. Tabela: document_access_logs (imutavel — auditoria)
|
||||
-- ==========================================================================
|
||||
CREATE TABLE IF NOT EXISTS public.document_access_logs (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
|
||||
documento_id uuid NOT NULL REFERENCES public.documents(id) ON DELETE CASCADE,
|
||||
tenant_id uuid NOT NULL,
|
||||
|
||||
-- Acao realizada
|
||||
acao text NOT NULL,
|
||||
-- visualizou | baixou | imprimiu | compartilhou | assinou
|
||||
user_id uuid,
|
||||
ip inet,
|
||||
user_agent text,
|
||||
|
||||
acessado_em timestamptz DEFAULT now() NOT NULL,
|
||||
|
||||
CONSTRAINT document_access_logs_pkey PRIMARY KEY (id),
|
||||
|
||||
CONSTRAINT dal_acao_check CHECK (
|
||||
acao = ANY (ARRAY['visualizou', 'baixou', 'imprimiu', 'compartilhou', 'assinou'])
|
||||
)
|
||||
);
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX IF NOT EXISTS dal_documento_idx
|
||||
ON public.document_access_logs USING btree (documento_id, acessado_em DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS dal_tenant_idx
|
||||
ON public.document_access_logs USING btree (tenant_id, acessado_em DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS dal_user_idx
|
||||
ON public.document_access_logs USING btree (user_id, acessado_em DESC);
|
||||
|
||||
-- RLS — somente INSERT (imutavel) + SELECT
|
||||
ALTER TABLE public.document_access_logs ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "dal: tenant members can insert"
|
||||
ON public.document_access_logs
|
||||
FOR INSERT
|
||||
WITH CHECK (true);
|
||||
|
||||
CREATE POLICY "dal: tenant members can select"
|
||||
ON public.document_access_logs
|
||||
FOR SELECT
|
||||
USING (tenant_id IN (
|
||||
SELECT tm.tenant_id FROM public.tenant_members tm
|
||||
WHERE tm.user_id = auth.uid() AND tm.status = 'active'
|
||||
));
|
||||
|
||||
-- Comentarios
|
||||
COMMENT ON TABLE public.document_access_logs IS 'Log imutavel de acessos a documentos. Conformidade CFP e LGPD. Sem UPDATE/DELETE.';
|
||||
COMMENT ON COLUMN public.document_access_logs.acao IS 'visualizou|baixou|imprimiu|compartilhou|assinou.';
|
||||
|
||||
|
||||
-- ==========================================================================
|
||||
-- 8. Tabela: document_signatures (assinatura eletronica)
|
||||
-- ==========================================================================
|
||||
CREATE TABLE IF NOT EXISTS public.document_signatures (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
|
||||
documento_id uuid NOT NULL REFERENCES public.documents(id) ON DELETE CASCADE,
|
||||
tenant_id uuid NOT NULL,
|
||||
|
||||
-- Signatario
|
||||
signatario_tipo text NOT NULL,
|
||||
-- paciente | responsavel_legal | terapeuta
|
||||
signatario_id uuid,
|
||||
signatario_nome text,
|
||||
signatario_email text,
|
||||
|
||||
-- Ordem e status
|
||||
ordem smallint DEFAULT 1 NOT NULL,
|
||||
status text NOT NULL DEFAULT 'pendente',
|
||||
-- pendente | enviado | assinado | recusado | expirado
|
||||
|
||||
-- Dados da assinatura (preenchidos ao assinar)
|
||||
ip inet,
|
||||
user_agent text,
|
||||
assinado_em timestamptz,
|
||||
hash_documento text,
|
||||
|
||||
-- Controle
|
||||
criado_em timestamptz DEFAULT now(),
|
||||
atualizado_em timestamptz DEFAULT now(),
|
||||
|
||||
CONSTRAINT document_signatures_pkey PRIMARY KEY (id),
|
||||
|
||||
CONSTRAINT ds_signatario_tipo_check CHECK (
|
||||
signatario_tipo = ANY (ARRAY['paciente', 'responsavel_legal', 'terapeuta'])
|
||||
),
|
||||
CONSTRAINT ds_status_check CHECK (
|
||||
status = ANY (ARRAY['pendente', 'enviado', 'assinado', 'recusado', 'expirado'])
|
||||
)
|
||||
);
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX IF NOT EXISTS ds_documento_idx
|
||||
ON public.document_signatures USING btree (documento_id, ordem);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ds_tenant_idx
|
||||
ON public.document_signatures USING btree (tenant_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ds_status_idx
|
||||
ON public.document_signatures USING btree (documento_id, status);
|
||||
|
||||
-- Trigger updated_at
|
||||
CREATE TRIGGER trg_ds_updated_at
|
||||
BEFORE UPDATE ON public.document_signatures
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
-- Trigger: ao assinar, registrar na patient_timeline
|
||||
CREATE OR REPLACE FUNCTION public.fn_document_signature_timeline()
|
||||
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER AS $$
|
||||
DECLARE
|
||||
v_patient_id uuid;
|
||||
v_tenant_id uuid;
|
||||
v_doc_nome text;
|
||||
BEGIN
|
||||
IF NEW.status = 'assinado' AND (OLD.status IS NULL OR OLD.status <> 'assinado') THEN
|
||||
SELECT d.patient_id, d.tenant_id, d.nome_original
|
||||
INTO v_patient_id, v_tenant_id, v_doc_nome
|
||||
FROM public.documents d
|
||||
WHERE d.id = NEW.documento_id;
|
||||
|
||||
IF v_patient_id IS NOT NULL THEN
|
||||
INSERT INTO public.patient_timeline (
|
||||
patient_id, tenant_id, evento_tipo,
|
||||
titulo, descricao, icone_cor,
|
||||
link_ref_tipo, link_ref_id,
|
||||
gerado_por, ocorrido_em
|
||||
) VALUES (
|
||||
v_patient_id,
|
||||
v_tenant_id,
|
||||
'documento_assinado',
|
||||
'Documento assinado: ' || COALESCE(v_doc_nome, 'documento'),
|
||||
'Assinado por ' || COALESCE(NEW.signatario_nome, NEW.signatario_tipo),
|
||||
'green',
|
||||
'documento',
|
||||
NEW.documento_id,
|
||||
NEW.signatario_id,
|
||||
NEW.assinado_em
|
||||
);
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER trg_ds_timeline
|
||||
AFTER UPDATE ON public.document_signatures
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.fn_document_signature_timeline();
|
||||
|
||||
-- RLS
|
||||
ALTER TABLE public.document_signatures ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "ds: tenant members access"
|
||||
ON public.document_signatures
|
||||
USING (tenant_id IN (
|
||||
SELECT tm.tenant_id FROM public.tenant_members tm
|
||||
WHERE tm.user_id = auth.uid() AND tm.status = 'active'
|
||||
))
|
||||
WITH CHECK (tenant_id IN (
|
||||
SELECT tm.tenant_id FROM public.tenant_members tm
|
||||
WHERE tm.user_id = auth.uid() AND tm.status = 'active'
|
||||
));
|
||||
|
||||
-- Comentarios
|
||||
COMMENT ON TABLE public.document_signatures IS 'Assinaturas eletronicas de documentos. Cada signatario tem seu registro.';
|
||||
COMMENT ON COLUMN public.document_signatures.signatario_tipo IS 'paciente|responsavel_legal|terapeuta.';
|
||||
COMMENT ON COLUMN public.document_signatures.status IS 'pendente|enviado|assinado|recusado|expirado.';
|
||||
COMMENT ON COLUMN public.document_signatures.hash_documento IS 'Hash SHA-256 do documento no momento da assinatura. Garante integridade.';
|
||||
COMMENT ON COLUMN public.document_signatures.ip IS 'IP do signatario no momento da assinatura.';
|
||||
|
||||
|
||||
-- ==========================================================================
|
||||
-- 9. Tabela: document_share_links (links temporarios)
|
||||
-- ==========================================================================
|
||||
CREATE TABLE IF NOT EXISTS public.document_share_links (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
|
||||
documento_id uuid NOT NULL REFERENCES public.documents(id) ON DELETE CASCADE,
|
||||
tenant_id uuid NOT NULL,
|
||||
|
||||
-- Token unico para o link
|
||||
token text NOT NULL DEFAULT encode(gen_random_bytes(32), 'hex'),
|
||||
|
||||
-- Limites
|
||||
expira_em timestamptz NOT NULL,
|
||||
usos_max smallint DEFAULT 5 NOT NULL,
|
||||
usos smallint DEFAULT 0 NOT NULL,
|
||||
|
||||
-- Quem criou
|
||||
criado_por uuid NOT NULL,
|
||||
criado_em timestamptz DEFAULT now(),
|
||||
|
||||
-- Controle
|
||||
ativo boolean DEFAULT true NOT NULL,
|
||||
|
||||
CONSTRAINT document_share_links_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT dsl_token_unique UNIQUE (token)
|
||||
);
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX IF NOT EXISTS dsl_documento_idx
|
||||
ON public.document_share_links USING btree (documento_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS dsl_token_idx
|
||||
ON public.document_share_links USING btree (token)
|
||||
WHERE ativo = true;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS dsl_expira_idx
|
||||
ON public.document_share_links USING btree (expira_em)
|
||||
WHERE ativo = true;
|
||||
|
||||
-- RLS
|
||||
ALTER TABLE public.document_share_links ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "dsl: creator full access"
|
||||
ON public.document_share_links
|
||||
USING (criado_por = auth.uid())
|
||||
WITH CHECK (criado_por = auth.uid());
|
||||
|
||||
-- Politica publica de leitura por token (para acesso externo sem login)
|
||||
CREATE POLICY "dsl: public read by token"
|
||||
ON public.document_share_links
|
||||
FOR SELECT
|
||||
USING (ativo = true AND expira_em > now() AND usos < usos_max);
|
||||
|
||||
-- Comentarios
|
||||
COMMENT ON TABLE public.document_share_links IS 'Links temporarios assinados para compartilhar documento com profissional externo.';
|
||||
COMMENT ON COLUMN public.document_share_links.token IS 'Token unico gerado automaticamente (32 bytes hex).';
|
||||
COMMENT ON COLUMN public.document_share_links.expira_em IS 'Data/hora de expiracao do link.';
|
||||
COMMENT ON COLUMN public.document_share_links.usos_max IS 'Numero maximo de acessos permitidos.';
|
||||
COMMENT ON COLUMN public.document_share_links.usos IS 'Numero de vezes que o link ja foi acessado.';
|
||||
|
||||
|
||||
-- ==========================================================================
|
||||
-- FIM DA MIGRACAO 005
|
||||
-- ==========================================================================
|
||||
@@ -0,0 +1,260 @@
|
||||
-- ==========================================================================
|
||||
-- Agencia PSI — Migracao: tabelas de Templates de Documentos
|
||||
-- ==========================================================================
|
||||
-- Criado por: Leonardo Nohama
|
||||
-- Data: 2026-03-29 · Sao Carlos/SP — Brasil
|
||||
--
|
||||
-- Proposito:
|
||||
-- Templates de documentos (declaracao, atestado, recibo, relatorio etc.)
|
||||
-- e registro de cada documento gerado (instancia PDF).
|
||||
--
|
||||
-- Tabelas: document_templates, document_generated.
|
||||
--
|
||||
-- Relacionamentos:
|
||||
-- document_templates.tenant_id → tenants(id)
|
||||
-- document_templates.owner_id → auth.users(id)
|
||||
-- document_generated.template_id → document_templates(id)
|
||||
-- document_generated.patient_id → patients(id)
|
||||
-- document_generated.tenant_id → tenants(id)
|
||||
--
|
||||
-- Templates globais: is_global = true, tenant_id = NULL.
|
||||
-- Templates do tenant: is_global = false, tenant_id preenchido.
|
||||
-- ==========================================================================
|
||||
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 1. Tabela: document_templates
|
||||
-- --------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS public.document_templates (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
|
||||
-- Contexto
|
||||
tenant_id uuid,
|
||||
owner_id uuid,
|
||||
|
||||
-- Identificacao
|
||||
nome_template text NOT NULL,
|
||||
tipo text NOT NULL DEFAULT 'outro',
|
||||
-- declaracao_comparecimento | atestado_psicologico
|
||||
-- relatorio_acompanhamento | recibo_pagamento
|
||||
-- termo_consentimento | encaminhamento | outro
|
||||
descricao text,
|
||||
|
||||
-- Corpo do template
|
||||
corpo_html text NOT NULL DEFAULT '',
|
||||
cabecalho_html text,
|
||||
rodape_html text,
|
||||
|
||||
-- Variaveis que o template utiliza
|
||||
variaveis text[] DEFAULT '{}',
|
||||
-- Ex: {paciente_nome, paciente_cpf, data_sessao, terapeuta_nome, ...}
|
||||
|
||||
-- Personalizacao visual
|
||||
logo_url text,
|
||||
|
||||
-- Escopo
|
||||
is_global boolean DEFAULT false NOT NULL,
|
||||
-- true = template padrao do sistema (visivel para todos)
|
||||
-- false = template criado pelo tenant/terapeuta
|
||||
|
||||
-- Controle
|
||||
ativo boolean DEFAULT true NOT NULL,
|
||||
created_at timestamptz DEFAULT now(),
|
||||
updated_at timestamptz DEFAULT now(),
|
||||
|
||||
CONSTRAINT document_templates_pkey PRIMARY KEY (id),
|
||||
|
||||
CONSTRAINT dt_tipo_check CHECK (
|
||||
tipo = ANY (ARRAY[
|
||||
'declaracao_comparecimento', 'atestado_psicologico',
|
||||
'relatorio_acompanhamento', 'recibo_pagamento',
|
||||
'termo_consentimento', 'encaminhamento',
|
||||
'contrato_servicos', 'tcle', 'autorizacao_menor',
|
||||
'laudo_psicologico', 'parecer_psicologico',
|
||||
'termo_sigilo', 'declaracao_inicio_tratamento',
|
||||
'termo_alta', 'tcle_online', 'outro'
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 2. Indices — document_templates
|
||||
-- --------------------------------------------------------------------------
|
||||
CREATE INDEX IF NOT EXISTS dt_tenant_idx
|
||||
ON public.document_templates USING btree (tenant_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS dt_owner_idx
|
||||
ON public.document_templates USING btree (owner_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS dt_global_idx
|
||||
ON public.document_templates USING btree (is_global)
|
||||
WHERE is_global = true;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS dt_tipo_idx
|
||||
ON public.document_templates USING btree (tipo);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS dt_nome_trgm_idx
|
||||
ON public.document_templates USING gin (nome_template gin_trgm_ops);
|
||||
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 3. Trigger updated_at
|
||||
-- --------------------------------------------------------------------------
|
||||
CREATE TRIGGER trg_dt_updated_at
|
||||
BEFORE UPDATE ON public.document_templates
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 4. RLS — document_templates
|
||||
-- --------------------------------------------------------------------------
|
||||
ALTER TABLE public.document_templates ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Templates globais: todos podem ler
|
||||
CREATE POLICY "dt: global templates readable by all"
|
||||
ON public.document_templates
|
||||
FOR SELECT
|
||||
USING (is_global = true);
|
||||
|
||||
-- Templates do tenant: membros do tenant podem ler
|
||||
CREATE POLICY "dt: tenant members can select"
|
||||
ON public.document_templates
|
||||
FOR SELECT
|
||||
USING (
|
||||
is_global = false
|
||||
AND tenant_id IN (
|
||||
SELECT tm.tenant_id FROM public.tenant_members tm
|
||||
WHERE tm.user_id = auth.uid() AND tm.status = 'active'
|
||||
)
|
||||
);
|
||||
|
||||
-- Owner pode inserir/atualizar/deletar seus templates
|
||||
CREATE POLICY "dt: owner can insert"
|
||||
ON public.document_templates
|
||||
FOR INSERT
|
||||
WITH CHECK (owner_id = auth.uid() AND is_global = false);
|
||||
|
||||
CREATE POLICY "dt: owner can update"
|
||||
ON public.document_templates
|
||||
FOR UPDATE
|
||||
USING (owner_id = auth.uid() AND is_global = false)
|
||||
WITH CHECK (owner_id = auth.uid() AND is_global = false);
|
||||
|
||||
CREATE POLICY "dt: owner can delete"
|
||||
ON public.document_templates
|
||||
FOR DELETE
|
||||
USING (owner_id = auth.uid() AND is_global = false);
|
||||
|
||||
-- SaaS admin pode gerenciar templates globais (usa funcao public.is_saas_admin())
|
||||
CREATE POLICY "dt: saas admin can insert global"
|
||||
ON public.document_templates
|
||||
FOR INSERT
|
||||
WITH CHECK (is_global = true AND public.is_saas_admin());
|
||||
|
||||
CREATE POLICY "dt: saas admin can update global"
|
||||
ON public.document_templates
|
||||
FOR UPDATE
|
||||
USING (is_global = true AND public.is_saas_admin())
|
||||
WITH CHECK (is_global = true AND public.is_saas_admin());
|
||||
|
||||
CREATE POLICY "dt: saas admin can delete global"
|
||||
ON public.document_templates
|
||||
FOR DELETE
|
||||
USING (is_global = true AND public.is_saas_admin());
|
||||
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 5. Comentarios — document_templates
|
||||
-- --------------------------------------------------------------------------
|
||||
COMMENT ON TABLE public.document_templates IS 'Templates de documentos para geracao automatica (declaracao, atestado, recibo etc.).';
|
||||
COMMENT ON COLUMN public.document_templates.nome_template IS 'Nome do template. Ex: Declaracao de Comparecimento.';
|
||||
COMMENT ON COLUMN public.document_templates.tipo IS 'declaracao_comparecimento|atestado_psicologico|relatorio_acompanhamento|recibo_pagamento|termo_consentimento|encaminhamento|outro.';
|
||||
COMMENT ON COLUMN public.document_templates.corpo_html IS 'Corpo do template em HTML com variaveis {{nome_variavel}}.';
|
||||
COMMENT ON COLUMN public.document_templates.cabecalho_html IS 'HTML do cabecalho (logo, nome da clinica etc.).';
|
||||
COMMENT ON COLUMN public.document_templates.rodape_html IS 'HTML do rodape (CRP, endereco, contato etc.).';
|
||||
COMMENT ON COLUMN public.document_templates.variaveis IS 'Array com nomes das variaveis usadas no template. Ex: {paciente_nome, data_sessao}.';
|
||||
COMMENT ON COLUMN public.document_templates.is_global IS 'true = template padrao do sistema visivel para todos. false = template do tenant.';
|
||||
COMMENT ON COLUMN public.document_templates.logo_url IS 'URL do logo personalizado para o cabecalho do documento.';
|
||||
|
||||
|
||||
-- ==========================================================================
|
||||
-- 6. Tabela: document_generated (cada PDF gerado)
|
||||
-- ==========================================================================
|
||||
CREATE TABLE IF NOT EXISTS public.document_generated (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
|
||||
-- Origem
|
||||
template_id uuid NOT NULL REFERENCES public.document_templates(id) ON DELETE RESTRICT,
|
||||
patient_id uuid NOT NULL REFERENCES public.patients(id) ON DELETE CASCADE,
|
||||
tenant_id uuid NOT NULL,
|
||||
|
||||
-- Dados usados no preenchimento (snapshot — permite auditoria futura)
|
||||
dados_preenchidos jsonb NOT NULL DEFAULT '{}',
|
||||
|
||||
-- PDF gerado
|
||||
pdf_path text NOT NULL,
|
||||
storage_bucket text NOT NULL DEFAULT 'generated-docs',
|
||||
|
||||
-- Vinculo opcional com documento pai (se o PDF gerado tambem for registrado em documents)
|
||||
documento_id uuid REFERENCES public.documents(id) ON DELETE SET NULL,
|
||||
|
||||
-- Quem gerou
|
||||
gerado_por uuid NOT NULL,
|
||||
gerado_em timestamptz DEFAULT now() NOT NULL,
|
||||
|
||||
CONSTRAINT document_generated_pkey PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 7. Indices — document_generated
|
||||
-- --------------------------------------------------------------------------
|
||||
CREATE INDEX IF NOT EXISTS dg_template_idx
|
||||
ON public.document_generated USING btree (template_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS dg_patient_idx
|
||||
ON public.document_generated USING btree (patient_id, gerado_em DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS dg_tenant_idx
|
||||
ON public.document_generated USING btree (tenant_id, gerado_em DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS dg_gerado_por_idx
|
||||
ON public.document_generated USING btree (gerado_por, gerado_em DESC);
|
||||
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 8. RLS — document_generated
|
||||
-- --------------------------------------------------------------------------
|
||||
ALTER TABLE public.document_generated ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "dg: generator full access"
|
||||
ON public.document_generated
|
||||
USING (gerado_por = auth.uid())
|
||||
WITH CHECK (gerado_por = auth.uid());
|
||||
|
||||
-- Membros do tenant podem visualizar
|
||||
CREATE POLICY "dg: tenant members can select"
|
||||
ON public.document_generated
|
||||
FOR SELECT
|
||||
USING (tenant_id IN (
|
||||
SELECT tm.tenant_id FROM public.tenant_members tm
|
||||
WHERE tm.user_id = auth.uid() AND tm.status = 'active'
|
||||
));
|
||||
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 9. Comentarios — document_generated
|
||||
-- --------------------------------------------------------------------------
|
||||
COMMENT ON TABLE public.document_generated IS 'Registro de cada documento PDF gerado a partir de um template.';
|
||||
COMMENT ON COLUMN public.document_generated.template_id IS 'Template usado para gerar o documento.';
|
||||
COMMENT ON COLUMN public.document_generated.dados_preenchidos IS 'Snapshot JSON dos dados usados no preenchimento. Permite auditoria futura.';
|
||||
COMMENT ON COLUMN public.document_generated.pdf_path IS 'Caminho do PDF gerado no Supabase Storage bucket.';
|
||||
COMMENT ON COLUMN public.document_generated.documento_id IS 'FK opcional para documents — se o PDF gerado tambem foi registrado como documento do paciente.';
|
||||
COMMENT ON COLUMN public.document_generated.gerado_por IS 'Usuario que gerou o documento (auth.uid()).';
|
||||
|
||||
|
||||
-- ==========================================================================
|
||||
-- FIM DA MIGRACAO 006
|
||||
-- ==========================================================================
|
||||
@@ -0,0 +1,93 @@
|
||||
-- ==========================================================================
|
||||
-- Agencia PSI — Migracao: Storage Buckets para Documentos
|
||||
-- ==========================================================================
|
||||
-- Criado por: Leonardo Nohama
|
||||
-- Data: 2026-03-29 · Sao Carlos/SP — Brasil
|
||||
--
|
||||
-- Cria os buckets no Supabase Storage para documentos de pacientes
|
||||
-- e PDFs gerados pelo sistema.
|
||||
-- ==========================================================================
|
||||
|
||||
-- Bucket: documents (uploads de terapeuta/paciente)
|
||||
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
||||
VALUES (
|
||||
'documents',
|
||||
'documents',
|
||||
false,
|
||||
52428800, -- 50 MB
|
||||
ARRAY[
|
||||
'application/pdf',
|
||||
'image/jpeg', 'image/png', 'image/webp', 'image/gif',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'text/plain'
|
||||
]
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Bucket: generated-docs (PDFs gerados pelo sistema)
|
||||
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
||||
VALUES (
|
||||
'generated-docs',
|
||||
'generated-docs',
|
||||
false,
|
||||
20971520, -- 20 MB
|
||||
ARRAY['application/pdf']
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- Storage RLS Policies — bucket: documents
|
||||
-- --------------------------------------------------------------------------
|
||||
|
||||
-- Upload: usuario autenticado pode fazer upload no path do seu tenant
|
||||
CREATE POLICY "documents: authenticated upload"
|
||||
ON storage.objects
|
||||
FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (bucket_id = 'documents');
|
||||
|
||||
-- Download: usuario autenticado pode ler arquivos do seu tenant
|
||||
CREATE POLICY "documents: authenticated read"
|
||||
ON storage.objects
|
||||
FOR SELECT
|
||||
TO authenticated
|
||||
USING (bucket_id = 'documents');
|
||||
|
||||
-- Delete: usuario autenticado pode deletar seus arquivos
|
||||
CREATE POLICY "documents: authenticated delete"
|
||||
ON storage.objects
|
||||
FOR DELETE
|
||||
TO authenticated
|
||||
USING (bucket_id = 'documents');
|
||||
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- Storage RLS Policies — bucket: generated-docs
|
||||
-- --------------------------------------------------------------------------
|
||||
|
||||
CREATE POLICY "generated-docs: authenticated upload"
|
||||
ON storage.objects
|
||||
FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (bucket_id = 'generated-docs');
|
||||
|
||||
CREATE POLICY "generated-docs: authenticated read"
|
||||
ON storage.objects
|
||||
FOR SELECT
|
||||
TO authenticated
|
||||
USING (bucket_id = 'generated-docs');
|
||||
|
||||
CREATE POLICY "generated-docs: authenticated delete"
|
||||
ON storage.objects
|
||||
FOR DELETE
|
||||
TO authenticated
|
||||
USING (bucket_id = 'generated-docs');
|
||||
|
||||
|
||||
-- ==========================================================================
|
||||
-- FIM DA MIGRACAO
|
||||
-- ==========================================================================
|
||||
661
database-novo/migrations/migration_patients.sql
Normal file
661
database-novo/migrations/migration_patients.sql
Normal file
@@ -0,0 +1,661 @@
|
||||
-- =============================================================================
|
||||
-- MIGRATION: patients — melhorias completas
|
||||
-- Gerado em: 2025-03
|
||||
-- Estratégia: cirúrgico — só adiciona, nunca destrói o que existe
|
||||
-- =============================================================================
|
||||
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 1. ALTERAÇÕES NA TABELA patients
|
||||
-- Novos campos adicionados sem tocar nos existentes
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
ALTER TABLE public.patients
|
||||
-- Identidade & pronomes
|
||||
ADD COLUMN IF NOT EXISTS nome_social text,
|
||||
ADD COLUMN IF NOT EXISTS pronomes text,
|
||||
|
||||
-- Dados socioeconômicos (opcionais, clínicamente relevantes)
|
||||
ADD COLUMN IF NOT EXISTS etnia text,
|
||||
ADD COLUMN IF NOT EXISTS religiao text,
|
||||
ADD COLUMN IF NOT EXISTS faixa_renda text,
|
||||
|
||||
-- Preferências de comunicação (alimenta lembretes automáticos)
|
||||
ADD COLUMN IF NOT EXISTS canal_preferido text DEFAULT 'whatsapp',
|
||||
ADD COLUMN IF NOT EXISTS horario_contato_inicio time DEFAULT '08:00',
|
||||
ADD COLUMN IF NOT EXISTS horario_contato_fim time DEFAULT '20:00',
|
||||
ADD COLUMN IF NOT EXISTS idioma text DEFAULT 'pt-BR',
|
||||
|
||||
-- Origem estruturada (permite filtros e relatórios)
|
||||
ADD COLUMN IF NOT EXISTS origem text,
|
||||
|
||||
-- Financeiro
|
||||
ADD COLUMN IF NOT EXISTS metodo_pagamento_preferido text,
|
||||
|
||||
-- Ciclo de vida
|
||||
ADD COLUMN IF NOT EXISTS motivo_saida text,
|
||||
ADD COLUMN IF NOT EXISTS data_saida date,
|
||||
ADD COLUMN IF NOT EXISTS encaminhado_para text,
|
||||
|
||||
-- Risco clínico (flag de atenção visível no topo do cadastro)
|
||||
ADD COLUMN IF NOT EXISTS risco_elevado boolean DEFAULT false NOT NULL,
|
||||
ADD COLUMN IF NOT EXISTS risco_nota text,
|
||||
ADD COLUMN IF NOT EXISTS risco_sinalizado_em timestamp with time zone,
|
||||
ADD COLUMN IF NOT EXISTS risco_sinalizado_por uuid REFERENCES auth.users(id) ON DELETE SET NULL;
|
||||
|
||||
|
||||
-- Constraints de validação para novos campos enum-like
|
||||
ALTER TABLE public.patients
|
||||
DROP CONSTRAINT IF EXISTS patients_canal_preferido_check,
|
||||
ADD CONSTRAINT patients_canal_preferido_check
|
||||
CHECK (canal_preferido IS NULL OR canal_preferido = ANY (
|
||||
ARRAY['whatsapp','email','sms','telefone']
|
||||
));
|
||||
|
||||
ALTER TABLE public.patients
|
||||
DROP CONSTRAINT IF EXISTS patients_metodo_pagamento_check,
|
||||
ADD CONSTRAINT patients_metodo_pagamento_check
|
||||
CHECK (metodo_pagamento_preferido IS NULL OR metodo_pagamento_preferido = ANY (
|
||||
ARRAY['pix','cartao','dinheiro','deposito','convenio']
|
||||
));
|
||||
|
||||
ALTER TABLE public.patients
|
||||
DROP CONSTRAINT IF EXISTS patients_faixa_renda_check,
|
||||
ADD CONSTRAINT patients_faixa_renda_check
|
||||
CHECK (faixa_renda IS NULL OR faixa_renda = ANY (
|
||||
ARRAY['ate_1sm','1_3sm','3_6sm','6_10sm','acima_10sm','nao_informado']
|
||||
));
|
||||
|
||||
-- Constraint: risco_elevado = true exige nota e sinalizante
|
||||
ALTER TABLE public.patients
|
||||
DROP CONSTRAINT IF EXISTS patients_risco_consistency_check,
|
||||
ADD CONSTRAINT patients_risco_consistency_check
|
||||
CHECK (
|
||||
(risco_elevado = false)
|
||||
OR (risco_elevado = true AND risco_nota IS NOT NULL AND risco_sinalizado_por IS NOT NULL)
|
||||
);
|
||||
|
||||
-- Comments
|
||||
COMMENT ON COLUMN public.patients.nome_social IS 'Nome social preferido — exibido no lugar do nome completo quando preenchido';
|
||||
COMMENT ON COLUMN public.patients.pronomes IS 'Pronomes preferidos: ele/dele, ela/dela, eles/deles, etc.';
|
||||
COMMENT ON COLUMN public.patients.etnia IS 'Autodeclaração étnico-racial (opcional)';
|
||||
COMMENT ON COLUMN public.patients.religiao IS 'Religião ou espiritualidade (opcional, relevante clinicamente)';
|
||||
COMMENT ON COLUMN public.patients.faixa_renda IS 'Faixa de renda em salários mínimos — usado para precificação solidária';
|
||||
COMMENT ON COLUMN public.patients.canal_preferido IS 'Canal de comunicação preferido para lembretes e notificações';
|
||||
COMMENT ON COLUMN public.patients.horario_contato_inicio IS 'Início da janela de horário preferida para contato';
|
||||
COMMENT ON COLUMN public.patients.horario_contato_fim IS 'Fim da janela de horário preferida para contato';
|
||||
COMMENT ON COLUMN public.patients.origem IS 'Como o paciente chegou: indicacao, agendador, redes_sociais, encaminhamento, outro';
|
||||
COMMENT ON COLUMN public.patients.metodo_pagamento_preferido IS 'Método de pagamento habitual — sugerido ao criar cobrança';
|
||||
COMMENT ON COLUMN public.patients.motivo_saida IS 'Motivo da alta, inativação ou encaminhamento';
|
||||
COMMENT ON COLUMN public.patients.data_saida IS 'Data em que o paciente foi desligado/encaminhado';
|
||||
COMMENT ON COLUMN public.patients.encaminhado_para IS 'Nome ou serviço para onde o paciente foi encaminhado';
|
||||
COMMENT ON COLUMN public.patients.risco_elevado IS 'Flag de atenção clínica — exibe alerta no topo do cadastro e prontuário';
|
||||
COMMENT ON COLUMN public.patients.risco_nota IS 'Descrição do risco (obrigatória quando risco_elevado = true)';
|
||||
COMMENT ON COLUMN public.patients.risco_sinalizado_em IS 'Timestamp em que o risco foi sinalizado';
|
||||
COMMENT ON COLUMN public.patients.risco_sinalizado_por IS 'Usuário que sinalizou o risco';
|
||||
|
||||
-- Índices úteis para filtros frequentes
|
||||
CREATE INDEX IF NOT EXISTS idx_patients_risco_elevado
|
||||
ON public.patients (tenant_id, risco_elevado)
|
||||
WHERE risco_elevado = true;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_patients_status_tenant
|
||||
ON public.patients (tenant_id, status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_patients_origem
|
||||
ON public.patients (tenant_id, origem)
|
||||
WHERE origem IS NOT NULL;
|
||||
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 2. TABELA patient_contacts
|
||||
-- Substitui os campos soltos nome_parente/telefone_parente na tabela principal
|
||||
-- Os campos antigos ficam intactos (retrocompatibilidade)
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.patient_contacts (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
patient_id uuid NOT NULL REFERENCES public.patients(id) ON DELETE CASCADE,
|
||||
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
||||
|
||||
-- Identificação
|
||||
nome text NOT NULL,
|
||||
tipo text NOT NULL, -- emergencia | responsavel_legal | profissional_saude | outro
|
||||
relacao text, -- mãe, pai, psiquiatra, médico, cônjuge...
|
||||
|
||||
-- Contato
|
||||
telefone text,
|
||||
email text,
|
||||
cpf text,
|
||||
|
||||
-- Profissional de saúde
|
||||
especialidade text, -- preenchido quando tipo = profissional_saude
|
||||
registro_profissional text, -- CRM, CRP, etc.
|
||||
|
||||
-- Flags
|
||||
is_primario boolean DEFAULT false NOT NULL, -- contato principal de emergência
|
||||
ativo boolean DEFAULT true NOT NULL,
|
||||
|
||||
-- Auditoria
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
|
||||
CONSTRAINT patient_contacts_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT patient_contacts_tipo_check CHECK (tipo = ANY (
|
||||
ARRAY['emergencia','responsavel_legal','profissional_saude','outro']
|
||||
))
|
||||
);
|
||||
|
||||
COMMENT ON TABLE public.patient_contacts IS 'Contatos vinculados ao paciente: emergência, responsável legal, outros profissionais de saúde';
|
||||
COMMENT ON COLUMN public.patient_contacts.tipo IS 'Categoria do contato: emergencia | responsavel_legal | profissional_saude | outro';
|
||||
COMMENT ON COLUMN public.patient_contacts.is_primario IS 'Contato de emergência principal — exibido em destaque no cadastro';
|
||||
|
||||
-- Garante no máximo 1 contato primário por paciente
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_patient_contacts_primario
|
||||
ON public.patient_contacts (patient_id)
|
||||
WHERE is_primario = true AND ativo = true;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_patient_contacts_patient
|
||||
ON public.patient_contacts (patient_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_patient_contacts_tenant
|
||||
ON public.patient_contacts (tenant_id);
|
||||
|
||||
-- updated_at automático
|
||||
CREATE TRIGGER trg_patient_contacts_updated_at
|
||||
BEFORE UPDATE ON public.patient_contacts
|
||||
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
-- RLS — mesmas regras de patients
|
||||
ALTER TABLE public.patient_contacts ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY patient_contacts_select ON public.patient_contacts
|
||||
FOR SELECT USING (
|
||||
public.is_clinic_tenant(tenant_id)
|
||||
AND public.is_tenant_member(tenant_id)
|
||||
AND public.tenant_has_feature(tenant_id, 'patients.view')
|
||||
);
|
||||
|
||||
CREATE POLICY patient_contacts_write ON public.patient_contacts
|
||||
USING (
|
||||
public.is_clinic_tenant(tenant_id)
|
||||
AND public.is_tenant_member(tenant_id)
|
||||
AND public.tenant_has_feature(tenant_id, 'patients.edit')
|
||||
)
|
||||
WITH CHECK (
|
||||
public.is_clinic_tenant(tenant_id)
|
||||
AND public.is_tenant_member(tenant_id)
|
||||
AND public.tenant_has_feature(tenant_id, 'patients.edit')
|
||||
);
|
||||
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 3. TABELA patient_status_history
|
||||
-- Trilha de auditoria de todas as mudanças de status do paciente
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.patient_status_history (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
patient_id uuid NOT NULL REFERENCES public.patients(id) ON DELETE CASCADE,
|
||||
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
||||
|
||||
status_anterior text, -- NULL na primeira inserção
|
||||
status_novo text NOT NULL,
|
||||
motivo text,
|
||||
encaminhado_para text, -- preenchido quando status = Encaminhado
|
||||
data_saida date, -- preenchido quando Alta/Encaminhado/Arquivado
|
||||
|
||||
alterado_por uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
alterado_em timestamp with time zone DEFAULT now() NOT NULL,
|
||||
|
||||
CONSTRAINT patient_status_history_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT psh_status_novo_check CHECK (status_novo = ANY (
|
||||
ARRAY['Ativo','Inativo','Alta','Encaminhado','Arquivado']
|
||||
))
|
||||
);
|
||||
|
||||
COMMENT ON TABLE public.patient_status_history IS 'Histórico imutável de todas as mudanças de status do paciente — não editar, apenas inserir';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_psh_patient
|
||||
ON public.patient_status_history (patient_id, alterado_em DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_psh_tenant
|
||||
ON public.patient_status_history (tenant_id, alterado_em DESC);
|
||||
|
||||
-- RLS
|
||||
ALTER TABLE public.patient_status_history ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY psh_select ON public.patient_status_history
|
||||
FOR SELECT USING (
|
||||
public.is_clinic_tenant(tenant_id)
|
||||
AND public.is_tenant_member(tenant_id)
|
||||
AND public.tenant_has_feature(tenant_id, 'patients.view')
|
||||
);
|
||||
|
||||
CREATE POLICY psh_insert ON public.patient_status_history
|
||||
FOR INSERT WITH CHECK (
|
||||
public.is_clinic_tenant(tenant_id)
|
||||
AND public.is_tenant_member(tenant_id)
|
||||
AND public.tenant_has_feature(tenant_id, 'patients.edit')
|
||||
);
|
||||
|
||||
-- Trigger: registra automaticamente no histórico quando status muda em patients
|
||||
CREATE OR REPLACE FUNCTION public.trg_fn_patient_status_history()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql SECURITY DEFINER
|
||||
AS $$
|
||||
BEGIN
|
||||
IF (TG_OP = 'INSERT') OR (OLD.status IS DISTINCT FROM NEW.status) THEN
|
||||
INSERT INTO public.patient_status_history (
|
||||
patient_id, tenant_id,
|
||||
status_anterior, status_novo,
|
||||
motivo, encaminhado_para, data_saida,
|
||||
alterado_por, alterado_em
|
||||
) VALUES (
|
||||
NEW.id, NEW.tenant_id,
|
||||
CASE WHEN TG_OP = 'INSERT' THEN NULL ELSE OLD.status END,
|
||||
NEW.status,
|
||||
NEW.motivo_saida,
|
||||
NEW.encaminhado_para,
|
||||
NEW.data_saida,
|
||||
auth.uid(),
|
||||
now()
|
||||
);
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_patient_status_history ON public.patients;
|
||||
CREATE TRIGGER trg_patient_status_history
|
||||
AFTER INSERT OR UPDATE OF status ON public.patients
|
||||
FOR EACH ROW EXECUTE FUNCTION public.trg_fn_patient_status_history();
|
||||
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4. TABELA patient_timeline
|
||||
-- Feed cronológico automático de eventos relevantes do paciente
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.patient_timeline (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
patient_id uuid NOT NULL REFERENCES public.patients(id) ON DELETE CASCADE,
|
||||
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
||||
|
||||
-- Tipo do evento
|
||||
evento_tipo text NOT NULL,
|
||||
-- Exemplos: primeira_sessao | sessao_realizada | sessao_cancelada | falta |
|
||||
-- status_alterado | risco_sinalizado | documento_assinado |
|
||||
-- escala_respondida | pagamento_vencido | pagamento_recebido |
|
||||
-- tarefa_combinada | contato_adicionado | prontuario_editado
|
||||
|
||||
titulo text NOT NULL, -- Ex: "Sessão realizada"
|
||||
descricao text, -- Ex: "Sessão 47 · presencial · 50min"
|
||||
icone_cor text DEFAULT 'gray', -- green | blue | amber | red | gray
|
||||
link_ref_tipo text, -- agenda_evento | financial_record | documento | escala
|
||||
link_ref_id uuid, -- FK genérico — sem constraint formal (polimórfico)
|
||||
gerado_por uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
ocorrido_em timestamp with time zone DEFAULT now() NOT NULL,
|
||||
|
||||
CONSTRAINT patient_timeline_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT pt_evento_tipo_check CHECK (evento_tipo = ANY (ARRAY[
|
||||
'primeira_sessao','sessao_realizada','sessao_cancelada','falta',
|
||||
'status_alterado','risco_sinalizado','risco_removido',
|
||||
'documento_assinado','documento_adicionado',
|
||||
'escala_respondida','escala_enviada',
|
||||
'pagamento_vencido','pagamento_recebido',
|
||||
'tarefa_combinada','contato_adicionado',
|
||||
'prontuario_editado','nota_adicionada','manual'
|
||||
])),
|
||||
CONSTRAINT pt_icone_cor_check CHECK (icone_cor = ANY (
|
||||
ARRAY['green','blue','amber','red','gray','purple']
|
||||
))
|
||||
);
|
||||
|
||||
COMMENT ON TABLE public.patient_timeline IS 'Feed cronológico de eventos do paciente — alimentado por triggers e inserções manuais';
|
||||
COMMENT ON COLUMN public.patient_timeline.link_ref_tipo IS 'Tipo da entidade referenciada (polimórfico): agenda_evento | financial_record | documento | escala';
|
||||
COMMENT ON COLUMN public.patient_timeline.link_ref_id IS 'ID da entidade referenciada — sem FK formal para suportar múltiplos tipos';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pt_patient_ocorrido
|
||||
ON public.patient_timeline (patient_id, ocorrido_em DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pt_tenant
|
||||
ON public.patient_timeline (tenant_id, ocorrido_em DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pt_evento_tipo
|
||||
ON public.patient_timeline (patient_id, evento_tipo);
|
||||
|
||||
-- RLS
|
||||
ALTER TABLE public.patient_timeline ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY pt_select ON public.patient_timeline
|
||||
FOR SELECT USING (
|
||||
public.is_clinic_tenant(tenant_id)
|
||||
AND public.is_tenant_member(tenant_id)
|
||||
AND public.tenant_has_feature(tenant_id, 'patients.view')
|
||||
);
|
||||
|
||||
CREATE POLICY pt_insert ON public.patient_timeline
|
||||
FOR INSERT WITH CHECK (
|
||||
public.is_clinic_tenant(tenant_id)
|
||||
AND public.is_tenant_member(tenant_id)
|
||||
AND public.tenant_has_feature(tenant_id, 'patients.edit')
|
||||
);
|
||||
|
||||
-- Trigger: registra na timeline quando risco é sinalizado/removido
|
||||
CREATE OR REPLACE FUNCTION public.trg_fn_patient_risco_timeline()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql SECURITY DEFINER
|
||||
AS $$
|
||||
BEGIN
|
||||
IF OLD.risco_elevado IS DISTINCT FROM NEW.risco_elevado THEN
|
||||
INSERT INTO public.patient_timeline (
|
||||
patient_id, tenant_id,
|
||||
evento_tipo, titulo, descricao, icone_cor,
|
||||
gerado_por, ocorrido_em
|
||||
) VALUES (
|
||||
NEW.id, NEW.tenant_id,
|
||||
CASE WHEN NEW.risco_elevado THEN 'risco_sinalizado' ELSE 'risco_removido' END,
|
||||
CASE WHEN NEW.risco_elevado THEN 'Risco elevado sinalizado' ELSE 'Sinalização de risco removida' END,
|
||||
NEW.risco_nota,
|
||||
CASE WHEN NEW.risco_elevado THEN 'red' ELSE 'green' END,
|
||||
auth.uid(),
|
||||
now()
|
||||
);
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_patient_risco_timeline ON public.patients;
|
||||
CREATE TRIGGER trg_patient_risco_timeline
|
||||
AFTER UPDATE OF risco_elevado ON public.patients
|
||||
FOR EACH ROW EXECUTE FUNCTION public.trg_fn_patient_risco_timeline();
|
||||
|
||||
-- Trigger: registra na timeline quando status muda
|
||||
CREATE OR REPLACE FUNCTION public.trg_fn_patient_status_timeline()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql SECURITY DEFINER
|
||||
AS $$
|
||||
BEGIN
|
||||
IF (TG_OP = 'INSERT') OR (OLD.status IS DISTINCT FROM NEW.status) THEN
|
||||
INSERT INTO public.patient_timeline (
|
||||
patient_id, tenant_id,
|
||||
evento_tipo, titulo, descricao, icone_cor,
|
||||
gerado_por, ocorrido_em
|
||||
) VALUES (
|
||||
NEW.id, NEW.tenant_id,
|
||||
'status_alterado',
|
||||
'Status alterado para ' || NEW.status,
|
||||
CASE
|
||||
WHEN TG_OP = 'INSERT' THEN 'Paciente cadastrado'
|
||||
ELSE 'De ' || OLD.status || ' → ' || NEW.status ||
|
||||
CASE WHEN NEW.motivo_saida IS NOT NULL THEN ' · ' || NEW.motivo_saida ELSE '' END
|
||||
END,
|
||||
CASE NEW.status
|
||||
WHEN 'Ativo' THEN 'green'
|
||||
WHEN 'Alta' THEN 'blue'
|
||||
WHEN 'Inativo' THEN 'gray'
|
||||
WHEN 'Encaminhado' THEN 'amber'
|
||||
WHEN 'Arquivado' THEN 'gray'
|
||||
ELSE 'gray'
|
||||
END,
|
||||
auth.uid(),
|
||||
now()
|
||||
);
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_patient_status_timeline ON public.patients;
|
||||
CREATE TRIGGER trg_patient_status_timeline
|
||||
AFTER INSERT OR UPDATE OF status ON public.patients
|
||||
FOR EACH ROW EXECUTE FUNCTION public.trg_fn_patient_status_timeline();
|
||||
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 5. VIEW v_patient_engajamento
|
||||
-- Score calculado em tempo real — sem armazenar, sem inconsistência
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
CREATE OR REPLACE VIEW public.v_patient_engajamento
|
||||
WITH (security_invoker = on)
|
||||
AS
|
||||
WITH sessoes AS (
|
||||
SELECT
|
||||
ae.patient_id,
|
||||
ae.tenant_id,
|
||||
COUNT(*) FILTER (WHERE ae.status = 'realizado') AS total_realizadas,
|
||||
COUNT(*) FILTER (WHERE ae.status IN ('realizado','cancelado','faltou')) AS total_marcadas,
|
||||
COUNT(*) FILTER (WHERE ae.status = 'faltou') AS total_faltas,
|
||||
MAX(ae.inicio_em) FILTER (WHERE ae.status = 'realizado') AS ultima_sessao_em,
|
||||
MIN(ae.inicio_em) FILTER (WHERE ae.status = 'realizado') AS primeira_sessao_em,
|
||||
COUNT(*) FILTER (WHERE ae.status = 'realizado'
|
||||
AND ae.inicio_em >= now() - interval '30 days') AS sessoes_ultimo_mes
|
||||
FROM public.agenda_eventos ae
|
||||
WHERE ae.patient_id IS NOT NULL
|
||||
GROUP BY ae.patient_id, ae.tenant_id
|
||||
),
|
||||
financeiro AS (
|
||||
SELECT
|
||||
fr.patient_id,
|
||||
fr.tenant_id,
|
||||
COALESCE(SUM(fr.final_amount) FILTER (WHERE fr.status = 'paid'), 0) AS total_pago,
|
||||
COALESCE(AVG(fr.final_amount) FILTER (WHERE fr.status = 'paid'), 0) AS ticket_medio,
|
||||
COUNT(*) FILTER (WHERE fr.status IN ('pending','overdue')
|
||||
AND fr.due_date < now()) AS cobr_vencidas,
|
||||
COUNT(*) FILTER (WHERE fr.status IN ('pending','overdue')) AS cobr_pendentes,
|
||||
COUNT(*) FILTER (WHERE fr.type = 'receita' AND fr.status = 'paid') AS cobr_pagas
|
||||
FROM public.financial_records fr
|
||||
WHERE fr.patient_id IS NOT NULL
|
||||
AND fr.deleted_at IS NULL
|
||||
GROUP BY fr.patient_id, fr.tenant_id
|
||||
)
|
||||
SELECT
|
||||
p.id AS patient_id,
|
||||
p.tenant_id,
|
||||
p.nome_completo,
|
||||
p.status,
|
||||
p.risco_elevado,
|
||||
|
||||
-- Sessões
|
||||
COALESCE(s.total_realizadas, 0) AS total_sessoes,
|
||||
COALESCE(s.sessoes_ultimo_mes, 0) AS sessoes_ultimo_mes,
|
||||
s.primeira_sessao_em,
|
||||
s.ultima_sessao_em,
|
||||
EXTRACT(DAY FROM now() - s.ultima_sessao_em)::int AS dias_sem_sessao,
|
||||
|
||||
-- Taxa de comparecimento (%)
|
||||
CASE
|
||||
WHEN COALESCE(s.total_marcadas, 0) = 0 THEN NULL
|
||||
ELSE ROUND((s.total_realizadas::numeric / s.total_marcadas) * 100, 1)
|
||||
END AS taxa_comparecimento,
|
||||
|
||||
-- Financeiro
|
||||
COALESCE(f.total_pago, 0) AS ltv_total,
|
||||
ROUND(COALESCE(f.ticket_medio, 0), 2) AS ticket_medio,
|
||||
COALESCE(f.cobr_vencidas, 0) AS cobr_vencidas,
|
||||
COALESCE(f.cobr_pagas, 0) AS cobr_pagas,
|
||||
|
||||
-- Taxa de pagamentos em dia (%)
|
||||
CASE
|
||||
WHEN COALESCE(f.cobr_pagas + f.cobr_vencidas, 0) = 0 THEN NULL
|
||||
ELSE ROUND(
|
||||
f.cobr_pagas::numeric / (f.cobr_pagas + f.cobr_vencidas) * 100, 1
|
||||
)
|
||||
END AS taxa_pagamentos_dia,
|
||||
|
||||
-- Score de engajamento composto (0-100)
|
||||
-- Pesos: comparecimento 50%, pagamentos 30%, recência 20%
|
||||
ROUND(
|
||||
LEAST(100,
|
||||
COALESCE(
|
||||
(
|
||||
-- Comparecimento (50 pts)
|
||||
CASE WHEN COALESCE(s.total_marcadas, 0) = 0 THEN 50
|
||||
ELSE LEAST(50, (s.total_realizadas::numeric / s.total_marcadas) * 50)
|
||||
END
|
||||
+
|
||||
-- Pagamentos em dia (30 pts)
|
||||
CASE WHEN COALESCE(f.cobr_pagas + f.cobr_vencidas, 0) = 0 THEN 30
|
||||
ELSE LEAST(30, f.cobr_pagas::numeric / (f.cobr_pagas + f.cobr_vencidas) * 30)
|
||||
END
|
||||
+
|
||||
-- Recência (20 pts — penaliza quem está há muito tempo sem sessão)
|
||||
CASE
|
||||
WHEN s.ultima_sessao_em IS NULL THEN 0
|
||||
WHEN EXTRACT(DAY FROM now() - s.ultima_sessao_em) <= 14 THEN 20
|
||||
WHEN EXTRACT(DAY FROM now() - s.ultima_sessao_em) <= 30 THEN 15
|
||||
WHEN EXTRACT(DAY FROM now() - s.ultima_sessao_em) <= 60 THEN 8
|
||||
ELSE 0
|
||||
END
|
||||
), 0
|
||||
)
|
||||
)
|
||||
, 0) AS engajamento_score,
|
||||
|
||||
-- Duração do tratamento
|
||||
CASE
|
||||
WHEN s.primeira_sessao_em IS NULL THEN NULL
|
||||
ELSE EXTRACT(DAY FROM now() - s.primeira_sessao_em)::int
|
||||
END AS duracao_tratamento_dias
|
||||
|
||||
FROM public.patients p
|
||||
LEFT JOIN sessoes s ON s.patient_id = p.id AND s.tenant_id = p.tenant_id
|
||||
LEFT JOIN financeiro f ON f.patient_id = p.id AND f.tenant_id = p.tenant_id;
|
||||
|
||||
COMMENT ON VIEW public.v_patient_engajamento IS
|
||||
'Score de engajamento e métricas consolidadas por paciente. Calculado em tempo real via RLS (security_invoker=on).';
|
||||
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 6. VIEW v_patients_risco
|
||||
-- Lista rápida de pacientes que precisam de atenção imediata
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
CREATE OR REPLACE VIEW public.v_patients_risco
|
||||
WITH (security_invoker = on)
|
||||
AS
|
||||
SELECT
|
||||
p.id,
|
||||
p.tenant_id,
|
||||
p.nome_completo,
|
||||
p.status,
|
||||
p.risco_elevado,
|
||||
p.risco_nota,
|
||||
p.risco_sinalizado_em,
|
||||
e.dias_sem_sessao,
|
||||
e.engajamento_score,
|
||||
e.taxa_comparecimento,
|
||||
-- Motivo do alerta
|
||||
CASE
|
||||
WHEN p.risco_elevado THEN 'risco_sinalizado'
|
||||
WHEN COALESCE(e.dias_sem_sessao, 999) > 30
|
||||
AND p.status = 'Ativo' THEN 'sem_sessao_30d'
|
||||
WHEN COALESCE(e.taxa_comparecimento, 100) < 60 THEN 'baixo_comparecimento'
|
||||
WHEN COALESCE(e.cobr_vencidas, 0) > 0 THEN 'cobranca_vencida'
|
||||
ELSE 'ok'
|
||||
END AS alerta_tipo
|
||||
FROM public.patients p
|
||||
JOIN public.v_patient_engajamento e ON e.patient_id = p.id
|
||||
WHERE p.status = 'Ativo'
|
||||
AND (
|
||||
p.risco_elevado = true
|
||||
OR COALESCE(e.dias_sem_sessao, 999) > 30
|
||||
OR COALESCE(e.taxa_comparecimento, 100) < 60
|
||||
OR COALESCE(e.cobr_vencidas, 0) > 0
|
||||
);
|
||||
|
||||
COMMENT ON VIEW public.v_patients_risco IS
|
||||
'Pacientes ativos que precisam de atenção: risco clínico, sem sessão há 30+ dias, baixo comparecimento ou cobrança vencida';
|
||||
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 7. Migração de dados: popular patient_contacts com os dados já existentes
|
||||
-- Roda só uma vez — protegido por WHERE NOT EXISTS
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
INSERT INTO public.patient_contacts (
|
||||
patient_id, tenant_id,
|
||||
nome, tipo, relacao,
|
||||
telefone, is_primario, ativo
|
||||
)
|
||||
SELECT
|
||||
p.id,
|
||||
p.tenant_id,
|
||||
p.nome_parente,
|
||||
'emergencia',
|
||||
p.grau_parentesco,
|
||||
p.telefone_parente,
|
||||
true,
|
||||
true
|
||||
FROM public.patients p
|
||||
WHERE p.nome_parente IS NOT NULL
|
||||
AND p.telefone_parente IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM public.patient_contacts pc
|
||||
WHERE pc.patient_id = p.id AND pc.tipo = 'emergencia'
|
||||
);
|
||||
|
||||
-- Migra responsável legal quando diferente do parente de emergência
|
||||
INSERT INTO public.patient_contacts (
|
||||
patient_id, tenant_id,
|
||||
nome, tipo, relacao,
|
||||
telefone, cpf,
|
||||
is_primario, ativo
|
||||
)
|
||||
SELECT
|
||||
p.id,
|
||||
p.tenant_id,
|
||||
p.nome_responsavel,
|
||||
'responsavel_legal',
|
||||
'Responsável legal',
|
||||
p.telefone_responsavel,
|
||||
p.cpf_responsavel,
|
||||
false,
|
||||
true
|
||||
FROM public.patients p
|
||||
WHERE p.nome_responsavel IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM public.patient_contacts pc
|
||||
WHERE pc.patient_id = p.id AND pc.tipo = 'responsavel_legal'
|
||||
);
|
||||
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 8. Seed do histórico de status para pacientes já existentes
|
||||
-- Cria a primeira entrada de histórico com o status atual
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
INSERT INTO public.patient_status_history (
|
||||
patient_id, tenant_id,
|
||||
status_anterior, status_novo,
|
||||
motivo, alterado_em
|
||||
)
|
||||
SELECT
|
||||
p.id,
|
||||
p.tenant_id,
|
||||
NULL,
|
||||
p.status,
|
||||
'Status inicial — migração de dados',
|
||||
COALESCE(p.created_at, now())
|
||||
FROM public.patients p
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM public.patient_status_history psh
|
||||
WHERE psh.patient_id = p.id
|
||||
);
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- FIM DO MIGRATION
|
||||
-- Resumo do que foi feito:
|
||||
-- 1. ALTER TABLE patients — 16 novos campos (pronomes, risco, origem, etc.)
|
||||
-- 2. CREATE TABLE patient_contacts — múltiplos contatos por paciente
|
||||
-- 3. CREATE TABLE patient_status_history — trilha imutável de mudanças de status
|
||||
-- 4. CREATE TABLE patient_timeline — feed cronológico de eventos
|
||||
-- 5. Triggers automáticos — status history, timeline de risco e status
|
||||
-- 6. VIEW v_patient_engajamento — score 0-100 + métricas calculadas em tempo real
|
||||
-- 7. VIEW v_patients_risco — lista de pacientes que precisam de atenção
|
||||
-- 8. Migração de dados — popula patient_contacts e status_history com dados existentes
|
||||
-- =============================================================================
|
||||
Reference in New Issue
Block a user