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:
96
database-novo/README-GENERATE-DASHBOARD.md
Normal file
96
database-novo/README-GENERATE-DASHBOARD.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# README — generate-dashboard.js
|
||||
|
||||
Script Node.js que lê o `schema.sql` do backup mais recente e gera um `dashboard.html` interativo com a visão completa do banco de dados do projeto.
|
||||
|
||||
---
|
||||
|
||||
## Como usar
|
||||
|
||||
Coloque o `generate-dashboard.js` na **raiz do projeto** (mesma pasta do `db.cjs`) e rode:
|
||||
|
||||
```bash
|
||||
# Usa o backup mais recente automaticamente
|
||||
node generate-dashboard.js
|
||||
|
||||
# Ou especifica uma data
|
||||
node generate-dashboard.js 2026-03-27
|
||||
```
|
||||
|
||||
O arquivo `dashboard.html` será gerado na raiz do projeto. Basta abrir no browser.
|
||||
|
||||
---
|
||||
|
||||
## Fluxo recomendado
|
||||
|
||||
Sempre que fizer alterações no banco, rode os dois comandos em sequência:
|
||||
|
||||
```bash
|
||||
node db.cjs backup # gera o backup em database-novo/backups/YYYY-MM-DD/
|
||||
node generate-dashboard.js # lê o backup mais recente e gera o dashboard.html
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## O que o dashboard mostra
|
||||
|
||||
- **Visão geral** — cards com os 9 domínios do projeto, quantidade de tabelas e FKs por domínio
|
||||
- **Tabelas** — todas as 86 tabelas com colunas, tipos, badges PK/FK
|
||||
- **Foreign Keys** — cada FK aparece como link clicável que pula direto para a tabela destino
|
||||
- **Views** — lista das 24 views do schema público
|
||||
- **Busca** — busca em tempo real por nome de tabela ou nome de coluna
|
||||
- **Sidebar** — navegação por domínio
|
||||
|
||||
---
|
||||
|
||||
## Estrutura de pastas esperada
|
||||
|
||||
O script espera essa estrutura para funcionar:
|
||||
|
||||
```
|
||||
raiz-do-projeto/
|
||||
├── db.cjs
|
||||
├── db.config.json
|
||||
├── generate-dashboard.js ← script
|
||||
├── dashboard.html ← gerado aqui
|
||||
└── database-novo/
|
||||
└── backups/
|
||||
└── 2026-03-27/
|
||||
├── schema.sql ← lido pelo script
|
||||
├── data.sql
|
||||
└── full_dump.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tabelas novas não aparecem no domínio certo?
|
||||
|
||||
Quando você criar uma migration nova com uma tabela nova, ela aparecerá no dashboard na seção **"Outros"** e o script vai avisar no terminal:
|
||||
|
||||
```
|
||||
⚠ Tabelas novas sem domínio definido (aparecerão em "Outros"):
|
||||
- minha_tabela_nova
|
||||
→ Edite DOMAIN_TABLES no script para mapeá-las.
|
||||
```
|
||||
|
||||
Para corrigir, abra o `generate-dashboard.js` e adicione a tabela no domínio correto dentro do objeto `DOMAIN_TABLES` no topo do arquivo:
|
||||
|
||||
```js
|
||||
const DOMAIN_TABLES = {
|
||||
'Agenda': [
|
||||
'agenda_eventos',
|
||||
'agenda_configuracoes',
|
||||
// ...
|
||||
'minha_tabela_nova', // ← adiciona aqui
|
||||
],
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
Depois rode `node generate-dashboard.js` novamente.
|
||||
|
||||
---
|
||||
|
||||
## Requisitos
|
||||
|
||||
- Node.js instalado (qualquer versão >= 14)
|
||||
- Sem dependências externas — usa apenas módulos nativos (`fs`, `path`)
|
||||
489
database-novo/agenciapsi-db-dashboard.html
Normal file
489
database-novo/agenciapsi-db-dashboard.html
Normal file
File diff suppressed because one or more lines are too long
1651
database-novo/backups/2026-03-27/data.sql
Normal file
1651
database-novo/backups/2026-03-27/data.sql
Normal file
File diff suppressed because it is too large
Load Diff
21202
database-novo/backups/2026-03-27/full_dump.sql
Normal file
21202
database-novo/backups/2026-03-27/full_dump.sql
Normal file
File diff suppressed because it is too large
Load Diff
19202
database-novo/backups/2026-03-27/schema.sql
Normal file
19202
database-novo/backups/2026-03-27/schema.sql
Normal file
File diff suppressed because it is too large
Load Diff
1799
database-novo/backups/2026-03-29/data.sql
Normal file
1799
database-novo/backups/2026-03-29/data.sql
Normal file
File diff suppressed because it is too large
Load Diff
22428
database-novo/backups/2026-03-29/full_dump.sql
Normal file
22428
database-novo/backups/2026-03-29/full_dump.sql
Normal file
File diff suppressed because it is too large
Load Diff
20279
database-novo/backups/2026-03-29/schema.sql
Normal file
20279
database-novo/backups/2026-03-29/schema.sql
Normal file
File diff suppressed because it is too large
Load Diff
457
database-novo/generate-dashboard.cjs
Normal file
457
database-novo/generate-dashboard.cjs
Normal file
@@ -0,0 +1,457 @@
|
||||
#!/usr/bin/env node
|
||||
// =============================================================================
|
||||
// AgenciaPsi — Dashboard Generator
|
||||
// =============================================================================
|
||||
// Uso:
|
||||
// node generate-dashboard.js → usa backup mais recente
|
||||
// node generate-dashboard.js 2026-03-27 → usa backup de data específica
|
||||
//
|
||||
// Lê de: ./database-novo/backups/YYYY-MM-DD/schema.sql
|
||||
// Gera: ./dashboard.html (na mesma pasta do script)
|
||||
// =============================================================================
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const BACKUPS_DIR = path.join(__dirname, 'backups');
|
||||
const OUTPUT_FILE = path.join(__dirname, 'dashboard.html');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cores por domínio
|
||||
// ---------------------------------------------------------------------------
|
||||
const DOMAIN_COLORS = {
|
||||
'SaaS / Planos': '#4f8cff',
|
||||
'Tenants / Multi-tenant': '#6ee7b7',
|
||||
'Pacientes': '#f472b6',
|
||||
'Agenda': '#fb923c',
|
||||
'Financeiro': '#a78bfa',
|
||||
'Serviços / Commitments': '#34d399',
|
||||
'Notificações': '#38bdf8',
|
||||
'Email / Comunicação': '#fbbf24',
|
||||
'SaaS Admin': '#94a3b8',
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mapeamento domínio → tabelas
|
||||
// Adicione novas tabelas aqui quando criar migrations
|
||||
// ---------------------------------------------------------------------------
|
||||
const DOMAIN_TABLES = {
|
||||
'SaaS / Planos': [
|
||||
'plans','plan_features','plan_prices','plan_public','plan_public_bullets',
|
||||
'features','modules','module_features','subscriptions','subscription_events',
|
||||
'subscription_intents_personal','subscription_intents_tenant','subscription_intents_legacy',
|
||||
'addon_products','addon_credits','addon_transactions',
|
||||
'tenant_features','tenant_modules','entitlements_invalidation','tenant_feature_exceptions_log',
|
||||
],
|
||||
'Tenants / Multi-tenant': [
|
||||
'tenants','tenant_members','tenant_invites','owner_users',
|
||||
'profiles','company_profiles','billing_contracts','payment_settings','support_sessions',
|
||||
],
|
||||
'Pacientes': [
|
||||
'patients','patient_groups','patient_group_patient','patient_tags',
|
||||
'patient_patient_tag','patient_discounts','patient_invites','patient_intake_requests',
|
||||
],
|
||||
'Agenda': [
|
||||
'agenda_eventos','agenda_configuracoes','agenda_bloqueios','agenda_excecoes',
|
||||
'agenda_online_slots','agenda_regras_semanais','agenda_slots_bloqueados_semanais',
|
||||
'agenda_slots_regras','agendador_configuracoes','agendador_solicitacoes',
|
||||
'feriados','recurrence_rules','recurrence_exceptions','recurrence_rule_services',
|
||||
],
|
||||
'Financeiro': [
|
||||
'financial_records','financial_categories','financial_exceptions',
|
||||
'therapist_payouts','therapist_payout_records',
|
||||
'insurance_plans','insurance_plan_services','professional_pricing',
|
||||
],
|
||||
'Serviços / Commitments': [
|
||||
'services','determined_commitments','determined_commitment_fields',
|
||||
'commitment_services','commitment_time_logs',
|
||||
],
|
||||
'Notificações': [
|
||||
'notifications','notification_channels','notification_templates',
|
||||
'notification_queue','notification_logs','notification_preferences',
|
||||
'notification_schedules','twilio_subaccount_usage',
|
||||
],
|
||||
'Email / Comunicação': [
|
||||
'email_templates_global','email_templates_tenant','email_layout_config',
|
||||
'global_notices','notice_dismissals','login_carousel_slides',
|
||||
],
|
||||
'SaaS Admin': [
|
||||
'saas_admins','saas_docs','saas_doc_votos','saas_faq','saas_faq_itens',
|
||||
'user_settings','dev_user_credentials','_db_migrations',
|
||||
],
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1. Resolve qual schema.sql usar
|
||||
// ---------------------------------------------------------------------------
|
||||
function resolveSchema() {
|
||||
const arg = process.argv[2];
|
||||
|
||||
if (!fs.existsSync(BACKUPS_DIR)) {
|
||||
console.error(`✖ Pasta não encontrada: ${BACKUPS_DIR}`);
|
||||
console.error(` Certifique-se que o script está na raiz do projeto.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const available = fs.readdirSync(BACKUPS_DIR)
|
||||
.filter(f => /^\d{4}-\d{2}-\d{2}$/.test(f))
|
||||
.sort()
|
||||
.reverse();
|
||||
|
||||
if (available.length === 0) {
|
||||
console.error('✖ Nenhum backup encontrado em database-novo/backups/');
|
||||
console.error(' Rode primeiro: node db.cjs backup');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const date = (arg && /^\d{4}-\d{2}-\d{2}$/.test(arg)) ? arg : available[0];
|
||||
|
||||
if (!available.includes(date)) {
|
||||
console.error(`✖ Backup não encontrado para: ${date}`);
|
||||
console.error(` Disponíveis: ${available.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const schemaPath = path.join(BACKUPS_DIR, date, 'schema.sql');
|
||||
if (!fs.existsSync(schemaPath)) {
|
||||
console.error(`✖ schema.sql não encontrado em database-novo/backups/${date}/`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return { schemaPath, date, available };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2. Parse do schema.sql — extrai tabelas, colunas e FKs
|
||||
// ---------------------------------------------------------------------------
|
||||
function parseSchema(content) {
|
||||
const tables = {};
|
||||
|
||||
// Tabelas public.*
|
||||
const tableRe = /CREATE TABLE (public\.\S+)\s*\(([\s\S]*?)\);/gm;
|
||||
let m;
|
||||
while ((m = tableRe.exec(content)) !== null) {
|
||||
const name = m[1].replace('public.', '');
|
||||
const body = m[2];
|
||||
const columns = [];
|
||||
|
||||
for (let line of body.split('\n')) {
|
||||
line = line.trim().replace(/,$/, '');
|
||||
if (!line || line.startsWith('--')) continue;
|
||||
if (/^(CONSTRAINT|PRIMARY KEY|UNIQUE|CHECK|FOREIGN KEY|EXCLUDE)/i.test(line)) continue;
|
||||
|
||||
const col = line.match(
|
||||
/^(\w+)\s+([\w\[\]"()\s,]+?)(?:\s+DEFAULT\s+|\s+NOT NULL|\s+NULL|\s+GENERATED|\s+REFERENCES\s|$)/
|
||||
);
|
||||
if (col) {
|
||||
columns.push({
|
||||
name: col[1],
|
||||
type: col[2].trim().split('(')[0].trim(),
|
||||
pk: col[1] === 'id',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
tables[name] = { columns, fks: [] };
|
||||
}
|
||||
|
||||
// FKs via ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY
|
||||
const fkRe = /ALTER TABLE ONLY public\.(\w+)\s+ADD CONSTRAINT \S+ FOREIGN KEY \((\w+)\) REFERENCES public\.(\w+)\((\w+)\)/gm;
|
||||
while ((m = fkRe.exec(content)) !== null) {
|
||||
const [, fromTable, fromCol, toTable, toCol] = m;
|
||||
if (tables[fromTable]) {
|
||||
tables[fromTable].fks.push({ from_col: fromCol, to_table: toTable, to_col: toCol });
|
||||
}
|
||||
}
|
||||
|
||||
// Views
|
||||
const viewRe = /CREATE(?:\s+OR REPLACE)?\s+VIEW\s+public\.(\S+)\s+AS/gm;
|
||||
const views = [];
|
||||
while ((m = viewRe.exec(content)) !== null) views.push(m[1]);
|
||||
|
||||
return { tables, views };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3. Monta os domínios
|
||||
// Tabelas novas que ainda não estão mapeadas vão para "Outros"
|
||||
// ---------------------------------------------------------------------------
|
||||
function buildDomains(tables) {
|
||||
const mapped = new Set(Object.values(DOMAIN_TABLES).flat());
|
||||
const others = Object.keys(tables).filter(t => !mapped.has(t));
|
||||
|
||||
const domains = {};
|
||||
for (const [domain, list] of Object.entries(DOMAIN_TABLES)) {
|
||||
const present = list.filter(t => tables[t]);
|
||||
if (present.length > 0) domains[domain] = present;
|
||||
}
|
||||
if (others.length > 0) {
|
||||
domains['Outros'] = others;
|
||||
DOMAIN_COLORS['Outros'] = '#6b7280';
|
||||
}
|
||||
|
||||
return domains;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 4. Gera o HTML final (standalone, sem dependências externas de JS)
|
||||
// ---------------------------------------------------------------------------
|
||||
function generateHTML(tables, views, domains, date, available) {
|
||||
const totalFKs = Object.values(tables).reduce((a, t) => a + t.fks.length, 0);
|
||||
const totalCols = Object.values(tables).reduce((a, t) => a + t.columns.length, 0);
|
||||
const generated = new Date().toLocaleString('pt-BR');
|
||||
|
||||
// Serializa dados para embutir no HTML
|
||||
const jsonData = JSON.stringify({ tables, views, domains });
|
||||
const jsonColors = JSON.stringify(DOMAIN_COLORS);
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AgenciaPsi DB · ${date}</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
|
||||
:root{--bg:#0b0d12;--bg2:#111520;--bg3:#181e2d;--border:#1e2740;--border2:#263050;--text:#e2e8f8;--text2:#7d8fb3;--text3:#4a5a80;--accent:#4f8cff;--accent2:#6ee7b7;--pk:#fbbf24;--fk:#f472b6}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{background:var(--bg);color:var(--text);font-family:'Space Grotesk',sans-serif;min-height:100vh;overflow-x:hidden}
|
||||
|
||||
.topbar{position:sticky;top:0;z-index:100;background:rgba(11,13,18,.94);backdrop-filter:blur(12px);border-bottom:1px solid var(--border);padding:0 28px;height:56px;display:flex;align-items:center;gap:20px}
|
||||
.brand{font-weight:700;font-size:15px;letter-spacing:-.3px}.brand span{color:var(--accent)}
|
||||
.gen{font-size:11px;color:var(--text3);font-family:'IBM Plex Mono',monospace}
|
||||
.pills{display:flex;gap:10px;margin-left:auto}
|
||||
.pill{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text2);background:var(--bg3);border:1px solid var(--border);border-radius:20px;padding:4px 12px}
|
||||
.pill strong{color:var(--text);font-size:13px}
|
||||
.search{background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:6px 12px;color:var(--text);font-family:'Space Grotesk',sans-serif;font-size:13px;outline:none;width:200px;transition:border-color .2s,width .2s}
|
||||
.search:focus{border-color:var(--accent);width:280px}
|
||||
.search::placeholder{color:var(--text3)}
|
||||
|
||||
.layout{display:flex;height:calc(100vh - 56px)}
|
||||
|
||||
.sidebar{width:240px;flex-shrink:0;background:var(--bg2);border-right:1px solid var(--border);overflow-y:auto;padding:16px 0}
|
||||
.sidebar::-webkit-scrollbar{width:4px}.sidebar::-webkit-scrollbar-thumb{background:var(--border2);border-radius:2px}
|
||||
.sb-h{font-size:10px;font-weight:600;letter-spacing:1px;text-transform:uppercase;color:var(--text3);padding:8px 20px 4px}
|
||||
.sb-i{display:flex;align-items:center;gap:10px;padding:7px 20px;cursor:pointer;font-size:13px;color:var(--text2);border-left:2px solid transparent;transition:all .15s;user-select:none}
|
||||
.sb-i:hover{color:var(--text);background:var(--bg3)}
|
||||
.sb-i.active{color:var(--text);border-left-color:var(--accent);background:rgba(79,140,255,.08)}
|
||||
.sb-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
||||
.sb-c{margin-left:auto;font-size:11px;color:var(--text3);font-family:'IBM Plex Mono',monospace}
|
||||
|
||||
.main{flex:1;overflow-y:auto}
|
||||
.main::-webkit-scrollbar{width:5px}.main::-webkit-scrollbar-thumb{background:var(--border2);border-radius:2px}
|
||||
|
||||
.overview{padding:32px 36px;border-bottom:1px solid var(--border)}
|
||||
.ov-t{font-size:22px;font-weight:700;margin-bottom:6px}
|
||||
.ov-s{font-size:14px;color:var(--text2);margin-bottom:28px}
|
||||
.dgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:14px}
|
||||
.dc{background:var(--bg3);border:1px solid var(--border);border-radius:12px;padding:16px 18px;cursor:pointer;transition:all .2s;position:relative;overflow:hidden}
|
||||
.dc::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:var(--c)}
|
||||
.dc:hover{border-color:var(--border2);transform:translateY(-1px)}
|
||||
.dc-n{font-size:14px;font-weight:600;margin-bottom:6px}
|
||||
.dc-m{font-size:12px;color:var(--text2);font-family:'IBM Plex Mono',monospace}
|
||||
.dc-m span{font-weight:600}
|
||||
|
||||
.section{padding:28px 36px}
|
||||
.sec-h{display:flex;align-items:center;gap:14px;margin-bottom:20px}
|
||||
.sec-t{font-size:18px;font-weight:700}
|
||||
.sec-b{font-size:11px;font-family:'IBM Plex Mono',monospace;background:var(--bg3);border:1px solid var(--border);border-radius:20px;padding:3px 10px;color:var(--text2)}
|
||||
|
||||
.tgrid{display:flex;flex-direction:column;gap:10px}
|
||||
.tc{background:var(--bg3);border:1px solid var(--border);border-radius:10px;overflow:hidden;transition:border-color .15s}
|
||||
.tc:hover{border-color:var(--border2)}.tc.hl{border-color:var(--accent)}
|
||||
.tc-h{display:flex;align-items:center;gap:12px;padding:12px 16px;cursor:pointer;user-select:none}
|
||||
.tc-n{font-family:'IBM Plex Mono',monospace;font-size:13px;font-weight:600}
|
||||
.tc-m{font-size:11px;color:var(--text3);font-family:'IBM Plex Mono',monospace}
|
||||
.tc-f{font-size:11px;color:var(--fk);font-family:'IBM Plex Mono',monospace;margin-left:4px}
|
||||
.tc-tg{margin-left:auto;color:var(--text3);font-size:11px;transition:transform .2s}
|
||||
.tc-tg.open{transform:rotate(180deg)}
|
||||
.tc-b{display:none;border-top:1px solid var(--border)}.tc-b.open{display:block}
|
||||
.cols{padding:6px 0}
|
||||
.cr{display:flex;align-items:center;gap:10px;padding:5px 16px;font-size:12px;font-family:'IBM Plex Mono',monospace;color:var(--text2)}
|
||||
.cr:hover{background:rgba(255,255,255,.02)}
|
||||
.bdg{font-size:9px;font-weight:700;letter-spacing:.5px;padding:1px 5px;border-radius:3px;width:26px;text-align:center;flex-shrink:0}
|
||||
.bdg.pk{background:rgba(251,191,36,.15);color:var(--pk)}.bdg.fk{background:rgba(244,114,182,.15);color:var(--fk)}.bdg.x{background:transparent}
|
||||
.cn{color:var(--text)}.ct{color:var(--text3);margin-left:auto;font-size:11px}
|
||||
.fksec{border-top:1px solid var(--border);padding:10px 16px}
|
||||
.fkt{font-size:10px;font-weight:600;letter-spacing:1px;color:var(--text3);text-transform:uppercase;margin-bottom:8px}
|
||||
.fkr{display:flex;align-items:center;gap:8px;font-size:12px;font-family:'IBM Plex Mono',monospace;color:var(--text2);padding:3px 0}
|
||||
.fka{color:var(--fk)}.fkl{color:var(--accent);cursor:pointer}.fkl:hover{text-decoration:underline}
|
||||
|
||||
.vsec{padding:0 36px 32px}
|
||||
.vgrid{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}
|
||||
.vc{background:rgba(110,231,183,.08);border:1px solid rgba(110,231,183,.2);border-radius:6px;padding:5px 12px;font-size:12px;font-family:'IBM Plex Mono',monospace;color:var(--accent2)}
|
||||
.empty{padding:40px;text-align:center;color:var(--text3);font-size:14px}
|
||||
mark{background:rgba(79,140,255,.3);color:#fff;border-radius:2px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="topbar">
|
||||
<div class="brand">Agência<span>Psi</span> DB</div>
|
||||
<span class="gen">${date} · ${generated}</span>
|
||||
<input class="search" id="si" placeholder="Buscar tabela ou coluna..." oninput="search(this.value)">
|
||||
<div class="pills">
|
||||
<div class="pill"><strong>${Object.keys(tables).length}</strong> tabelas</div>
|
||||
<div class="pill"><strong>${totalFKs}</strong> FKs</div>
|
||||
<div class="pill"><strong>${views.length}</strong> views</div>
|
||||
<div class="pill"><strong>${totalCols}</strong> colunas</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout">
|
||||
<nav class="sidebar" id="sb"></nav>
|
||||
<main class="main" id="mn"></main>
|
||||
</div>
|
||||
<script>
|
||||
const D=${jsonData};
|
||||
const C=${jsonColors};
|
||||
const T2D={};
|
||||
Object.entries(D.domains).forEach(([d,ts])=>ts.forEach(t=>T2D[t]=d));
|
||||
let dom=null,q='';
|
||||
function gc(d){return C[d]||'#6b7280';}
|
||||
|
||||
function buildSB(){
|
||||
let h=\`<div class="sb-h">Visão Geral</div>
|
||||
<div class="sb-i \${!dom?'active':''}" onclick="sel(null)">
|
||||
<div class="sb-dot" style="background:#4f8cff"></div>Todos
|
||||
<span class="sb-c">\${Object.keys(D.tables).length}</span>
|
||||
</div>
|
||||
<div class="sb-h" style="margin-top:8px">Domínios</div>\`;
|
||||
for(const[d,ts]of Object.entries(D.domains)){
|
||||
h+=\`<div class="sb-i \${dom===d?'active':''}" onclick="sel(\`+JSON.stringify(d)+\`)">
|
||||
<div class="sb-dot" style="background:\${gc(d)}"></div>\${d}
|
||||
<span class="sb-c">\${ts.length}</span>
|
||||
</div>\`;
|
||||
}
|
||||
document.getElementById('sb').innerHTML=h;
|
||||
}
|
||||
|
||||
function buildMN(){
|
||||
const mn=document.getElementById('mn');
|
||||
let h='';
|
||||
if(q){
|
||||
const matches=Object.entries(D.tables).filter(([n,t])=>n.includes(q)||t.columns.some(c=>c.name.includes(q)));
|
||||
h+=\`<div class="section"><div class="sec-h"><div class="sec-t">"\${q}"</div><div class="sec-b">\${matches.length} tabelas</div></div><div class="tgrid">\`;
|
||||
h+=matches.length?matches.map(([n,t])=>card(n,t,q)).join(''):'<div class="empty">Nenhum resultado.</div>';
|
||||
h+='</div></div>';
|
||||
} else {
|
||||
const ds=dom?{[dom]:D.domains[dom]}:D.domains;
|
||||
if(!dom){
|
||||
h+=\`<div class="overview"><div class="ov-t">AgenciaPsi — Banco de Dados</div>
|
||||
<div class="ov-s">Schema público · \${Object.keys(D.tables).length} tabelas · \${Object.values(D.tables).reduce((a,t)=>a+t.fks.length,0)} FKs · \${D.views.length} views</div>
|
||||
<div class="dgrid">\`;
|
||||
for(const[d,ts]of Object.entries(D.domains)){
|
||||
const fks=ts.reduce((a,t)=>a+(D.tables[t]?.fks?.length||0),0);
|
||||
h+=\`<div class="dc" style="--c:\${gc(d)}" onclick="sel(\`+JSON.stringify(d)+\`)">
|
||||
<div class="dc-n">\${d}</div>
|
||||
<div class="dc-m"><span style="color:\${gc(d)}">\${ts.length}</span> tabelas · \${fks} FKs</div>
|
||||
</div>\`;
|
||||
}
|
||||
h+='</div></div>';
|
||||
}
|
||||
for(const[d,ts]of Object.entries(ds)){
|
||||
h+=\`<div class="section"><div class="sec-h">
|
||||
<div class="sec-t" style="color:\${gc(d)}">\${d}</div>
|
||||
<div class="sec-b">\${ts.length} tabelas</div>
|
||||
</div><div class="tgrid">\`;
|
||||
ts.forEach(n=>{if(D.tables[n])h+=card(n,D.tables[n],'');});
|
||||
h+='</div></div>';
|
||||
}
|
||||
if(!dom){
|
||||
h+=\`<div class="vsec"><div class="sec-h">
|
||||
<div class="sec-t" style="color:#6ee7b7">Views</div>
|
||||
<div class="sec-b">\${D.views.length}</div>
|
||||
</div><div class="vgrid">\${D.views.map(v=>\`<div class="vc">\${v}</div>\`).join('')}</div></div>\`;
|
||||
}
|
||||
}
|
||||
mn.innerHTML=h;
|
||||
}
|
||||
|
||||
function card(name,t,hl){
|
||||
const fkCols=new Set(t.fks.map(f=>f.from_col));
|
||||
const c=gc(T2D[name]);
|
||||
const cols=t.columns.map(col=>{
|
||||
let n=col.name;
|
||||
if(hl&&n.includes(hl))n=n.replace(new RegExp(\`(\${hl})\`,'gi'),'<mark>$1</mark>');
|
||||
const b=col.pk?'pk':fkCols.has(col.name)?'fk':'x';
|
||||
const l=col.pk?'PK':fkCols.has(col.name)?'FK':'';
|
||||
return \`<div class="cr"><span class="bdg \${b}">\${l}</span><span class="cn">\${n}</span><span class="ct">\${col.type}</span></div>\`;
|
||||
}).join('');
|
||||
const fks=t.fks.length?\`<div class="fksec"><div class="fkt">Foreign Keys</div>\${
|
||||
t.fks.map(f=>\`<div class="fkr"><span>\${f.from_col}</span><span class="fka">→</span><span class="fkl" onclick="jump('\${f.to_table}')">\${f.to_table}.\${f.to_col}</span></div>\`).join('')
|
||||
}</div>\`:'';
|
||||
return \`<div class="tc \${hl&&name.includes(hl)?'hl':''}" id="tc-\${name}">
|
||||
<div class="tc-h" onclick="tog('\${name}')">
|
||||
<div style="width:8px;height:8px;border-radius:50%;background:\${c};flex-shrink:0"></div>
|
||||
<div class="tc-n">\${name}</div>
|
||||
<span class="tc-m">\${t.columns.length} cols</span>
|
||||
\${t.fks.length?\`<span class="tc-f">\${t.fks.length} FK</span>\`:''}
|
||||
<span class="tc-tg" id="tg-\${name}">▼</span>
|
||||
</div>
|
||||
<div class="tc-b" id="bd-\${name}"><div class="cols">\${cols}</div>\${fks}</div>
|
||||
</div>\`;
|
||||
}
|
||||
|
||||
function tog(n){
|
||||
document.getElementById('bd-'+n)?.classList.toggle('open');
|
||||
document.getElementById('tg-'+n)?.classList.toggle('open');
|
||||
}
|
||||
function sel(d){
|
||||
dom=d;q='';document.getElementById('si').value='';
|
||||
buildSB();buildMN();document.getElementById('mn').scrollTop=0;
|
||||
}
|
||||
function jump(name){
|
||||
dom=T2D[name]||null;q='';document.getElementById('si').value='';
|
||||
buildSB();buildMN();
|
||||
setTimeout(()=>{
|
||||
const el=document.getElementById('tc-'+name);
|
||||
if(!el)return;
|
||||
el.scrollIntoView({behavior:'smooth',block:'center'});
|
||||
const bd=document.getElementById('bd-'+name);
|
||||
const tg=document.getElementById('tg-'+name);
|
||||
if(bd&&!bd.classList.contains('open')){bd.classList.add('open');tg?.classList.add('open');}
|
||||
el.style.borderColor='#4f8cff';
|
||||
setTimeout(()=>el.style.borderColor='',2000);
|
||||
},80);
|
||||
}
|
||||
let st;
|
||||
function search(v){
|
||||
clearTimeout(st);q=v.trim();
|
||||
st=setTimeout(()=>{dom=null;buildSB();buildMN();},200);
|
||||
}
|
||||
buildSB();buildMN();
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 5. Execução
|
||||
// ---------------------------------------------------------------------------
|
||||
console.log('\n═══ AgenciaPsi — Dashboard Generator ═══\n');
|
||||
|
||||
const { schemaPath, date, available } = resolveSchema();
|
||||
console.log(` → Schema: ${schemaPath}`);
|
||||
if (available.length > 1) console.log(` → Outros backups: ${available.slice(1).join(', ')}`);
|
||||
|
||||
const content = fs.readFileSync(schemaPath, 'utf8');
|
||||
console.log(` → Lendo schema... (${(content.length / 1024).toFixed(0)} KB)`);
|
||||
|
||||
const { tables, views } = parseSchema(content);
|
||||
const domains = buildDomains(tables);
|
||||
const totalFKs = Object.values(tables).reduce((a, t) => a + t.fks.length, 0);
|
||||
|
||||
console.log(` → ${Object.keys(tables).length} tabelas · ${totalFKs} FKs · ${views.length} views`);
|
||||
|
||||
// Avisa sobre tabelas novas não mapeadas
|
||||
if (domains['Outros']) {
|
||||
console.log(`\n ⚠ Tabelas novas sem domínio definido (aparecerão em "Outros"):`);
|
||||
domains['Outros'].forEach(t => console.log(` - ${t}`));
|
||||
console.log(` → Edite DOMAIN_TABLES no script para mapeá-las.\n`);
|
||||
}
|
||||
|
||||
const html = generateHTML(tables, views, domains, date, available);
|
||||
fs.writeFileSync(OUTPUT_FILE, html, 'utf8');
|
||||
|
||||
console.log(`\n✔ Gerado: ${OUTPUT_FILE}`);
|
||||
console.log(` Tamanho: ${(fs.statSync(OUTPUT_FILE).size / 1024).toFixed(0)} KB`);
|
||||
console.log(` Abra no browser: file://${OUTPUT_FILE}\n`);
|
||||
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
|
||||
-- =============================================================================
|
||||
@@ -60,7 +60,14 @@ VALUES
|
||||
-- ── Branding / API / Auditoria ──
|
||||
('f393178c-284d-422f-b096-8793f85428d5', 'custom_branding', 'Custom branding', '2026-03-01 09:59:15.432733+00', 'Personalização de marca', 'Marca Personalizada'),
|
||||
('d6f54674-ea8b-484b-af0e-99127a510da2', 'api_access', 'API/Integrations access', '2026-03-01 09:59:15.432733+00', 'Integrações/API', 'Acesso à API'),
|
||||
('a5593d96-dd95-46bb-bef0-bd379b56ad50', 'audit_log', 'Audit log capability', '2026-03-01 09:59:15.432733+00', 'Auditoria', 'Log de Auditoria')
|
||||
('a5593d96-dd95-46bb-bef0-bd379b56ad50', 'audit_log', 'Audit log capability', '2026-03-01 09:59:15.432733+00', 'Auditoria', 'Log de Auditoria'),
|
||||
|
||||
-- ── Documentos ──
|
||||
('b1a2c3d4-1111-4aaa-bbbb-000000000001', 'documents.upload', 'Upload e gestão de arquivos do paciente', '2026-03-29 00:00:00.000000+00', 'Upload de documentos', 'Upload de Documentos'),
|
||||
('b1a2c3d4-1111-4aaa-bbbb-000000000002', 'documents.templates', 'Geração de documentos a partir de templates', '2026-03-29 00:00:00.000000+00', 'Templates de documentos', 'Templates de Documentos'),
|
||||
('b1a2c3d4-1111-4aaa-bbbb-000000000003', 'documents.signatures', 'Assinatura eletrônica de documentos', '2026-03-29 00:00:00.000000+00', 'Assinaturas eletrônicas', 'Assinaturas Eletrônicas'),
|
||||
('b1a2c3d4-1111-4aaa-bbbb-000000000004', 'documents.share_links', 'Links temporários para compartilhamento de documentos', '2026-03-29 00:00:00.000000+00', 'Links de compartilhamento', 'Links de Compartilhamento'),
|
||||
('b1a2c3d4-1111-4aaa-bbbb-000000000005', 'documents.patient_portal', 'Acesso a documentos pelo portal do paciente', '2026-03-29 00:00:00.000000+00', 'Portal do paciente (documentos)', 'Portal do Paciente (Documentos)')
|
||||
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
key = EXCLUDED.key,
|
||||
@@ -71,7 +78,7 @@ ON CONFLICT (id) DO UPDATE SET
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE 'seed_011_features: 26 features inseridas/atualizadas.';
|
||||
RAISE NOTICE 'seed_011_features: 31 features inseridas/atualizadas.';
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
@@ -52,7 +52,13 @@ INSERT INTO public.plan_features (plan_id, feature_id, enabled, limits) VALUES
|
||||
-- PRO exclusivo
|
||||
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', 'f393178c-284d-422f-b096-8793f85428d5', true, NULL), -- custom_branding
|
||||
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', 'd6f54674-ea8b-484b-af0e-99127a510da2', true, NULL), -- api_access
|
||||
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', 'a5593d96-dd95-46bb-bef0-bd379b56ad50', true, NULL); -- audit_log
|
||||
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', 'a5593d96-dd95-46bb-bef0-bd379b56ad50', true, NULL), -- audit_log
|
||||
-- Documentos
|
||||
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', 'b1a2c3d4-1111-4aaa-bbbb-000000000001', true, '{"max_storage_mb": 5000}'), -- documents.upload
|
||||
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', 'b1a2c3d4-1111-4aaa-bbbb-000000000002', true, NULL), -- documents.templates
|
||||
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', 'b1a2c3d4-1111-4aaa-bbbb-000000000003', true, NULL), -- documents.signatures
|
||||
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', 'b1a2c3d4-1111-4aaa-bbbb-000000000004', true, NULL), -- documents.share_links
|
||||
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', 'b1a2c3d4-1111-4aaa-bbbb-000000000005', true, NULL); -- documents.patient_portal
|
||||
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════
|
||||
@@ -88,7 +94,13 @@ INSERT INTO public.plan_features (plan_id, feature_id, enabled, limits) VALUES
|
||||
('01a5867f-0705-4714-ac97-a23470949157', '74fc1321-4d17-49c3-b72e-db3a7f4be451', false, NULL), -- rooms (PRO)
|
||||
('01a5867f-0705-4714-ac97-a23470949157', 'f393178c-284d-422f-b096-8793f85428d5', false, NULL), -- custom_branding (PRO)
|
||||
('01a5867f-0705-4714-ac97-a23470949157', 'd6f54674-ea8b-484b-af0e-99127a510da2', false, NULL), -- api_access (PRO)
|
||||
('01a5867f-0705-4714-ac97-a23470949157', 'a5593d96-dd95-46bb-bef0-bd379b56ad50', false, NULL); -- audit_log (PRO)
|
||||
('01a5867f-0705-4714-ac97-a23470949157', 'a5593d96-dd95-46bb-bef0-bd379b56ad50', false, NULL), -- audit_log (PRO)
|
||||
-- Documentos
|
||||
('01a5867f-0705-4714-ac97-a23470949157', 'b1a2c3d4-1111-4aaa-bbbb-000000000001', true, '{"max_storage_mb": 500}'), -- documents.upload (FREE)
|
||||
('01a5867f-0705-4714-ac97-a23470949157', 'b1a2c3d4-1111-4aaa-bbbb-000000000002', false, NULL), -- documents.templates (PRO)
|
||||
('01a5867f-0705-4714-ac97-a23470949157', 'b1a2c3d4-1111-4aaa-bbbb-000000000003', false, NULL), -- documents.signatures (PRO)
|
||||
('01a5867f-0705-4714-ac97-a23470949157', 'b1a2c3d4-1111-4aaa-bbbb-000000000004', false, NULL), -- documents.share_links (PRO)
|
||||
('01a5867f-0705-4714-ac97-a23470949157', 'b1a2c3d4-1111-4aaa-bbbb-000000000005', false, NULL); -- documents.patient_portal (PRO)
|
||||
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════
|
||||
@@ -122,7 +134,13 @@ INSERT INTO public.plan_features (plan_id, feature_id, enabled, limits) VALUES
|
||||
-- PRO exclusivo
|
||||
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', 'f393178c-284d-422f-b096-8793f85428d5', true, NULL), -- custom_branding
|
||||
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', 'd6f54674-ea8b-484b-af0e-99127a510da2', true, NULL), -- api_access
|
||||
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', 'a5593d96-dd95-46bb-bef0-bd379b56ad50', true, NULL); -- audit_log
|
||||
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', 'a5593d96-dd95-46bb-bef0-bd379b56ad50', true, NULL), -- audit_log
|
||||
-- Documentos
|
||||
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', 'b1a2c3d4-1111-4aaa-bbbb-000000000001', true, '{"max_storage_mb": 5000}'), -- documents.upload
|
||||
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', 'b1a2c3d4-1111-4aaa-bbbb-000000000002', true, NULL), -- documents.templates
|
||||
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', 'b1a2c3d4-1111-4aaa-bbbb-000000000003', true, NULL), -- documents.signatures
|
||||
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', 'b1a2c3d4-1111-4aaa-bbbb-000000000004', true, NULL), -- documents.share_links
|
||||
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', 'b1a2c3d4-1111-4aaa-bbbb-000000000005', true, NULL); -- documents.patient_portal
|
||||
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════
|
||||
@@ -152,7 +170,13 @@ INSERT INTO public.plan_features (plan_id, feature_id, enabled, limits) VALUES
|
||||
-- PRO-only (desabilitado)
|
||||
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', 'f393178c-284d-422f-b096-8793f85428d5', false, NULL), -- custom_branding (PRO)
|
||||
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', 'd6f54674-ea8b-484b-af0e-99127a510da2', false, NULL), -- api_access (PRO)
|
||||
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', 'a5593d96-dd95-46bb-bef0-bd379b56ad50', false, NULL); -- audit_log (PRO)
|
||||
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', 'a5593d96-dd95-46bb-bef0-bd379b56ad50', false, NULL), -- audit_log (PRO)
|
||||
-- Documentos
|
||||
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', 'b1a2c3d4-1111-4aaa-bbbb-000000000001', true, '{"max_storage_mb": 500}'), -- documents.upload (FREE)
|
||||
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', 'b1a2c3d4-1111-4aaa-bbbb-000000000002', false, NULL), -- documents.templates (PRO)
|
||||
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', 'b1a2c3d4-1111-4aaa-bbbb-000000000003', false, NULL), -- documents.signatures (PRO)
|
||||
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', 'b1a2c3d4-1111-4aaa-bbbb-000000000004', false, NULL), -- documents.share_links (PRO)
|
||||
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', 'b1a2c3d4-1111-4aaa-bbbb-000000000005', false, NULL); -- documents.patient_portal (PRO)
|
||||
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════
|
||||
|
||||
230
database-novo/seeds/seed_015_document_templates.sql
Normal file
230
database-novo/seeds/seed_015_document_templates.sql
Normal file
@@ -0,0 +1,230 @@
|
||||
-- ==========================================================================
|
||||
-- Agencia PSI — Seed: Templates globais de documentos
|
||||
-- ==========================================================================
|
||||
-- 4 templates padrao do sistema (is_global = true).
|
||||
-- Disponiveis para todos os tenants como base para personalizacao.
|
||||
-- ==========================================================================
|
||||
|
||||
INSERT INTO public.document_templates (
|
||||
id, tenant_id, owner_id, nome_template, tipo, descricao,
|
||||
corpo_html, cabecalho_html, rodape_html,
|
||||
variaveis, is_global, ativo
|
||||
) VALUES
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 1. Declaração de Comparecimento
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
(
|
||||
gen_random_uuid(), NULL, NULL,
|
||||
'Declaração de Comparecimento',
|
||||
'declaracao_comparecimento',
|
||||
'Declara que o paciente compareceu à sessão na data e horário indicados.',
|
||||
E'<h2 style="text-align:center; margin-bottom:30px;">DECLARAÇÃO DE COMPARECIMENTO</h2>\n\n<p>Declaro, para os devidos fins, que <strong>{{paciente_nome}}</strong>, portador(a) do CPF nº <strong>{{paciente_cpf}}</strong>, compareceu a esta clínica/consultório no dia <strong>{{data_sessao}}</strong>, no horário das <strong>{{hora_inicio}}</strong> às <strong>{{hora_fim}}</strong>, para atendimento psicológico.</p>\n\n<p style="margin-top:40px;">{{cidade_estado}}, {{data_atual_extenso}}.</p>\n\n<div class="signature-line" style="margin-top:80px; text-align:center;">\n <hr style="width:300px; margin:0 auto 8px; border:none; border-top:1px solid #333;" />\n <strong>{{terapeuta_nome}}</strong><br/>\n Psicólogo(a) — CRP {{terapeuta_crp}}\n</div>',
|
||||
E'<div style="text-align:center;">\n <strong>{{clinica_nome}}</strong><br/>\n <span style="font-size:10pt; color:#666;">{{clinica_endereco}} · {{clinica_telefone}}</span>\n</div>',
|
||||
E'<div style="text-align:center; font-size:9pt; color:#999;">\n {{clinica_nome}} · {{clinica_endereco}} · {{clinica_telefone}}\n</div>',
|
||||
ARRAY['paciente_nome','paciente_cpf','data_sessao','hora_inicio','hora_fim','cidade_estado','data_atual_extenso','terapeuta_nome','terapeuta_crp','clinica_nome','clinica_endereco','clinica_telefone'],
|
||||
true, true
|
||||
),
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 2. Atestado Psicológico
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
(
|
||||
gen_random_uuid(), NULL, NULL,
|
||||
'Atestado Psicológico',
|
||||
'atestado_psicologico',
|
||||
'Atesta acompanhamento psicológico do paciente conforme Resolução CFP.',
|
||||
E'<h2 style="text-align:center; margin-bottom:30px;">ATESTADO PSICOLÓGICO</h2>\n\n<p>Atesto, para os devidos fins, que <strong>{{paciente_nome}}</strong>, nascido(a) em <strong>{{paciente_data_nascimento}}</strong>, encontra-se em acompanhamento psicológico neste consultório/clínica.</p>\n\n<p>O presente atestado é emitido com base no atendimento realizado, em conformidade com o Código de Ética Profissional do Psicólogo e as Resoluções do Conselho Federal de Psicologia.</p>\n\n<p style="margin-top:40px;">{{cidade_estado}}, {{data_atual_extenso}}.</p>\n\n<div class="signature-line" style="margin-top:80px; text-align:center;">\n <hr style="width:300px; margin:0 auto 8px; border:none; border-top:1px solid #333;" />\n <strong>{{terapeuta_nome}}</strong><br/>\n Psicólogo(a) — CRP {{terapeuta_crp}}\n</div>',
|
||||
E'<div style="text-align:center;">\n <strong>{{clinica_nome}}</strong><br/>\n <span style="font-size:10pt; color:#666;">{{clinica_endereco}}</span>\n</div>',
|
||||
E'<div style="text-align:center; font-size:9pt; color:#999;">\n {{clinica_nome}} · {{clinica_endereco}} · {{clinica_telefone}}\n</div>',
|
||||
ARRAY['paciente_nome','paciente_data_nascimento','cidade_estado','data_atual_extenso','terapeuta_nome','terapeuta_crp','clinica_nome','clinica_endereco','clinica_telefone'],
|
||||
true, true
|
||||
),
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 3. Relatório de Acompanhamento
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
(
|
||||
gen_random_uuid(), NULL, NULL,
|
||||
'Relatório de Acompanhamento',
|
||||
'relatorio_acompanhamento',
|
||||
'Modelo base para relatório de acompanhamento psicológico.',
|
||||
E'<h2 style="text-align:center; margin-bottom:30px;">RELATÓRIO DE ACOMPANHAMENTO PSICOLÓGICO</h2>\n\n<table style="width:100%; border-collapse:collapse; margin-bottom:20px;">\n <tr><td style="padding:4px 8px; font-weight:bold; width:180px;">Paciente:</td><td style="padding:4px 8px;">{{paciente_nome}}</td></tr>\n <tr><td style="padding:4px 8px; font-weight:bold;">Data de nascimento:</td><td style="padding:4px 8px;">{{paciente_data_nascimento}}</td></tr>\n <tr><td style="padding:4px 8px; font-weight:bold;">CPF:</td><td style="padding:4px 8px;">{{paciente_cpf}}</td></tr>\n <tr><td style="padding:4px 8px; font-weight:bold;">Profissional:</td><td style="padding:4px 8px;">{{terapeuta_nome}} — CRP {{terapeuta_crp}}</td></tr>\n <tr><td style="padding:4px 8px; font-weight:bold;">Data do relatório:</td><td style="padding:4px 8px;">{{data_atual}}</td></tr>\n</table>\n\n<h3>1. Demanda inicial</h3>\n<p>[Descreva a queixa ou demanda que motivou o início do acompanhamento.]</p>\n\n<h3>2. Procedimentos utilizados</h3>\n<p>[Descreva os métodos, técnicas e instrumentos utilizados.]</p>\n\n<h3>3. Evolução observada</h3>\n<p>[Descreva a evolução do paciente ao longo do acompanhamento.]</p>\n\n<h3>4. Considerações finais</h3>\n<p>[Conclusões e recomendações.]</p>\n\n<p style="margin-top:40px;">{{cidade_estado}}, {{data_atual_extenso}}.</p>\n\n<div class="signature-line" style="margin-top:80px; text-align:center;">\n <hr style="width:300px; margin:0 auto 8px; border:none; border-top:1px solid #333;" />\n <strong>{{terapeuta_nome}}</strong><br/>\n Psicólogo(a) — CRP {{terapeuta_crp}}\n</div>',
|
||||
E'<div style="text-align:center;">\n <strong>{{clinica_nome}}</strong><br/>\n <span style="font-size:10pt; color:#666;">{{clinica_endereco}} · {{clinica_telefone}}</span>\n</div>',
|
||||
E'<div style="text-align:center; font-size:9pt; color:#999;">\n Este documento é de caráter sigiloso, conforme Art. 9º do Código de Ética do Psicólogo.\n</div>',
|
||||
ARRAY['paciente_nome','paciente_data_nascimento','paciente_cpf','terapeuta_nome','terapeuta_crp','data_atual','data_atual_extenso','cidade_estado','clinica_nome','clinica_endereco','clinica_telefone'],
|
||||
true, true
|
||||
),
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 4. Recibo de Pagamento
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
(
|
||||
gen_random_uuid(), NULL, NULL,
|
||||
'Recibo de Pagamento',
|
||||
'recibo_pagamento',
|
||||
'Recibo para pagamento de sessão de atendimento psicológico.',
|
||||
E'<h2 style="text-align:center; margin-bottom:30px;">RECIBO DE PAGAMENTO</h2>\n\n<p>Recebi de <strong>{{paciente_nome}}</strong>, CPF nº <strong>{{paciente_cpf}}</strong>, a quantia de <strong>{{valor}}</strong> ({{valor_extenso}}), referente a sessão de atendimento psicológico realizada em <strong>{{data_sessao}}</strong>.</p>\n\n<p><strong>Forma de pagamento:</strong> {{forma_pagamento}}</p>\n\n<p style="margin-top:40px;">{{cidade_estado}}, {{data_atual_extenso}}.</p>\n\n<div class="signature-line" style="margin-top:80px; text-align:center;">\n <hr style="width:300px; margin:0 auto 8px; border:none; border-top:1px solid #333;" />\n <strong>{{terapeuta_nome}}</strong><br/>\n Psicólogo(a) — CRP {{terapeuta_crp}}\n</div>',
|
||||
E'<div style="text-align:center;">\n <strong>{{clinica_nome}}</strong><br/>\n <span style="font-size:10pt; color:#666;">{{clinica_endereco}}</span>\n</div>',
|
||||
E'<div style="text-align:center; font-size:9pt; color:#999;">\n {{clinica_nome}} · CNPJ: {{clinica_cnpj}} · {{clinica_telefone}}\n</div>',
|
||||
ARRAY['paciente_nome','paciente_cpf','valor','valor_extenso','data_sessao','forma_pagamento','cidade_estado','data_atual_extenso','terapeuta_nome','terapeuta_crp','clinica_nome','clinica_endereco','clinica_cnpj','clinica_telefone'],
|
||||
true, true
|
||||
)
|
||||
|
||||
,
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 5. Contrato de Prestação de Serviços Psicológicos
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
(
|
||||
gen_random_uuid(), NULL, NULL,
|
||||
'Contrato de Prestação de Serviços',
|
||||
'contrato_servicos',
|
||||
'Contrato padrão entre profissional/clínica e paciente para prestação de serviços psicológicos.',
|
||||
E'<h2 style="text-align:center; margin-bottom:30px;">CONTRATO DE PRESTAÇÃO DE SERVIÇOS PSICOLÓGICOS</h2>\n\n<p>Pelo presente instrumento particular, de um lado <strong>{{terapeuta_nome}}</strong>, Psicólogo(a), inscrito(a) no CRP sob o nº <strong>{{terapeuta_crp}}</strong>, CPF nº <strong>{{terapeuta_cpf}}</strong>, doravante denominado(a) <strong>CONTRATADO(A)</strong>, e de outro lado <strong>{{paciente_nome}}</strong>, CPF nº <strong>{{paciente_cpf}}</strong>, doravante denominado(a) <strong>CONTRATANTE</strong>, têm entre si justo e contratado o seguinte:</p>\n\n<h3>CLÁUSULA 1ª — DO OBJETO</h3>\n<p>O presente contrato tem por objeto a prestação de serviços de atendimento psicológico clínico, na modalidade <strong>{{modalidade_atendimento}}</strong>, pelo(a) CONTRATADO(A) ao CONTRATANTE.</p>\n\n<h3>CLÁUSULA 2ª — DA PERIODICIDADE</h3>\n<p>As sessões ocorrerão com frequência <strong>{{frequencia_sessoes}}</strong>, com duração aproximada de <strong>{{duracao_sessao}}</strong> minutos, em dia e horário previamente acordados entre as partes.</p>\n\n<h3>CLÁUSULA 3ª — DOS HONORÁRIOS</h3>\n<p>O valor de cada sessão será de <strong>{{valor}}</strong> ({{valor_extenso}}), a ser pago <strong>{{forma_pagamento}}</strong>.</p>\n<p>Em caso de reajuste, o(a) CONTRATADO(A) comunicará o CONTRATANTE com antecedência mínima de 30 (trinta) dias.</p>\n\n<h3>CLÁUSULA 4ª — DO CANCELAMENTO E FALTAS</h3>\n<p>O cancelamento ou remarcação de sessões deverá ser comunicado com antecedência mínima de <strong>24 (vinte e quatro) horas</strong>. Faltas sem aviso prévio serão cobradas integralmente.</p>\n\n<h3>CLÁUSULA 5ª — DO SIGILO</h3>\n<p>O(A) CONTRATADO(A) compromete-se a manter sigilo absoluto sobre todas as informações obtidas durante o atendimento, conforme o Código de Ética Profissional do Psicólogo (Resolução CFP nº 010/2005) e a Lei Geral de Proteção de Dados (Lei nº 13.709/2018).</p>\n\n<h3>CLÁUSULA 6ª — DA RESCISÃO</h3>\n<p>O presente contrato poderá ser rescindido por qualquer das partes, a qualquer tempo, mediante comunicação prévia, sem ônus adicionais além das sessões já realizadas.</p>\n\n<h3>CLÁUSULA 7ª — DO FORO</h3>\n<p>Para dirimir quaisquer controvérsias oriundas deste contrato, as partes elegem o foro da Comarca de <strong>{{cidade_estado}}</strong>.</p>\n\n<p style="margin-top:30px;">E por estarem assim justas e contratadas, as partes assinam o presente instrumento em 2 (duas) vias de igual teor e forma.</p>\n\n<p style="margin-top:20px;">{{cidade_estado}}, {{data_atual_extenso}}.</p>\n\n<div style="display:flex; justify-content:space-between; margin-top:80px;">\n <div style="text-align:center; width:45%;">\n <hr style="border:none; border-top:1px solid #333; margin-bottom:8px;" />\n <strong>{{terapeuta_nome}}</strong><br/>\n Psicólogo(a) — CRP {{terapeuta_crp}}\n </div>\n <div style="text-align:center; width:45%;">\n <hr style="border:none; border-top:1px solid #333; margin-bottom:8px;" />\n <strong>{{paciente_nome}}</strong><br/>\n CPF: {{paciente_cpf}}\n </div>\n</div>',
|
||||
E'<div style="text-align:center;">\n <strong>{{clinica_nome}}</strong><br/>\n <span style="font-size:10pt; color:#666;">{{clinica_endereco}} · {{clinica_telefone}}</span>\n</div>',
|
||||
E'<div style="text-align:center; font-size:9pt; color:#999;">\n {{clinica_nome}} · CNPJ: {{clinica_cnpj}} · {{clinica_endereco}}\n</div>',
|
||||
ARRAY['paciente_nome','paciente_cpf','terapeuta_nome','terapeuta_crp','terapeuta_cpf','modalidade_atendimento','frequencia_sessoes','duracao_sessao','valor','valor_extenso','forma_pagamento','cidade_estado','data_atual_extenso','clinica_nome','clinica_endereco','clinica_telefone','clinica_cnpj'],
|
||||
true, true
|
||||
),
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 6. Termo de Consentimento Livre e Esclarecido (TCLE)
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
(
|
||||
gen_random_uuid(), NULL, NULL,
|
||||
'Termo de Consentimento Livre e Esclarecido',
|
||||
'tcle',
|
||||
'TCLE para início de acompanhamento psicológico, conforme exigências éticas do CFP.',
|
||||
E'<h2 style="text-align:center; margin-bottom:30px;">TERMO DE CONSENTIMENTO LIVRE E ESCLARECIDO</h2>\n\n<p>Eu, <strong>{{paciente_nome}}</strong>, CPF nº <strong>{{paciente_cpf}}</strong>, declaro que fui devidamente informado(a) e esclarecido(a) pelo(a) psicólogo(a) <strong>{{terapeuta_nome}}</strong>, CRP <strong>{{terapeuta_crp}}</strong>, sobre os seguintes pontos:</p>\n\n<h3>1. Natureza do serviço</h3>\n<p>O atendimento psicológico consiste em sessões de psicoterapia, com abordagem <strong>{{abordagem_terapeutica}}</strong>, visando o acolhimento e o tratamento das demandas apresentadas.</p>\n\n<h3>2. Sigilo profissional</h3>\n<p>Todas as informações compartilhadas durante as sessões são estritamente confidenciais, conforme o Código de Ética Profissional do Psicólogo (Resolução CFP nº 010/2005), podendo ser quebrado apenas nas hipóteses previstas em lei.</p>\n\n<h3>3. Registro de informações</h3>\n<p>O(A) profissional poderá realizar registros das sessões (prontuário psicológico) para fins de acompanhamento clínico, mantidos em sigilo conforme a LGPD (Lei nº 13.709/2018) e as normas do CFP.</p>\n\n<h3>4. Direitos do paciente</h3>\n<ul>\n <li>Receber informações sobre o processo terapêutico;</li>\n <li>Interromper o atendimento a qualquer momento;</li>\n <li>Solicitar encaminhamento a outro profissional;</li>\n <li>Ter acesso às informações registradas em seu prontuário.</li>\n</ul>\n\n<h3>5. Limitações</h3>\n<p>O acompanhamento psicológico não substitui tratamento médico ou psiquiátrico quando necessário. O(A) profissional poderá sugerir encaminhamentos complementares.</p>\n\n<p style="margin-top:30px;">Declaro que li e compreendi as informações acima e <strong>consinto livremente</strong> com o início do acompanhamento psicológico.</p>\n\n<p style="margin-top:20px;">{{cidade_estado}}, {{data_atual_extenso}}.</p>\n\n<div style="display:flex; justify-content:space-between; margin-top:80px;">\n <div style="text-align:center; width:45%;">\n <hr style="border:none; border-top:1px solid #333; margin-bottom:8px;" />\n <strong>{{paciente_nome}}</strong><br/>\n CPF: {{paciente_cpf}}\n </div>\n <div style="text-align:center; width:45%;">\n <hr style="border:none; border-top:1px solid #333; margin-bottom:8px;" />\n <strong>{{terapeuta_nome}}</strong><br/>\n Psicólogo(a) — CRP {{terapeuta_crp}}\n </div>\n</div>',
|
||||
E'<div style="text-align:center;">\n <strong>{{clinica_nome}}</strong><br/>\n <span style="font-size:10pt; color:#666;">{{clinica_endereco}}</span>\n</div>',
|
||||
E'<div style="text-align:center; font-size:9pt; color:#999;">\n Este termo é regido pela Resolução CFP nº 010/2005 e pela Lei nº 13.709/2018 (LGPD).\n</div>',
|
||||
ARRAY['paciente_nome','paciente_cpf','terapeuta_nome','terapeuta_crp','abordagem_terapeutica','cidade_estado','data_atual_extenso','clinica_nome','clinica_endereco'],
|
||||
true, true
|
||||
),
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 7. Encaminhamento
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
(
|
||||
gen_random_uuid(), NULL, NULL,
|
||||
'Encaminhamento',
|
||||
'encaminhamento',
|
||||
'Carta de encaminhamento do paciente para outro profissional ou serviço.',
|
||||
E'<h2 style="text-align:center; margin-bottom:30px;">ENCAMINHAMENTO</h2>\n\n<p>Ao(À) profissional/serviço de <strong>{{especialidade_destino}}</strong>,</p>\n\n<p style="margin-top:20px;">Encaminho o(a) paciente <strong>{{paciente_nome}}</strong>, nascido(a) em <strong>{{paciente_data_nascimento}}</strong>, para avaliação e acompanhamento em <strong>{{especialidade_destino}}</strong>.</p>\n\n<p><strong>Motivo do encaminhamento:</strong></p>\n<p>{{motivo_encaminhamento}}</p>\n\n<p><strong>Informações relevantes:</strong></p>\n<p>{{informacoes_relevantes}}</p>\n\n<p style="margin-top:20px;">Coloco-me à disposição para troca de informações que se façam necessárias ao melhor atendimento do(a) paciente, respeitadas as normas de sigilo profissional.</p>\n\n<p style="margin-top:40px;">{{cidade_estado}}, {{data_atual_extenso}}.</p>\n\n<div class="signature-line" style="margin-top:80px; text-align:center;">\n <hr style="width:300px; margin:0 auto 8px; border:none; border-top:1px solid #333;" />\n <strong>{{terapeuta_nome}}</strong><br/>\n Psicólogo(a) — CRP {{terapeuta_crp}}<br/>\n <span style="font-size:10pt; color:#666;">{{terapeuta_email}} · {{terapeuta_telefone}}</span>\n</div>',
|
||||
E'<div style="text-align:center;">\n <strong>{{clinica_nome}}</strong><br/>\n <span style="font-size:10pt; color:#666;">{{clinica_endereco}} · {{clinica_telefone}}</span>\n</div>',
|
||||
E'<div style="text-align:center; font-size:9pt; color:#999;">\n {{clinica_nome}} · {{clinica_endereco}} · {{clinica_telefone}}\n</div>',
|
||||
ARRAY['paciente_nome','paciente_data_nascimento','especialidade_destino','motivo_encaminhamento','informacoes_relevantes','cidade_estado','data_atual_extenso','terapeuta_nome','terapeuta_crp','terapeuta_email','terapeuta_telefone','clinica_nome','clinica_endereco','clinica_telefone'],
|
||||
true, true
|
||||
),
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 8. Autorização para Atendimento de Menor
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
(
|
||||
gen_random_uuid(), NULL, NULL,
|
||||
'Autorização para Atendimento de Menor',
|
||||
'autorizacao_menor',
|
||||
'Termo de autorização dos responsáveis legais para atendimento psicológico de criança ou adolescente.',
|
||||
E'<h2 style="text-align:center; margin-bottom:30px;">AUTORIZAÇÃO PARA ATENDIMENTO PSICOLÓGICO DE MENOR</h2>\n\n<p>Eu, <strong>{{responsavel_nome}}</strong>, CPF nº <strong>{{responsavel_cpf}}</strong>, na qualidade de <strong>{{grau_parentesco}}</strong> e responsável legal do(a) menor <strong>{{paciente_nome}}</strong>, nascido(a) em <strong>{{paciente_data_nascimento}}</strong>, <strong>AUTORIZO</strong> a realização de atendimento psicológico pelo(a) profissional abaixo identificado(a).</p>\n\n<h3>Profissional responsável</h3>\n<p><strong>{{terapeuta_nome}}</strong><br/>\nPsicólogo(a) — CRP {{terapeuta_crp}}</p>\n\n<h3>Declarações</h3>\n<p>Declaro estar ciente de que:</p>\n<ul>\n <li>O atendimento seguirá as normas do Código de Ética Profissional do Psicólogo e do Estatuto da Criança e do Adolescente (ECA);</li>\n <li>As informações do atendimento são sigilosas, sendo compartilhadas com o responsável apenas o necessário para o bem-estar do(a) menor, conforme julgamento técnico do(a) profissional;</li>\n <li>Posso solicitar informações sobre a evolução do tratamento a qualquer momento;</li>\n <li>Posso revogar esta autorização a qualquer tempo, mediante comunicação por escrito.</li>\n</ul>\n\n<p style="margin-top:40px;">{{cidade_estado}}, {{data_atual_extenso}}.</p>\n\n<div style="display:flex; justify-content:space-between; margin-top:80px;">\n <div style="text-align:center; width:45%;">\n <hr style="border:none; border-top:1px solid #333; margin-bottom:8px;" />\n <strong>{{responsavel_nome}}</strong><br/>\n Responsável legal<br/>\n CPF: {{responsavel_cpf}}\n </div>\n <div style="text-align:center; width:45%;">\n <hr style="border:none; border-top:1px solid #333; margin-bottom:8px;" />\n <strong>{{terapeuta_nome}}</strong><br/>\n Psicólogo(a) — CRP {{terapeuta_crp}}\n </div>\n</div>',
|
||||
E'<div style="text-align:center;">\n <strong>{{clinica_nome}}</strong><br/>\n <span style="font-size:10pt; color:#666;">{{clinica_endereco}}</span>\n</div>',
|
||||
E'<div style="text-align:center; font-size:9pt; color:#999;">\n {{clinica_nome}} · {{clinica_endereco}} · {{clinica_telefone}}\n</div>',
|
||||
ARRAY['paciente_nome','paciente_data_nascimento','responsavel_nome','responsavel_cpf','grau_parentesco','terapeuta_nome','terapeuta_crp','cidade_estado','data_atual_extenso','clinica_nome','clinica_endereco','clinica_telefone'],
|
||||
true, true
|
||||
),
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 9. Laudo Psicológico
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
(
|
||||
gen_random_uuid(), NULL, NULL,
|
||||
'Laudo Psicológico',
|
||||
'laudo_psicologico',
|
||||
'Modelo de laudo psicológico conforme Resolução CFP nº 06/2019.',
|
||||
E'<h2 style="text-align:center; margin-bottom:30px;">LAUDO PSICOLÓGICO</h2>\n\n<table style="width:100%; border-collapse:collapse; margin-bottom:20px;">\n <tr><td style="padding:4px 8px; font-weight:bold; width:200px;">Solicitante:</td><td style="padding:4px 8px;">{{solicitante}}</td></tr>\n <tr><td style="padding:4px 8px; font-weight:bold;">Finalidade:</td><td style="padding:4px 8px;">{{finalidade}}</td></tr>\n <tr><td style="padding:4px 8px; font-weight:bold;">Avaliado(a):</td><td style="padding:4px 8px;">{{paciente_nome}}</td></tr>\n <tr><td style="padding:4px 8px; font-weight:bold;">Data de nascimento:</td><td style="padding:4px 8px;">{{paciente_data_nascimento}}</td></tr>\n <tr><td style="padding:4px 8px; font-weight:bold;">CPF:</td><td style="padding:4px 8px;">{{paciente_cpf}}</td></tr>\n <tr><td style="padding:4px 8px; font-weight:bold;">Profissional:</td><td style="padding:4px 8px;">{{terapeuta_nome}} — CRP {{terapeuta_crp}}</td></tr>\n <tr><td style="padding:4px 8px; font-weight:bold;">Data do laudo:</td><td style="padding:4px 8px;">{{data_atual}}</td></tr>\n</table>\n\n<h3>1. Descrição da demanda</h3>\n<p>[Apresente a demanda e o motivo da avaliação, conforme solicitação recebida.]</p>\n\n<h3>2. Procedimentos</h3>\n<p>[Descreva os recursos e instrumentos técnicos utilizados na avaliação: entrevistas, testes psicológicos (com nome, autor e parecer favorável do SATEPSI quando aplicável), observação, etc.]</p>\n\n<h3>3. Análise</h3>\n<p>[Apresente de forma clara e fundamentada os dados obtidos, integrando os resultados dos procedimentos realizados à luz da literatura científica da Psicologia. Não inclua informações que não tenham relação com a demanda.]</p>\n\n<h3>4. Conclusão</h3>\n<p>[Apresente o resultado da avaliação, indicando a resposta à demanda inicial. A conclusão deve ser coerente com a análise e os procedimentos utilizados.]</p>\n\n<p style="margin-top:40px; font-size:10pt; color:#666;"><em>Este laudo foi elaborado em conformidade com a Resolução CFP nº 06/2019, que institui regras para a elaboração de documentos escritos produzidos pelo psicólogo no exercício profissional.</em></p>\n\n<p style="margin-top:20px;">{{cidade_estado}}, {{data_atual_extenso}}.</p>\n\n<div class="signature-line" style="margin-top:80px; text-align:center;">\n <hr style="width:300px; margin:0 auto 8px; border:none; border-top:1px solid #333;" />\n <strong>{{terapeuta_nome}}</strong><br/>\n Psicólogo(a) — CRP {{terapeuta_crp}}\n</div>',
|
||||
E'<div style="text-align:center;">\n <strong>{{clinica_nome}}</strong><br/>\n <span style="font-size:10pt; color:#666;">{{clinica_endereco}} · {{clinica_telefone}}</span>\n</div>',
|
||||
E'<div style="text-align:center; font-size:9pt; color:#999;">\n Este documento é de caráter sigiloso, conforme Art. 9º do Código de Ética do Psicólogo e Resolução CFP nº 06/2019.\n</div>',
|
||||
ARRAY['paciente_nome','paciente_data_nascimento','paciente_cpf','solicitante','finalidade','terapeuta_nome','terapeuta_crp','data_atual','data_atual_extenso','cidade_estado','clinica_nome','clinica_endereco','clinica_telefone'],
|
||||
true, true
|
||||
),
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 10. Parecer Psicológico
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
(
|
||||
gen_random_uuid(), NULL, NULL,
|
||||
'Parecer Psicológico',
|
||||
'parecer_psicologico',
|
||||
'Manifestação técnica sobre questão específica no campo da Psicologia, conforme Resolução CFP nº 06/2019.',
|
||||
E'<h2 style="text-align:center; margin-bottom:30px;">PARECER PSICOLÓGICO</h2>\n\n<table style="width:100%; border-collapse:collapse; margin-bottom:20px;">\n <tr><td style="padding:4px 8px; font-weight:bold; width:200px;">Parecer nº:</td><td style="padding:4px 8px;">{{numero_parecer}}</td></tr>\n <tr><td style="padding:4px 8px; font-weight:bold;">Solicitante:</td><td style="padding:4px 8px;">{{solicitante}}</td></tr>\n <tr><td style="padding:4px 8px; font-weight:bold;">Assunto:</td><td style="padding:4px 8px;">{{assunto}}</td></tr>\n <tr><td style="padding:4px 8px; font-weight:bold;">Profissional:</td><td style="padding:4px 8px;">{{terapeuta_nome}} — CRP {{terapeuta_crp}}</td></tr>\n</table>\n\n<h3>1. Exposição de motivos</h3>\n<p>[Descreva a questão ou consulta que originou o pedido de parecer, indicando quem solicitou e com qual finalidade.]</p>\n\n<h3>2. Análise fundamentada</h3>\n<p>[Apresente a análise técnica, com base em referencial teórico-científico da Psicologia, normas do CFP e legislação pertinente. O parecer deve se restringir ao campo de conhecimento do psicólogo.]</p>\n\n<h3>3. Conclusão</h3>\n<p>[Apresente a resposta técnica à questão formulada, de forma objetiva e fundamentada.]</p>\n\n<p style="margin-top:30px; font-size:10pt; color:#666;"><em>Parecer elaborado em conformidade com a Resolução CFP nº 06/2019.</em></p>\n\n<p style="margin-top:20px;">{{cidade_estado}}, {{data_atual_extenso}}.</p>\n\n<div class="signature-line" style="margin-top:80px; text-align:center;">\n <hr style="width:300px; margin:0 auto 8px; border:none; border-top:1px solid #333;" />\n <strong>{{terapeuta_nome}}</strong><br/>\n Psicólogo(a) — CRP {{terapeuta_crp}}\n</div>',
|
||||
E'<div style="text-align:center;">\n <strong>{{clinica_nome}}</strong><br/>\n <span style="font-size:10pt; color:#666;">{{clinica_endereco}}</span>\n</div>',
|
||||
E'<div style="text-align:center; font-size:9pt; color:#999;">\n Este documento é de caráter sigiloso, conforme Resolução CFP nº 06/2019.\n</div>',
|
||||
ARRAY['numero_parecer','solicitante','assunto','terapeuta_nome','terapeuta_crp','cidade_estado','data_atual_extenso','clinica_nome','clinica_endereco'],
|
||||
true, true
|
||||
),
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 11. Termo de Sigilo e Confidencialidade
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
(
|
||||
gen_random_uuid(), NULL, NULL,
|
||||
'Termo de Sigilo e Confidencialidade',
|
||||
'termo_sigilo',
|
||||
'Termo reforçando o compromisso de sigilo entre profissional e paciente.',
|
||||
E'<h2 style="text-align:center; margin-bottom:30px;">TERMO DE SIGILO E CONFIDENCIALIDADE</h2>\n\n<p>Eu, <strong>{{terapeuta_nome}}</strong>, Psicólogo(a), CRP <strong>{{terapeuta_crp}}</strong>, declaro ao(à) paciente <strong>{{paciente_nome}}</strong>, CPF nº <strong>{{paciente_cpf}}</strong>, que:</p>\n\n<ol style="line-height:2;">\n <li>Todas as informações verbais, escritas ou de qualquer natureza, obtidas durante o processo terapêutico, serão mantidas em <strong>sigilo absoluto</strong>;</li>\n <li>O prontuário psicológico é de acesso exclusivo do profissional e do paciente, nos termos da Resolução CFP nº 001/2009;</li>\n <li>Os dados pessoais serão tratados conforme a Lei Geral de Proteção de Dados (Lei nº 13.709/2018), sendo utilizados exclusivamente para fins de acompanhamento clínico;</li>\n <li>A quebra de sigilo somente ocorrerá nas hipóteses previstas no Código de Ética do Psicólogo:\n <ul>\n <li>Situações de risco à vida do paciente ou de terceiros;</li>\n <li>Determinação judicial;</li>\n <li>Atendimento de menor — informações necessárias aos responsáveis;</li>\n </ul>\n </li>\n <li>Dados anonimizados poderão ser utilizados para fins de estudo ou pesquisa, mediante consentimento prévio específico.</li>\n</ol>\n\n<p style="margin-top:30px;">Este termo entra em vigor na data de sua assinatura e permanece válido mesmo após o encerramento do acompanhamento.</p>\n\n<p style="margin-top:20px;">{{cidade_estado}}, {{data_atual_extenso}}.</p>\n\n<div style="display:flex; justify-content:space-between; margin-top:80px;">\n <div style="text-align:center; width:45%;">\n <hr style="border:none; border-top:1px solid #333; margin-bottom:8px;" />\n <strong>{{terapeuta_nome}}</strong><br/>\n Psicólogo(a) — CRP {{terapeuta_crp}}\n </div>\n <div style="text-align:center; width:45%;">\n <hr style="border:none; border-top:1px solid #333; margin-bottom:8px;" />\n <strong>{{paciente_nome}}</strong><br/>\n CPF: {{paciente_cpf}}\n </div>\n</div>',
|
||||
E'<div style="text-align:center;">\n <strong>{{clinica_nome}}</strong><br/>\n <span style="font-size:10pt; color:#666;">{{clinica_endereco}}</span>\n</div>',
|
||||
E'<div style="text-align:center; font-size:9pt; color:#999;">\n Documento regido pelo Código de Ética Profissional do Psicólogo e pela LGPD (Lei nº 13.709/2018).\n</div>',
|
||||
ARRAY['paciente_nome','paciente_cpf','terapeuta_nome','terapeuta_crp','cidade_estado','data_atual_extenso','clinica_nome','clinica_endereco'],
|
||||
true, true
|
||||
),
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 12. Declaração de Início de Tratamento
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
(
|
||||
gen_random_uuid(), NULL, NULL,
|
||||
'Declaração de Início de Tratamento',
|
||||
'declaracao_inicio_tratamento',
|
||||
'Declara que o paciente iniciou acompanhamento psicológico a partir de determinada data.',
|
||||
E'<h2 style="text-align:center; margin-bottom:30px;">DECLARAÇÃO DE INÍCIO DE TRATAMENTO</h2>\n\n<p>Declaro, para os devidos fins, que <strong>{{paciente_nome}}</strong>, portador(a) do CPF nº <strong>{{paciente_cpf}}</strong>, encontra-se em acompanhamento psicológico neste consultório/clínica desde <strong>{{data_inicio_tratamento}}</strong>, com frequência <strong>{{frequencia_sessoes}}</strong>.</p>\n\n<p>O presente documento é expedido a pedido do(a) interessado(a) e não contém informações de caráter diagnóstico ou sigiloso.</p>\n\n<p style="margin-top:40px;">{{cidade_estado}}, {{data_atual_extenso}}.</p>\n\n<div class="signature-line" style="margin-top:80px; text-align:center;">\n <hr style="width:300px; margin:0 auto 8px; border:none; border-top:1px solid #333;" />\n <strong>{{terapeuta_nome}}</strong><br/>\n Psicólogo(a) — CRP {{terapeuta_crp}}\n</div>',
|
||||
E'<div style="text-align:center;">\n <strong>{{clinica_nome}}</strong><br/>\n <span style="font-size:10pt; color:#666;">{{clinica_endereco}} · {{clinica_telefone}}</span>\n</div>',
|
||||
E'<div style="text-align:center; font-size:9pt; color:#999;">\n {{clinica_nome}} · {{clinica_endereco}} · {{clinica_telefone}}\n</div>',
|
||||
ARRAY['paciente_nome','paciente_cpf','data_inicio_tratamento','frequencia_sessoes','cidade_estado','data_atual_extenso','terapeuta_nome','terapeuta_crp','clinica_nome','clinica_endereco','clinica_telefone'],
|
||||
true, true
|
||||
),
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 13. Termo de Alta Terapêutica
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
(
|
||||
gen_random_uuid(), NULL, NULL,
|
||||
'Termo de Alta Terapêutica',
|
||||
'termo_alta',
|
||||
'Documento formalizando o encerramento do acompanhamento psicológico.',
|
||||
E'<h2 style="text-align:center; margin-bottom:30px;">TERMO DE ALTA TERAPÊUTICA</h2>\n\n<p>Comunico que o(a) paciente <strong>{{paciente_nome}}</strong>, CPF nº <strong>{{paciente_cpf}}</strong>, que esteve em acompanhamento psicológico desde <strong>{{data_inicio_tratamento}}</strong>, recebe <strong>alta terapêutica</strong> nesta data.</p>\n\n<h3>Motivo da alta</h3>\n<p>{{motivo_alta}}</p>\n\n<h3>Orientações</h3>\n<p>{{orientacoes_pos_alta}}</p>\n\n<p style="margin-top:20px;">O(A) paciente foi informado(a) de que poderá retornar ao acompanhamento a qualquer momento, caso considere necessário.</p>\n\n<p style="margin-top:40px;">{{cidade_estado}}, {{data_atual_extenso}}.</p>\n\n<div style="display:flex; justify-content:space-between; margin-top:80px;">\n <div style="text-align:center; width:45%;">\n <hr style="border:none; border-top:1px solid #333; margin-bottom:8px;" />\n <strong>{{terapeuta_nome}}</strong><br/>\n Psicólogo(a) — CRP {{terapeuta_crp}}\n </div>\n <div style="text-align:center; width:45%;">\n <hr style="border:none; border-top:1px solid #333; margin-bottom:8px;" />\n <strong>{{paciente_nome}}</strong><br/>\n Ciente\n </div>\n</div>',
|
||||
E'<div style="text-align:center;">\n <strong>{{clinica_nome}}</strong><br/>\n <span style="font-size:10pt; color:#666;">{{clinica_endereco}}</span>\n</div>',
|
||||
E'<div style="text-align:center; font-size:9pt; color:#999;">\n {{clinica_nome}} · {{clinica_endereco}} · {{clinica_telefone}}\n</div>',
|
||||
ARRAY['paciente_nome','paciente_cpf','data_inicio_tratamento','motivo_alta','orientacoes_pos_alta','cidade_estado','data_atual_extenso','terapeuta_nome','terapeuta_crp','clinica_nome','clinica_endereco','clinica_telefone'],
|
||||
true, true
|
||||
),
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 14. Termo de Consentimento para Atendimento Online
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
(
|
||||
gen_random_uuid(), NULL, NULL,
|
||||
'Termo de Consentimento para Atendimento Online',
|
||||
'tcle_online',
|
||||
'Consentimento específico para atendimento psicológico por meios tecnológicos (Resolução CFP nº 11/2018).',
|
||||
E'<h2 style="text-align:center; margin-bottom:30px;">TERMO DE CONSENTIMENTO PARA ATENDIMENTO PSICOLÓGICO ONLINE</h2>\n\n<p>Eu, <strong>{{paciente_nome}}</strong>, CPF nº <strong>{{paciente_cpf}}</strong>, declaro que fui informado(a) e concordo com a realização de atendimento psicológico na modalidade <strong>online</strong>, por meio de Tecnologias da Informação e Comunicação (TICs), conforme a Resolução CFP nº 11/2018.</p>\n\n<h3>1. Plataforma utilizada</h3>\n<p>As sessões serão realizadas por meio de <strong>{{plataforma_online}}</strong>, devendo ambas as partes garantir ambiente adequado, com privacidade e conexão estável.</p>\n\n<h3>2. Condições do atendimento</h3>\n<ul>\n <li>O(A) profissional está cadastrado(a) no e-Psi (plataforma do CFP) para prestação de serviços psicológicos online;</li>\n <li>Todas as regras de sigilo profissional aplicam-se igualmente ao atendimento online;</li>\n <li>A plataforma utilizada oferece criptografia e segurança dos dados;</li>\n <li>Não é permitida a gravação das sessões por nenhuma das partes, salvo acordo prévio por escrito.</li>\n</ul>\n\n<h3>3. Situações excepcionais</h3>\n<p>Em caso de instabilidade técnica que comprometa a sessão, o(a) profissional e o(a) paciente acordarão a melhor forma de dar continuidade (reagendar, trocar de plataforma, ou realizar sessão presencial).</p>\n\n<h3>4. Limitações</h3>\n<p>O(A) profissional reserva-se o direito de indicar atendimento presencial quando avaliar que a modalidade online não é adequada ao caso clínico.</p>\n\n<p style="margin-top:30px;">Declaro que compreendi as condições acima e <strong>consinto</strong> com a realização das sessões na modalidade online.</p>\n\n<p style="margin-top:20px;">{{cidade_estado}}, {{data_atual_extenso}}.</p>\n\n<div style="display:flex; justify-content:space-between; margin-top:80px;">\n <div style="text-align:center; width:45%;">\n <hr style="border:none; border-top:1px solid #333; margin-bottom:8px;" />\n <strong>{{paciente_nome}}</strong><br/>\n CPF: {{paciente_cpf}}\n </div>\n <div style="text-align:center; width:45%;">\n <hr style="border:none; border-top:1px solid #333; margin-bottom:8px;" />\n <strong>{{terapeuta_nome}}</strong><br/>\n Psicólogo(a) — CRP {{terapeuta_crp}}\n </div>\n</div>',
|
||||
E'<div style="text-align:center;">\n <strong>{{clinica_nome}}</strong><br/>\n <span style="font-size:10pt; color:#666;">{{clinica_endereco}}</span>\n</div>',
|
||||
E'<div style="text-align:center; font-size:9pt; color:#999;">\n Atendimento online regulamentado pela Resolução CFP nº 11/2018. Profissional cadastrado(a) no e-Psi.\n</div>',
|
||||
ARRAY['paciente_nome','paciente_cpf','plataforma_online','terapeuta_nome','terapeuta_crp','cidade_estado','data_atual_extenso','clinica_nome','clinica_endereco'],
|
||||
true, true
|
||||
)
|
||||
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- ==========================================================================
|
||||
-- FIM DO SEED — 14 templates globais
|
||||
-- ==========================================================================
|
||||
Reference in New Issue
Block a user