Compare commits
33 Commits
319f976d2b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d088a89fb7 | ||
|
|
0658e2e9bf | ||
|
|
bfe148ef12 | ||
|
|
3f1786c9bf | ||
|
|
53a4980396 | ||
|
|
a89d1f5560 | ||
|
|
29ed349cf2 | ||
|
|
d6d2fe29d1 | ||
|
|
66f67cd40f | ||
|
|
84d65e49c0 | ||
|
|
f66f6f3fde | ||
|
|
ee09b30987 | ||
|
|
587079e414 | ||
|
|
06fb369beb | ||
|
|
f4b185ae17 | ||
|
|
f733db8436 | ||
|
|
d58dc21297 | ||
|
|
b1c0cb47c0 | ||
|
|
89b4ecaba1 | ||
|
|
6eff67bf22 | ||
|
|
62e79e243a | ||
|
|
3a671b1e9e | ||
|
|
b3bb817e3f | ||
|
|
676042268b | ||
|
|
ec6b6ef53a | ||
|
|
76a3b60333 | ||
|
|
410c08d693 | ||
|
|
a4b2c96b0d | ||
|
|
a47200fdf7 | ||
|
|
7c32ae1f6f | ||
|
|
db99863fac | ||
|
|
c2ef85fcab | ||
|
|
deea8861f8 |
53
.claude/settings.local.json
Normal file
53
.claude/settings.local.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(node:*)",
|
||||||
|
"Bash(powershell:*)",
|
||||||
|
"Bash(grep:*)",
|
||||||
|
"Bash(cd \"D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai\" && sed -i \\\\\n 's/console\\\\.time\\(tlabel\\)/const _perfEnd = logPerf\\('\\\\''router.guard'\\\\'', tlabel\\)/g' \\\\\n src/router/guards.js && echo \"console.time substituído\")",
|
||||||
|
"Bash(cd \"D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai\" && sed -i \\\\\n 's/console\\\\.timeEnd\\(tlabel\\)/_perfEnd\\(\\)/g' \\\\\n src/router/guards.js && echo \"console.timeEnd substituído\")",
|
||||||
|
"Bash(cd \"D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai\" && npm install --save-dev vitest @vitest/ui 2>&1 | tail -5)",
|
||||||
|
"Bash(find \"D:\\\\leonohama\\\\AgenciaPsi.com.br\\\\Sistema\\\\agenciapsi-primesakai/DBS\" -name \"*.sql\" -type f 2>/dev/null | head -10)",
|
||||||
|
"Bash(find \"D:\\\\leonohama\\\\AgenciaPsi.com.br\\\\Sistema\\\\agenciapsi-primesakai/DBS\" -name \"*.sql\" -type f 2>/dev/null | xargs grep -l \"agenda_eventos\" | head -3)",
|
||||||
|
"Bash(find \"D:\\\\leonohama\\\\AgenciaPsi.com.br\\\\Sistema\\\\agenciapsi-primesakai/DBS/2026-03-11\" -name \"*.sql\" -type f 2>/dev/null | head -3)",
|
||||||
|
"Bash(find \"D:\\\\leonohama\\\\AgenciaPsi.com.br\\\\Sistema\\\\agenciapsi-primesakai\" -type f -name \"*.sql\" 2>/dev/null | xargs grep -l \"agenda_eventos\" 2>/dev/null | head -5)",
|
||||||
|
"Bash(find /d/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/src -name \"*[Pp]ricing*\" -o -name \"*[Pp]reco*\" -o -name \"*[Vv]alor*\" 2>/dev/null | head -20)",
|
||||||
|
"Bash(where python:*)",
|
||||||
|
"Bash(cd \"/d/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai\" && C:/Users/lmnohama/AppData/Local/Programs/Python/Python310/python.exe -c \"\nimport zipfile\nimport xml.etree.ElementTree as ET\n\nfor fname in ['spec-wizard.docx', 'spec-v2.docx']:\n print\\('=== ' + fname + ' ==='\\)\n try:\n with zipfile.ZipFile\\(fname, 'r'\\) as z:\n with z.open\\('word/document.xml'\\) as f:\n tree = ET.parse\\(f\\)\n root = tree.getroot\\(\\)\n texts = []\n for para in root.iter\\('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}p'\\):\n parts = []\n for t in para.iter\\('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}t'\\):\n if t.text:\n parts.append\\(t.text\\)\n line = ''.join\\(parts\\)\n texts.append\\(line\\)\n print\\('\\\\n'.join\\(texts\\)\\)\n except Exception as e:\n print\\('Error: ' + str\\(e\\)\\)\n print\\(\\)\n\")",
|
||||||
|
"Bash(C:/Users/lmnohama/AppData/Local/Programs/Python/Python310/python.exe -c \"\nimport sys\nwith open\\('/d/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/DBS/2026-03-12/schema.sql', 'r', encoding='utf-8'\\) as f:\n lines = f.readlines\\(\\)\nprint\\(f'Total lines: {len\\(lines\\)}'\\)\n\" 2>&1)",
|
||||||
|
"Bash(C:/Users/lmnohama/AppData/Local/Programs/Python/Python310/python.exe -c \"\nimport sys\nfpath = 'D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/DBS/2026-03-12/schema.sql'\nwith open\\(fpath, 'r', encoding='utf-8'\\) as f:\n lines = f.readlines\\(\\)\nsys.stdout.buffer.write\\(\\('Total lines: ' + str\\(len\\(lines\\)\\) + '\\\\n'\\).encode\\('utf-8'\\)\\)\n\" 2>&1)",
|
||||||
|
"Bash(find /d/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai -type f \\\\\\( -name \"*convenio*\" -o -name \"*Convenio*\" \\\\\\) 2>/dev/null | head -20)",
|
||||||
|
"Bash(find:*)",
|
||||||
|
"Bash(ls:*)",
|
||||||
|
"Bash(npx vite:*)",
|
||||||
|
"Bash(powershell -Command \"$content = [System.IO.File]::ReadAllText\\(''src/views/pages/clinic/clinic/ClinicFeaturesPage.vue'', [System.Text.Encoding]::UTF8\\); $content = $content -replace [char]0x201C, ''\"\"'' -replace [char]0x201D, ''\"\"''; [System.IO.File]::WriteAllText\\(''src/views/pages/clinic/clinic/ClinicFeaturesPage.vue'', $content, [System.Text.Encoding]::UTF8\\)\")",
|
||||||
|
"Bash(xargs cat:*)",
|
||||||
|
"Bash(xxd \"D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/DBS/2026-03-21/schema.sql\")",
|
||||||
|
"Bash(iconv -f UTF-16LE -t UTF-8 \"D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/DBS/2026-03-21/schema.sql\")",
|
||||||
|
"Bash(mkdir -p docs/billing docs/planos docs/subscription-health docs/estrategia docs/specs)",
|
||||||
|
"Bash(mkdir -p database/backups database/migrations database/seeds database/fixes database/snippets)",
|
||||||
|
"Bash(cd:*)",
|
||||||
|
"Bash(mv disparando-whatsapp-local.md docs/whatsapp.md)",
|
||||||
|
"Bash(mv comandos.txt docs/)",
|
||||||
|
"Bash(mv dados-padrões-da-agenda.txt docs/)",
|
||||||
|
"Bash(mv USER_ARCHETYPES.html docs/)",
|
||||||
|
"Bash(mv Novo-DB/migration_*.sql database/migrations/)",
|
||||||
|
"Bash(mv Novo-DB/seed_*.sql database/seeds/)",
|
||||||
|
"Bash(mv Novo-DB/fix_*.sql database/fixes/)",
|
||||||
|
"Bash(npm install:*)",
|
||||||
|
"Bash(cp:*)",
|
||||||
|
"Bash(npm uninstall:*)",
|
||||||
|
"Bash(rm:*)",
|
||||||
|
"Bash(docker ps:*)",
|
||||||
|
"Bash(docker exec:*)",
|
||||||
|
"Bash(\"D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/database-novo/backups/2026-03-23/schema.sql\")",
|
||||||
|
"Bash(\"D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/database-novo/backups/2026-03-23/data.sql\")",
|
||||||
|
"Bash(\"D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/database-novo/backups/2026-03-23/full_dump.sql\")",
|
||||||
|
"Bash(wc:*)",
|
||||||
|
"Bash(python _wizard_patch.py)",
|
||||||
|
"Bash(rm _wizard_patch.py)",
|
||||||
|
"Bash(npm ls:*)",
|
||||||
|
"Bash(find /d/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/src/features/patients -type f \\\\\\(-name *.vue -o -name *.js \\\\\\))"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
2
.env
Normal file
2
.env
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
VITE_SUPABASE_URL=http://127.0.0.1:54321
|
||||||
|
VITE_SUPABASE_ANON_KEY=sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH
|
||||||
4
.env.local
Normal file
4
.env.local
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
VITE_SUPABASE_URL=http://127.0.0.1:54321
|
||||||
|
VITE_SUPABASE_ANON_KEY=sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH
|
||||||
|
VITE_QA_MODE=true
|
||||||
|
VITE_QA_PASS=123Mudar@
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -5,10 +5,14 @@ coverage
|
|||||||
.nitro
|
.nitro
|
||||||
.cache
|
.cache
|
||||||
.output
|
.output
|
||||||
.env
|
# .env
|
||||||
dist
|
dist/
|
||||||
|
dist-*/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea
|
.idea
|
||||||
.eslintcache
|
.eslintcache
|
||||||
api-generator/typedoc.json
|
api-generator/typedoc.json
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
|
Dev-documentacao/
|
||||||
|
supabase/
|
||||||
|
evolution-api/
|
||||||
|
|||||||
15
.hintrc
Normal file
15
.hintrc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"development"
|
||||||
|
],
|
||||||
|
"hints": {
|
||||||
|
"compat-api/css": [
|
||||||
|
"default",
|
||||||
|
{
|
||||||
|
"ignore": [
|
||||||
|
"background-color: color-mix(in srgb, var(--primary-color) 50%, transparent)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
31
ARCHITECTURE_NOTES.md
Normal file
31
ARCHITECTURE_NOTES.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
### Observação sobre `tenant_admin` com UUID coincidente
|
||||||
|
|
||||||
|
Foi identificado que o registro de `tenant_members` possui:
|
||||||
|
|
||||||
|
- `tenant_id = 816b24fe-a0c3-4409-b79b-c6c0a6935d03`
|
||||||
|
- `user_id = 816b24fe-a0c3-4409-b79b-c6c0a6935d03`
|
||||||
|
- `role = tenant_admin`
|
||||||
|
|
||||||
|
À primeira vista pode parecer inconsistência, mas não é.
|
||||||
|
|
||||||
|
Verificação realizada:
|
||||||
|
O UUID `816b24fe-a0c3-4409-b79b-c6c0a6935d03` existe em `auth.users`
|
||||||
|
(email: admin@agenciapsi.com.br).
|
||||||
|
|
||||||
|
Portanto:
|
||||||
|
|
||||||
|
- `tenant_members.user_id` referencia corretamente `auth.users.id`
|
||||||
|
- Não há violação de integridade referencial
|
||||||
|
- O registro é válido
|
||||||
|
|
||||||
|
Trata-se de um caso em que:
|
||||||
|
|
||||||
|
- O usuário administrador principal possui um UUID específico
|
||||||
|
- O tenant foi criado com o mesmo UUID
|
||||||
|
- O administrador é `tenant_admin` desse próprio tenant
|
||||||
|
|
||||||
|
Esse padrão não quebra a arquitetura multi-tenant e é funcionalmente válido.
|
||||||
|
A coincidência entre `tenant_id` e `user_id` é apenas estrutural, não conceitual.
|
||||||
|
|
||||||
|
Conclusão:
|
||||||
|
Nenhuma correção estrutural é necessária.
|
||||||
274
AUDITORIA.md
Normal file
274
AUDITORIA.md
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
# Auditoria Técnica — AgenciaPsi MVP
|
||||||
|
**Data:** 2026-03-11
|
||||||
|
**Stack:** Vue 3 · PrimeVue · Supabase · PostgreSQL · FullCalendar
|
||||||
|
**Modelo:** claude-sonnet-4-6
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Visão Geral Arquitetural
|
||||||
|
|
||||||
|
**Pontos fortes:**
|
||||||
|
- Estrutura feature-based bem definida (`features/agenda`, `features/patients`)
|
||||||
|
- Separação correta: repository → composable → page
|
||||||
|
- Multi-tenancy é first-class: `tenant_id` em todos os queries críticos
|
||||||
|
- Sistema de guards robusto: RBAC + entitlements + tenantFeatures + session race-condition handling
|
||||||
|
- `useRecurrence` é bem arquitetado: virtual occurrences no frontend + exceções no banco (sem N linhas futuras)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Bugs Críticos
|
||||||
|
|
||||||
|
### ✅ [RESOLVIDO] `useRecurrence.js` — variável `occurrenceCount` não declarada
|
||||||
|
|
||||||
|
**Bug original:** branches `custom_weekdays`, `monthly` e `yearly` usavam `occurrenceCount` sem declará-la → `ReferenceError` em runtime. Nenhum dos três contava ocorrências anteriores ao range, então `max_occurrences` nunca funcionava corretamente.
|
||||||
|
|
||||||
|
**Correção (Sessão 2):** Cada branch ganhou `let occurrenceCount = 0` + fase de pré-contagem de `ruleStart` até `effStart`.
|
||||||
|
|
||||||
|
**Resolvido em:** Sessão 2 — 2026-03-11
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ [RESOLVIDO] Exceção de remarcação fora do range não aparece
|
||||||
|
|
||||||
|
**Bug original:** `loadExceptions` só buscava `original_date` no range. Se `original_date` estivesse fora mas `new_date` caísse dentro, a sessão remarcada não aparecia.
|
||||||
|
|
||||||
|
**Correção (Sessão 3):**
|
||||||
|
- `loadExceptions`: duas queries paralelas — `q1` (original_date no range) + `q2` (reschedule com new_date no range). Mescladas e deduplicadas por `id`.
|
||||||
|
- `expandRules` post-pass: itera exceções não consumidas (`handledExIds`), injeta inbound reschedules com `buildOccurrence(rule, newDate, ex.original_date, ex)`.
|
||||||
|
|
||||||
|
**Resolvido em:** Sessão 3 — 2026-03-11
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Segurança
|
||||||
|
|
||||||
|
### ✅ [RESOLVIDO] SQL dumps no repositório
|
||||||
|
|
||||||
|
**Arquivos:** `schema.sql`, `backup.sql`, `data_dump.sql`, `full_dump.sql`
|
||||||
|
|
||||||
|
Verificado via `git log --all --full-history` — arquivos **nunca foram commitados**. Movidos para pasta externa ao repositório. Nenhuma purga de histórico necessária.
|
||||||
|
|
||||||
|
**Resolvido em:** Sessão 3 — 2026-03-11
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ [RESOLVIDO] `useAgendaEvents` — sem `tenant_id` em nenhuma operação
|
||||||
|
|
||||||
|
**Correção (Sessão 2):** `tenant_id` injetado em `create()`, `loadMyRange()`, `update()`, `remove()`, `removeSeriesFrom()`, `removeAllSeries()`. Helpers `assertTenantId()` e `getUid()` adicionados.
|
||||||
|
|
||||||
|
**Resolvido em:** Sessão 2 — 2026-03-11
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ [RESOLVIDO] `loadRules` em `useRecurrence` sem filtro `tenant_id`
|
||||||
|
|
||||||
|
**Correção (Sessão 2):** `loadRules` e `loadAndExpand` aceitam `tenantId` opcional e aplicam `.eq('tenant_id', tenantId)`. Call site em `AgendaTerapeutaPage._reloadRange` passa `tenantStore.activeTenantId`.
|
||||||
|
|
||||||
|
**Resolvido em:** Sessão 2 — 2026-03-11
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ [RESOLVIDO] `console.log` expõe dados de pacientes no browser
|
||||||
|
|
||||||
|
**Correção (Sessão 2):** Todos os `console.*` substituídos pelo `supportLogger`. Logs só aparecem quando modo suporte está ativo (token válido no banco).
|
||||||
|
|
||||||
|
**Resolvido em:** Sessão 2 — 2026-03-11
|
||||||
|
**Arquivos criados:** `src/support/supportLogger.js`, `src/support/supportDebugStore.js`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🟡 [ABERTO] `window.__guardsBound` / `window.__supabaseAuthListenerBound`
|
||||||
|
|
||||||
|
Usar `window.*` para controle de listeners é frágil em hot-reload.
|
||||||
|
**Solução:** Gerenciar via módulo singleton ou `app.config.globalProperties`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ [RESOLVIDO] `globalRole` do `profiles` sem cache no guard
|
||||||
|
|
||||||
|
Adicionados `globalRoleCacheUid` + `globalRoleCache` no `guards.js`.
|
||||||
|
Cache invalida em: uid change, SIGNED_OUT, SIGNED_IN com user diferente.
|
||||||
|
Query ao banco ocorre apenas na primeira navegação por sessão.
|
||||||
|
|
||||||
|
**Resolvido em:** Sessão 4 — 2026-03-11
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Duplicações e Inconsistências
|
||||||
|
|
||||||
|
### ✅ [RESOLVIDO] Dois composables para a mesma entidade
|
||||||
|
|
||||||
|
`src/composables/useAgendaEvents.js` era código morto (sem imports). Deletado.
|
||||||
|
O autoritativo `src/features/agenda/composables/useAgendaEvents.js` permanece.
|
||||||
|
|
||||||
|
**Resolvido em:** Sessão 3 — 2026-03-11
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ [RESOLVIDO] Dois mappers para agenda
|
||||||
|
|
||||||
|
`src/features/agenda/domain/agenda.mappers.js` estava vazio e sem imports. Deletado.
|
||||||
|
`src/features/agenda/domain/agenda.types.js` também sem imports. Deletado. Diretório `domain/` removido.
|
||||||
|
O autoritativo `src/features/agenda/services/agendaMappers.js` permanece.
|
||||||
|
|
||||||
|
**Resolvido em:** Sessão 4 — 2026-03-11
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ [RESOLVIDO] N+1 Query — migração `paciente_id` → `patient_id`
|
||||||
|
|
||||||
|
**Resolvido em:** Sessão 3 — 2026-03-11
|
||||||
|
|
||||||
|
**Migration executada:** `migrations/unify_patient_id.sql`
|
||||||
|
- UPDATE copiou `paciente_id` → `patient_id` onde null (resultado: 0 órfãos — todos já tinham `patient_id`)
|
||||||
|
- `ALTER TABLE agenda_eventos DROP COLUMN paciente_id` executado com sucesso
|
||||||
|
|
||||||
|
**Código atualizado:**
|
||||||
|
- `useAgendaEvents.js`: `paciente_id` removido do `BASE_SELECT`; `create()`/`update()` stripam `paciente_id` do payload
|
||||||
|
- `agendaRepository.js`: workaround N+1 de orphan ids removido
|
||||||
|
- `agendaMappers.js`: `paciente_id` agora é alias de `patient_id` (UI only)
|
||||||
|
- `AgendaTerapeutaPage.vue` + `AgendaClinicaPage.vue`: `pickDbFields` usa `patient_id`
|
||||||
|
- `AgendamentosRecebidosPage.vue`: `dbFields` removeu `paciente_id`
|
||||||
|
- `PatientsListPage.vue` + `AgendaEventDialog.vue`: `.or()` → `.eq('patient_id', id)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Limpeza Necessária
|
||||||
|
|
||||||
|
### ✅ [RESOLVIDO] Template Sakai removido — bundle de produção
|
||||||
|
|
||||||
|
**Resolvido em:** Sessão 3 — 2026-03-11
|
||||||
|
|
||||||
|
**Removidos:** `src/views/uikit/` (15 arquivos), `src/views/utilities/Blocks.vue`, `src/components/BlockViewer.vue`, `src/components/FloatingConfigurator.vue`, `src/views/pages/Documentation.vue`, `src/assets/demo/`, `src/navigation/menus/sakai.demo.menu.js`, `src/router/routes.demo.js`, `src/assets/styles.scss` (@use demo removido)
|
||||||
|
|
||||||
|
**Referências limpas:** `package.json` renomeado para `agenciapsi`, demoRoutes e sakaiDemoMenu removidos dos index files, `FloatingConfigurator` removido de Login, NotFound, Access, Error, ResetPasswordPage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🟡 [PARCIAL] Arquivos obsoletos no projeto
|
||||||
|
|
||||||
|
**Deletados (Sessão 4):**
|
||||||
|
- `src/layout/ConfiguracoesPage-old.vue`
|
||||||
|
- `src/features/agenda/domain/` (diretório inteiro — 2 arquivos não usados)
|
||||||
|
|
||||||
|
**Ainda presentes:**
|
||||||
|
- `src/layout/ConfiguracoesPage - Copia.vue` — verificar se está no git (staged como D)
|
||||||
|
- `src/views/pages/public/Landingpage-v1 - bkp.vue`
|
||||||
|
- `comandos.txt` (na raiz)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ [RESOLVIDO] Logs excessivos em produção
|
||||||
|
|
||||||
|
`console.time/timeLog/timeEnd/warn/error` em `guards.js` substituídos por `logGuard()`, `logError()`, `logPerf()`.
|
||||||
|
|
||||||
|
**Resolvido em:** Sessão 2 — 2026-03-11
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Status das Features do MVP
|
||||||
|
|
||||||
|
| Feature | Status | Observação |
|
||||||
|
|---|---|---|
|
||||||
|
| Agenda de sessões | ✅ Implementado | FullCalendar + composables |
|
||||||
|
| Cadastro de pacientes | ✅ Implementado | CRUD completo |
|
||||||
|
| Recorrência de sessões | ✅ Corrigido | Bugs de occurrenceCount e cross-range resolvidos |
|
||||||
|
| Sessões presenciais/online | ✅ Implementado | campo `modalidade` |
|
||||||
|
| Controle de faltas | ✅ Implementado | `exception_type = 'patient_missed'` |
|
||||||
|
| Remarcação | ✅ Corrigido | Bug cross-range resolvido (Sessão 3) |
|
||||||
|
| Bloqueio de agenda | ✅ Implementado | `BloqueioDialog.vue` |
|
||||||
|
| Agendamento online | ✅ Implementado | `AgendadorPublicoPage.vue` |
|
||||||
|
| Prontuário | ✅ Integrado | Seção "Sessões" adicionada ao `PatientProntuario.vue` |
|
||||||
|
| Notificações/lembretes | ❌ Não implementado | Sem trigger/edge function |
|
||||||
|
| Financeiro/faturamento | ⚠️ Parcial | Páginas de plano mas sem sessão→pagamento |
|
||||||
|
| Relatórios | ✅ Implementado | `RelatoriosPage.vue` — terapeuta — sessões, faltas, taxa, gráfico |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Backlog Técnico
|
||||||
|
|
||||||
|
- [ ] Cache de `globalRole` no guard (reduzir queries por navegação)
|
||||||
|
- [ ] Implementar notificações: WhatsApp/Email via Supabase Edge Functions
|
||||||
|
- [ ] Integração prontuário ↔ sessões
|
||||||
|
- [ ] Integração sessão ↔ pagamento (financeiro)
|
||||||
|
- [ ] Relatórios básicos: sessões realizadas, faltas, receita
|
||||||
|
- [ ] Migrar domínio de recorrência para TypeScript
|
||||||
|
- [ ] Consolidar dois mappers de agenda (`agendaMappers.js` vs `domain/agenda.mappers.js`)
|
||||||
|
- [ ] Remover arquivos obsoletos (ConfiguracoesPage-old, Landingpage-v1 bkp, etc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Prioridades de Ação
|
||||||
|
|
||||||
|
### ✅ Fazer AGORA — todos concluídos
|
||||||
|
1. `[x]` ~~Remover dumps SQL~~ → nunca commitados, movidos para fora do repo
|
||||||
|
2. `[x]` ~~Corrigir bug `occurrenceCount`~~ → pré-contagem em todos os branches
|
||||||
|
3. `[x]` ~~Adicionar `tenant_id` ao `useAgendaEvents` e `loadRules`~~ → injetado em todas as operações
|
||||||
|
4. `[x]` ~~Remover `console.log` com dados de pacientes~~ → `supportLogger`
|
||||||
|
|
||||||
|
### ✅ Fazer em seguida — todos concluídos
|
||||||
|
5. `[x]` ~~Corrigir bug remarcação cross-range~~ → 2 queries + post-pass em `expandRules`
|
||||||
|
6. `[x]` ~~Consolidar dois `useAgendaEvents`~~ → legado deletado
|
||||||
|
7. `[x]` ~~Unificar `paciente_id` + `patient_id`~~ → migration executada + código limpo
|
||||||
|
8. `[x]` ~~Remover Sakai de demo~~ → removido + menu SaaS limpo
|
||||||
|
|
||||||
|
### Backlog
|
||||||
|
9. `[x]` Cache de `globalRole` no guard — `globalRoleCacheUid/globalRoleCache` em guards.js
|
||||||
|
10. `[ ]` Notificações (WhatsApp/Email via Edge Functions) ← próximo
|
||||||
|
11. `[x]` Integração prontuário ↔ sessões — seção "Sessões" em `PatientProntuario.vue`
|
||||||
|
12. `[x]` Relatórios básicos — `RelatoriosPage.vue` em /therapist/relatorios
|
||||||
|
13. `[x]` Consolidar mappers de agenda — `domain/` deletado, `agendaMappers.js` é único
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Sistema de Suporte Técnico SaaS
|
||||||
|
|
||||||
|
Sistema seguro para admins SaaS acessarem a agenda de terapeutas em modo debug.
|
||||||
|
|
||||||
|
| Arquivo | Responsabilidade |
|
||||||
|
|---|---|
|
||||||
|
| `migrations/support_sessions.sql` | Tabela, índices, RLS, RPCs (token gerado via `gen_random_uuid()` duplo — sem pgcrypto) |
|
||||||
|
| `src/support/supportLogger.js` | Logger centralizado — silencioso fora do modo suporte |
|
||||||
|
| `src/support/supportDebugStore.js` | Store Pinia — valida token via RPC `validate_support_session` |
|
||||||
|
| `src/support/supportSessionService.js` | CRUD de sessões de suporte (criar/listar/revogar) |
|
||||||
|
| `src/support/components/SupportDebugBanner.vue` | Banner fixo na agenda com painel de logs filtráveis |
|
||||||
|
| `src/views/pages/saas/SaasSupportPage.vue` | Painel SaaS para gerenciar sessões de suporte |
|
||||||
|
|
||||||
|
**RPCs no banco:**
|
||||||
|
- `create_support_session(p_tenant_id, p_ttl_minutes)` → `{ token, expires_at, session_id }`
|
||||||
|
- `validate_support_session(p_token)` → `{ valid, tenant_id }`
|
||||||
|
- `revoke_support_session(p_token)` → `boolean`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Histórico de Sessões
|
||||||
|
|
||||||
|
### Sessão 1 — 2026-03-11
|
||||||
|
- Auditoria técnica completa gerada
|
||||||
|
- Nenhum item resolvido
|
||||||
|
|
||||||
|
### Sessão 2 — 2026-03-11
|
||||||
|
- Sistema de suporte técnico SaaS implementado (migration + 5 arquivos criados)
|
||||||
|
- Bug `occurrenceCount` corrigido (itens 2 e 4)
|
||||||
|
- `tenant_id` adicionado ao `useAgendaEvents` e `loadRules` (item 3)
|
||||||
|
- `console.*` substituídos por `supportLogger`
|
||||||
|
|
||||||
|
### Sessão 4 — 2026-03-11
|
||||||
|
- Cache `globalRole` adicionado ao guard (item 9) — sem query ao banco por navegação
|
||||||
|
- Integração prontuário ↔ sessões (item 11) — painel "Sessões" em `PatientProntuario.vue`
|
||||||
|
- `RelatoriosPage.vue` criada em `/therapist/relatorios` (item 12) — cards, gráfico Chart.js, tabela DataTable
|
||||||
|
- Consolidação mappers (item 13) — `domain/agenda.mappers.js` vazio deletado + `agenda.types.js` + dir `domain/`
|
||||||
|
- `ConfiguracoesPage-old.vue` deletado (limpeza)
|
||||||
|
|
||||||
|
### Sessão 3 — 2026-03-11
|
||||||
|
- Bug remarcação cross-range resolvido (item 5)
|
||||||
|
- `logPerf is not defined` em guards.js corrigido
|
||||||
|
- `pgcrypto` → substituído por `gen_random_uuid()` duplo no support_sessions
|
||||||
|
- Sakai demo removido completamente (item 8) + `styles.scss` corrigido
|
||||||
|
- `useAgendaEvents` legado deletado (item 6)
|
||||||
|
- `paciente_id` unificado em `patient_id` — migration executada (item 7)
|
||||||
|
- SQL dumps confirmados como nunca commitados (item 1 encerrado)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Para retomar: devolva este arquivo ao início da conversa e indique qual item quer atacar.*
|
||||||
BIN
Atestado_Psicológico_1774873197838.pdf
Normal file
BIN
Atestado_Psicológico_1774873197838.pdf
Normal file
Binary file not shown.
BIN
Atestado_Psicológico_1774873520538.pdf
Normal file
BIN
Atestado_Psicológico_1774873520538.pdf
Normal file
Binary file not shown.
163
CHANGELOG.md
163
CHANGELOG.md
@@ -1,70 +1,155 @@
|
|||||||
# Changelog
|
# CHANGELOG — Banco de Dados AgênciaPsi
|
||||||
|
|
||||||
## 4.3.0 (2025-02-26)
|
Registro histórico de todas as migrations aplicadas no banco.
|
||||||
|
Formato: data | arquivo | o que mudou | por quê
|
||||||
|
|
||||||
**Implemented New Features and Enhancements**
|
---
|
||||||
|
|
||||||
- Update PrimeVue version
|
## [001] — 2026-03-03
|
||||||
|
**Arquivo:** `migration_001.sql`
|
||||||
|
**Seed:** `seed_001.sql`
|
||||||
|
|
||||||
## 4.2.0 (2024-12-09)
|
### Contexto
|
||||||
|
O schema original foi construído de forma incremental e acumulou
|
||||||
|
inconsistências no modelo de identidade. Usuários não tinham um
|
||||||
|
tipo de conta definido formalmente, tenants não distinguiam
|
||||||
|
terapeuta de clínica, e não existia suporte a paciente como
|
||||||
|
tipo de conta de plataforma.
|
||||||
|
|
||||||
**Implemented New Features and Enhancements**
|
### O que mudou
|
||||||
|
|
||||||
- Refactored dashboard sections to components
|
#### `profiles`
|
||||||
- Migrate sass from @import to @use
|
- ✅ Adicionada coluna `account_type text NOT NULL DEFAULT 'free'`
|
||||||
|
- Valores: `free | patient | therapist | clinic`
|
||||||
|
- Imutável após sair de `free` (trigger `trg_account_type_immutable`)
|
||||||
|
- Usuários com role=`patient` migrados para `account_type='patient'`
|
||||||
|
- Usuários com tenant `saas` ativo migrados para `account_type='therapist'`
|
||||||
|
|
||||||
## 4.1.0 (2024-07-29)
|
#### `tenants`
|
||||||
|
- ✅ Novos valores aceitos em `kind`:
|
||||||
|
- `therapist` → terapeuta individual (substitui `saas`)
|
||||||
|
- `clinic_coworking` → clínica tipo 1: gestão de salas
|
||||||
|
- `clinic_reception` → clínica tipo 2: secretaria + múltiplas agendas
|
||||||
|
- `clinic_full` → clínica tipo 3: coworking + secretaria
|
||||||
|
- ✅ `kind` agora é imutável após criação (trigger `trg_tenant_kind_immutable`)
|
||||||
|
- ✅ 10 tenants `saas` órfãos (sem admin, sem subscriptions) deletados
|
||||||
|
- ✅ Tenants `saas` com admin ativo migrados para `kind='therapist'`
|
||||||
|
- ⚠️ `saas` e `clinic` (legados) mantidos no CHECK por compatibilidade.
|
||||||
|
Não criar novos tenants com esses kinds.
|
||||||
|
|
||||||
- Changed menu button location at topbar
|
#### `plans`
|
||||||
- Add border to overlay menu
|
- ✅ Adicionado `patient` como valor válido em `target`
|
||||||
- Animation for mobile mask
|
- ✅ Inserido plano `patient_free` (gratuito, target=patient)
|
||||||
- Fixed chart colors
|
|
||||||
|
|
||||||
## 4.0.0 (2024-07-29)
|
#### Novas funções
|
||||||
|
| Função | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `provision_account_tenant(user_id, kind, name?)` | Cria tenant + membership + atualiza account_type. Chamar no onboarding. |
|
||||||
|
| `is_therapist_tenant(tenant_id)` | Retorna true se tenant é do tipo therapist |
|
||||||
|
| `is_clinic_tenant(tenant_id)` | Atualizada: inclui todos os subtipos de clínica |
|
||||||
|
| `guard_tenant_kind_immutable()` | Trigger: bloqueia alteração de tenants.kind |
|
||||||
|
| `guard_account_type_immutable()` | Trigger: bloqueia alteração de account_type após escolha |
|
||||||
|
| `guard_patient_cannot_own_tenant()` | Trigger: bloqueia paciente de ser tenant_admin/therapist |
|
||||||
|
|
||||||
- Updated to PrimeVue v4
|
#### Funções atualizadas
|
||||||
|
| Função | O que mudou |
|
||||||
|
|--------|-------------|
|
||||||
|
| `handle_new_user()` | Agora insere `account_type='free'` |
|
||||||
|
| `handle_new_user_create_personal_tenant()` | Desabilitada — tenant criado no onboarding |
|
||||||
|
| `ensure_personal_tenant()` | Busca por `kind IN ('therapist','saas')` e delega para `provision_account_tenant` |
|
||||||
|
|
||||||
## 3.10.0 (2024-03-11)
|
### Regras de negócio agora garantidas no banco
|
||||||
|
1. **Paciente é para sempre paciente** — `account_type` imutável após escolha
|
||||||
|
2. **Terapeuta nunca vira clínica e vice-versa** — `tenants.kind` imutável
|
||||||
|
3. **Paciente não pode ter tenant** — trigger bloqueia na inserção
|
||||||
|
4. **Cada tipo de conta tem seu tipo de tenant** — `provision_account_tenant` garante
|
||||||
|
|
||||||
**Migration Guide**
|
### Usuários de seed (apenas dev/staging)
|
||||||
|
| Email | Tipo | Tenant |
|
||||||
|
|-------|------|--------|
|
||||||
|
| paciente@agenciapsi.com.br | patient | nenhum |
|
||||||
|
| terapeuta@agenciapsi.com.br | therapist | tenant próprio (therapist) + vinculado à Clínica 3 |
|
||||||
|
| clinica1@agenciapsi.com.br | clinic | clinic_coworking |
|
||||||
|
| clinica2@agenciapsi.com.br | clinic | clinic_reception |
|
||||||
|
| clinica3@agenciapsi.com.br | clinic | clinic_full |
|
||||||
|
| saas@agenciapsi.com.br | saas_admin | nenhum |
|
||||||
|
> Senha de todos: `Teste@123`
|
||||||
|
|
||||||
- Update theme files.
|
---
|
||||||
|
|
||||||
**Implemented New Features and Enhancements**
|
## [002] — seed_002.sql
|
||||||
|
|
||||||
- Upgrade to PrimeVue 3.49.1
|
**Arquivo:** `Novo-DB/seed_002.sql`
|
||||||
|
|
||||||
## 3.9.0 (2023-11-01)
|
### O que cria
|
||||||
|
|
||||||
**Migration Guide**
|
#### Migration embutida
|
||||||
|
- ✅ `profiles.platform_roles text[] NOT NULL DEFAULT '{}'` — adicionada via `ADD COLUMN IF NOT EXISTS` (idempotente)
|
||||||
|
|
||||||
- Update theme files.
|
#### Usuários de teste
|
||||||
|
| Email | Senha | Papel | Tenant |
|
||||||
|
|-------|-------|-------|--------|
|
||||||
|
| `supervisor@agenciapsi.com.br` | `Teste@123` | `supervisor` em `tenant_members` | Clínica Bem Estar (Full) |
|
||||||
|
| `editor@agenciapsi.com.br` | `Teste@123` | `therapist` em `tenant_members` + `platform_roles = '{editor}'` | Clínica Bem Estar (Full) |
|
||||||
|
|
||||||
**Implemented New Features and Enhancements**
|
UUIDs reservados:
|
||||||
|
- Supervisor: `aaaaaaaa-0007-0007-0007-000000000007`
|
||||||
|
- Editor: `aaaaaaaa-0008-0008-0008-000000000008`
|
||||||
|
|
||||||
- Upgrade to PrimeVue 3.39.0
|
---
|
||||||
|
|
||||||
## 3.8.0 (2023-07-24)
|
## [PENDENTE] — Migration necessária: `platform_roles` em `profiles`
|
||||||
|
|
||||||
**Migration Guide**
|
**Contexto:**
|
||||||
|
Implementação das áreas de **Supervisor** (papel de tenant) e **Editor** (papel de plataforma).
|
||||||
|
O papel de Editor é atribuído pelo `saas_admin` e armazenado diretamente no perfil do usuário,
|
||||||
|
independente de qual tenant ele pertence.
|
||||||
|
|
||||||
- Update theme files.
|
### O que precisa ser aplicado no banco
|
||||||
- Update assets style files
|
|
||||||
- Remove code highlight
|
|
||||||
|
|
||||||
**Implemented New Features and Enhancements**
|
#### `profiles`
|
||||||
|
- ⚠️ **Adicionar coluna** `platform_roles text[] NOT NULL DEFAULT '{}'`
|
||||||
|
- Armazena papéis globais de plataforma. Ex.: `'{editor}'`
|
||||||
|
- Quem pode escrever: somente `saas_admin` (via RLS ou função privilegiada)
|
||||||
|
- Quem pode ter: qualquer usuário autenticado, **exceto** `account_type = 'patient'`
|
||||||
|
- Valores previstos: `editor` (mais podem ser adicionados futuramente)
|
||||||
|
|
||||||
- Upgrade to PrimeVue 3.30.2
|
#### SQL sugerido
|
||||||
|
```sql
|
||||||
|
ALTER TABLE public.profiles
|
||||||
|
ADD COLUMN IF NOT EXISTS platform_roles text[] NOT NULL DEFAULT '{}';
|
||||||
|
|
||||||
## 3.7.0 (2023-05-06)
|
-- Comentário descritivo
|
||||||
|
COMMENT ON COLUMN public.profiles.platform_roles IS
|
||||||
|
'Papéis globais de plataforma, independentes de tenant. Ex: editor de microlearning. Atribuído pelo saas_admin.';
|
||||||
|
|
||||||
- Upgrade to PrimeVue 3.28.0
|
-- RLS: somente saas_admin pode atualizar platform_roles (exemplo)
|
||||||
|
-- CREATE POLICY "saas_admin pode atualizar platform_roles"
|
||||||
|
-- ON public.profiles FOR UPDATE
|
||||||
|
-- USING (auth.uid() IN (SELECT id FROM public.profiles WHERE role = 'saas_admin'))
|
||||||
|
-- WITH CHECK (true);
|
||||||
|
```
|
||||||
|
|
||||||
**Implemented New Features and Enhancements**
|
#### `tenant_members` (sem alteração necessária)
|
||||||
|
- O papel `supervisor` já é suportado como valor text em `tenant_members.role`.
|
||||||
|
- Nenhuma alteração de schema é necessária — basta inserir memberships com `role = 'supervisor'`.
|
||||||
|
|
||||||
## 3.6.0 (2023-04-12)
|
### Impacto se não aplicado
|
||||||
|
- Área do Editor (`/editor`) fica inacessível a todos (coluna ausente → `platform_roles` vem `null` → acesso negado).
|
||||||
|
- Área do Supervisor (`/supervisor`) funciona normalmente — não depende desta migration.
|
||||||
|
|
||||||
**Implemented New Features and Enhancements**
|
---
|
||||||
|
|
||||||
- Upgrade to PrimeVue 3.26.1
|
## Futuro — registrado mas não implementado
|
||||||
- Upgrade to vite 4.2.1
|
|
||||||
|
### Vínculo Terapeuta ↔ Clínica (a implementar)
|
||||||
|
- Terapeuta autoriza explicitamente que secretaria gerencie suas sessões
|
||||||
|
- Permissão só válida se clínica tiver `kind IN ('clinic_reception', 'clinic_full')`
|
||||||
|
- Secretaria acessa apenas sessões — não prontuário nem anotações
|
||||||
|
- Dissociação bloqueada se houver `agenda_eventos` futuros (`inicio_em > now()`)
|
||||||
|
- Após dissociação: cada parte fica com seus próprios pacientes
|
||||||
|
- Requer: coluna de permissão no vínculo + função de dissociação com validação
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Última atualização: 2026-03-03*
|
||||||
|
|||||||
232
TESTES.md
Normal file
232
TESTES.md
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
# Guia de Testes — AgenciaPsi
|
||||||
|
|
||||||
|
## Testes Automatizados
|
||||||
|
|
||||||
|
### Pré-requisito
|
||||||
|
Vitest já instalado (`npm install` resolve). Não precisa de banco, Supabase ou variáveis de ambiente.
|
||||||
|
|
||||||
|
### Comandos
|
||||||
|
|
||||||
|
| Comando | Descrição |
|
||||||
|
|---|---|
|
||||||
|
| `npm test` | Roda todos os testes uma vez e exibe resultado |
|
||||||
|
| `npm run test:watch` | Modo watch — re-roda ao salvar arquivos |
|
||||||
|
| `npm run test:ui` | Abre UI visual no browser (`http://localhost:51204`) |
|
||||||
|
|
||||||
|
### Arquivos de teste
|
||||||
|
|
||||||
|
| Arquivo | O que cobre |
|
||||||
|
|---|---|
|
||||||
|
| `src/features/agenda/composables/__tests__/useRecurrence.spec.js` | Geração de datas por tipo de regra, max_occurrences global, exceções, remarcação cross-range |
|
||||||
|
| `src/features/agenda/services/__tests__/agendaMappers.spec.js` | Mapeamento para FullCalendar, ícones de status, cores, buildNextSessions, minutesToDuration, buildWeeklyBreakBackgroundEvents |
|
||||||
|
|
||||||
|
### Quando rodar
|
||||||
|
- Antes de commitar qualquer mudança em `useRecurrence.js` ou `agendaMappers.js`
|
||||||
|
- Ao adicionar novo tipo de frequência (mensal, quinzenal, etc.)
|
||||||
|
- Ao mexer em exceções de recorrência
|
||||||
|
- Em CI/CD antes do deploy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testes Manuais
|
||||||
|
|
||||||
|
### Preparação
|
||||||
|
1. Limpar dados de teste no banco:
|
||||||
|
```sql
|
||||||
|
TRUNCATE TABLE recurrence_exceptions CASCADE;
|
||||||
|
TRUNCATE TABLE recurrence_rules CASCADE;
|
||||||
|
TRUNCATE TABLE agenda_eventos CASCADE;
|
||||||
|
TRUNCATE TABLE agendador_solicitacoes CASCADE;
|
||||||
|
```
|
||||||
|
2. Fazer login com seu usuário real
|
||||||
|
3. Selecionar a clínica/tenant correto
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. Evento Avulso
|
||||||
|
|
||||||
|
| Passo | Esperado |
|
||||||
|
|---|---|
|
||||||
|
| Clicar em um horário livre na agenda | Dialog de criação abre |
|
||||||
|
| Preencher paciente, horário, modalidade → Salvar | Evento aparece no calendário |
|
||||||
|
| Clicar no evento → Editar horário → Salvar | Horário atualiza |
|
||||||
|
| Clicar no evento → Marcar como "Faltou" | Cor muda para vermelho, ícone ✗ |
|
||||||
|
| Clicar no evento → Marcar como "Realizado" | Cor muda para cinza, ícone ✓ |
|
||||||
|
| Clicar no evento → Cancelar sessão | Cor muda para laranja, ícone ∅ |
|
||||||
|
| Clicar no evento → Excluir | Evento some do calendário |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Recorrência Semanal
|
||||||
|
|
||||||
|
| Passo | Esperado |
|
||||||
|
|---|---|
|
||||||
|
| Criar evento com frequência "Semanal" | Ocorrências aparecem em todas as semanas seguintes com ícone ↻ |
|
||||||
|
| Navegar para a semana seguinte | Ocorrências continuam aparecendo |
|
||||||
|
| Navegar para além do end_date | Não aparecem ocorrências após a data final |
|
||||||
|
| Criar série com "4 sessões" (max_occurrences) | Exatamente 4 ocorrências visíveis no calendário |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Recorrência Quinzenal e Dias Específicos
|
||||||
|
|
||||||
|
| Passo | Esperado |
|
||||||
|
|---|---|
|
||||||
|
| Criar série "Quinzenal" | Ocorrências aparecem a cada 2 semanas |
|
||||||
|
| Criar série "Dias específicos" (ex: seg + qua) | Ambos os dias aparecem toda semana |
|
||||||
|
| Navegar para semanas futuras | Padrão se mantém |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Edição de Série
|
||||||
|
|
||||||
|
| Passo | Esperado |
|
||||||
|
|---|---|
|
||||||
|
| Clicar em ocorrência → Editar → "Somente este" → mudar horário | Só aquela data muda; as outras continuam iguais |
|
||||||
|
| Clicar em ocorrência → Cancelar → "Somente este" | Só aquela data some (ou aparece cancelada) |
|
||||||
|
| Clicar em ocorrência → Cancelar → "Este e os seguintes" | A partir daquela data, sem mais ocorrências |
|
||||||
|
| Clicar em ocorrência → Cancelar → "Todos" | Série inteira some |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Remarcação Cross-Range ⭐
|
||||||
|
|
||||||
|
Este é o caso mais importante a testar.
|
||||||
|
|
||||||
|
| Passo | Esperado |
|
||||||
|
|---|---|
|
||||||
|
| Criar série semanal (ex: toda segunda) | Ocorrências nas segundas |
|
||||||
|
| Clicar na sessão da **semana 1** → Remarcar para **terça da semana 2** | — |
|
||||||
|
| Navegar para a **semana 1** | Segunda da semana 1 aparece vazia ou como "remarcado" |
|
||||||
|
| Navegar para a **semana 2** | Terça aparece com ícone ↺ e status "remarcado" |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Bloqueio de Agenda
|
||||||
|
|
||||||
|
| Passo | Esperado |
|
||||||
|
|---|---|
|
||||||
|
| Criar bloqueio de horário | Aparece no calendário com visual diferente (ícone ⊘) |
|
||||||
|
| Tentar agendar no horário bloqueado | Aviso de conflito |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Agendamento Online (Agendador Público)
|
||||||
|
|
||||||
|
| Passo | Esperado |
|
||||||
|
|---|---|
|
||||||
|
| Acessar URL pública do agendador | Página pública abre sem login |
|
||||||
|
| Selecionar data/horário disponível → Enviar solicitação | Confirmação exibida |
|
||||||
|
| No painel do terapeuta → "Agendamentos Recebidos" | Solicitação aparece na lista |
|
||||||
|
| Clicar em "Confirmar" | Evento criado na agenda |
|
||||||
|
| Clicar em "Recusar" | Solicitação removida, sem evento na agenda |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. Suporte Técnico SaaS
|
||||||
|
|
||||||
|
| Passo | Esperado |
|
||||||
|
|---|---|
|
||||||
|
| Logar como `saas_admin` → Menu "Suporte Técnico" | Página de suporte abre |
|
||||||
|
| Selecionar um tenant → "Criar Sessão de Suporte" | URL com token é gerada |
|
||||||
|
| Copiar URL e abrir em outra aba | Agenda do terapeuta abre com banner de debug no rodapé |
|
||||||
|
| No banner → filtrar logs por categoria | Logs filtram corretamente |
|
||||||
|
| No banner → "Desativar suporte" | Banner some |
|
||||||
|
| No painel SaaS → "Revogar" na sessão ativa | Token invalidado |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. Multi-Tenancy (se você tem 2 clínicas cadastradas)
|
||||||
|
|
||||||
|
| Passo | Esperado |
|
||||||
|
|---|---|
|
||||||
|
| Criar evento na clínica A | Evento aparece na agenda da clínica A |
|
||||||
|
| Trocar para clínica B | Evento da clínica A **não aparece** |
|
||||||
|
| Criar evento na clínica B | Aparece apenas na clínica B |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pedindo ao Claude para Executar os Testes
|
||||||
|
|
||||||
|
### Como usar o Claude Code para rodar e corrigir testes
|
||||||
|
|
||||||
|
O Claude Code (este agente) consegue rodar os testes, ler os erros e corrigir os problemas automaticamente. Basta iniciar a conversa com o contexto certo.
|
||||||
|
|
||||||
|
### Prompt de retomada recomendado
|
||||||
|
|
||||||
|
Cole isso no início de uma nova sessão com o Claude:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> Estou desenvolvendo o AgenciaPsi. Temos testes automatizados com Vitest.
|
||||||
|
>
|
||||||
|
> **Arquivos de teste:**
|
||||||
|
> - `src/features/agenda/composables/__tests__/useRecurrence.spec.js` — testa `generateDates`, `expandRules`, `mergeWithStoredSessions`
|
||||||
|
> - `src/features/agenda/services/__tests__/agendaMappers.spec.js` — testa mapeamento para FullCalendar
|
||||||
|
>
|
||||||
|
> **Rodar os testes:** `npm test`
|
||||||
|
>
|
||||||
|
> Por favor, rode os testes agora e me informe o resultado. Se houver falhas, analise a causa e corrija.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### O que o Claude consegue fazer automaticamente
|
||||||
|
|
||||||
|
| Pedido | O Claude faz |
|
||||||
|
|---|---|
|
||||||
|
| "Rode os testes" | Executa `npm test` e exibe o resultado |
|
||||||
|
| "Tem algum teste falhando?" | Roda e diagnóstica a causa raiz |
|
||||||
|
| "Corrija os testes que falham" | Analisa erro, ajusta o código ou o teste e re-roda |
|
||||||
|
| "Adicionei a funcionalidade X, crie testes para ela" | Lê o código e escreve novos casos no spec |
|
||||||
|
| "O teste Y está errado, o comportamento correto é Z" | Atualiza a asserção e confirma que passa |
|
||||||
|
|
||||||
|
### Boas práticas ao pedir testes ao Claude
|
||||||
|
|
||||||
|
- **Forneça o `AUDITORIA.md`** no início da sessão — dá contexto sobre a arquitetura e decisões já tomadas
|
||||||
|
- **Descreva o comportamento esperado** em português, não o código — o Claude escreve o código do teste
|
||||||
|
- **Se um teste falhar e você achar que o código está certo**, diga isso explicitamente: *"o teste está errado, não o código"* — o Claude vai ajustar a asserção
|
||||||
|
- **Se um teste falhar e você achar que o código está errado**, diga: *"o comportamento esperado é X"* — o Claude vai corrigir a implementação
|
||||||
|
|
||||||
|
### Exemplo de sessão típica
|
||||||
|
|
||||||
|
```
|
||||||
|
Você: Rodei npm test e 2 testes falharam. Analise e corrija.
|
||||||
|
|
||||||
|
Claude: [roda npm test, lê os erros, corrige o código ou as asserções, re-roda até 63/63 passarem]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adicionando Novos Testes
|
||||||
|
|
||||||
|
### Para `useRecurrence.spec.js`
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { generateDates, expandRules, mergeWithStoredSessions } from '../useRecurrence.js'
|
||||||
|
|
||||||
|
it('meu novo caso', () => {
|
||||||
|
const r = {
|
||||||
|
id: 'rule-1', type: 'weekly', weekdays: [1], interval: 1,
|
||||||
|
start_date: '2026-03-02', end_date: null, max_occurrences: null,
|
||||||
|
status: 'ativo', start_time: '09:00', end_time: '10:00',
|
||||||
|
// ... outros campos necessários
|
||||||
|
}
|
||||||
|
const dates = generateDates(r, new Date(2026, 2, 1), new Date(2026, 2, 31))
|
||||||
|
expect(dates.length).toBe(/* esperado */)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Para `agendaMappers.spec.js`
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { mapAgendaEventosToCalendarEvents } from '../agendaMappers.js'
|
||||||
|
|
||||||
|
it('meu novo caso de mapeamento', () => {
|
||||||
|
const [ev] = mapAgendaEventosToCalendarEvents([{
|
||||||
|
id: 'ev-1', titulo: 'Teste', tipo: 'sessao', status: 'agendado',
|
||||||
|
inicio_em: '2026-03-10T09:00:00', fim_em: '2026-03-10T10:00:00',
|
||||||
|
owner_id: 'owner-1',
|
||||||
|
}])
|
||||||
|
expect(ev.extendedProps./* campo */).toBe(/* esperado */)
|
||||||
|
})
|
||||||
|
```
|
||||||
159
blueprints/DialogConfirmation-blueprint.md
Normal file
159
blueprints/DialogConfirmation-blueprint.md
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
# DialogConfirmation — Padrão de Componente
|
||||||
|
|
||||||
|
> **Stack**: Vue 3 + PrimeVue 4 + Tailwind CSS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Regras gerais
|
||||||
|
|
||||||
|
| Propriedade | Valor obrigatório |
|
||||||
|
|---|---|
|
||||||
|
| `group` | sempre `"headless"` — desacopla o template do trigger |
|
||||||
|
| `ConfirmDialog` | declarado **uma única vez**, no componente pai (página) |
|
||||||
|
| Filhos | disparam via `useConfirm()` com `group: 'headless'` — sem declarar `ConfirmDialog` próprio |
|
||||||
|
| `icon` | passado em `confirm.require({ icon })` — classe PrimeIcons sem o prefixo `pi` (ex: `'pi-trash'`) |
|
||||||
|
| `color` | passado em `confirm.require({ color })` — hex; define o fundo do círculo e a cor do botão Confirmar |
|
||||||
|
| `ConfirmationService` | obrigatório em `main.js` — `app.use(ConfirmationService)` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arquitetura pai / filho
|
||||||
|
|
||||||
|
```
|
||||||
|
Pai (página)
|
||||||
|
└── <ConfirmDialog group="headless" /> ← único, renderiza aqui
|
||||||
|
├── Filho A (componente qualquer) → confirm.require({ group: 'headless', ... })
|
||||||
|
└── Filho B (Dialog interno) → confirm.require({ group: 'headless', ... })
|
||||||
|
```
|
||||||
|
|
||||||
|
> O `ConfirmDialog` **não** deve ser colocado dentro de um `<Dialog>` filho — isso causaria dois popups simultâneos. Sempre no pai.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup obrigatório — `main.js`
|
||||||
|
|
||||||
|
```js
|
||||||
|
import ConfirmationService from 'primevue/confirmationservice'
|
||||||
|
|
||||||
|
app.use(ConfirmationService) // sem isso, useConfirm() não funciona
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Template do `ConfirmDialog` (somente no pai)
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- Declarado uma única vez, antes do conteúdo principal -->
|
||||||
|
<ConfirmDialog group="headless">
|
||||||
|
<template #container="{ message, acceptCallback, rejectCallback }">
|
||||||
|
<div class="flex flex-col items-center p-8 bg-surface-0 dark:bg-surface-900 rounded-xl shadow-xl">
|
||||||
|
|
||||||
|
<!-- Círculo central: cor e ícone vindos de message -->
|
||||||
|
<div
|
||||||
|
class="rounded-full inline-flex justify-center items-center h-24 w-24 -mt-20"
|
||||||
|
:style="{ background: message.color || 'var(--p-primary-color)', color: '#fff' }"
|
||||||
|
>
|
||||||
|
<i :class="`pi ${message.icon || 'pi-question'} !text-4xl`"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="font-bold text-2xl block mb-2 mt-6">{{ message.header }}</span>
|
||||||
|
<p class="mb-0 text-center text-[var(--text-color-secondary)]">{{ message.message }}</p>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 mt-6">
|
||||||
|
<!-- Confirmar: cor dinâmica via message.color -->
|
||||||
|
<Button
|
||||||
|
label="Confirmar"
|
||||||
|
class="rounded-full"
|
||||||
|
:style="{
|
||||||
|
background: message.color || 'var(--p-primary-color)',
|
||||||
|
borderColor: message.color || 'var(--p-primary-color)'
|
||||||
|
}"
|
||||||
|
@click="acceptCallback"
|
||||||
|
/>
|
||||||
|
<!-- Cancelar: sempre outlined, neutro -->
|
||||||
|
<Button
|
||||||
|
label="Cancelar"
|
||||||
|
variant="outlined"
|
||||||
|
class="rounded-full"
|
||||||
|
@click="rejectCallback"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ConfirmDialog>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Uso nos componentes (pai ou filhos)
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { useConfirm } from 'primevue/useconfirm'
|
||||||
|
const confirm = useConfirm()
|
||||||
|
|
||||||
|
function confirmDelete(item) {
|
||||||
|
confirm.require({
|
||||||
|
group: 'headless',
|
||||||
|
header: 'Excluir item?',
|
||||||
|
message: `"${item.name}" será removido permanentemente. Essa ação não pode ser desfeita.`,
|
||||||
|
icon: 'pi-trash',
|
||||||
|
color: '#ef4444',
|
||||||
|
accept: () => onDelete(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Paleta de ícones e cores por ação
|
||||||
|
|
||||||
|
| Ação | `icon` | `color` | Observação |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Excluir / Remover | `pi-trash` | `#ef4444` | Vermelho — ação destrutiva |
|
||||||
|
| Salvar / Confirmar | `pi-save` | `var(--p-primary-color)` | Cor primária do tema |
|
||||||
|
| Editar / Atualizar | `pi-pencil` | `#f97316` | Laranja — mudança de estado |
|
||||||
|
| Aviso / Atenção | `pi-exclamation-triangle` | `#eab308` | Amarelo — ação reversível |
|
||||||
|
| Info / Neutro | `pi-info-circle` | `#3b82f6` | Azul — informativo |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Referência completa de `confirm.require`
|
||||||
|
|
||||||
|
```js
|
||||||
|
confirm.require({
|
||||||
|
group: 'headless', // obrigatório — aponta para o ConfirmDialog correto
|
||||||
|
header: 'Título do popup', // linha em negrito
|
||||||
|
message: 'Descrição clara.', // linha secundária
|
||||||
|
icon: 'pi-trash', // sufixo PrimeIcons sem o "pi " inicial
|
||||||
|
color: '#ef4444', // hex — fundo do círculo + cor do botão Confirmar
|
||||||
|
accept: () => { /* ação confirmada */ },
|
||||||
|
reject: () => { /* opcional — ação cancelada */ }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checklist antes de usar
|
||||||
|
|
||||||
|
- [ ] `ConfirmationService` registrado no `main.js`
|
||||||
|
- [ ] `<ConfirmDialog group="headless">` declarado **apenas no pai**, antes do conteúdo
|
||||||
|
- [ ] Filhos usam `useConfirm()` com `group: 'headless'` — sem `ConfirmDialog` próprio
|
||||||
|
- [ ] `icon` passado como sufixo PrimeIcons: `'pi-trash'`, não `'pi pi-trash'`
|
||||||
|
- [ ] `color` em hex para ações com semântica de cor (delete = `#ef4444`)
|
||||||
|
- [ ] `header` curto e direto | `message` com contexto suficiente para o usuário decidir
|
||||||
|
- [ ] `accept` contém a ação real — `reject` é opcional
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Variações de confirmação
|
||||||
|
|
||||||
|
| Contexto | `header` | `icon` | `color` |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Excluir registro | `'Excluir <entidade>?'` | `pi-trash` | `#ef4444` |
|
||||||
|
| Remover item de lista | `'Remover campo?'` | `pi-trash` | `#ef4444` |
|
||||||
|
| Salvar com impacto | `'Confirmar alterações?'` | `pi-save` | primária |
|
||||||
|
| Atualizar com risco | `'Atualizar <entidade>?'` | `pi-pencil` | `#f97316` |
|
||||||
|
| Ação irreversível genérica | `'Tem certeza?'` | `pi-exclamation-triangle` | `#eab308` |
|
||||||
183
blueprints/dialog-blueprint.md
Normal file
183
blueprints/dialog-blueprint.md
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
# Dialog — Padrão de Componente
|
||||||
|
|
||||||
|
> **Stack**: Vue 3 + PrimeVue 4 + Tailwind CSS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Regras gerais
|
||||||
|
|
||||||
|
| Propriedade | Valor obrigatório |
|
||||||
|
|---|---|
|
||||||
|
| `modal` | sempre `true` |
|
||||||
|
| `maximizable` | sempre presente — botão nativo do PrimeVue, sem estado manual |
|
||||||
|
| `:draggable` | sempre `false` |
|
||||||
|
| `:closable` | `!saving` — desabilita o X durante operações assíncronas |
|
||||||
|
| `:dismissableMask` | `!saving` — impede fechar clicando fora durante saving |
|
||||||
|
| `pt:mask:class` | `backdrop-blur-xs` |
|
||||||
|
| Largura | `w-[50rem]` (padrão); responsivo via `:breakpoints` |
|
||||||
|
| Breakpoints | `{ '1199px': '90vw', '768px': '94vw' }` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estrutura obrigatória
|
||||||
|
|
||||||
|
```
|
||||||
|
<Dialog>
|
||||||
|
├── #header ← dot de cor (se aplicável), título/subtítulo, btn Excluir
|
||||||
|
├── Banner ← preview visual (opcional — apenas quando há cor/identidade visual)
|
||||||
|
├── Corpo ← campos do formulário
|
||||||
|
└── #footer ← Cancelar (flat) | Salvar (primary)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuração completa do `<Dialog>`
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<Dialog
|
||||||
|
v-model:visible="visible"
|
||||||
|
modal
|
||||||
|
:draggable="false"
|
||||||
|
:closable="!saving"
|
||||||
|
:dismissableMask="!saving"
|
||||||
|
maximizable
|
||||||
|
class="dc-dialog w-[50rem]"
|
||||||
|
:breakpoints="{ '1199px': '90vw', '768px': '94vw' }"
|
||||||
|
:pt="{
|
||||||
|
header: { class: '!p-3 !rounded-t-[12px] border-b border-[var(--surface-border)] shadow-[0_1px_0_0_rgba(255,255,255,0.06)] bg-gray-100' },
|
||||||
|
content: { class: '!p-3' },
|
||||||
|
footer: { class: '!p-0 !rounded-b-[12px] border-t border-[var(--surface-border)] shadow-[0_1px_0_0_rgba(255,255,255,0.06)] bg-gray-100' },
|
||||||
|
pcCloseButton: { root: { class: '!rounded-md hover:!text-red-500' } },
|
||||||
|
pcMaximizeButton: { root: { class: '!rounded-md hover:!text-primary' } },
|
||||||
|
}"
|
||||||
|
pt:mask:class="backdrop-blur-xs"
|
||||||
|
>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Detalhes do `pt`
|
||||||
|
|
||||||
|
| Chave | O que faz |
|
||||||
|
|---|---|
|
||||||
|
| `header` | `!p-3` padding uniforme; `!rounded-t-[12px]` borda top arredondada; `border-b` + `shadow` separador com profundidade; `bg-gray-100` fundo levemente cinza |
|
||||||
|
| `content` | `!p-3` padding interno do corpo |
|
||||||
|
| `footer` | `!p-0` remove padding nativo (controlado pelo wrapper interno); `!rounded-b-[12px]` borda bottom arredondada; `border-t` + `shadow` separador; `bg-gray-100` fundo levemente cinza |
|
||||||
|
| `pcCloseButton` | `!rounded-md` remove o círculo nativo; `hover:!text-red-500` feedback de danger no hover |
|
||||||
|
| `pcMaximizeButton` | `!rounded-md` remove o círculo nativo; `hover:!text-primary` feedback de cor primária no hover |
|
||||||
|
|
||||||
|
> O `!` (important) é necessário porque o PrimeVue injeta estilos inline nos botões e no root do Dialog — sem ele o Tailwind perde a disputa de especificidade.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Header — slot `#header`
|
||||||
|
|
||||||
|
```
|
||||||
|
[dot-cor] [título / subtítulo] [btn-excluir] ← Close e Maximize nativos vêm após
|
||||||
|
```
|
||||||
|
|
||||||
|
- O PrimeVue injeta **Maximize** e **Close** automaticamente à direita do slot `#header`.
|
||||||
|
- O botão **Excluir** fica **sempre no header**, nunca no footer.
|
||||||
|
- Excluir desabilitado quando o registro é nativo/padrão: `:disabled="saving || isNativeRecord"`.
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template #header>
|
||||||
|
<div class="flex w-full items-center justify-between gap-3 px-1">
|
||||||
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
|
<!-- Dot de cor (omitir se não houver cor associada) -->
|
||||||
|
<span
|
||||||
|
class="shrink-0 w-3.5 h-3.5 rounded-full border-2 border-white/30
|
||||||
|
shadow-[0_0_0_3px_rgba(0,0,0,0.08)] transition-colors duration-200"
|
||||||
|
:style="{ backgroundColor: previewBgColor }"
|
||||||
|
/>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<div class="text-base font-semibold truncate">
|
||||||
|
{{ form.name || (mode === 'create' ? 'Novo item' : 'Editar item') }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs opacity-50">
|
||||||
|
{{ mode === 'create' ? 'Criando novo registro' : 'Editando registro' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
|
<!-- Excluir — visível apenas em edit, desabilitado se nativo -->
|
||||||
|
<Button
|
||||||
|
v-if="mode === 'edit' && canDelete !== undefined"
|
||||||
|
icon="pi pi-trash"
|
||||||
|
severity="danger"
|
||||||
|
text
|
||||||
|
rounded
|
||||||
|
:disabled="saving || isNativeRecord"
|
||||||
|
v-tooltip.top="'Excluir'"
|
||||||
|
@click="emitDelete"
|
||||||
|
/>
|
||||||
|
<!-- Maximize e Close nativos do PrimeVue são injetados aqui automaticamente -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Footer — slot `#footer`
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex items-center justify-end gap-2 px-3 py-3">
|
||||||
|
<!-- Cancelar: sempre flat, hover vermelho suave -->
|
||||||
|
<Button
|
||||||
|
label="Cancelar"
|
||||||
|
severity="secondary"
|
||||||
|
text
|
||||||
|
class="rounded-full hover:!text-red-500"
|
||||||
|
:disabled="saving"
|
||||||
|
@click="close"
|
||||||
|
/>
|
||||||
|
<!-- Salvar: sempre primary -->
|
||||||
|
<Button
|
||||||
|
label="Salvar"
|
||||||
|
icon="pi pi-check"
|
||||||
|
class="rounded-full"
|
||||||
|
:loading="saving"
|
||||||
|
:disabled="!canSubmit"
|
||||||
|
@click="submit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Regra**: Cancelar = `severity="secondary" text` + `hover:!text-red-500`. Salvar = primary (sem severity, usa o padrão do tema). Padding controlado pelo `div` interno (`px-3 py-3`), não pelo `pt.footer`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Maximizar
|
||||||
|
|
||||||
|
Use a prop nativa `maximizable`. O PrimeVue injeta e gerencia o botão automaticamente — sem `ref`, sem `isMaximized`, sem `<Button>` manual.
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<Dialog maximizable ...>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checklist antes de publicar um Dialog
|
||||||
|
|
||||||
|
- [ ] `modal`, `:draggable="false"`, `:closable="!saving"`, `:dismissableMask="!saving"` presentes
|
||||||
|
- [ ] `maximizable` na prop (botão nativo, sem estado manual)
|
||||||
|
- [ ] `class="dc-dialog w-[50rem]"` + `:breakpoints="{ '1199px': '90vw', '768px': '94vw' }"`
|
||||||
|
- [ ] `pt` completo: header, content, footer, pcCloseButton, pcMaximizeButton
|
||||||
|
- [ ] Header com `bg-gray-100`, `border-b`, shadow e `!rounded-t-[12px]`
|
||||||
|
- [ ] Footer com `bg-gray-100`, `border-t`, shadow e `!rounded-b-[12px]`
|
||||||
|
- [ ] Botão **Excluir** no header (nunca no footer), desabilitado se nativo
|
||||||
|
- [ ] Cancelar = `text` + `hover:!text-red-500` | Salvar = primary
|
||||||
|
- [ ] Padding do footer via `px-3 py-3` no `div` interno
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Variações de largura
|
||||||
|
|
||||||
|
| Uso | Classe |
|
||||||
|
|---|---|
|
||||||
|
| Formulário simples | `w-[36rem]` |
|
||||||
|
| Formulário padrão | `w-[50rem]` ← **padrão** |
|
||||||
|
| Formulário complexo | `w-[70rem]` |
|
||||||
|
| Tela cheia | `maximizable` — usuário controla |
|
||||||
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`)
|
||||||
119
database-novo/README.md
Normal file
119
database-novo/README.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# database-novo
|
||||||
|
|
||||||
|
Banco de dados do AgenciaPsi — organizado, documentado e com CLI para gerenciamento.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd database-novo
|
||||||
|
|
||||||
|
# Instalação do zero (schema + fixes + seeds + backup)
|
||||||
|
node db.cjs setup
|
||||||
|
|
||||||
|
# Ver estado do banco
|
||||||
|
node db.cjs status
|
||||||
|
|
||||||
|
# Backup
|
||||||
|
node db.cjs backup
|
||||||
|
|
||||||
|
# Restaurar (perdi o banco!)
|
||||||
|
node db.cjs restore
|
||||||
|
```
|
||||||
|
|
||||||
|
Para o guia completo, veja **`docs/setup_guide.md`**.
|
||||||
|
|
||||||
|
## Comandos do CLI
|
||||||
|
|
||||||
|
| Comando | O que faz |
|
||||||
|
|---------|-----------|
|
||||||
|
| `node db.cjs setup` | Instala do zero (schema + fixes + seeds) |
|
||||||
|
| `node db.cjs backup` | Exporta backup com data para `backups/` |
|
||||||
|
| `node db.cjs restore [data]` | Restaura de um backup |
|
||||||
|
| `node db.cjs migrate` | Aplica migrations pendentes |
|
||||||
|
| `node db.cjs seed [grupo]` | Roda seeds (all, users, system, test_data) |
|
||||||
|
| `node db.cjs status` | Estado do banco, backups, migrations |
|
||||||
|
| `node db.cjs diff` | Compara schema atual vs último backup |
|
||||||
|
| `node db.cjs reset` | Reseta e reinstala tudo |
|
||||||
|
| `node db.cjs verify` | Verifica integridade dos dados |
|
||||||
|
|
||||||
|
## Estrutura
|
||||||
|
|
||||||
|
```
|
||||||
|
database-novo/
|
||||||
|
│
|
||||||
|
├── db.cjs # CLI de gerenciamento do banco
|
||||||
|
├── db.config.json # Configuração (container, seeds, fixes)
|
||||||
|
│
|
||||||
|
├── schema/ # Schema SQL separado por seção
|
||||||
|
│ ├── 00_full/schema.sql # Schema completo (referência)
|
||||||
|
│ ├── 01_extensions/ # Schemas + extensões PostgreSQL
|
||||||
|
│ ├── 02_types/ # Enums (auth, public, infra)
|
||||||
|
│ ├── 03_functions/ # 11 arquivos por domínio
|
||||||
|
│ ├── 04_tables/ # 10 arquivos por domínio
|
||||||
|
│ ├── 05_views/ # 24 views
|
||||||
|
│ ├── 06_indexes/ # Índices
|
||||||
|
│ ├── 07_foreign_keys/ # PKs, FKs, constraints
|
||||||
|
│ ├── 08_triggers/ # Triggers
|
||||||
|
│ ├── 09_policies/ # 217 RLS policies
|
||||||
|
│ └── 10_grants/ # Grants
|
||||||
|
│
|
||||||
|
├── seeds/ # Seeds de dados
|
||||||
|
│ ├── seed_001_fixed.sql # 6 usuários base + tenants
|
||||||
|
│ ├── seed_002.sql # Supervisor + Editor
|
||||||
|
│ ├── seed_003.sql # Therapist2, Therapist3, Secretary
|
||||||
|
│ ├── seed_010_plans.sql # 7 planos + 4 preços
|
||||||
|
│ ├── seed_011_features.sql # 26 features
|
||||||
|
│ ├── seed_012_plan_features.sql # 85 vínculos plano↔feature
|
||||||
|
│ ├── seed_013_subscriptions.sql # 9 subscriptions + compromissos
|
||||||
|
│ ├── seed_014_global_data.sql # 11 email + 16 notif templates + 3 slides
|
||||||
|
│ ├── seed_020_test_data.sql # Dados de teste (50 pacientes, eventos, etc.)
|
||||||
|
│ ├── seed_020_test_data_cleanup.sql # Limpeza dos dados de teste
|
||||||
|
│ └── run_all_seeds.sh # Script bash alternativo
|
||||||
|
│
|
||||||
|
├── migrations/ # Migrations incrementais
|
||||||
|
│
|
||||||
|
├── fixes/ # 7 correções aplicadas
|
||||||
|
│
|
||||||
|
├── backups/ # Backups com data (auto-gerenciados)
|
||||||
|
│ └── 2026-03-23/ # schema.sql + data.sql + full_dump.sql
|
||||||
|
│
|
||||||
|
└── docs/ # Documentação
|
||||||
|
├── setup_guide.md # Guia completo de instalação e uso
|
||||||
|
├── schema_map.md # Mapa das 84 tabelas
|
||||||
|
├── business_rules.md # Regras de negócio
|
||||||
|
└── users_test.md # 11 usuários de teste (UUIDs + vínculos)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Planos
|
||||||
|
|
||||||
|
| Key | Target | Preço | Limites |
|
||||||
|
|-----|--------|-------|---------|
|
||||||
|
| `patient_free` | patient | R$0 | — |
|
||||||
|
| `therapist_free` | therapist | R$0 | 40 agendamentos/mês, 50 lembretes/mês |
|
||||||
|
| `therapist_pro` | therapist | R$49/mês · R$490/ano | Ilimitado |
|
||||||
|
| `clinic_free` | clinic | R$0 | 30 pacientes, 5 terapeutas, 40 agend/mês |
|
||||||
|
| `clinic_pro` | clinic | R$149/mês · R$1490/ano | Ilimitado |
|
||||||
|
| `supervisor_free` | supervisor | R$0 | Até 3 supervisionados |
|
||||||
|
| `supervisor_pro` | supervisor | R$0 | Até 20 supervisionados |
|
||||||
|
|
||||||
|
## Usuários de Teste
|
||||||
|
|
||||||
|
Senha de todos: `Teste@123`
|
||||||
|
|
||||||
|
| Email | Plano | Tipo |
|
||||||
|
|-------|-------|------|
|
||||||
|
| paciente@agenciapsi.com.br | patient_free | Paciente |
|
||||||
|
| terapeuta@agenciapsi.com.br | therapist_free | Terapeuta solo + Clínica 3 |
|
||||||
|
| clinica1@agenciapsi.com.br | clinic_free | Clínica coworking |
|
||||||
|
| clinica2@agenciapsi.com.br | clinic_free | Clínica recepção |
|
||||||
|
| clinica3@agenciapsi.com.br | clinic_free | Clínica full |
|
||||||
|
| saas@agenciapsi.com.br | — | Admin plataforma |
|
||||||
|
| supervisor@agenciapsi.com.br | supervisor_free | Supervisor |
|
||||||
|
| editor@agenciapsi.com.br | therapist_free | Editor |
|
||||||
|
| therapist2@agenciapsi.com.br | therapist_free | Terapeuta |
|
||||||
|
| therapist3@agenciapsi.com.br | therapist_free | Terapeuta |
|
||||||
|
| secretary@agenciapsi.com.br | — | Secretária (Clínica 2) |
|
||||||
|
|
||||||
|
## Idempotência
|
||||||
|
|
||||||
|
Todos os seeds são idempotentes (ON CONFLICT DO UPDATE ou DELETE + INSERT). Podem ser re-executados quantas vezes necessário.
|
||||||
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
1347
database-novo/backups/2026-03-23/data.sql
Normal file
1347
database-novo/backups/2026-03-23/data.sql
Normal file
File diff suppressed because it is too large
Load Diff
20658
database-novo/backups/2026-03-23/full_dump.sql
Normal file
20658
database-novo/backups/2026-03-23/full_dump.sql
Normal file
File diff suppressed because it is too large
Load Diff
18970
database-novo/backups/2026-03-23/schema.sql
Normal file
18970
database-novo/backups/2026-03-23/schema.sql
Normal file
File diff suppressed because it is too large
Load Diff
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
733
database-novo/db.cjs
Normal file
733
database-novo/db.cjs
Normal file
@@ -0,0 +1,733 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// =============================================================================
|
||||||
|
// AgenciaPsi — Database CLI
|
||||||
|
// =============================================================================
|
||||||
|
// Uso: node db.cjs <comando> [opcoes]
|
||||||
|
//
|
||||||
|
// Comandos:
|
||||||
|
// setup Instalação do zero (schema + seeds)
|
||||||
|
// backup Exporta backup com data atual
|
||||||
|
// restore [data] Restaura de um backup (ex: 2026-03-23)
|
||||||
|
// migrate Aplica migrations pendentes
|
||||||
|
// seed [grupo] Roda seeds (all|users|system|test_data)
|
||||||
|
// status Mostra estado atual do banco
|
||||||
|
// diff Compara schema atual vs último backup
|
||||||
|
// reset Reseta o banco e reinstala tudo
|
||||||
|
// verify Verifica integridade dos dados essenciais
|
||||||
|
// help Mostra ajuda
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const { execSync, spawnSync } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Config
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const ROOT = __dirname;
|
||||||
|
const CONFIG = JSON.parse(fs.readFileSync(path.join(ROOT, 'db.config.json'), 'utf8'));
|
||||||
|
const CONTAINER = CONFIG.container;
|
||||||
|
const DB = CONFIG.database;
|
||||||
|
const USER = CONFIG.user;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Colors (sem dependências externas)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const c = {
|
||||||
|
reset: '\x1b[0m',
|
||||||
|
bold: '\x1b[1m',
|
||||||
|
red: '\x1b[31m',
|
||||||
|
green: '\x1b[32m',
|
||||||
|
yellow: '\x1b[33m',
|
||||||
|
blue: '\x1b[34m',
|
||||||
|
cyan: '\x1b[36m',
|
||||||
|
gray: '\x1b[90m'
|
||||||
|
};
|
||||||
|
|
||||||
|
function log(msg) {
|
||||||
|
console.log(msg);
|
||||||
|
}
|
||||||
|
function info(msg) {
|
||||||
|
log(`${c.cyan}ℹ${c.reset} ${msg}`);
|
||||||
|
}
|
||||||
|
function ok(msg) {
|
||||||
|
log(`${c.green}✔${c.reset} ${msg}`);
|
||||||
|
}
|
||||||
|
function warn(msg) {
|
||||||
|
log(`${c.yellow}⚠${c.reset} ${msg}`);
|
||||||
|
}
|
||||||
|
function err(msg) {
|
||||||
|
log(`${c.red}✖${c.reset} ${msg}`);
|
||||||
|
}
|
||||||
|
function title(msg) {
|
||||||
|
log(`\n${c.bold}${c.blue}═══ ${msg} ═══${c.reset}\n`);
|
||||||
|
}
|
||||||
|
function step(msg) {
|
||||||
|
log(`${c.gray} →${c.reset} ${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function dockerRunning() {
|
||||||
|
try {
|
||||||
|
const result = spawnSync('docker', ['inspect', '-f', '{{.State.Running}}', CONTAINER], {
|
||||||
|
encoding: 'utf8',
|
||||||
|
timeout: 10000,
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
return result.stdout.trim() === 'true';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function psql(sql, opts = {}) {
|
||||||
|
const cmd = `docker exec -i -e PGCLIENTENCODING=UTF8 ${CONTAINER} psql -U ${USER} -d ${DB} ${opts.tuples ? '-t' : ''} ${opts.quiet ? '-q' : ''} -c "${sql.replace(/"/g, '\\"')}"`;
|
||||||
|
return execSync(cmd, { encoding: 'utf8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, PYTHONIOENCODING: 'utf-8', LANG: 'C.UTF-8' } }).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function psqlFile(filePath) {
|
||||||
|
const absPath = path.resolve(filePath);
|
||||||
|
const content = fs.readFileSync(absPath, 'utf8');
|
||||||
|
// Prepend SET client_encoding to ensure UTF-8 inside the session
|
||||||
|
const utf8Content = "SET client_encoding TO 'UTF8';\n" + content;
|
||||||
|
const cmd = `docker exec -i -e PGCLIENTENCODING=UTF8 ${CONTAINER} psql -U ${USER} -d ${DB} -q`;
|
||||||
|
return execSync(cmd, { input: utf8Content, encoding: 'utf8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, PYTHONIOENCODING: 'utf-8', LANG: 'C.UTF-8' } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function pgDump(args) {
|
||||||
|
const cmd = `docker exec -e PGCLIENTENCODING=UTF8 ${CONTAINER} pg_dump -U ${USER} -d ${DB} ${args}`;
|
||||||
|
return execSync(cmd, { encoding: 'utf8', timeout: 120000, maxBuffer: 50 * 1024 * 1024 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function today() {
|
||||||
|
return new Date().toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileHash(filePath) {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureDir(dir) {
|
||||||
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireDocker() {
|
||||||
|
if (!dockerRunning()) {
|
||||||
|
err(`Container "${CONTAINER}" não está rodando.`);
|
||||||
|
log(`\n Inicie o Supabase primeiro:`);
|
||||||
|
log(` ${c.cyan}npx supabase start${c.reset}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function listBackups() {
|
||||||
|
const dir = path.join(ROOT, 'backups');
|
||||||
|
if (!fs.existsSync(dir)) return [];
|
||||||
|
return fs
|
||||||
|
.readdirSync(dir)
|
||||||
|
.filter((f) => /^\d{4}-\d{2}-\d{2}$/.test(f))
|
||||||
|
.sort()
|
||||||
|
.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Migration tracking table
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function ensureMigrationTable() {
|
||||||
|
psql(`
|
||||||
|
CREATE TABLE IF NOT EXISTS _db_migrations (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
filename TEXT NOT NULL UNIQUE,
|
||||||
|
hash TEXT NOT NULL,
|
||||||
|
category TEXT NOT NULL DEFAULT 'migration',
|
||||||
|
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAppliedMigrations() {
|
||||||
|
ensureMigrationTable();
|
||||||
|
const result = psql('SELECT filename, hash, category, applied_at::text FROM _db_migrations ORDER BY id;', { tuples: true });
|
||||||
|
if (!result) return [];
|
||||||
|
return result
|
||||||
|
.split('\n')
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((line) => {
|
||||||
|
const [filename, hash, category, applied_at] = line.split('|').map((s) => s.trim());
|
||||||
|
return { filename, hash, category, applied_at };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function recordMigration(filename, hash, category) {
|
||||||
|
psql(`INSERT INTO _db_migrations (filename, hash, category) VALUES ('${filename}', '${hash}', '${category}') ON CONFLICT (filename) DO UPDATE SET hash = EXCLUDED.hash, applied_at = now();`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Commands
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const commands = {};
|
||||||
|
|
||||||
|
// ---- SETUP ----
|
||||||
|
commands.setup = function () {
|
||||||
|
title('Setup — Instalação do zero');
|
||||||
|
requireDocker();
|
||||||
|
|
||||||
|
// 1. Schema
|
||||||
|
const schemaFile = path.join(ROOT, CONFIG.schema);
|
||||||
|
if (!fs.existsSync(schemaFile)) {
|
||||||
|
err(`Schema não encontrado: ${schemaFile}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
info('Aplicando schema...');
|
||||||
|
psqlFile(schemaFile);
|
||||||
|
ok('Schema aplicado');
|
||||||
|
|
||||||
|
// 2. Fixes
|
||||||
|
info('Aplicando fixes...');
|
||||||
|
for (const fix of CONFIG.fixes) {
|
||||||
|
const fixPath = path.join(ROOT, 'fixes', fix);
|
||||||
|
if (fs.existsSync(fixPath)) {
|
||||||
|
step(fix);
|
||||||
|
psqlFile(fixPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ok(`${CONFIG.fixes.length} fixes aplicados`);
|
||||||
|
|
||||||
|
// 3. Seeds
|
||||||
|
commands.seed('all');
|
||||||
|
|
||||||
|
// 4. Migration table
|
||||||
|
ensureMigrationTable();
|
||||||
|
|
||||||
|
// 5. Record seeds as applied
|
||||||
|
const allSeeds = [...CONFIG.seeds.users, ...CONFIG.seeds.system];
|
||||||
|
for (const seed of allSeeds) {
|
||||||
|
const seedPath = path.join(ROOT, 'seeds', seed);
|
||||||
|
if (fs.existsSync(seedPath)) {
|
||||||
|
recordMigration(seed, fileHash(seedPath), 'seed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const fix of CONFIG.fixes) {
|
||||||
|
const fixPath = path.join(ROOT, 'fixes', fix);
|
||||||
|
if (fs.existsSync(fixPath)) {
|
||||||
|
recordMigration(fix, fileHash(fixPath), 'fix');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ok('Setup completo!');
|
||||||
|
log('');
|
||||||
|
|
||||||
|
// 6. Auto-backup
|
||||||
|
info('Criando backup pós-setup...');
|
||||||
|
commands.backup();
|
||||||
|
|
||||||
|
// 7. Verify
|
||||||
|
commands.verify();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- BACKUP ----
|
||||||
|
commands.backup = function () {
|
||||||
|
title('Backup');
|
||||||
|
requireDocker();
|
||||||
|
|
||||||
|
const date = today();
|
||||||
|
const dir = path.join(ROOT, 'backups', date);
|
||||||
|
ensureDir(dir);
|
||||||
|
|
||||||
|
const infraSchemas = ['storage', 'realtime', '_realtime', 'supabase_functions', 'extensions', 'graphql', 'graphql_public', 'pgsodium', 'vault', 'net', '_analytics'];
|
||||||
|
const excludeFlags = infraSchemas.map((s) => `--exclude-schema=${s}`).join(' ');
|
||||||
|
|
||||||
|
step('Exportando schema...');
|
||||||
|
const schema = pgDump('--schema-only --no-owner --no-privileges');
|
||||||
|
fs.writeFileSync(path.join(dir, 'schema.sql'), schema);
|
||||||
|
|
||||||
|
step('Exportando dados...');
|
||||||
|
const data = pgDump(`--data-only --no-owner --no-privileges ${excludeFlags}`);
|
||||||
|
fs.writeFileSync(path.join(dir, 'data.sql'), data);
|
||||||
|
|
||||||
|
step('Exportando dump completo...');
|
||||||
|
const full = pgDump('--no-owner --no-privileges');
|
||||||
|
fs.writeFileSync(path.join(dir, 'full_dump.sql'), full);
|
||||||
|
|
||||||
|
const sizes = ['schema.sql', 'data.sql', 'full_dump.sql'].map((f) => {
|
||||||
|
const stat = fs.statSync(path.join(dir, f));
|
||||||
|
return `${f}: ${(stat.size / 1024).toFixed(0)}KB`;
|
||||||
|
});
|
||||||
|
|
||||||
|
ok(`Backup salvo em backups/${date}/`);
|
||||||
|
sizes.forEach((s) => step(s));
|
||||||
|
|
||||||
|
// Cleanup old backups
|
||||||
|
cleanupBackups();
|
||||||
|
};
|
||||||
|
|
||||||
|
function cleanupBackups() {
|
||||||
|
const backups = listBackups();
|
||||||
|
const cutoff = new Date();
|
||||||
|
cutoff.setDate(cutoff.getDate() - CONFIG.backupRetentionDays);
|
||||||
|
const cutoffStr = cutoff.toISOString().slice(0, 10);
|
||||||
|
|
||||||
|
let removed = 0;
|
||||||
|
for (const b of backups) {
|
||||||
|
if (b < cutoffStr) {
|
||||||
|
const dir = path.join(ROOT, 'backups', b);
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (removed > 0) {
|
||||||
|
info(`${removed} backup(s) antigo(s) removido(s) (>${CONFIG.backupRetentionDays} dias)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- RESTORE ----
|
||||||
|
commands.restore = function (dateArg) {
|
||||||
|
title('Restore');
|
||||||
|
requireDocker();
|
||||||
|
|
||||||
|
const backups = listBackups();
|
||||||
|
if (backups.length === 0) {
|
||||||
|
err('Nenhum backup encontrado.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = dateArg || backups[0];
|
||||||
|
const dir = path.join(ROOT, 'backups', date);
|
||||||
|
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
err(`Backup não encontrado: ${date}`);
|
||||||
|
log(`\n Backups disponíveis:`);
|
||||||
|
backups.forEach((b) => log(` ${c.cyan}${b}${c.reset}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullDump = path.join(dir, 'full_dump.sql');
|
||||||
|
const schemaFile = path.join(dir, 'schema.sql');
|
||||||
|
const dataFile = path.join(dir, 'data.sql');
|
||||||
|
|
||||||
|
// Safety backup before restore
|
||||||
|
info('Criando backup de segurança antes do restore...');
|
||||||
|
try {
|
||||||
|
commands.backup();
|
||||||
|
} catch {
|
||||||
|
warn('Não foi possível criar backup de segurança (banco pode estar vazio)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(fullDump)) {
|
||||||
|
info(`Restaurando de backups/${date}/full_dump.sql ...`);
|
||||||
|
|
||||||
|
// Drop and recreate public schema
|
||||||
|
step('Limpando schema public...');
|
||||||
|
psql('DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO postgres; GRANT ALL ON SCHEMA public TO public;');
|
||||||
|
|
||||||
|
step('Aplicando full dump...');
|
||||||
|
psqlFile(fullDump);
|
||||||
|
} else if (fs.existsSync(schemaFile) && fs.existsSync(dataFile)) {
|
||||||
|
info(`Restaurando de backups/${date}/ (schema + data)...`);
|
||||||
|
|
||||||
|
step('Limpando schema public...');
|
||||||
|
psql('DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO postgres; GRANT ALL ON SCHEMA public TO public;');
|
||||||
|
|
||||||
|
step('Aplicando schema...');
|
||||||
|
psqlFile(schemaFile);
|
||||||
|
|
||||||
|
step('Aplicando dados...');
|
||||||
|
psqlFile(dataFile);
|
||||||
|
} else {
|
||||||
|
err(`Backup incompleto em ${date}/`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ok(`Banco restaurado de backups/${date}/`);
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
commands.verify();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- MIGRATE ----
|
||||||
|
commands.migrate = function () {
|
||||||
|
title('Migrate');
|
||||||
|
requireDocker();
|
||||||
|
ensureMigrationTable();
|
||||||
|
|
||||||
|
const migrationsDir = path.join(ROOT, 'migrations');
|
||||||
|
if (!fs.existsSync(migrationsDir)) {
|
||||||
|
info('Nenhuma pasta migrations/ encontrada.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs
|
||||||
|
.readdirSync(migrationsDir)
|
||||||
|
.filter((f) => f.endsWith('.sql'))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
info('Nenhuma migration encontrada.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const applied = getAppliedMigrations().map((m) => m.filename);
|
||||||
|
const pending = files.filter((f) => !applied.includes(f));
|
||||||
|
|
||||||
|
if (pending.length === 0) {
|
||||||
|
ok('Todas as migrations já foram aplicadas.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-backup before migrating
|
||||||
|
info('Criando backup antes de migrar...');
|
||||||
|
commands.backup();
|
||||||
|
|
||||||
|
info(`${pending.length} migration(s) pendente(s):`);
|
||||||
|
for (const file of pending) {
|
||||||
|
step(`Aplicando ${file}...`);
|
||||||
|
const filePath = path.join(migrationsDir, file);
|
||||||
|
try {
|
||||||
|
psqlFile(filePath);
|
||||||
|
recordMigration(file, fileHash(filePath), 'migration');
|
||||||
|
ok(` ${file}`);
|
||||||
|
} catch (e) {
|
||||||
|
err(` FALHA em ${file}: ${e.message}`);
|
||||||
|
err('Migration abortada. Banco pode estar em estado parcial.');
|
||||||
|
err('Use "node db.cjs restore" para voltar ao backup.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ok(`${pending.length} migration(s) aplicada(s)`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- SEED ----
|
||||||
|
commands.seed = function (group) {
|
||||||
|
const validGroups = ['all', 'users', 'system', 'test_data'];
|
||||||
|
if (!group) group = 'all';
|
||||||
|
|
||||||
|
if (!validGroups.includes(group)) {
|
||||||
|
err(`Grupo inválido: ${group}`);
|
||||||
|
log(` Grupos válidos: ${validGroups.join(', ')}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
title(`Seeds — ${group}`);
|
||||||
|
requireDocker();
|
||||||
|
|
||||||
|
const groups = group === 'all' ? ['users', 'system'] : [group];
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
for (const g of groups) {
|
||||||
|
const seeds = CONFIG.seeds[g];
|
||||||
|
if (!seeds || seeds.length === 0) continue;
|
||||||
|
|
||||||
|
info(`Grupo: ${g}`);
|
||||||
|
for (const seed of seeds) {
|
||||||
|
const seedPath = path.join(ROOT, 'seeds', seed);
|
||||||
|
if (!fs.existsSync(seedPath)) {
|
||||||
|
warn(` Arquivo não encontrado: ${seed}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
step(seed);
|
||||||
|
try {
|
||||||
|
psqlFile(seedPath);
|
||||||
|
total++;
|
||||||
|
} catch (e) {
|
||||||
|
err(` FALHA em ${seed}: ${e.stderr || e.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ok(`${total} seed(s) aplicado(s)`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- STATUS ----
|
||||||
|
commands.status = function () {
|
||||||
|
title('Status');
|
||||||
|
requireDocker();
|
||||||
|
|
||||||
|
// Docker
|
||||||
|
ok(`Container: ${CONTAINER} (rodando)`);
|
||||||
|
|
||||||
|
// Backups
|
||||||
|
const backups = listBackups();
|
||||||
|
if (backups.length > 0) {
|
||||||
|
ok(`Último backup: ${backups[0]}`);
|
||||||
|
info(`Total de backups: ${backups.length}`);
|
||||||
|
} else {
|
||||||
|
warn('Nenhum backup encontrado');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrations
|
||||||
|
try {
|
||||||
|
const applied = getAppliedMigrations();
|
||||||
|
if (applied.length > 0) {
|
||||||
|
info(`Migrations aplicadas: ${applied.length}`);
|
||||||
|
applied.slice(-5).forEach((m) => {
|
||||||
|
step(`${m.filename} ${c.gray}(${m.category}, ${m.applied_at})${c.reset}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pending
|
||||||
|
const migrationsDir = path.join(ROOT, 'migrations');
|
||||||
|
if (fs.existsSync(migrationsDir)) {
|
||||||
|
const files = fs.readdirSync(migrationsDir).filter((f) => f.endsWith('.sql'));
|
||||||
|
const pending = files.filter((f) => !applied.map((m) => m.filename).includes(f));
|
||||||
|
if (pending.length > 0) {
|
||||||
|
warn(`${pending.length} migration(s) pendente(s):`);
|
||||||
|
pending.forEach((f) => step(`${c.yellow}${f}${c.reset}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
info('Tabela _db_migrations não existe (rode setup primeiro)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// DB counts
|
||||||
|
log('');
|
||||||
|
info('Dados no banco:');
|
||||||
|
const counts = [
|
||||||
|
['auth.users', 'SELECT count(*) FROM auth.users'],
|
||||||
|
['profiles', 'SELECT count(*) FROM profiles'],
|
||||||
|
['tenants', 'SELECT count(*) FROM tenants'],
|
||||||
|
['plans', 'SELECT count(*) FROM plans'],
|
||||||
|
['features', 'SELECT count(*) FROM features'],
|
||||||
|
['plan_features', 'SELECT count(*) FROM plan_features'],
|
||||||
|
['subscriptions', 'SELECT count(*) FROM subscriptions'],
|
||||||
|
['email_templates_global', 'SELECT count(*) FROM email_templates_global'],
|
||||||
|
['notification_templates', 'SELECT count(*) FROM notification_templates']
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [label, sql] of counts) {
|
||||||
|
try {
|
||||||
|
const count = psql(sql, { tuples: true }).trim();
|
||||||
|
const color = parseInt(count) > 0 ? c.green : c.red;
|
||||||
|
step(`${label}: ${color}${count}${c.reset}`);
|
||||||
|
} catch {
|
||||||
|
step(`${label}: ${c.gray}(tabela não existe)${c.reset}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- DIFF ----
|
||||||
|
commands.diff = function () {
|
||||||
|
title('Diff — Schema');
|
||||||
|
requireDocker();
|
||||||
|
|
||||||
|
const backups = listBackups();
|
||||||
|
if (backups.length === 0) {
|
||||||
|
err('Nenhum backup para comparar. Rode "node db.cjs backup" primeiro.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastBackup = backups[0];
|
||||||
|
const lastSchemaPath = path.join(ROOT, 'backups', lastBackup, 'schema.sql');
|
||||||
|
if (!fs.existsSync(lastSchemaPath)) {
|
||||||
|
err(`Schema não encontrado no backup ${lastBackup}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
info('Exportando schema atual...');
|
||||||
|
const currentSchema = pgDump('--schema-only --no-owner --no-privileges');
|
||||||
|
|
||||||
|
const lastSchema = fs.readFileSync(lastSchemaPath, 'utf8');
|
||||||
|
|
||||||
|
// Extract table definitions for comparison
|
||||||
|
const extractTables = (sql) => {
|
||||||
|
const tables = {};
|
||||||
|
const regex = /CREATE TABLE (?:IF NOT EXISTS )?(\S+)\s*\(([\s\S]*?)\);/g;
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(sql)) !== null) {
|
||||||
|
tables[match[1]] = match[2].trim();
|
||||||
|
}
|
||||||
|
return tables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentTables = extractTables(currentSchema);
|
||||||
|
const lastTables = extractTables(lastSchema);
|
||||||
|
|
||||||
|
const allTables = new Set([...Object.keys(currentTables), ...Object.keys(lastTables)]);
|
||||||
|
|
||||||
|
let added = 0,
|
||||||
|
removed = 0,
|
||||||
|
changed = 0,
|
||||||
|
unchanged = 0;
|
||||||
|
|
||||||
|
for (const table of [...allTables].sort()) {
|
||||||
|
if (!lastTables[table]) {
|
||||||
|
log(` ${c.green}+ ${table}${c.reset} (nova)`);
|
||||||
|
added++;
|
||||||
|
} else if (!currentTables[table]) {
|
||||||
|
log(` ${c.red}- ${table}${c.reset} (removida)`);
|
||||||
|
removed++;
|
||||||
|
} else if (currentTables[table] !== lastTables[table]) {
|
||||||
|
log(` ${c.yellow}~ ${table}${c.reset} (alterada)`);
|
||||||
|
changed++;
|
||||||
|
} else {
|
||||||
|
unchanged++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log('');
|
||||||
|
ok(`Comparado com backup de ${lastBackup}:`);
|
||||||
|
step(`${added} nova(s), ${changed} alterada(s), ${removed} removida(s), ${unchanged} sem mudança`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- RESET ----
|
||||||
|
commands.reset = function () {
|
||||||
|
title('Reset — CUIDADO');
|
||||||
|
requireDocker();
|
||||||
|
|
||||||
|
// Safety backup
|
||||||
|
info('Criando backup antes do reset...');
|
||||||
|
try {
|
||||||
|
commands.backup();
|
||||||
|
} catch {
|
||||||
|
warn('Não foi possível criar backup');
|
||||||
|
}
|
||||||
|
|
||||||
|
warn('Resetando schema public...');
|
||||||
|
psql('DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO postgres; GRANT ALL ON SCHEMA public TO public;');
|
||||||
|
ok('Schema public resetado');
|
||||||
|
|
||||||
|
// Re-run setup
|
||||||
|
commands.setup();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- VERIFY ----
|
||||||
|
commands.verify = function () {
|
||||||
|
title('Verificação de integridade');
|
||||||
|
requireDocker();
|
||||||
|
|
||||||
|
const checks = [
|
||||||
|
{ name: 'auth.users', sql: 'SELECT count(*) FROM auth.users', min: 1 },
|
||||||
|
{ name: 'profiles', sql: 'SELECT count(*) FROM profiles', min: 1 },
|
||||||
|
{ name: 'tenants', sql: 'SELECT count(*) FROM tenants', min: 1 },
|
||||||
|
{ name: 'plans', sql: 'SELECT count(*) FROM plans', min: 7 },
|
||||||
|
{ name: 'features', sql: 'SELECT count(*) FROM features', min: 20 },
|
||||||
|
{ name: 'plan_features', sql: 'SELECT count(*) FROM plan_features', min: 50 },
|
||||||
|
{ name: 'subscriptions', sql: 'SELECT count(*) FROM subscriptions', min: 1 },
|
||||||
|
{ name: 'email_templates', sql: 'SELECT count(*) FROM email_templates_global', min: 10 }
|
||||||
|
];
|
||||||
|
|
||||||
|
let pass = 0,
|
||||||
|
fail = 0;
|
||||||
|
|
||||||
|
for (const check of checks) {
|
||||||
|
try {
|
||||||
|
const count = parseInt(psql(check.sql, { tuples: true }).trim());
|
||||||
|
if (count >= check.min) {
|
||||||
|
ok(`${check.name}: ${count} (mín: ${check.min})`);
|
||||||
|
pass++;
|
||||||
|
} else {
|
||||||
|
err(`${check.name}: ${count} (esperado ≥ ${check.min})`);
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
err(`${check.name}: tabela não existe`);
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check entitlements view
|
||||||
|
try {
|
||||||
|
const ent = psql('SELECT count(*) FROM v_tenant_entitlements;', { tuples: true }).trim();
|
||||||
|
ok(`v_tenant_entitlements: ${ent} registros`);
|
||||||
|
pass++;
|
||||||
|
} catch {
|
||||||
|
err('v_tenant_entitlements: view não existe');
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
|
||||||
|
log('');
|
||||||
|
if (fail === 0) {
|
||||||
|
ok(`${c.bold}Todos os ${pass} checks passaram!${c.reset}`);
|
||||||
|
} else {
|
||||||
|
err(`${fail} check(s) falharam, ${pass} passaram`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- HELP ----
|
||||||
|
commands.help = function () {
|
||||||
|
log(`
|
||||||
|
${c.bold}AgenciaPsi — Database CLI${c.reset}
|
||||||
|
|
||||||
|
${c.cyan}Uso:${c.reset} node db.cjs <comando> [opções]
|
||||||
|
|
||||||
|
${c.cyan}Comandos:${c.reset}
|
||||||
|
|
||||||
|
${c.bold}setup${c.reset} Instalação do zero (schema + fixes + seeds)
|
||||||
|
Cria backup automático após concluir
|
||||||
|
|
||||||
|
${c.bold}backup${c.reset} Exporta banco para backups/YYYY-MM-DD/
|
||||||
|
Gera: schema.sql, data.sql, full_dump.sql
|
||||||
|
|
||||||
|
${c.bold}restore [data]${c.reset} Restaura de um backup
|
||||||
|
Sem data = último backup disponível
|
||||||
|
Ex: node db.cjs restore 2026-03-23
|
||||||
|
|
||||||
|
${c.bold}migrate${c.reset} Aplica migrations pendentes (pasta migrations/)
|
||||||
|
Backup automático antes de aplicar
|
||||||
|
|
||||||
|
${c.bold}seed [grupo]${c.reset} Roda seeds (all, users, system, test_data)
|
||||||
|
Ex: node db.cjs seed system
|
||||||
|
|
||||||
|
${c.bold}status${c.reset} Mostra estado do banco, backups, migrations
|
||||||
|
|
||||||
|
${c.bold}diff${c.reset} Compara schema atual vs último backup
|
||||||
|
|
||||||
|
${c.bold}reset${c.reset} Reseta o banco e reinstala tudo do zero
|
||||||
|
${c.yellow}⚠ Cria backup antes de resetar${c.reset}
|
||||||
|
|
||||||
|
${c.bold}verify${c.reset} Verifica integridade dos dados essenciais
|
||||||
|
|
||||||
|
${c.bold}help${c.reset} Mostra esta ajuda
|
||||||
|
|
||||||
|
${c.cyan}Exemplos:${c.reset}
|
||||||
|
|
||||||
|
${c.gray}# Primeira vez — instala tudo${c.reset}
|
||||||
|
node db.cjs setup
|
||||||
|
|
||||||
|
${c.gray}# Backup diário${c.reset}
|
||||||
|
node db.cjs backup
|
||||||
|
|
||||||
|
${c.gray}# Perdi o banco — restaurar${c.reset}
|
||||||
|
node db.cjs restore
|
||||||
|
|
||||||
|
${c.gray}# Nova migration${c.reset}
|
||||||
|
node db.cjs migrate
|
||||||
|
|
||||||
|
${c.gray}# Ver o que tem no banco${c.reset}
|
||||||
|
node db.cjs status
|
||||||
|
`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Main
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const [, , cmd, ...args] = process.argv;
|
||||||
|
|
||||||
|
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
||||||
|
commands.help();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!commands[cmd]) {
|
||||||
|
err(`Comando desconhecido: ${cmd}`);
|
||||||
|
log(` Use ${c.cyan}node db.cjs help${c.reset} para ver os comandos disponíveis.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
commands[cmd](...args);
|
||||||
|
} catch (e) {
|
||||||
|
err(`Erro: ${e.message}`);
|
||||||
|
if (process.env.DEBUG) console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
34
database-novo/db.config.json
Normal file
34
database-novo/db.config.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"container": "supabase_db_agenciapsi-primesakai",
|
||||||
|
"database": "postgres",
|
||||||
|
"user": "postgres",
|
||||||
|
"backupRetentionDays": 30,
|
||||||
|
"schema": "schema/00_full/schema.sql",
|
||||||
|
"seeds": {
|
||||||
|
"users": [
|
||||||
|
"seed_001_fixed.sql",
|
||||||
|
"seed_002.sql",
|
||||||
|
"seed_003.sql"
|
||||||
|
],
|
||||||
|
"system": [
|
||||||
|
"seed_010_plans.sql",
|
||||||
|
"seed_011_features.sql",
|
||||||
|
"seed_012_plan_features.sql",
|
||||||
|
"seed_013_subscriptions.sql",
|
||||||
|
"seed_014_global_data.sql"
|
||||||
|
],
|
||||||
|
"test_data": [
|
||||||
|
"seed_020_test_data.sql"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fixes": [
|
||||||
|
"fix_addon_credits_fk.sql",
|
||||||
|
"fix_addon_rls_saas_admin.sql",
|
||||||
|
"fix_missing_subscriptions.sql",
|
||||||
|
"fix_notification_templates_rls_admin.sql",
|
||||||
|
"fix_seed_patient_groups.sql",
|
||||||
|
"fix_subscriptions_validate_scope.sql",
|
||||||
|
"fix_template_keys_match_populate.sql",
|
||||||
|
"fix_encoding_accents.sql"
|
||||||
|
]
|
||||||
|
}
|
||||||
176
database-novo/docs/business_rules.md
Normal file
176
database-novo/docs/business_rules.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# Regras de Negócio — Banco de Dados AgenciaPsi
|
||||||
|
|
||||||
|
## 1. Planos e Targets
|
||||||
|
|
||||||
|
| Target | Planos | Escopo da Subscription |
|
||||||
|
|--------|--------|----------------------|
|
||||||
|
| `patient` | patient_free | `user_id` (sem tenant_id) |
|
||||||
|
| `therapist` | therapist_free, therapist_pro | `user_id` (sem tenant_id) |
|
||||||
|
| `clinic` | clinic_free, clinic_pro | `tenant_id` (sem user_id) |
|
||||||
|
| `supervisor` | supervisor_free, supervisor_pro | `user_id` (sem tenant_id) |
|
||||||
|
|
||||||
|
**Constraint `subscriptions_owner_xor`**: Uma subscription DEVE ter `tenant_id` XOR `user_id`, nunca ambos.
|
||||||
|
|
||||||
|
**Trigger `subscriptions_validate_scope`**: Valida que o target do plano casa com o escopo:
|
||||||
|
- `clinic` → exige `tenant_id`, rejeita `user_id`
|
||||||
|
- `therapist`, `supervisor`, `patient` → exige `user_id`, rejeita `tenant_id`
|
||||||
|
|
||||||
|
## 2. Planos Core (protegidos)
|
||||||
|
|
||||||
|
Os planos `clinic_free`, `clinic_pro`, `therapist_free`, `therapist_pro` são **core**:
|
||||||
|
- **Não podem ter `key` alterada** (trigger `trg_no_change_core_plan_key`)
|
||||||
|
- **Não podem ter `target` alterado** (trigger `trg_no_change_plan_target`)
|
||||||
|
- **Não podem ser deletados** (trigger `trg_no_delete_core_plans`)
|
||||||
|
|
||||||
|
Para bypass (migração): `SET LOCAL app.plan_migration_bypass = '1'`
|
||||||
|
|
||||||
|
## 3. Entitlements (Features)
|
||||||
|
|
||||||
|
### Resolução de features para TENANTS (clínicas)
|
||||||
|
```
|
||||||
|
tenant_has_feature(tenant_id, feature_key) =
|
||||||
|
EXISTS em v_tenant_entitlements (via plano)
|
||||||
|
OR
|
||||||
|
EXISTS em tenant_features (override direto)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Resolução de features para USERS (terapeutas, supervisores)
|
||||||
|
```
|
||||||
|
user_has_feature(user_id, feature_key) =
|
||||||
|
EXISTS em v_user_entitlements (via plano pessoal)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cadeia de resolução
|
||||||
|
```
|
||||||
|
subscription → plan → plan_features → features
|
||||||
|
↓
|
||||||
|
plan_features.limits (jsonb) → limites quantitativos
|
||||||
|
```
|
||||||
|
|
||||||
|
### Views de entitlements
|
||||||
|
- `v_tenant_active_subscription` → subscription ativa do tenant
|
||||||
|
- `v_user_active_subscription` → subscription ativa do user
|
||||||
|
- `v_tenant_entitlements` → feature_key + allowed
|
||||||
|
- `v_tenant_entitlements_full` → + limits + plan_id + plan_key
|
||||||
|
- `v_user_entitlements` → feature_key + allowed (para planos pessoais)
|
||||||
|
|
||||||
|
## 4. Tipos de Tenant
|
||||||
|
|
||||||
|
| kind | Descrição | Criação |
|
||||||
|
|------|-----------|---------|
|
||||||
|
| `therapist` | Terapeuta solo | Automático ao criar conta de terapeuta |
|
||||||
|
| `clinic_coworking` | Clínica coworking | Manual |
|
||||||
|
| `clinic_reception` | Clínica com recepção | Manual |
|
||||||
|
| `clinic_full` | Clínica completa | Manual |
|
||||||
|
| `supervisor` | Supervisor | Automático |
|
||||||
|
| `saas` | Sistema (legado) | — |
|
||||||
|
| `clinic` | Legado | — |
|
||||||
|
|
||||||
|
**O `kind` é imutável após criação** (trigger `trg_tenant_kind_immutable`).
|
||||||
|
|
||||||
|
## 5. Roles e Permissões
|
||||||
|
|
||||||
|
### Profile roles
|
||||||
|
| Role | Descrição |
|
||||||
|
|------|-----------|
|
||||||
|
| `saas_admin` | Administrador da plataforma |
|
||||||
|
| `tenant_member` | Membro de um ou mais tenants |
|
||||||
|
| `portal_user` | Paciente (acesso ao portal) |
|
||||||
|
| `patient` | Paciente (legado) |
|
||||||
|
|
||||||
|
### Tenant member roles
|
||||||
|
| Role | Descrição |
|
||||||
|
|------|-----------|
|
||||||
|
| `tenant_admin` | Admin do tenant (dono) |
|
||||||
|
| `therapist` | Terapeuta membro |
|
||||||
|
| `clinic_admin` | Admin da clínica (secretária com poderes) |
|
||||||
|
| `secretary` | Secretária |
|
||||||
|
| `supervisor` | Supervisor |
|
||||||
|
| `patient` | Paciente do tenant |
|
||||||
|
|
||||||
|
### Platform roles (array em profiles)
|
||||||
|
| Role | Descrição |
|
||||||
|
|------|-----------|
|
||||||
|
| `editor` | Editor de conteúdo da plataforma |
|
||||||
|
|
||||||
|
## 6. Compromissos Determinados
|
||||||
|
|
||||||
|
A função `seed_determined_commitments(tenant_id)` cria 5 tipos nativos:
|
||||||
|
|
||||||
|
| native_key | Nome | locked | active |
|
||||||
|
|------------|------|--------|--------|
|
||||||
|
| `session` | Sessão | true | true |
|
||||||
|
| `reading` | Leitura | false | true |
|
||||||
|
| `supervision` | Supervisão | false | true |
|
||||||
|
| `class` | Aula | false | **false** |
|
||||||
|
| `analysis` | Análise Pessoal | false | true |
|
||||||
|
|
||||||
|
- `session` é **locked** (não pode ser editado/deletado)
|
||||||
|
- O `native_key = 'session'` é usado pelo agendador online para identificar o compromisso padrão
|
||||||
|
|
||||||
|
## 7. Grupos de Pacientes Padrão
|
||||||
|
|
||||||
|
A função `seed_default_patient_groups(tenant_id)` cria 3 grupos sistema:
|
||||||
|
|
||||||
|
| Nome | Cor | is_system |
|
||||||
|
|------|-----|-----------|
|
||||||
|
| Crianças | #60a5fa | true |
|
||||||
|
| Adolescentes | #a78bfa | true |
|
||||||
|
| Idosos | #34d399 | true |
|
||||||
|
|
||||||
|
Grupos sistema não podem ser editados/deletados (trigger `prevent_system_group_changes`).
|
||||||
|
|
||||||
|
## 8. Subscriptions — Status
|
||||||
|
|
||||||
|
| Status | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `pending` | Aguardando ativação |
|
||||||
|
| `active` | Ativa |
|
||||||
|
| `past_due` | Pagamento atrasado |
|
||||||
|
| `suspended` | Suspensa |
|
||||||
|
| `cancelled` | Cancelada |
|
||||||
|
| `expired` | Expirada |
|
||||||
|
|
||||||
|
## 9. Templates de Email
|
||||||
|
|
||||||
|
**Globais** (`email_templates_global`): templates padrão da plataforma, gerenciados pelo saas_admin.
|
||||||
|
|
||||||
|
**Tenant** (`email_templates_tenant`): overrides por tenant. Se existir, usa o do tenant; se não, usa o global.
|
||||||
|
|
||||||
|
### Keys de template
|
||||||
|
| Domínio | Templates |
|
||||||
|
|---------|-----------|
|
||||||
|
| session | reminder, confirmation, cancellation, rescheduled |
|
||||||
|
| intake | received, approved, rejected |
|
||||||
|
| scheduler | request_accepted, request_rejected |
|
||||||
|
| system | welcome, password_reset |
|
||||||
|
|
||||||
|
Canais: `email`, `whatsapp`, `sms`
|
||||||
|
|
||||||
|
## 10. Notificações — Sistema
|
||||||
|
|
||||||
|
| Canal | Tipos |
|
||||||
|
|-------|-------|
|
||||||
|
| WhatsApp | lembrete_sessao, confirmacao_sessao, cancelamento_sessao |
|
||||||
|
| SMS | lembrete_sessao |
|
||||||
|
|
||||||
|
### Schedule keys
|
||||||
|
| Key | Descrição |
|
||||||
|
|-----|-----------|
|
||||||
|
| `lembrete_24h` | 24 horas antes |
|
||||||
|
| `lembrete_2h` | 2 horas antes |
|
||||||
|
| `lembrete_30min` | 30 minutos antes |
|
||||||
|
| `confirmacao_imediata` | Imediato após confirmar |
|
||||||
|
| `cancelamento_imediato` | Imediato após cancelar |
|
||||||
|
|
||||||
|
## 11. RLS (Row Level Security)
|
||||||
|
|
||||||
|
Todas as tabelas do schema `public` têm RLS habilitado. As policies usam:
|
||||||
|
- `auth.uid()` — ID do usuário autenticado
|
||||||
|
- `is_saas_admin()` — verifica se é admin da plataforma
|
||||||
|
- `is_tenant_member(tenant_id)` — verifica se pertence ao tenant
|
||||||
|
- `is_tenant_admin(tenant_id)` — verifica se é admin do tenant
|
||||||
|
- `current_member_role(tenant_id)` — role do membro no tenant
|
||||||
|
- `tenant_has_feature(tenant_id, feature_key)` — verifica feature
|
||||||
|
|
||||||
|
**Se as features/plan_features não existirem no banco, as policies de RLS bloqueiam o acesso.**
|
||||||
191
database-novo/docs/schema_map.md
Normal file
191
database-novo/docs/schema_map.md
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
# Schema Map — AgenciaPsi
|
||||||
|
|
||||||
|
Mapa completo do banco de dados PostgreSQL 17, extraído de `schema.sql` (2026-03-23).
|
||||||
|
**84 tabelas** no schema `public` + tabelas de infraestrutura (auth, storage, realtime).
|
||||||
|
|
||||||
|
## Domínios
|
||||||
|
|
||||||
|
### Core (11 tabelas)
|
||||||
|
| Tabela | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `profiles` | Perfil do usuário (role, account_type, full_name, platform_roles) |
|
||||||
|
| `tenants` | Organizações (clínicas, terapeutas solo, supervisores) |
|
||||||
|
| `tenant_members` | Vínculo usuário↔tenant com role (tenant_admin, therapist, secretary, etc.) |
|
||||||
|
| `tenant_invites` | Convites pendentes para ingressar em um tenant |
|
||||||
|
| `tenant_features` | Overrides de features por tenant (exceções comerciais) |
|
||||||
|
| `tenant_feature_exceptions_log` | Log de alterações em tenant_features |
|
||||||
|
| `saas_admins` | Administradores da plataforma |
|
||||||
|
| `owner_users` | Mapeamento owner_id→user_id para RLS |
|
||||||
|
| `user_settings` | Configurações pessoais do usuário |
|
||||||
|
| `company_profiles` | Perfil da empresa/clínica (logo, endereço, etc.) |
|
||||||
|
| `dev_user_credentials` | Credenciais de teste (apenas dev) |
|
||||||
|
|
||||||
|
### Plans & Billing (20 tabelas)
|
||||||
|
| Tabela | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `plans` | Planos disponíveis (key, target, price_cents, max_supervisees) |
|
||||||
|
| `plan_prices` | Preços por intervalo (month/year) com versionamento |
|
||||||
|
| `plan_features` | Vínculo plano↔feature com limites (limits jsonb) |
|
||||||
|
| `plan_public` | Info pública dos planos (para página de preços) |
|
||||||
|
| `plan_public_bullets` | Bullets de marketing dos planos |
|
||||||
|
| `features` | Features do sistema (key, name, descricao) |
|
||||||
|
| `entitlements_invalidation` | Cache invalidation de entitlements |
|
||||||
|
| `subscriptions` | Assinaturas ativas (user_id XOR tenant_id) |
|
||||||
|
| `subscription_events` | Histórico de eventos de assinatura |
|
||||||
|
| `subscription_intents_personal` | Intenções de assinatura pessoal |
|
||||||
|
| `subscription_intents_tenant` | Intenções de assinatura de tenant |
|
||||||
|
| `subscription_intents_legacy` | Intenções legadas |
|
||||||
|
| `billing_contracts` | Contratos de cobrança |
|
||||||
|
| `addon_credits` | Créditos de add-ons por tenant |
|
||||||
|
| `addon_products` | Produtos add-on disponíveis |
|
||||||
|
| `addon_transactions` | Transações de add-ons |
|
||||||
|
| `modules` | Módulos do sistema |
|
||||||
|
| `module_features` | Features por módulo |
|
||||||
|
| `tenant_modules` | Módulos ativos por tenant |
|
||||||
|
|
||||||
|
### Agenda (11 tabelas)
|
||||||
|
| Tabela | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `agenda_bloqueios` | Bloqueios de horário |
|
||||||
|
| `agenda_configuracoes` | Configurações da agenda por tenant_member |
|
||||||
|
| `agenda_eventos` | Eventos da agenda (sessões, bloqueios) |
|
||||||
|
| `agenda_excecoes` | Exceções na agenda (horários extras, bloqueios pontuais) |
|
||||||
|
| `agenda_online_slots` | Slots de agendamento online |
|
||||||
|
| `agenda_regras_semanais` | Regras semanais de disponibilidade |
|
||||||
|
| `agenda_slots_bloqueados_semanais` | Slots bloqueados na semana |
|
||||||
|
| `agenda_slots_regras` | Regras de slots |
|
||||||
|
| `recurrence_rules` | Regras de recorrência de sessões |
|
||||||
|
| `recurrence_exceptions` | Exceções a recorrências |
|
||||||
|
| `recurrence_rule_services` | Serviços vinculados a recorrências |
|
||||||
|
|
||||||
|
### Agendador Online (2 tabelas)
|
||||||
|
| Tabela | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `agendador_configuracoes` | Configurações do agendador online público |
|
||||||
|
| `agendador_solicitacoes` | Solicitações de agendamento recebidas |
|
||||||
|
|
||||||
|
### Pacientes (8 tabelas)
|
||||||
|
| Tabela | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `patients` | Pacientes vinculados a um tenant |
|
||||||
|
| `patient_groups` | Grupos de pacientes (sistema + customizados) |
|
||||||
|
| `patient_group_patient` | Vínculo paciente↔grupo |
|
||||||
|
| `patient_tags` | Tags personalizadas |
|
||||||
|
| `patient_patient_tag` | Vínculo paciente↔tag |
|
||||||
|
| `patient_intake_requests` | Solicitações de cadastro (triagem) |
|
||||||
|
| `patient_invites` | Convites para portal do paciente |
|
||||||
|
| `patient_discounts` | Descontos por paciente |
|
||||||
|
|
||||||
|
### Compromissos Determinados (4 tabelas)
|
||||||
|
| Tabela | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `determined_commitments` | Tipos de compromisso (sessão, leitura, supervisão, etc.) |
|
||||||
|
| `determined_commitment_fields` | Campos customizados por tipo de compromisso |
|
||||||
|
| `commitment_services` | Serviços vinculados a compromissos |
|
||||||
|
| `commitment_time_logs` | Logs de tempo por compromisso |
|
||||||
|
|
||||||
|
### Financeiro (9 tabelas)
|
||||||
|
| Tabela | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `financial_records` | Lançamentos financeiros (receita/despesa) |
|
||||||
|
| `financial_categories` | Categorias de lançamento |
|
||||||
|
| `financial_exceptions` | Exceções financeiras |
|
||||||
|
| `payment_settings` | Configurações de pagamento por tenant |
|
||||||
|
| `professional_pricing` | Precificação por profissional |
|
||||||
|
| `therapist_payouts` | Repasses a terapeutas |
|
||||||
|
| `therapist_payout_records` | Registros de repasse |
|
||||||
|
| `services` | Serviços oferecidos |
|
||||||
|
| `insurance_plans` + `insurance_plan_services` | Convênios e serviços por convênio |
|
||||||
|
|
||||||
|
### Notificações (10 tabelas)
|
||||||
|
| Tabela | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `notification_channels` | Canais de notificação por tenant |
|
||||||
|
| `notification_logs` | Logs de envio |
|
||||||
|
| `notification_preferences` | Preferências do paciente (opt-in/out) |
|
||||||
|
| `notification_queue` | Fila de envio |
|
||||||
|
| `notification_schedules` | Agendamentos de notificação |
|
||||||
|
| `notification_templates` | Templates WhatsApp/SMS (default + tenant) |
|
||||||
|
| `notifications` | Notificações in-app |
|
||||||
|
| `email_templates_global` | Templates de email globais (plataforma) |
|
||||||
|
| `email_templates_tenant` | Overrides de templates por tenant |
|
||||||
|
| `email_layout_config` | Configuração de layout de email |
|
||||||
|
|
||||||
|
### SaaS Admin / UI (8 tabelas)
|
||||||
|
| Tabela | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `saas_docs` | Documentação da plataforma |
|
||||||
|
| `saas_doc_votos` | Votos em docs |
|
||||||
|
| `saas_faq` | Categorias de FAQ |
|
||||||
|
| `saas_faq_itens` | Itens de FAQ |
|
||||||
|
| `feriados` | Feriados nacionais/regionais |
|
||||||
|
| `global_notices` | Avisos globais da plataforma |
|
||||||
|
| `login_carousel_slides` | Slides do carrossel de login |
|
||||||
|
| `notice_dismissals` | Dismissals de avisos por usuário |
|
||||||
|
|
||||||
|
### Suporte (1 tabela)
|
||||||
|
| Tabela | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `support_sessions` | Sessões de suporte técnico |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Views Principais
|
||||||
|
|
||||||
|
| View | Descrição |
|
||||||
|
|------|-----------|
|
||||||
|
| `v_tenant_active_subscription` | Subscription ativa por tenant |
|
||||||
|
| `v_user_active_subscription` | Subscription ativa por user |
|
||||||
|
| `v_tenant_entitlements` | Features habilitadas por tenant (via plano) |
|
||||||
|
| `v_tenant_entitlements_full` | Entitlements + limits + plan info |
|
||||||
|
| `v_tenant_entitlements_json` | Entitlements agregados como JSON |
|
||||||
|
| `v_user_entitlements` | Features habilitadas por user (via plano) |
|
||||||
|
| `v_tenant_members_with_profiles` | Membros do tenant com dados do perfil |
|
||||||
|
| `v_tenant_staff` | Staff do tenant (membros + convites) |
|
||||||
|
| `v_tenant_people` | Todas as pessoas do tenant |
|
||||||
|
| `v_plan_active_prices` | Preços ativos dos planos |
|
||||||
|
| `v_public_pricing` | Preços públicos para página de marketing |
|
||||||
|
| `v_subscription_health` | Saúde das subscriptions |
|
||||||
|
| `v_cashflow_projection` | Projeção de fluxo de caixa |
|
||||||
|
| `v_commitment_totals` | Totais de compromissos |
|
||||||
|
| `v_patient_groups_with_counts` | Grupos com contagem de pacientes |
|
||||||
|
| `v_tag_patient_counts` | Tags com contagem de pacientes |
|
||||||
|
| `subscription_intents` | View unificada de intenções (com INSTEAD OF trigger) |
|
||||||
|
| `owner_feature_entitlements` | Entitlements por owner |
|
||||||
|
| `current_tenant_id` | Tenant ativo do usuário corrente |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Funções Críticas
|
||||||
|
|
||||||
|
| Função | Tipo | Descrição |
|
||||||
|
|--------|------|-----------|
|
||||||
|
| `tenant_has_feature(uuid, text)` | Query | Verifica se tenant tem feature (plano + override) |
|
||||||
|
| `user_has_feature(uuid, text)` | Query | Verifica se user tem feature via plano pessoal |
|
||||||
|
| `has_feature(uuid, text)` | Query | Alias genérico |
|
||||||
|
| `seed_determined_commitments(uuid)` | Seed | Cria 5 tipos de compromisso nativos por tenant |
|
||||||
|
| `seed_default_patient_groups(uuid)` | Seed | Cria 3 grupos de pacientes padrão |
|
||||||
|
| `seed_default_financial_categories(uuid)` | Seed | Cria categorias financeiras padrão |
|
||||||
|
| `subscriptions_validate_scope()` | Trigger | Valida XOR (user_id vs tenant_id) por target |
|
||||||
|
| `activate_subscription_from_intent(uuid)` | RPC | Ativa subscription a partir de intent |
|
||||||
|
| `handle_new_user()` | Trigger | Cria profile + tenant pessoal ao cadastrar |
|
||||||
|
| `ensure_personal_tenant()` | RPC | Garante que o user tem um tenant pessoal |
|
||||||
|
| `populate_notification_queue()` | Cron | Popula fila de notificações |
|
||||||
|
| `agendador_slots_disponiveis(text, date)` | RPC | Retorna slots disponíveis para agendamento |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Enums (public schema)
|
||||||
|
|
||||||
|
| Tipo | Valores |
|
||||||
|
|------|---------|
|
||||||
|
| `commitment_log_source` | manual, auto |
|
||||||
|
| `determined_field_type` | text, textarea, number, date, select, boolean |
|
||||||
|
| `financial_record_type` | receita, despesa |
|
||||||
|
| `recurrence_exception_type` | cancel_session, reschedule_session, patient_missed, therapist_canceled, holiday_block |
|
||||||
|
| `recurrence_type` | weekly, biweekly, monthly, yearly, custom_weekdays |
|
||||||
|
| `status_agenda_serie` | ativo, pausado, cancelado |
|
||||||
|
| `status_evento_agenda` | agendado, realizado, faltou, cancelado, remarcar |
|
||||||
|
| `status_excecao_agenda` | pendente, ativo, arquivado |
|
||||||
|
| `tipo_evento_agenda` | sessao, bloqueio |
|
||||||
|
| `tipo_excecao_agenda` | bloqueio, horario_extra |
|
||||||
297
database-novo/docs/setup_guide.md
Normal file
297
database-novo/docs/setup_guide.md
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
# Guia de Instalação e Uso — AgenciaPsi Database
|
||||||
|
|
||||||
|
## Pré-requisitos
|
||||||
|
|
||||||
|
1. **Docker Desktop** instalado e rodando
|
||||||
|
2. **Node.js** 18+ instalado
|
||||||
|
3. **Supabase CLI** instalado (`npm install -g supabase`)
|
||||||
|
|
||||||
|
## Instalação do Zero (banco vazio)
|
||||||
|
|
||||||
|
### 1. Iniciar o Supabase
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Na raiz do projeto (agenciapsi-primesakai/)
|
||||||
|
npx supabase start
|
||||||
|
```
|
||||||
|
|
||||||
|
Aguarde até o container `supabase_db_agenciapsi-primesakai` estar rodando.
|
||||||
|
|
||||||
|
### 2. Verificar se o container está ok
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker ps | grep supabase_db
|
||||||
|
```
|
||||||
|
|
||||||
|
Deve mostrar o container com status `Up`.
|
||||||
|
|
||||||
|
### 3. Instalar o banco completo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd database-novo
|
||||||
|
node db.cjs setup
|
||||||
|
```
|
||||||
|
|
||||||
|
Isso faz tudo automaticamente:
|
||||||
|
- Aplica o schema completo (84 tabelas, funções, triggers, policies)
|
||||||
|
- Aplica os 7 fixes conhecidos
|
||||||
|
- Cria os 11 usuários de teste
|
||||||
|
- Cria os 7 planos + 4 preços
|
||||||
|
- Cria as 26 features + 85 vínculos plano↔feature
|
||||||
|
- Cria as 9 subscriptions + compromissos determinados
|
||||||
|
- Cria os templates de email, notificação e carousel
|
||||||
|
- Cria backup automático pós-instalação
|
||||||
|
- Verifica integridade no final
|
||||||
|
|
||||||
|
### 4. Verificar
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs status
|
||||||
|
```
|
||||||
|
|
||||||
|
Deve mostrar todos os counts verdes.
|
||||||
|
|
||||||
|
## Backup
|
||||||
|
|
||||||
|
### Criar backup manual
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs backup
|
||||||
|
```
|
||||||
|
|
||||||
|
Salva em `backups/YYYY-MM-DD/` com 3 arquivos:
|
||||||
|
- `schema.sql` — estrutura do banco
|
||||||
|
- `data.sql` — dados (sem schemas de infra)
|
||||||
|
- `full_dump.sql` — tudo junto
|
||||||
|
|
||||||
|
### Backup automático
|
||||||
|
|
||||||
|
O backup é feito automaticamente:
|
||||||
|
- Após o `setup`
|
||||||
|
- Antes de cada `migrate`
|
||||||
|
- Antes de cada `restore`
|
||||||
|
- Antes de cada `reset`
|
||||||
|
|
||||||
|
### Retenção
|
||||||
|
|
||||||
|
Backups com mais de 30 dias são removidos automaticamente. Para alterar, edite `backupRetentionDays` no `db.config.json`.
|
||||||
|
|
||||||
|
## Restaurar o Banco
|
||||||
|
|
||||||
|
### Restaurar do último backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs restore
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restaurar de uma data específica
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs restore 2026-03-23
|
||||||
|
```
|
||||||
|
|
||||||
|
O restore:
|
||||||
|
1. Cria backup de segurança do estado atual
|
||||||
|
2. Limpa o schema public
|
||||||
|
3. Aplica o full_dump.sql do backup
|
||||||
|
4. Verifica integridade
|
||||||
|
|
||||||
|
## Migrations (alterações no banco)
|
||||||
|
|
||||||
|
### Criar uma migration
|
||||||
|
|
||||||
|
Crie um arquivo SQL na pasta `migrations/` com nome sequencial:
|
||||||
|
|
||||||
|
```
|
||||||
|
migrations/
|
||||||
|
├── 001_add_column_x.sql
|
||||||
|
├── 002_create_table_y.sql
|
||||||
|
└── 003_fix_something.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
O nome deve começar com número para garantir a ordem.
|
||||||
|
|
||||||
|
### Aplicar migrations pendentes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
O CLI:
|
||||||
|
1. Cria backup automático
|
||||||
|
2. Compara com a tabela `_db_migrations` no banco
|
||||||
|
3. Aplica apenas as que ainda não foram executadas
|
||||||
|
4. Registra cada migration aplicada
|
||||||
|
5. Se uma falhar, para imediatamente (use `restore` para voltar)
|
||||||
|
|
||||||
|
### Ver migrations aplicadas
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs status
|
||||||
|
```
|
||||||
|
|
||||||
|
## Seeds (dados de teste)
|
||||||
|
|
||||||
|
### Rodar todos os seeds
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs seed all # ou simplesmente: node db.cjs seed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rodar grupo específico
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs seed users # Apenas usuários (seed_001 a 003)
|
||||||
|
node db.cjs seed system # Apenas sistema (seed_010 a 014)
|
||||||
|
node db.cjs seed test_data # Dados de teste (seed_020)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ordem dos seeds
|
||||||
|
|
||||||
|
| # | Arquivo | O que faz |
|
||||||
|
|---|---------|-----------|
|
||||||
|
| 1 | `seed_001_fixed.sql` | 6 usuários base + tenants |
|
||||||
|
| 2 | `seed_002.sql` | Supervisor + Editor |
|
||||||
|
| 3 | `seed_003.sql` | Therapist2, Therapist3, Secretary |
|
||||||
|
| 4 | `seed_010_plans.sql` | 7 planos + 4 preços |
|
||||||
|
| 5 | `seed_011_features.sql` | 26 features |
|
||||||
|
| 6 | `seed_012_plan_features.sql` | 85 vínculos plano↔feature |
|
||||||
|
| 7 | `seed_013_subscriptions.sql` | 9 subscriptions + compromissos |
|
||||||
|
| 8 | `seed_014_global_data.sql` | Templates + carousel |
|
||||||
|
|
||||||
|
## Outros Comandos
|
||||||
|
|
||||||
|
### Ver status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs status
|
||||||
|
```
|
||||||
|
|
||||||
|
Mostra: container, backups, migrations aplicadas/pendentes, counts de todas as tabelas.
|
||||||
|
|
||||||
|
### Comparar mudanças
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs diff
|
||||||
|
```
|
||||||
|
|
||||||
|
Compara o schema atual no banco com o último backup. Mostra tabelas adicionadas, removidas ou alteradas.
|
||||||
|
|
||||||
|
### Verificar integridade
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs verify
|
||||||
|
```
|
||||||
|
|
||||||
|
Checa se os dados essenciais existem (plans, features, subscriptions, etc).
|
||||||
|
|
||||||
|
### Reset completo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs reset
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠ CUIDADO**: Apaga tudo e reinstala do zero. Cria backup antes.
|
||||||
|
|
||||||
|
## Estrutura de Pastas
|
||||||
|
|
||||||
|
```
|
||||||
|
database-novo/
|
||||||
|
├── db.js ← CLI principal
|
||||||
|
├── db.config.json ← Configuração (container, seeds, fixes)
|
||||||
|
│
|
||||||
|
├── schema/ ← Schema SQL separado por seção
|
||||||
|
│ ├── 00_full/ ← Schema completo (referência)
|
||||||
|
│ ├── 01_extensions/ ← Extensões PostgreSQL
|
||||||
|
│ ├── 02_types/ ← Enums e tipos
|
||||||
|
│ ├── 03_functions/ ← Funções (11 arquivos por domínio)
|
||||||
|
│ ├── 04_tables/ ← Tabelas (10 arquivos por domínio)
|
||||||
|
│ ├── 05_views/ ← 24 views
|
||||||
|
│ ├── 06_indexes/ ← Índices
|
||||||
|
│ ├── 07_foreign_keys/ ← PKs, FKs, constraints
|
||||||
|
│ ├── 08_triggers/ ← Triggers
|
||||||
|
│ ├── 09_policies/ ← 217 RLS policies
|
||||||
|
│ └── 10_grants/ ← Grants
|
||||||
|
│
|
||||||
|
├── seeds/ ← Seeds de dados
|
||||||
|
│ ├── seed_001_fixed.sql
|
||||||
|
│ ├── ...
|
||||||
|
│ └── run_all_seeds.sh
|
||||||
|
│
|
||||||
|
├── migrations/ ← Migrations (alterações incrementais)
|
||||||
|
│
|
||||||
|
├── fixes/ ← Correções aplicadas
|
||||||
|
│
|
||||||
|
├── backups/ ← Backups com data
|
||||||
|
│ ├── 2026-03-23/
|
||||||
|
│ └── ...
|
||||||
|
│
|
||||||
|
└── docs/ ← Documentação
|
||||||
|
├── setup_guide.md ← Este arquivo
|
||||||
|
├── schema_map.md ← Mapa de 84 tabelas
|
||||||
|
├── business_rules.md ← Regras de negócio
|
||||||
|
└── users_test.md ← Usuários de teste
|
||||||
|
```
|
||||||
|
|
||||||
|
## Credenciais de Teste
|
||||||
|
|
||||||
|
| Email | Senha | Tipo |
|
||||||
|
|-------|-------|------|
|
||||||
|
| paciente@agenciapsi.com.br | Teste@123 | Paciente |
|
||||||
|
| terapeuta@agenciapsi.com.br | Teste@123 | Terapeuta solo |
|
||||||
|
| clinica1@agenciapsi.com.br | Teste@123 | Clínica coworking |
|
||||||
|
| clinica2@agenciapsi.com.br | Teste@123 | Clínica recepção |
|
||||||
|
| clinica3@agenciapsi.com.br | Teste@123 | Clínica full |
|
||||||
|
| saas@agenciapsi.com.br | Teste@123 | Admin plataforma |
|
||||||
|
| supervisor@agenciapsi.com.br | Teste@123 | Supervisor |
|
||||||
|
| editor@agenciapsi.com.br | Teste@123 | Editor |
|
||||||
|
| therapist2@agenciapsi.com.br | Teste@123 | Terapeuta |
|
||||||
|
| therapist3@agenciapsi.com.br | Teste@123 | Terapeuta |
|
||||||
|
| secretary@agenciapsi.com.br | Teste@123 | Secretária |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Container não está rodando"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verificar
|
||||||
|
docker ps | grep supabase
|
||||||
|
|
||||||
|
# Reiniciar
|
||||||
|
npx supabase stop
|
||||||
|
npx supabase start
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Tabela não existe" após setup
|
||||||
|
|
||||||
|
O schema pode não ter sido aplicado corretamente. Rode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs reset
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Permission denied" / RLS bloqueando
|
||||||
|
|
||||||
|
Se features/plan_features estiverem vazios, o RLS bloqueia tudo. Rode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs seed system
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration falhou no meio
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Voltar ao estado anterior
|
||||||
|
node db.cjs restore
|
||||||
|
|
||||||
|
# Corrigir o SQL da migration, depois tentar de novo
|
||||||
|
node db.cjs migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quero começar do zero
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node db.cjs reset
|
||||||
|
```
|
||||||
|
|
||||||
|
Isso apaga tudo, reaplica schema, fixes, seeds, e verifica.
|
||||||
90
database-novo/docs/users_test.md
Normal file
90
database-novo/docs/users_test.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# Usuários de Teste — AgenciaPsi
|
||||||
|
|
||||||
|
Senha de todos: `Teste@123`
|
||||||
|
|
||||||
|
## Mapa de UUIDs
|
||||||
|
|
||||||
|
### Users (auth.users.id = profiles.id)
|
||||||
|
| Email | UUID | Nome |
|
||||||
|
|-------|------|------|
|
||||||
|
| paciente@agenciapsi.com.br | `aaaaaaaa-0001-0001-0001-000000000001` | Ana Paciente |
|
||||||
|
| terapeuta@agenciapsi.com.br | `aaaaaaaa-0002-0002-0002-000000000002` | Bruno Terapeuta |
|
||||||
|
| clinica1@agenciapsi.com.br | `aaaaaaaa-0003-0003-0003-000000000003` | Clínica Espaço Psi |
|
||||||
|
| clinica2@agenciapsi.com.br | `aaaaaaaa-0004-0004-0004-000000000004` | Clínica Mente sã |
|
||||||
|
| clinica3@agenciapsi.com.br | `aaaaaaaa-0005-0005-0005-000000000005` | Clínica Bem Estar |
|
||||||
|
| saas@agenciapsi.com.br | `aaaaaaaa-0006-0006-0006-000000000006` | Admin Plataforma |
|
||||||
|
| supervisor@agenciapsi.com.br | `aaaaaaaa-0007-0007-0007-000000000007` | Carlos Supervisor |
|
||||||
|
| editor@agenciapsi.com.br | `aaaaaaaa-0008-0008-0008-000000000008` | Diana Editora |
|
||||||
|
| therapist2@agenciapsi.com.br | `aaaaaaaa-0009-0009-0009-000000000009` | Eva Terapeuta |
|
||||||
|
| therapist3@agenciapsi.com.br | `aaaaaaaa-0010-0010-0010-000000000010` | Felipe Terapeuta |
|
||||||
|
| secretary@agenciapsi.com.br | `aaaaaaaa-0011-0011-0011-000000000011` | Gabriela Secretária |
|
||||||
|
|
||||||
|
### Tenants
|
||||||
|
| Nome | UUID | Kind |
|
||||||
|
|------|------|------|
|
||||||
|
| Bruno Terapeuta | `bbbbbbbb-0002-0002-0002-000000000002` | therapist |
|
||||||
|
| Clínica Espaço Psi | `bbbbbbbb-0003-0003-0003-000000000003` | clinic_coworking |
|
||||||
|
| Clínica Mente sã | `bbbbbbbb-0004-0004-0004-000000000004` | clinic_reception |
|
||||||
|
| Clínica Bem Estar | `bbbbbbbb-0005-0005-0005-000000000005` | clinic_full |
|
||||||
|
| Eva Terapeuta | `bbbbbbbb-0009-0009-0009-000000000009` | therapist |
|
||||||
|
| Felipe Terapeuta | `bbbbbbbb-0010-0010-0010-000000000010` | therapist |
|
||||||
|
|
||||||
|
## Mapa de Vínculos
|
||||||
|
|
||||||
|
```
|
||||||
|
paciente@ → portal_user / patient_free (user_id)
|
||||||
|
Sem tenant próprio
|
||||||
|
|
||||||
|
terapeuta@ → tenant_member / therapist
|
||||||
|
Tenant: bbbbbbbb-0002 (therapist) → tenant_admin
|
||||||
|
Clínica 3: bbbbbbbb-0005 → therapist
|
||||||
|
Subscription: therapist_free (user_id)
|
||||||
|
|
||||||
|
clinica1@ → tenant_member / clinic
|
||||||
|
Tenant: bbbbbbbb-0003 (clinic_coworking) → tenant_admin
|
||||||
|
Subscription: clinic_free (tenant_id)
|
||||||
|
|
||||||
|
clinica2@ → tenant_member / clinic
|
||||||
|
Tenant: bbbbbbbb-0004 (clinic_reception) → tenant_admin
|
||||||
|
Subscription: clinic_free (tenant_id)
|
||||||
|
|
||||||
|
clinica3@ → tenant_member / clinic
|
||||||
|
Tenant: bbbbbbbb-0005 (clinic_full) → tenant_admin
|
||||||
|
Subscription: clinic_free (tenant_id)
|
||||||
|
|
||||||
|
saas@ → saas_admin
|
||||||
|
Sem tenant, sem subscription
|
||||||
|
|
||||||
|
supervisor@ → tenant_member / therapist
|
||||||
|
Clínica 3: bbbbbbbb-0005 → supervisor
|
||||||
|
Subscription: supervisor_free (user_id)
|
||||||
|
|
||||||
|
editor@ → tenant_member / therapist + platform_roles: {editor}
|
||||||
|
Clínica 3: bbbbbbbb-0005 → therapist
|
||||||
|
Subscription: therapist_free (user_id)
|
||||||
|
|
||||||
|
therapist2@ → tenant_member / therapist
|
||||||
|
Tenant: bbbbbbbb-0009 (therapist) → tenant_admin
|
||||||
|
Clínica 3: bbbbbbbb-0005 → therapist
|
||||||
|
Subscription: therapist_free (user_id)
|
||||||
|
|
||||||
|
therapist3@ → tenant_member / therapist
|
||||||
|
Tenant: bbbbbbbb-0010 (therapist) → tenant_admin
|
||||||
|
Clínica 3: bbbbbbbb-0005 → therapist
|
||||||
|
Subscription: therapist_free (user_id)
|
||||||
|
|
||||||
|
secretary@ → tenant_member / therapist (profile)
|
||||||
|
Clínica 2: bbbbbbbb-0004 → clinic_admin
|
||||||
|
Sem subscription própria (usa plano da Clínica 2)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Clínica 3 — Bem Estar (Full) — Membros
|
||||||
|
|
||||||
|
| Membro | Role |
|
||||||
|
|--------|------|
|
||||||
|
| clinica3@ | tenant_admin |
|
||||||
|
| terapeuta@ | therapist |
|
||||||
|
| supervisor@ | supervisor |
|
||||||
|
| editor@ | therapist |
|
||||||
|
| therapist2@ | therapist |
|
||||||
|
| therapist3@ | therapist |
|
||||||
11
database-novo/fixes/fix_addon_credits_fk.sql
Normal file
11
database-novo/fixes/fix_addon_credits_fk.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Fix: addon_credits e addon_transactions tenant_id FK
|
||||||
|
-- Corrige FK que apontava para auth.users → agora aponta para public.tenants
|
||||||
|
-- Agência PSI — 2026-03-22
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
ALTER TABLE public.addon_credits DROP CONSTRAINT IF EXISTS addon_credits_tenant_id_fkey;
|
||||||
|
ALTER TABLE public.addon_credits ADD CONSTRAINT addon_credits_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id);
|
||||||
|
|
||||||
|
ALTER TABLE public.addon_transactions DROP CONSTRAINT IF EXISTS addon_transactions_tenant_id_fkey;
|
||||||
|
ALTER TABLE public.addon_transactions ADD CONSTRAINT addon_transactions_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id);
|
||||||
83
database-novo/fixes/fix_addon_rls_saas_admin.sql
Normal file
83
database-novo/fixes/fix_addon_rls_saas_admin.sql
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Fix: RLS addon_credits e addon_transactions
|
||||||
|
-- 1. SaaS Admin: acesso total
|
||||||
|
-- 2. Tenant members: SELECT nos seus créditos/transações
|
||||||
|
-- Agência PSI — 2026-03-22
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- ── addon_products: admin pode tudo (CRUD) ────────────────────
|
||||||
|
DROP POLICY IF EXISTS "addon_products_admin_all" ON public.addon_products;
|
||||||
|
CREATE POLICY "addon_products_admin_all"
|
||||||
|
ON public.addon_products FOR ALL
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
EXISTS (SELECT 1 FROM public.saas_admins WHERE user_id = auth.uid())
|
||||||
|
)
|
||||||
|
WITH CHECK (
|
||||||
|
EXISTS (SELECT 1 FROM public.saas_admins WHERE user_id = auth.uid())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ── addon_credits: admin pode ver todos ───────────────────────
|
||||||
|
DROP POLICY IF EXISTS "addon_credits_admin_select" ON public.addon_credits;
|
||||||
|
CREATE POLICY "addon_credits_admin_select"
|
||||||
|
ON public.addon_credits FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
EXISTS (SELECT 1 FROM public.saas_admins WHERE user_id = auth.uid())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ── addon_credits: admin pode inserir/atualizar ───────────────
|
||||||
|
DROP POLICY IF EXISTS "addon_credits_admin_write" ON public.addon_credits;
|
||||||
|
CREATE POLICY "addon_credits_admin_write"
|
||||||
|
ON public.addon_credits FOR ALL
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
EXISTS (SELECT 1 FROM public.saas_admins WHERE user_id = auth.uid())
|
||||||
|
)
|
||||||
|
WITH CHECK (
|
||||||
|
EXISTS (SELECT 1 FROM public.saas_admins WHERE user_id = auth.uid())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ── addon_transactions: admin pode ver todas ──────────────────
|
||||||
|
DROP POLICY IF EXISTS "addon_transactions_admin_select" ON public.addon_transactions;
|
||||||
|
CREATE POLICY "addon_transactions_admin_select"
|
||||||
|
ON public.addon_transactions FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
EXISTS (SELECT 1 FROM public.saas_admins WHERE user_id = auth.uid())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ── addon_transactions: admin pode inserir ────────────────────
|
||||||
|
DROP POLICY IF EXISTS "addon_transactions_admin_insert" ON public.addon_transactions;
|
||||||
|
CREATE POLICY "addon_transactions_admin_insert"
|
||||||
|
ON public.addon_transactions FOR INSERT
|
||||||
|
TO authenticated
|
||||||
|
WITH CHECK (
|
||||||
|
EXISTS (SELECT 1 FROM public.saas_admins WHERE user_id = auth.uid())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ══════════════════════════════════════════════════════════════
|
||||||
|
-- Corrige policies de tenant members (SELECT)
|
||||||
|
-- A policy original usava tenant_id = auth.uid(), mas o auth.uid()
|
||||||
|
-- é o user_id, não o tenant_id. Usa is_tenant_member() em vez disso.
|
||||||
|
-- ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- addon_credits: membro do tenant vê os créditos do seu tenant
|
||||||
|
DROP POLICY IF EXISTS "addon_credits_select_own" ON public.addon_credits;
|
||||||
|
CREATE POLICY "addon_credits_select_own"
|
||||||
|
ON public.addon_credits FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
public.is_tenant_member(tenant_id)
|
||||||
|
OR owner_id = auth.uid()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- addon_transactions: membro do tenant vê as transações do seu tenant
|
||||||
|
DROP POLICY IF EXISTS "addon_transactions_select_own" ON public.addon_transactions;
|
||||||
|
CREATE POLICY "addon_transactions_select_own"
|
||||||
|
ON public.addon_transactions FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
public.is_tenant_member(tenant_id)
|
||||||
|
OR owner_id = auth.uid()
|
||||||
|
);
|
||||||
179
database-novo/fixes/fix_encoding_accents.sql
Normal file
179
database-novo/fixes/fix_encoding_accents.sql
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- FIX: Corrige acentuação perdida (caracteres ?? no banco)
|
||||||
|
-- =============================================================================
|
||||||
|
-- Causa: Seeds aplicados originalmente sem encoding UTF-8 correto.
|
||||||
|
-- Os ?? são bytes literais 0x3F (ASCII ?) onde deveria haver UTF-8.
|
||||||
|
-- Este fix faz UPDATE direto nos valores conhecidos.
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
SET client_encoding TO 'UTF8';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. PROFILES — full_name
|
||||||
|
-- ============================================================
|
||||||
|
UPDATE profiles SET full_name = 'Clínica Espaço Psi' WHERE id = 'aaaaaaaa-0003-0003-0003-000000000003' AND full_name != 'Clínica Espaço Psi';
|
||||||
|
UPDATE profiles SET full_name = 'Clínica Mente Sã' WHERE id = 'aaaaaaaa-0004-0004-0004-000000000004' AND full_name != 'Clínica Mente Sã';
|
||||||
|
UPDATE profiles SET full_name = 'Clínica Bem Estar' WHERE id = 'aaaaaaaa-0005-0005-0005-000000000005' AND full_name != 'Clínica Bem Estar';
|
||||||
|
UPDATE profiles SET full_name = 'Gabriela Secretária' WHERE id = 'aaaaaaaa-0011-0011-0011-000000000011' AND full_name != 'Gabriela Secretária';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. TENANTS — name
|
||||||
|
-- ============================================================
|
||||||
|
UPDATE tenants SET name = 'Clínica Espaço Psi' WHERE id = 'bbbbbbbb-0003-0003-0003-000000000003';
|
||||||
|
UPDATE tenants SET name = 'Clínica Mente Sã' WHERE id = 'bbbbbbbb-0004-0004-0004-000000000004';
|
||||||
|
UPDATE tenants SET name = 'Clínica Bem Estar' WHERE id = 'bbbbbbbb-0005-0005-0005-000000000005';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. DETERMINED_COMMITMENTS — name
|
||||||
|
-- ============================================================
|
||||||
|
UPDATE determined_commitments SET name = 'Sessão' WHERE native_key = 'session';
|
||||||
|
UPDATE determined_commitments SET name = 'Supervisão' WHERE native_key = 'supervision';
|
||||||
|
UPDATE determined_commitments SET name = 'Análise Pessoal' WHERE native_key = 'analysis';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. PLANS — name, description
|
||||||
|
-- ============================================================
|
||||||
|
UPDATE plans SET name = 'THERAPIST PRO', description = 'Plano profissional para terapeutas' WHERE key = 'therapist_pro' AND description LIKE '%??%';
|
||||||
|
UPDATE plans SET name = 'CLINIC PRO', description = 'Plano profissional para clínicas' WHERE key = 'clinic_pro' AND description LIKE '%??%';
|
||||||
|
UPDATE plans SET name = 'THERAPIST FREE', description = 'Plano gratuito para terapeutas' WHERE key = 'therapist_free' AND description LIKE '%??%';
|
||||||
|
UPDATE plans SET name = 'CLINIC FREE', description = 'Plano gratuito para clínicas' WHERE key = 'clinic_free' AND description LIKE '%??%';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 5. FEATURES — name, description
|
||||||
|
-- ============================================================
|
||||||
|
UPDATE features SET name = 'Agenda - Visualizar', description = 'Visualização da agenda' WHERE key = 'agenda.view';
|
||||||
|
UPDATE features SET name = 'Agenda - Gerenciar', description = 'Gerenciamento completo da agenda' WHERE key = 'agenda.manage';
|
||||||
|
UPDATE features SET name = 'Pacientes', description = 'Módulo de pacientes' WHERE key = 'patients';
|
||||||
|
UPDATE features SET name = 'Pacientes - Visualizar', description = 'Visualização de pacientes' WHERE key = 'patients.view';
|
||||||
|
UPDATE features SET name = 'Pacientes - Gerenciar', description = 'Gerenciamento completo de pacientes' WHERE key = 'patients.manage';
|
||||||
|
UPDATE features SET name = 'Agendamento Online', description = 'Sistema de agendamento online' WHERE key = 'online_scheduling';
|
||||||
|
UPDATE features SET name = 'Agendamento Online - Gerenciar', description = 'Gerenciamento do agendamento online' WHERE key = 'online_scheduling.manage';
|
||||||
|
UPDATE features SET name = 'Agendamento Online - Público', description = 'Página pública do agendador' WHERE key = 'online_scheduling.public';
|
||||||
|
UPDATE features SET name = 'Lembretes', description = 'Sistema de lembretes automáticos' WHERE key = 'reminders';
|
||||||
|
UPDATE features SET name = 'Relatórios Básicos', description = 'Relatórios básicos' WHERE key = 'reports_basic';
|
||||||
|
UPDATE features SET name = 'Relatórios Avançados', description = 'Relatórios avançados com exportação' WHERE key = 'reports_advanced';
|
||||||
|
UPDATE features SET name = 'Secretária', description = 'Funcionalidade de secretária' WHERE key = 'secretary';
|
||||||
|
UPDATE features SET name = 'Recepção Compartilhada', description = 'Recepção compartilhada entre terapeutas' WHERE key = 'shared_reception';
|
||||||
|
UPDATE features SET name = 'Salas', description = 'Gerenciamento de salas' WHERE key = 'rooms';
|
||||||
|
UPDATE features SET name = 'Intake Público', description = 'Formulário de intake público' WHERE key = 'intake_public';
|
||||||
|
UPDATE features SET name = 'Intakes PRO', description = 'Funcionalidades avançadas de intake' WHERE key = 'intakes_pro';
|
||||||
|
UPDATE features SET name = 'Branding Personalizado', description = 'Personalização de marca' WHERE key = 'custom_branding';
|
||||||
|
UPDATE features SET name = 'Acesso API', description = 'Acesso via API' WHERE key = 'api_access';
|
||||||
|
UPDATE features SET name = 'Log de Auditoria', description = 'Log de auditoria completo' WHERE key = 'audit_log';
|
||||||
|
UPDATE features SET name = 'Lembrete SMS', description = 'Lembretes via SMS' WHERE key = 'sms_reminder';
|
||||||
|
UPDATE features SET name = 'Calendário da Clínica', description = 'Visão consolidada do calendário' WHERE key = 'clinic_calendar';
|
||||||
|
UPDATE features SET name = 'Relatórios Avançados (Clínica)', description = 'Relatórios avançados da clínica' WHERE key = 'advanced_reports';
|
||||||
|
UPDATE features SET name = 'Supervisor - Acesso', description = 'Acesso ao módulo de supervisão' WHERE key = 'supervisor.access';
|
||||||
|
UPDATE features SET name = 'Supervisor - Convidar', description = 'Convidar supervisionados' WHERE key = 'supervisor.invite';
|
||||||
|
UPDATE features SET name = 'Supervisor - Sessões', description = 'Gerenciar sessões de supervisão' WHERE key = 'supervisor.sessions';
|
||||||
|
UPDATE features SET name = 'Supervisor - Relatórios', description = 'Relatórios de supervisão' WHERE key = 'supervisor.reports';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 6. EMAIL_TEMPLATES_GLOBAL — subject, body_html, body_text
|
||||||
|
-- ============================================================
|
||||||
|
UPDATE email_templates_global SET
|
||||||
|
subject = 'Lembrete: sua sessão amanhã às {{session_time}}',
|
||||||
|
body_text = 'Olá {{patient_name}}, lembrete da sua sessão amanhã às {{session_time}} com {{therapist_name}}.'
|
||||||
|
WHERE key = 'session.reminder';
|
||||||
|
|
||||||
|
UPDATE email_templates_global SET
|
||||||
|
subject = 'Sessão confirmada — {{session_date}} às {{session_time}}',
|
||||||
|
body_text = 'Sua sessão com {{therapist_name}} em {{session_date}} às {{session_time}} foi confirmada.'
|
||||||
|
WHERE key = 'session.confirmation';
|
||||||
|
|
||||||
|
UPDATE email_templates_global SET
|
||||||
|
subject = 'Sessão cancelada — {{session_date}}',
|
||||||
|
body_text = 'A sessão de {{session_date}} às {{session_time}} com {{therapist_name}} foi cancelada.'
|
||||||
|
WHERE key = 'session.cancellation';
|
||||||
|
|
||||||
|
UPDATE email_templates_global SET
|
||||||
|
subject = 'Sessão reagendada — novo horário: {{session_date}} às {{session_time}}',
|
||||||
|
body_text = 'Sua sessão foi reagendada para {{session_date}} às {{session_time}} com {{therapist_name}}.'
|
||||||
|
WHERE key = 'session.rescheduled';
|
||||||
|
|
||||||
|
UPDATE email_templates_global SET
|
||||||
|
subject = 'Recebemos seu cadastro — {{patient_name}}',
|
||||||
|
body_text = 'Olá {{patient_name}}, recebemos seu formulário de cadastro. Entraremos em contato em breve.'
|
||||||
|
WHERE key = 'intake.received';
|
||||||
|
|
||||||
|
UPDATE email_templates_global SET
|
||||||
|
subject = 'Cadastro aprovado — Bem-vindo(a)!',
|
||||||
|
body_text = 'Olá {{patient_name}}, seu cadastro foi aprovado. Você já pode acessar a plataforma.'
|
||||||
|
WHERE key = 'intake.approved';
|
||||||
|
|
||||||
|
UPDATE email_templates_global SET
|
||||||
|
subject = 'Cadastro não aprovado',
|
||||||
|
body_text = 'Olá {{patient_name}}, infelizmente seu cadastro não foi aprovado no momento.'
|
||||||
|
WHERE key = 'intake.rejected';
|
||||||
|
|
||||||
|
UPDATE email_templates_global SET
|
||||||
|
subject = 'Solicitação aceita — {{session_date}} às {{session_time}}',
|
||||||
|
body_text = 'Sua solicitação de agendamento para {{session_date}} às {{session_time}} foi aceita.'
|
||||||
|
WHERE key = 'scheduler.request_accepted';
|
||||||
|
|
||||||
|
UPDATE email_templates_global SET
|
||||||
|
subject = 'Solicitação não disponível',
|
||||||
|
body_text = 'Infelizmente o horário solicitado não está disponível. Por favor, escolha outro horário.'
|
||||||
|
WHERE key = 'scheduler.request_rejected';
|
||||||
|
|
||||||
|
UPDATE email_templates_global SET
|
||||||
|
subject = 'Bem-vindo(a) à AgenciaPsi!',
|
||||||
|
body_text = 'Olá {{user_name}}, sua conta foi criada com sucesso. Acesse a plataforma para começar.'
|
||||||
|
WHERE key = 'system.welcome';
|
||||||
|
|
||||||
|
UPDATE email_templates_global SET
|
||||||
|
subject = 'Redefinição de senha — AgenciaPsi',
|
||||||
|
body_text = 'Clique no link abaixo para redefinir sua senha: {{reset_link}}'
|
||||||
|
WHERE key = 'system.password_reset';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 7. LOGIN_CAROUSEL_SLIDES — title, description
|
||||||
|
-- ============================================================
|
||||||
|
UPDATE login_carousel_slides SET
|
||||||
|
title = '<strong>Gestão clínica simplificada</strong>',
|
||||||
|
body = 'Gerencie agenda, pacientes e financeiro em um só lugar. Simples, rápido e seguro.'
|
||||||
|
WHERE ordem = 1;
|
||||||
|
|
||||||
|
UPDATE login_carousel_slides SET
|
||||||
|
title = '<strong>Múltiplos profissionais, uma só plataforma</strong>',
|
||||||
|
body = 'Ideal para clínicas com vários terapeutas. Cada profissional com sua agenda e seus pacientes.'
|
||||||
|
WHERE ordem = 2;
|
||||||
|
|
||||||
|
UPDATE login_carousel_slides SET
|
||||||
|
title = '<strong>Seguro, privado e sempre disponível</strong>',
|
||||||
|
body = 'Seus dados protegidos com criptografia. Acesse de qualquer lugar, a qualquer hora.'
|
||||||
|
WHERE ordem = 3;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 8. PATIENT_GROUPS (default groups) — name
|
||||||
|
-- ============================================================
|
||||||
|
UPDATE patient_groups SET nome = 'Crianças' WHERE nome LIKE 'Crian%' AND is_system = true;
|
||||||
|
UPDATE patient_groups SET nome = 'Adolescentes' WHERE nome LIKE 'Adolescen%' AND is_system = true;
|
||||||
|
UPDATE patient_groups SET nome = 'Idosos' WHERE nome LIKE 'Idoso%' AND is_system = true;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 9. AUTH.USERS — raw_user_meta_data (name field)
|
||||||
|
-- ============================================================
|
||||||
|
UPDATE auth.users SET raw_user_meta_data = jsonb_set(raw_user_meta_data, '{name}', '"Clínica Espaço Psi"') WHERE id = 'aaaaaaaa-0003-0003-0003-000000000003';
|
||||||
|
UPDATE auth.users SET raw_user_meta_data = jsonb_set(raw_user_meta_data, '{name}', '"Clínica Mente Sã"') WHERE id = 'aaaaaaaa-0004-0004-0004-000000000004';
|
||||||
|
UPDATE auth.users SET raw_user_meta_data = jsonb_set(raw_user_meta_data, '{name}', '"Clínica Bem Estar"') WHERE id = 'aaaaaaaa-0005-0005-0005-000000000005';
|
||||||
|
UPDATE auth.users SET raw_user_meta_data = jsonb_set(raw_user_meta_data, '{name}', '"Gabriela Secretária"') WHERE id = 'aaaaaaaa-0011-0011-0011-000000000011';
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
broken_count int;
|
||||||
|
BEGIN
|
||||||
|
SELECT count(*) INTO broken_count
|
||||||
|
FROM profiles WHERE full_name LIKE '%??%';
|
||||||
|
|
||||||
|
IF broken_count = 0 THEN
|
||||||
|
RAISE NOTICE 'fix_encoding_accents: Todos os acentos corrigidos com sucesso.';
|
||||||
|
ELSE
|
||||||
|
RAISE WARNING 'fix_encoding_accents: Ainda restam % registros com ?? em profiles.full_name', broken_count;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
220
database-novo/fixes/fix_missing_subscriptions.sql
Normal file
220
database-novo/fixes/fix_missing_subscriptions.sql
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- FIX: Atribuir plano free a usuários/tenants sem assinatura ativa
|
||||||
|
-- =============================================================================
|
||||||
|
-- Execute no SQL Editor do Supabase (service_role)
|
||||||
|
-- Idempotente: só insere onde não existe assinatura ativa.
|
||||||
|
--
|
||||||
|
-- Regras:
|
||||||
|
-- • tenant kind = 'therapist' → therapist_free (por user_id do admin)
|
||||||
|
-- • tenant kind IN (clinic_*) → clinic_free (por tenant_id)
|
||||||
|
-- • profiles.account_type = 'patient' / portal_user → patient_free (por user_id)
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
-- DIAGNÓSTICO — mostra o estado atual antes de corrigir
|
||||||
|
-- ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
r RECORD;
|
||||||
|
BEGIN
|
||||||
|
RAISE NOTICE '=== DIAGNÓSTICO DE ASSINATURAS ===';
|
||||||
|
RAISE NOTICE '';
|
||||||
|
|
||||||
|
-- Terapeutas sem plano
|
||||||
|
RAISE NOTICE '--- Terapeutas SEM assinatura ativa ---';
|
||||||
|
FOR r IN
|
||||||
|
SELECT
|
||||||
|
tm.user_id,
|
||||||
|
p.full_name,
|
||||||
|
t.id AS tenant_id,
|
||||||
|
t.name AS tenant_name
|
||||||
|
FROM public.tenant_members tm
|
||||||
|
JOIN public.tenants t ON t.id = tm.tenant_id
|
||||||
|
JOIN public.profiles p ON p.id = tm.user_id
|
||||||
|
WHERE t.kind = 'therapist'
|
||||||
|
AND tm.role = 'tenant_admin'
|
||||||
|
AND tm.status = 'active'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM public.subscriptions s
|
||||||
|
WHERE s.user_id = tm.user_id
|
||||||
|
AND s.status = 'active'
|
||||||
|
)
|
||||||
|
LOOP
|
||||||
|
RAISE NOTICE ' FALTANDO: % (%) — tenant %', r.full_name, r.user_id, r.tenant_id;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
-- Clínicas sem plano
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE '--- Clínicas SEM assinatura ativa ---';
|
||||||
|
FOR r IN
|
||||||
|
SELECT t.id, t.name, t.kind
|
||||||
|
FROM public.tenants t
|
||||||
|
WHERE t.kind IN ('clinic_coworking', 'clinic_reception', 'clinic_full', 'clinic')
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM public.subscriptions s
|
||||||
|
WHERE s.tenant_id = t.id
|
||||||
|
AND s.status = 'active'
|
||||||
|
)
|
||||||
|
LOOP
|
||||||
|
RAISE NOTICE ' FALTANDO: % (%) — kind %', r.name, r.id, r.kind;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
-- Pacientes sem plano
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE '--- Pacientes SEM assinatura ativa ---';
|
||||||
|
FOR r IN
|
||||||
|
SELECT p.id, p.full_name
|
||||||
|
FROM public.profiles p
|
||||||
|
WHERE p.account_type = 'patient'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM public.subscriptions s
|
||||||
|
WHERE s.user_id = p.id
|
||||||
|
AND s.status = 'active'
|
||||||
|
)
|
||||||
|
LOOP
|
||||||
|
RAISE NOTICE ' FALTANDO: % (%)', r.full_name, r.id;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE '=== FIM DO DIAGNÓSTICO — aplicando correções... ===';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
-- ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
-- CORREÇÃO 1: Terapeutas sem assinatura → therapist_free
|
||||||
|
-- Escopo: user_id do tenant_admin do tenant kind='therapist'
|
||||||
|
-- ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
INSERT INTO public.subscriptions (
|
||||||
|
user_id, plan_id, plan_key, status, interval,
|
||||||
|
current_period_start, current_period_end,
|
||||||
|
source, started_at, activated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
tm.user_id,
|
||||||
|
p.id,
|
||||||
|
p.key,
|
||||||
|
'active',
|
||||||
|
'month',
|
||||||
|
now(),
|
||||||
|
now() + interval '30 days',
|
||||||
|
'fix_seed',
|
||||||
|
now(),
|
||||||
|
now()
|
||||||
|
FROM public.tenant_members tm
|
||||||
|
JOIN public.tenants t ON t.id = tm.tenant_id
|
||||||
|
JOIN public.plans p ON p.key = 'therapist_free'
|
||||||
|
WHERE t.kind = 'therapist'
|
||||||
|
AND tm.role = 'tenant_admin'
|
||||||
|
AND tm.status = 'active'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM public.subscriptions s
|
||||||
|
WHERE s.user_id = tm.user_id
|
||||||
|
AND s.status = 'active'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
-- CORREÇÃO 2: Clínicas sem assinatura → clinic_free
|
||||||
|
-- Escopo: tenant_id
|
||||||
|
-- ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
INSERT INTO public.subscriptions (
|
||||||
|
tenant_id, plan_id, plan_key, status, interval,
|
||||||
|
current_period_start, current_period_end,
|
||||||
|
source, started_at, activated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
t.id,
|
||||||
|
p.id,
|
||||||
|
p.key,
|
||||||
|
'active',
|
||||||
|
'month',
|
||||||
|
now(),
|
||||||
|
now() + interval '30 days',
|
||||||
|
'fix_seed',
|
||||||
|
now(),
|
||||||
|
now()
|
||||||
|
FROM public.tenants t
|
||||||
|
JOIN public.plans p ON p.key = 'clinic_free'
|
||||||
|
WHERE t.kind IN ('clinic_coworking', 'clinic_reception', 'clinic_full', 'clinic')
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM public.subscriptions s
|
||||||
|
WHERE s.tenant_id = t.id
|
||||||
|
AND s.status = 'active'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
-- CORREÇÃO 3: Pacientes sem assinatura → patient_free
|
||||||
|
-- Escopo: user_id
|
||||||
|
-- ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
INSERT INTO public.subscriptions (
|
||||||
|
user_id, plan_id, plan_key, status, interval,
|
||||||
|
current_period_start, current_period_end,
|
||||||
|
source, started_at, activated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
pr.id,
|
||||||
|
p.id,
|
||||||
|
p.key,
|
||||||
|
'active',
|
||||||
|
'month',
|
||||||
|
now(),
|
||||||
|
now() + interval '30 days',
|
||||||
|
'fix_seed',
|
||||||
|
now(),
|
||||||
|
now()
|
||||||
|
FROM public.profiles pr
|
||||||
|
JOIN public.plans p ON p.key = 'patient_free'
|
||||||
|
WHERE pr.account_type = 'patient'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM public.subscriptions s
|
||||||
|
WHERE s.user_id = pr.id
|
||||||
|
AND s.status = 'active'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
-- CONFIRMAÇÃO — mostra o que foi inserido (source = 'fix_seed')
|
||||||
|
-- ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
r RECORD;
|
||||||
|
total INT := 0;
|
||||||
|
BEGIN
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE '=== ASSINATURAS CRIADAS NESTA EXECUÇÃO ===';
|
||||||
|
|
||||||
|
FOR r IN
|
||||||
|
SELECT
|
||||||
|
s.plan_key,
|
||||||
|
COALESCE(pr.full_name, t.name) AS nome,
|
||||||
|
COALESCE(s.user_id::text, s.tenant_id::text) AS owner_id
|
||||||
|
FROM public.subscriptions s
|
||||||
|
LEFT JOIN public.profiles pr ON pr.id = s.user_id
|
||||||
|
LEFT JOIN public.tenants t ON t.id = s.tenant_id
|
||||||
|
WHERE s.source = 'fix_seed'
|
||||||
|
AND s.started_at >= now() - interval '5 seconds'
|
||||||
|
ORDER BY s.plan_key, nome
|
||||||
|
LOOP
|
||||||
|
RAISE NOTICE ' ✅ % → % (%)', r.plan_key, r.nome, r.owner_id;
|
||||||
|
total := total + 1;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
IF total = 0 THEN
|
||||||
|
RAISE NOTICE ' (nenhuma nova assinatura criada — todos já tinham plano ativo)';
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE ' Total: % assinatura(s) criada(s).', total;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
45
database-novo/fixes/fix_notification_templates_rls_admin.sql
Normal file
45
database-novo/fixes/fix_notification_templates_rls_admin.sql
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Fix: RLS notification_templates — acesso SaaS Admin
|
||||||
|
-- Admin precisa criar/editar/excluir templates globais (tenant_id IS NULL)
|
||||||
|
-- Agência PSI — 2026-03-22
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- SaaS Admin: acesso total (SELECT + INSERT + UPDATE + DELETE)
|
||||||
|
DROP POLICY IF EXISTS "notif_templates_admin_all" ON public.notification_templates;
|
||||||
|
CREATE POLICY "notif_templates_admin_all"
|
||||||
|
ON public.notification_templates FOR ALL
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
EXISTS (SELECT 1 FROM public.saas_admins WHERE user_id = auth.uid())
|
||||||
|
)
|
||||||
|
WITH CHECK (
|
||||||
|
EXISTS (SELECT 1 FROM public.saas_admins WHERE user_id = auth.uid())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tenant member: pode ler os globais + os do seu tenant
|
||||||
|
DROP POLICY IF EXISTS "notif_templates_read_global" ON public.notification_templates;
|
||||||
|
CREATE POLICY "notif_templates_read_global"
|
||||||
|
ON public.notification_templates FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
deleted_at IS NULL
|
||||||
|
AND (
|
||||||
|
(tenant_id IS NULL AND is_default = true)
|
||||||
|
OR owner_id = auth.uid()
|
||||||
|
OR public.is_tenant_member(tenant_id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tenant member: pode inserir/atualizar templates do seu tenant
|
||||||
|
DROP POLICY IF EXISTS "notif_templates_write_owner" ON public.notification_templates;
|
||||||
|
CREATE POLICY "notif_templates_write_owner"
|
||||||
|
ON public.notification_templates FOR ALL
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
owner_id = auth.uid()
|
||||||
|
OR public.is_tenant_member(tenant_id)
|
||||||
|
)
|
||||||
|
WITH CHECK (
|
||||||
|
owner_id = auth.uid()
|
||||||
|
OR public.is_tenant_member(tenant_id)
|
||||||
|
);
|
||||||
37
database-novo/fixes/fix_seed_patient_groups.sql
Normal file
37
database-novo/fixes/fix_seed_patient_groups.sql
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Fix: cria função seed_default_patient_groups
|
||||||
|
-- Colunas reais: nome, cor, descricao, tenant_id (NOT NULL)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.seed_default_patient_groups(p_tenant_id uuid)
|
||||||
|
RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_owner_id uuid;
|
||||||
|
BEGIN
|
||||||
|
-- busca o owner (tenant_admin) do tenant
|
||||||
|
SELECT user_id INTO v_owner_id
|
||||||
|
FROM public.tenant_members
|
||||||
|
WHERE tenant_id = p_tenant_id
|
||||||
|
AND role = 'tenant_admin'
|
||||||
|
AND status = 'active'
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF v_owner_id IS NULL THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
INSERT INTO public.patient_groups (owner_id, nome, cor, is_system, tenant_id)
|
||||||
|
VALUES
|
||||||
|
(v_owner_id, 'Crianças', '#60a5fa', true, p_tenant_id),
|
||||||
|
(v_owner_id, 'Adolescentes', '#a78bfa', true, p_tenant_id),
|
||||||
|
(v_owner_id, 'Idosos', '#34d399', true, p_tenant_id)
|
||||||
|
ON CONFLICT (owner_id, nome) DO NOTHING;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION public.seed_default_patient_groups(uuid)
|
||||||
|
TO postgres, anon, authenticated, service_role;
|
||||||
50
database-novo/fixes/fix_subscriptions_validate_scope.sql
Normal file
50
database-novo/fixes/fix_subscriptions_validate_scope.sql
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
-- Fix: subscriptions_validate_scope — adiciona suporte a target='patient'
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.subscriptions_validate_scope()
|
||||||
|
RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_target text;
|
||||||
|
BEGIN
|
||||||
|
SELECT lower(p.target) INTO v_target
|
||||||
|
FROM public.plans p
|
||||||
|
WHERE p.id = NEW.plan_id;
|
||||||
|
|
||||||
|
IF v_target IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'Plano inválido (target nulo).';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_target = 'clinic' THEN
|
||||||
|
IF NEW.tenant_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'Assinatura clinic exige tenant_id.';
|
||||||
|
END IF;
|
||||||
|
IF NEW.user_id IS NOT NULL THEN
|
||||||
|
RAISE EXCEPTION 'Assinatura clinic não pode ter user_id (XOR).';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
ELSIF v_target = 'therapist' THEN
|
||||||
|
IF NEW.tenant_id IS NOT NULL THEN
|
||||||
|
RAISE EXCEPTION 'Assinatura therapist não deve ter tenant_id.';
|
||||||
|
END IF;
|
||||||
|
IF NEW.user_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'Assinatura therapist exige user_id.';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
ELSIF v_target = 'patient' THEN
|
||||||
|
IF NEW.tenant_id IS NOT NULL THEN
|
||||||
|
RAISE EXCEPTION 'Assinatura patient não deve ter tenant_id.';
|
||||||
|
END IF;
|
||||||
|
IF NEW.user_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'Assinatura patient exige user_id.';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
ELSE
|
||||||
|
RAISE EXCEPTION 'Target de plano inválido: %', v_target;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
ALTER FUNCTION public.subscriptions_validate_scope() OWNER TO supabase_admin;
|
||||||
78
database-novo/fixes/fix_template_keys_match_populate.sql
Normal file
78
database-novo/fixes/fix_template_keys_match_populate.sql
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Fix: Template keys devem casar com o que populate_notification_queue gera
|
||||||
|
-- Agência PSI — 2026-03-22
|
||||||
|
-- ============================================================
|
||||||
|
-- O populate gera: 'session.' || REPLACE(event_type, '_sessao', '') || '.' || channel
|
||||||
|
-- Ex: event_type='lembrete_sessao' → 'session.lembrete.whatsapp'
|
||||||
|
--
|
||||||
|
-- Os seeds usavam nomes em inglês (session.reminder.whatsapp).
|
||||||
|
-- Este fix renomeia para casar com o populate.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- ── 1. Renomeia templates existentes ──────────────────────────
|
||||||
|
|
||||||
|
UPDATE public.notification_templates
|
||||||
|
SET key = 'session.lembrete.whatsapp'
|
||||||
|
WHERE key = 'session.reminder.whatsapp';
|
||||||
|
|
||||||
|
UPDATE public.notification_templates
|
||||||
|
SET key = 'session.lembrete_2h.whatsapp'
|
||||||
|
WHERE key = 'session.reminder_2h.whatsapp';
|
||||||
|
|
||||||
|
UPDATE public.notification_templates
|
||||||
|
SET key = 'session.confirmacao.whatsapp'
|
||||||
|
WHERE key = 'session.confirmation.whatsapp';
|
||||||
|
|
||||||
|
UPDATE public.notification_templates
|
||||||
|
SET key = 'session.cancelamento.whatsapp'
|
||||||
|
WHERE key = 'session.cancellation.whatsapp';
|
||||||
|
|
||||||
|
UPDATE public.notification_templates
|
||||||
|
SET key = 'session.reagendamento.whatsapp'
|
||||||
|
WHERE key = 'session.reschedule.whatsapp';
|
||||||
|
|
||||||
|
UPDATE public.notification_templates
|
||||||
|
SET key = 'cobranca.pendente.whatsapp'
|
||||||
|
WHERE key = 'billing.pending.whatsapp';
|
||||||
|
|
||||||
|
UPDATE public.notification_templates
|
||||||
|
SET key = 'sistema.boas_vindas.whatsapp'
|
||||||
|
WHERE key = 'system.welcome.whatsapp';
|
||||||
|
|
||||||
|
-- ── SMS templates (mesmo padrão) ──────────────────────────────
|
||||||
|
|
||||||
|
UPDATE public.notification_templates
|
||||||
|
SET key = 'session.lembrete.sms'
|
||||||
|
WHERE key = 'session.reminder.sms';
|
||||||
|
|
||||||
|
UPDATE public.notification_templates
|
||||||
|
SET key = 'session.lembrete_2h.sms'
|
||||||
|
WHERE key = 'session.reminder_2h.sms';
|
||||||
|
|
||||||
|
UPDATE public.notification_templates
|
||||||
|
SET key = 'session.confirmacao.sms'
|
||||||
|
WHERE key = 'session.confirmation.sms';
|
||||||
|
|
||||||
|
UPDATE public.notification_templates
|
||||||
|
SET key = 'session.cancelamento.sms'
|
||||||
|
WHERE key = 'session.cancellation.sms';
|
||||||
|
|
||||||
|
UPDATE public.notification_templates
|
||||||
|
SET key = 'session.reagendamento.sms'
|
||||||
|
WHERE key = 'session.reschedule.sms';
|
||||||
|
|
||||||
|
UPDATE public.notification_templates
|
||||||
|
SET key = 'cobranca.pendente.sms'
|
||||||
|
WHERE key = 'billing.pending.sms';
|
||||||
|
|
||||||
|
UPDATE public.notification_templates
|
||||||
|
SET key = 'sistema.boas_vindas.sms'
|
||||||
|
WHERE key = 'system.welcome.sms';
|
||||||
|
|
||||||
|
|
||||||
|
-- ── 2. Verifica resultado ─────────────────────────────────────
|
||||||
|
|
||||||
|
SELECT key, channel, domain, event_type, is_default
|
||||||
|
FROM notification_templates
|
||||||
|
WHERE deleted_at IS NULL
|
||||||
|
ORDER BY channel, key;
|
||||||
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`);
|
||||||
132
database-novo/migrations/001_twilio_whatsapp_subaccount.sql
Normal file
132
database-novo/migrations/001_twilio_whatsapp_subaccount.sql
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Migration 001: Twilio WhatsApp Subaccounts
|
||||||
|
-- =============================================================================
|
||||||
|
-- Adiciona suporte a subcontas Twilio com número WhatsApp dedicado por tenant.
|
||||||
|
-- Cada clínica/terapeuta recebe sua própria subconta Twilio com número exclusivo.
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- ── 1. Campos de subconta Twilio em notification_channels ──────────────────
|
||||||
|
|
||||||
|
ALTER TABLE public.notification_channels
|
||||||
|
ADD COLUMN IF NOT EXISTS twilio_subaccount_sid text,
|
||||||
|
ADD COLUMN IF NOT EXISTS twilio_phone_number text,
|
||||||
|
ADD COLUMN IF NOT EXISTS twilio_phone_sid text,
|
||||||
|
ADD COLUMN IF NOT EXISTS webhook_url text,
|
||||||
|
ADD COLUMN IF NOT EXISTS cost_per_message_usd numeric(8,6) DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS price_per_message_brl numeric(8,4) DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS provisioned_at timestamp with time zone;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN public.notification_channels.twilio_subaccount_sid IS 'SID da subconta Twilio criada para este tenant';
|
||||||
|
COMMENT ON COLUMN public.notification_channels.twilio_phone_number IS 'Número WhatsApp provisionado (E.164, ex: +5511999990000)';
|
||||||
|
COMMENT ON COLUMN public.notification_channels.twilio_phone_sid IS 'SID do número de telefone na subconta Twilio';
|
||||||
|
COMMENT ON COLUMN public.notification_channels.webhook_url IS 'URL do webhook configurada na Twilio para receber callbacks de status';
|
||||||
|
COMMENT ON COLUMN public.notification_channels.cost_per_message_usd IS 'Custo real Twilio por mensagem WhatsApp (USD)';
|
||||||
|
COMMENT ON COLUMN public.notification_channels.price_per_message_brl IS 'Valor cobrado do tenant por mensagem (BRL, inclui margem SaaS)';
|
||||||
|
COMMENT ON COLUMN public.notification_channels.provisioned_at IS 'Timestamp do provisionamento da subconta';
|
||||||
|
|
||||||
|
-- Índice para busca rápida por subconta
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_notification_channels_twilio_subaccount_sid
|
||||||
|
ON public.notification_channels (twilio_subaccount_sid)
|
||||||
|
WHERE twilio_subaccount_sid IS NOT NULL;
|
||||||
|
|
||||||
|
-- ── 2. Tabela de consumo por subconta ─────────────────────────────────────
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS public.twilio_subaccount_usage (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
channel_id uuid NOT NULL,
|
||||||
|
twilio_subaccount_sid text NOT NULL,
|
||||||
|
period_start date NOT NULL,
|
||||||
|
period_end date NOT NULL,
|
||||||
|
messages_sent integer DEFAULT 0 NOT NULL,
|
||||||
|
messages_delivered integer DEFAULT 0 NOT NULL,
|
||||||
|
messages_failed integer DEFAULT 0 NOT NULL,
|
||||||
|
cost_usd numeric(12,6) DEFAULT 0 NOT NULL,
|
||||||
|
cost_brl numeric(12,4) DEFAULT 0 NOT NULL,
|
||||||
|
revenue_brl numeric(12,4) DEFAULT 0 NOT NULL,
|
||||||
|
margin_brl numeric(12,4) GENERATED ALWAYS AS (revenue_brl - cost_brl) STORED,
|
||||||
|
usd_brl_rate numeric(8,4) DEFAULT 0,
|
||||||
|
synced_at timestamp with time zone DEFAULT now(),
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT twilio_subaccount_usage_pkey PRIMARY KEY (id),
|
||||||
|
CONSTRAINT twilio_subaccount_usage_channel_fk
|
||||||
|
FOREIGN KEY (channel_id) REFERENCES public.notification_channels(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT twilio_subaccount_usage_period_check
|
||||||
|
CHECK (period_end >= period_start)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE public.twilio_subaccount_usage IS
|
||||||
|
'Consumo mensal de mensagens WhatsApp por subconta Twilio. Sincronizado via Edge Function.';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_twilio_usage_tenant_period
|
||||||
|
ON public.twilio_subaccount_usage (tenant_id, period_start DESC);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_twilio_usage_channel
|
||||||
|
ON public.twilio_subaccount_usage (channel_id, period_start DESC);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_twilio_usage_unique_period
|
||||||
|
ON public.twilio_subaccount_usage (channel_id, period_start, period_end);
|
||||||
|
|
||||||
|
ALTER TABLE public.twilio_subaccount_usage OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
-- ── 3. RLS: twilio_subaccount_usage ───────────────────────────────────────
|
||||||
|
|
||||||
|
ALTER TABLE public.twilio_subaccount_usage ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Tenant vê apenas seu próprio consumo
|
||||||
|
CREATE POLICY "tenant_select_own_usage"
|
||||||
|
ON public.twilio_subaccount_usage
|
||||||
|
FOR SELECT
|
||||||
|
USING (
|
||||||
|
tenant_id IN (
|
||||||
|
SELECT tenant_id FROM public.tenant_members
|
||||||
|
WHERE user_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Apenas service_role pode inserir/atualizar (via Edge Function)
|
||||||
|
CREATE POLICY "service_role_manage_usage"
|
||||||
|
ON public.twilio_subaccount_usage
|
||||||
|
FOR ALL
|
||||||
|
USING (auth.role() = 'service_role');
|
||||||
|
|
||||||
|
-- ── 4. RLS: notification_channels — acesso ao twilio_subaccount_sid ───────
|
||||||
|
-- As políticas existentes já cobrem SELECT/UPDATE. Nenhuma alteração necessária.
|
||||||
|
|
||||||
|
-- ── 5. View: resumo de subcontas para o painel SaaS admin ─────────────────
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW public.v_twilio_whatsapp_overview AS
|
||||||
|
SELECT
|
||||||
|
nc.id AS channel_id,
|
||||||
|
nc.tenant_id,
|
||||||
|
nc.owner_id,
|
||||||
|
nc.is_active,
|
||||||
|
nc.connection_status,
|
||||||
|
nc.display_name,
|
||||||
|
nc.twilio_subaccount_sid,
|
||||||
|
nc.twilio_phone_number,
|
||||||
|
nc.twilio_phone_sid,
|
||||||
|
nc.cost_per_message_usd,
|
||||||
|
nc.price_per_message_brl,
|
||||||
|
nc.provisioned_at,
|
||||||
|
nc.created_at,
|
||||||
|
nc.updated_at,
|
||||||
|
-- Uso do mês atual
|
||||||
|
COALESCE(u.messages_sent, 0) AS current_month_sent,
|
||||||
|
COALESCE(u.messages_delivered, 0) AS current_month_delivered,
|
||||||
|
COALESCE(u.messages_failed, 0) AS current_month_failed,
|
||||||
|
COALESCE(u.cost_usd, 0) AS current_month_cost_usd,
|
||||||
|
COALESCE(u.cost_brl, 0) AS current_month_cost_brl,
|
||||||
|
COALESCE(u.revenue_brl, 0) AS current_month_revenue_brl,
|
||||||
|
COALESCE(u.margin_brl, 0) AS current_month_margin_brl
|
||||||
|
FROM public.notification_channels nc
|
||||||
|
LEFT JOIN public.twilio_subaccount_usage u
|
||||||
|
ON u.channel_id = nc.id
|
||||||
|
AND u.period_start = date_trunc('month', CURRENT_DATE)::date
|
||||||
|
WHERE nc.channel = 'whatsapp'
|
||||||
|
AND nc.provider = 'twilio'
|
||||||
|
AND nc.deleted_at IS NULL;
|
||||||
|
|
||||||
|
COMMENT ON VIEW public.v_twilio_whatsapp_overview IS
|
||||||
|
'Visão consolidada de subcontas Twilio WhatsApp com uso do mês corrente.';
|
||||||
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
|
||||||
|
-- =============================================================================
|
||||||
26057
database-novo/schema/00_full/schema.sql
Normal file
26057
database-novo/schema/00_full/schema.sql
Normal file
File diff suppressed because it is too large
Load Diff
256
database-novo/schema/01_extensions/extensions.sql
Normal file
256
database-novo/schema/01_extensions/extensions.sql
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Extensions e Schemas
|
||||||
|
-- Extraído de schema.sql (2026-03-23)
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
--
|
||||||
|
-- PostgreSQL database dump
|
||||||
|
--
|
||||||
|
|
||||||
|
\restrict ABfzP9IZJ8pAzvgt6E9jKpFn1phQ3b3Lgk09BZZTle5el6ODr77nIXlXnCf1PS1
|
||||||
|
|
||||||
|
-- Dumped from database version 17.6
|
||||||
|
-- Dumped by pg_dump version 17.6
|
||||||
|
|
||||||
|
SET statement_timeout = 0;
|
||||||
|
SET lock_timeout = 0;
|
||||||
|
SET idle_in_transaction_session_timeout = 0;
|
||||||
|
SET transaction_timeout = 0;
|
||||||
|
SET client_encoding = 'UTF8';
|
||||||
|
SET standard_conforming_strings = on;
|
||||||
|
SELECT pg_catalog.set_config('search_path', '', false);
|
||||||
|
SET check_function_bodies = false;
|
||||||
|
SET xmloption = content;
|
||||||
|
SET client_min_messages = warning;
|
||||||
|
SET row_security = off;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: _realtime; Type: SCHEMA; Schema: -; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SCHEMA _realtime;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER SCHEMA _realtime OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: auth; Type: SCHEMA; Schema: -; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SCHEMA auth;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER SCHEMA auth OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pg_cron; Type: EXTENSION; Schema: -; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_cron WITH SCHEMA pg_catalog;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: EXTENSION pg_cron; Type: COMMENT; Schema: -; Owner:
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON EXTENSION pg_cron IS 'Job scheduler for PostgreSQL';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: extensions; Type: SCHEMA; Schema: -; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SCHEMA extensions;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER SCHEMA extensions OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: graphql; Type: SCHEMA; Schema: -; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SCHEMA graphql;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER SCHEMA graphql OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: graphql_public; Type: SCHEMA; Schema: -; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SCHEMA graphql_public;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER SCHEMA graphql_public OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pg_net; Type: EXTENSION; Schema: -; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_net WITH SCHEMA extensions;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: EXTENSION pg_net; Type: COMMENT; Schema: -; Owner:
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON EXTENSION pg_net IS 'Async HTTP';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pgbouncer; Type: SCHEMA; Schema: -; Owner: pgbouncer
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SCHEMA pgbouncer;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER SCHEMA pgbouncer OWNER TO pgbouncer;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: realtime; Type: SCHEMA; Schema: -; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SCHEMA realtime;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER SCHEMA realtime OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: storage; Type: SCHEMA; Schema: -; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SCHEMA storage;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER SCHEMA storage OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: supabase_functions; Type: SCHEMA; Schema: -; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SCHEMA supabase_functions;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER SCHEMA supabase_functions OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: vault; Type: SCHEMA; Schema: -; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SCHEMA vault;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER SCHEMA vault OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: btree_gist; Type: EXTENSION; Schema: -; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS btree_gist WITH SCHEMA public;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: EXTENSION btree_gist; Type: COMMENT; Schema: -; Owner:
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON EXTENSION btree_gist IS 'support for indexing common datatypes in GiST';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: citext; Type: EXTENSION; Schema: -; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS citext WITH SCHEMA public;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: EXTENSION citext; Type: COMMENT; Schema: -; Owner:
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON EXTENSION citext IS 'data type for case-insensitive character strings';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pg_graphql; Type: EXTENSION; Schema: -; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_graphql WITH SCHEMA graphql;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: EXTENSION pg_graphql; Type: COMMENT; Schema: -; Owner:
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON EXTENSION pg_graphql IS 'pg_graphql: GraphQL support';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pg_stat_statements; Type: EXTENSION; Schema: -; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_stat_statements WITH SCHEMA extensions;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: EXTENSION pg_stat_statements; Type: COMMENT; Schema: -; Owner:
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON EXTENSION pg_stat_statements IS 'track planning and execution statistics of all SQL statements executed';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pg_trgm; Type: EXTENSION; Schema: -; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: EXTENSION pg_trgm; Type: COMMENT; Schema: -; Owner:
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON EXTENSION pg_trgm IS 'text similarity measurement and index searching based on trigrams';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA extensions;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: EXTENSION pgcrypto; Type: COMMENT; Schema: -; Owner:
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON EXTENSION pgcrypto IS 'cryptographic functions';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: supabase_vault; Type: EXTENSION; Schema: -; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS supabase_vault WITH SCHEMA vault;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: EXTENSION supabase_vault; Type: COMMENT; Schema: -; Owner:
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON EXTENSION supabase_vault IS 'Supabase Vault Extension';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: uuid-ossp; Type: EXTENSION; Schema: -; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA extensions;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: EXTENSION "uuid-ossp"; Type: COMMENT; Schema: -; Owner:
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON EXTENSION "uuid-ossp" IS 'generate universally unique identifiers (UUIDs)';
|
||||||
|
|
||||||
|
|
||||||
123
database-novo/schema/02_types/auth_types.sql
Normal file
123
database-novo/schema/02_types/auth_types.sql
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Types (Enums) — auth schema
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: aal_level; Type: TYPE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE auth.aal_level AS ENUM (
|
||||||
|
'aal1',
|
||||||
|
'aal2',
|
||||||
|
'aal3'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE auth.aal_level OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: code_challenge_method; Type: TYPE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE auth.code_challenge_method AS ENUM (
|
||||||
|
's256',
|
||||||
|
'plain'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE auth.code_challenge_method OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: factor_status; Type: TYPE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE auth.factor_status AS ENUM (
|
||||||
|
'unverified',
|
||||||
|
'verified'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE auth.factor_status OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: factor_type; Type: TYPE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE auth.factor_type AS ENUM (
|
||||||
|
'totp',
|
||||||
|
'webauthn',
|
||||||
|
'phone'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE auth.factor_type OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: oauth_authorization_status; Type: TYPE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE auth.oauth_authorization_status AS ENUM (
|
||||||
|
'pending',
|
||||||
|
'approved',
|
||||||
|
'denied',
|
||||||
|
'expired'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE auth.oauth_authorization_status OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: oauth_client_type; Type: TYPE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE auth.oauth_client_type AS ENUM (
|
||||||
|
'public',
|
||||||
|
'confidential'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE auth.oauth_client_type OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: oauth_registration_type; Type: TYPE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE auth.oauth_registration_type AS ENUM (
|
||||||
|
'dynamic',
|
||||||
|
'manual'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE auth.oauth_registration_type OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: oauth_response_type; Type: TYPE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE auth.oauth_response_type AS ENUM (
|
||||||
|
'code'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE auth.oauth_response_type OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: one_time_token_type; Type: TYPE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE auth.one_time_token_type AS ENUM (
|
||||||
|
'confirmation_token',
|
||||||
|
'reauthentication_token',
|
||||||
|
'recovery_token',
|
||||||
|
'email_change_token_new',
|
||||||
|
'email_change_token_current',
|
||||||
|
'phone_change_token'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE auth.one_time_token_type OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: commitment_log_source; Type: TYPE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
88
database-novo/schema/02_types/infra_types.sql
Normal file
88
database-novo/schema/02_types/infra_types.sql
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Types — realtime + storage schemas
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE TYPE realtime.action AS ENUM (
|
||||||
|
'INSERT',
|
||||||
|
'UPDATE',
|
||||||
|
'DELETE',
|
||||||
|
'TRUNCATE',
|
||||||
|
'ERROR'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE realtime.action OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: equality_op; Type: TYPE; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE realtime.equality_op AS ENUM (
|
||||||
|
'eq',
|
||||||
|
'neq',
|
||||||
|
'lt',
|
||||||
|
'lte',
|
||||||
|
'gt',
|
||||||
|
'gte',
|
||||||
|
'in'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE realtime.equality_op OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: user_defined_filter; Type: TYPE; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE realtime.user_defined_filter AS (
|
||||||
|
column_name text,
|
||||||
|
op realtime.equality_op,
|
||||||
|
value text
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE realtime.user_defined_filter OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: wal_column; Type: TYPE; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE realtime.wal_column AS (
|
||||||
|
name text,
|
||||||
|
type_name text,
|
||||||
|
type_oid oid,
|
||||||
|
value jsonb,
|
||||||
|
is_pkey boolean,
|
||||||
|
is_selectable boolean
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE realtime.wal_column OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: wal_rls; Type: TYPE; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE realtime.wal_rls AS (
|
||||||
|
wal jsonb,
|
||||||
|
is_rls_enabled boolean,
|
||||||
|
subscription_ids uuid[],
|
||||||
|
errors text[]
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE realtime.wal_rls OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: buckettype; Type: TYPE; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE storage.buckettype AS ENUM (
|
||||||
|
'STANDARD',
|
||||||
|
'ANALYTICS',
|
||||||
|
'VECTOR'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE storage.buckettype OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
137
database-novo/schema/02_types/public_types.sql
Normal file
137
database-novo/schema/02_types/public_types.sql
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Types (Enums) — public schema
|
||||||
|
-- =============================================================================
|
||||||
|
-- commitment_log_source, determined_field_type, financial_record_type,
|
||||||
|
-- recurrence_exception_type, recurrence_type, status_agenda_serie,
|
||||||
|
-- status_evento_agenda, status_excecao_agenda, tipo_evento_agenda,
|
||||||
|
-- tipo_excecao_agenda
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE TYPE public.commitment_log_source AS ENUM (
|
||||||
|
'manual',
|
||||||
|
'auto'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE public.commitment_log_source OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: determined_field_type; Type: TYPE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE public.determined_field_type AS ENUM (
|
||||||
|
'text',
|
||||||
|
'textarea',
|
||||||
|
'number',
|
||||||
|
'date',
|
||||||
|
'select',
|
||||||
|
'boolean'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE public.determined_field_type OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: financial_record_type; Type: TYPE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE public.financial_record_type AS ENUM (
|
||||||
|
'receita',
|
||||||
|
'despesa'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE public.financial_record_type OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: recurrence_exception_type; Type: TYPE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE public.recurrence_exception_type AS ENUM (
|
||||||
|
'cancel_session',
|
||||||
|
'reschedule_session',
|
||||||
|
'patient_missed',
|
||||||
|
'therapist_canceled',
|
||||||
|
'holiday_block'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE public.recurrence_exception_type OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: recurrence_type; Type: TYPE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE public.recurrence_type AS ENUM (
|
||||||
|
'weekly',
|
||||||
|
'biweekly',
|
||||||
|
'monthly',
|
||||||
|
'yearly',
|
||||||
|
'custom_weekdays'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE public.recurrence_type OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: status_agenda_serie; Type: TYPE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE public.status_agenda_serie AS ENUM (
|
||||||
|
'ativo',
|
||||||
|
'pausado',
|
||||||
|
'cancelado'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE public.status_agenda_serie OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: status_evento_agenda; Type: TYPE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE public.status_evento_agenda AS ENUM (
|
||||||
|
'agendado',
|
||||||
|
'realizado',
|
||||||
|
'faltou',
|
||||||
|
'cancelado',
|
||||||
|
'remarcar'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE public.status_evento_agenda OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: status_excecao_agenda; Type: TYPE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE public.status_excecao_agenda AS ENUM (
|
||||||
|
'pendente',
|
||||||
|
'ativo',
|
||||||
|
'arquivado'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE public.status_excecao_agenda OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: tipo_evento_agenda; Type: TYPE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE public.tipo_evento_agenda AS ENUM (
|
||||||
|
'sessao',
|
||||||
|
'bloqueio'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE public.tipo_evento_agenda OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: tipo_excecao_agenda; Type: TYPE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE public.tipo_excecao_agenda AS ENUM (
|
||||||
|
'bloqueio',
|
||||||
|
'horario_extra'
|
||||||
|
);
|
||||||
|
|
||||||
650
database-novo/schema/03_functions/agenda.sql
Normal file
650
database-novo/schema/03_functions/agenda.sql
Normal file
@@ -0,0 +1,650 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Functions — Agenda
|
||||||
|
-- =============================================================================
|
||||||
|
-- agenda_cfg_sync, agendador_dias_disponiveis, agendador_gerar_slug,
|
||||||
|
-- agendador_slots_disponiveis, cancel_recurrence_from,
|
||||||
|
-- cancelar_eventos_serie, fn_agenda_regras_semanais_no_overlap,
|
||||||
|
-- split_recurrence_at, sync_busy_mirror, set_updated_at_recurrence
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION public.agenda_cfg_sync() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
begin
|
||||||
|
if new.agenda_view_mode = 'custom' then
|
||||||
|
new.usar_horario_admin_custom := true;
|
||||||
|
new.admin_inicio_visualizacao := new.agenda_custom_start;
|
||||||
|
new.admin_fim_visualizacao := new.agenda_custom_end;
|
||||||
|
else
|
||||||
|
new.usar_horario_admin_custom := false;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.agenda_cfg_sync() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agendador_dias_disponiveis(text, integer, integer); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.agendador_dias_disponiveis(p_slug text, p_ano integer, p_mes integer) RETURNS TABLE(data date, tem_slots boolean)
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_owner_id uuid;
|
||||||
|
v_antecedencia int;
|
||||||
|
v_agora timestamptz;
|
||||||
|
v_data date;
|
||||||
|
v_data_inicio date;
|
||||||
|
v_data_fim date;
|
||||||
|
v_db_dow int;
|
||||||
|
v_tem_slot boolean;
|
||||||
|
v_bloqueado boolean;
|
||||||
|
BEGIN
|
||||||
|
SELECT c.owner_id, c.antecedencia_minima_horas
|
||||||
|
INTO v_owner_id, v_antecedencia
|
||||||
|
FROM public.agendador_configuracoes c
|
||||||
|
WHERE c.link_slug = p_slug AND c.ativo = true
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF v_owner_id IS NULL THEN RETURN; END IF;
|
||||||
|
|
||||||
|
v_agora := now();
|
||||||
|
v_data_inicio := make_date(p_ano, p_mes, 1);
|
||||||
|
v_data_fim := (v_data_inicio + interval '1 month' - interval '1 day')::date;
|
||||||
|
|
||||||
|
v_data := v_data_inicio;
|
||||||
|
WHILE v_data <= v_data_fim LOOP
|
||||||
|
v_db_dow := extract(dow from v_data::timestamp)::int;
|
||||||
|
|
||||||
|
-- ── Dia inteiro bloqueado? (agenda_bloqueios) ─────────────────────────
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM public.agenda_bloqueios b
|
||||||
|
WHERE b.owner_id = v_owner_id
|
||||||
|
AND b.data_inicio <= v_data
|
||||||
|
AND COALESCE(b.data_fim, v_data) >= v_data
|
||||||
|
AND b.hora_inicio IS NULL -- bloqueio de dia inteiro
|
||||||
|
AND (
|
||||||
|
(NOT b.recorrente)
|
||||||
|
OR (b.recorrente AND b.dia_semana = v_db_dow)
|
||||||
|
)
|
||||||
|
) INTO v_bloqueado;
|
||||||
|
|
||||||
|
IF v_bloqueado THEN
|
||||||
|
v_data := v_data + 1;
|
||||||
|
CONTINUE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ── Tem slots disponíveis no dia? ─────────────────────────────────────
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM public.agenda_online_slots s
|
||||||
|
WHERE s.owner_id = v_owner_id
|
||||||
|
AND s.weekday = v_db_dow
|
||||||
|
AND s.enabled = true
|
||||||
|
AND (v_data::text || ' ' || s.time::text)::timestamp
|
||||||
|
AT TIME ZONE 'America/Sao_Paulo'
|
||||||
|
>= v_agora + (v_antecedencia || ' hours')::interval
|
||||||
|
) INTO v_tem_slot;
|
||||||
|
|
||||||
|
IF v_tem_slot THEN
|
||||||
|
data := v_data;
|
||||||
|
tem_slots := true;
|
||||||
|
RETURN NEXT;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
v_data := v_data + 1;
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.agendador_dias_disponiveis(p_slug text, p_ano integer, p_mes integer) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agendador_gerar_slug(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.agendador_gerar_slug() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_slug text;
|
||||||
|
v_exists boolean;
|
||||||
|
BEGIN
|
||||||
|
-- só gera se ativou e não tem slug ainda
|
||||||
|
IF NEW.ativo = true AND (NEW.link_slug IS NULL OR NEW.link_slug = '') THEN
|
||||||
|
LOOP
|
||||||
|
v_slug := lower(substring(replace(gen_random_uuid()::text, '-', ''), 1, 8));
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM public.agendador_configuracoes
|
||||||
|
WHERE link_slug = v_slug AND owner_id <> NEW.owner_id
|
||||||
|
) INTO v_exists;
|
||||||
|
EXIT WHEN NOT v_exists;
|
||||||
|
END LOOP;
|
||||||
|
NEW.link_slug := v_slug;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.agendador_gerar_slug() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agendador_slots_disponiveis(text, date); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.agendador_slots_disponiveis(p_slug text, p_data date) RETURNS TABLE(hora time without time zone, disponivel boolean)
|
||||||
|
CREATE FUNCTION public.agendador_gerar_slug() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_slug text;
|
||||||
|
v_exists boolean;
|
||||||
|
BEGIN
|
||||||
|
-- só gera se ativou e não tem slug ainda
|
||||||
|
IF NEW.ativo = true AND (NEW.link_slug IS NULL OR NEW.link_slug = '') THEN
|
||||||
|
LOOP
|
||||||
|
v_slug := lower(substring(replace(gen_random_uuid()::text, '-', ''), 1, 8));
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM public.agendador_configuracoes
|
||||||
|
WHERE link_slug = v_slug AND owner_id <> NEW.owner_id
|
||||||
|
) INTO v_exists;
|
||||||
|
EXIT WHEN NOT v_exists;
|
||||||
|
END LOOP;
|
||||||
|
NEW.link_slug := v_slug;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.agendador_gerar_slug() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agendador_slots_disponiveis(text, date); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.agendador_slots_disponiveis(p_slug text, p_data date) RETURNS TABLE(hora time without time zone, disponivel boolean)
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_owner_id uuid;
|
||||||
|
v_duracao int;
|
||||||
|
v_antecedencia int;
|
||||||
|
v_agora timestamptz;
|
||||||
|
v_db_dow int;
|
||||||
|
v_slot time;
|
||||||
|
v_slot_fim time;
|
||||||
|
v_slot_ts timestamptz;
|
||||||
|
v_ocupado boolean;
|
||||||
|
-- loop de recorrências
|
||||||
|
v_rule RECORD;
|
||||||
|
v_rule_start_dow int;
|
||||||
|
v_first_occ date;
|
||||||
|
v_day_diff int;
|
||||||
|
v_ex_type text;
|
||||||
|
BEGIN
|
||||||
|
SELECT c.owner_id, c.duracao_sessao_min, c.antecedencia_minima_horas
|
||||||
|
INTO v_owner_id, v_duracao, v_antecedencia
|
||||||
|
FROM public.agendador_configuracoes c
|
||||||
|
WHERE c.link_slug = p_slug AND c.ativo = true
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF v_owner_id IS NULL THEN RETURN; END IF;
|
||||||
|
|
||||||
|
v_agora := now();
|
||||||
|
v_db_dow := extract(dow from p_data::timestamp)::int;
|
||||||
|
|
||||||
|
-- ── Dia inteiro bloqueado? (agenda_bloqueios sem hora) ───────────────────
|
||||||
|
-- Se sim, não há nenhum slot disponível — retorna vazio.
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM public.agenda_bloqueios b
|
||||||
|
WHERE b.owner_id = v_owner_id
|
||||||
|
AND b.data_inicio <= p_data
|
||||||
|
AND COALESCE(b.data_fim, p_data) >= p_data
|
||||||
|
AND b.hora_inicio IS NULL -- bloqueio de dia inteiro
|
||||||
|
AND (
|
||||||
|
(NOT b.recorrente)
|
||||||
|
OR (b.recorrente AND b.dia_semana = v_db_dow)
|
||||||
|
)
|
||||||
|
) THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
FOR v_slot IN
|
||||||
|
SELECT s.time
|
||||||
|
FROM public.agenda_online_slots s
|
||||||
|
WHERE s.owner_id = v_owner_id
|
||||||
|
AND s.weekday = v_db_dow
|
||||||
|
AND s.enabled = true
|
||||||
|
ORDER BY s.time
|
||||||
|
LOOP
|
||||||
|
v_slot_fim := v_slot + (v_duracao || ' minutes')::interval;
|
||||||
|
v_ocupado := false;
|
||||||
|
|
||||||
|
-- ── Antecedência mínima ──────────────────────────────────────────────────
|
||||||
|
v_slot_ts := (p_data::text || ' ' || v_slot::text)::timestamp
|
||||||
|
AT TIME ZONE 'America/Sao_Paulo';
|
||||||
|
IF v_slot_ts < v_agora + (v_antecedencia || ' hours')::interval THEN
|
||||||
|
v_ocupado := true;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ── Bloqueio de horário específico (agenda_bloqueios com hora) ───────────
|
||||||
|
IF NOT v_ocupado THEN
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM public.agenda_bloqueios b
|
||||||
|
WHERE b.owner_id = v_owner_id
|
||||||
|
AND b.data_inicio <= p_data
|
||||||
|
AND COALESCE(b.data_fim, p_data) >= p_data
|
||||||
|
AND b.hora_inicio IS NOT NULL
|
||||||
|
AND b.hora_inicio < v_slot_fim
|
||||||
|
AND b.hora_fim > v_slot
|
||||||
|
AND (
|
||||||
|
(NOT b.recorrente)
|
||||||
|
OR (b.recorrente AND b.dia_semana = v_db_dow)
|
||||||
|
)
|
||||||
|
) INTO v_ocupado;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ── Eventos avulsos internos (agenda_eventos) ────────────────────────────
|
||||||
|
IF NOT v_ocupado THEN
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM public.agenda_eventos e
|
||||||
|
WHERE e.owner_id = v_owner_id
|
||||||
|
AND e.status::text NOT IN ('cancelado', 'faltou')
|
||||||
|
AND (e.inicio_em AT TIME ZONE 'America/Sao_Paulo')::date = p_data
|
||||||
|
AND (e.inicio_em AT TIME ZONE 'America/Sao_Paulo')::time < v_slot_fim
|
||||||
|
AND (e.fim_em AT TIME ZONE 'America/Sao_Paulo')::time > v_slot
|
||||||
|
) INTO v_ocupado;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ── Recorrências ativas (recurrence_rules) ───────────────────────────────
|
||||||
|
IF NOT v_ocupado THEN
|
||||||
|
FOR v_rule IN
|
||||||
|
SELECT
|
||||||
|
r.id,
|
||||||
|
r.start_date::date AS start_date,
|
||||||
|
r.end_date::date AS end_date,
|
||||||
|
r.start_time::time AS start_time,
|
||||||
|
r.end_time::time AS end_time,
|
||||||
|
COALESCE(r.interval, 1)::int AS interval
|
||||||
|
FROM public.recurrence_rules r
|
||||||
|
WHERE r.owner_id = v_owner_id
|
||||||
|
AND r.status = 'ativo'
|
||||||
|
AND p_data >= r.start_date::date
|
||||||
|
AND (r.end_date IS NULL OR p_data <= r.end_date::date)
|
||||||
|
AND v_db_dow = ANY(r.weekdays)
|
||||||
|
AND r.start_time::time < v_slot_fim
|
||||||
|
AND r.end_time::time > v_slot
|
||||||
|
LOOP
|
||||||
|
v_rule_start_dow := extract(dow from v_rule.start_date)::int;
|
||||||
|
v_first_occ := v_rule.start_date
|
||||||
|
+ (((v_db_dow - v_rule_start_dow + 7) % 7))::int;
|
||||||
|
v_day_diff := (p_data - v_first_occ)::int;
|
||||||
|
|
||||||
|
IF v_day_diff >= 0 AND v_day_diff % (7 * v_rule.interval) = 0 THEN
|
||||||
|
v_ex_type := NULL;
|
||||||
|
SELECT ex.type INTO v_ex_type
|
||||||
|
FROM public.recurrence_exceptions ex
|
||||||
|
WHERE ex.recurrence_id = v_rule.id
|
||||||
|
AND ex.original_date = p_data
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF v_ex_type IS NULL OR v_ex_type NOT IN (
|
||||||
|
'cancel_session', 'patient_missed',
|
||||||
|
'therapist_canceled', 'holiday_block',
|
||||||
|
'reschedule_session'
|
||||||
|
) THEN
|
||||||
|
v_ocupado := true;
|
||||||
|
EXIT;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ── Recorrências remarcadas para este dia ────────────────────────────────
|
||||||
|
IF NOT v_ocupado THEN
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM public.recurrence_exceptions ex
|
||||||
|
JOIN public.recurrence_rules r ON r.id = ex.recurrence_id
|
||||||
|
WHERE r.owner_id = v_owner_id
|
||||||
|
AND r.status = 'ativo'
|
||||||
|
AND ex.type = 'reschedule_session'
|
||||||
|
AND ex.new_date = p_data
|
||||||
|
AND COALESCE(ex.new_start_time, r.start_time)::time < v_slot_fim
|
||||||
|
AND COALESCE(ex.new_end_time, r.end_time)::time > v_slot
|
||||||
|
) INTO v_ocupado;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ── Solicitações públicas pendentes ──────────────────────────────────────
|
||||||
|
IF NOT v_ocupado THEN
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM public.agendador_solicitacoes sol
|
||||||
|
WHERE sol.owner_id = v_owner_id
|
||||||
|
AND sol.status = 'pendente'
|
||||||
|
AND sol.data_solicitada = p_data
|
||||||
|
AND sol.hora_solicitada = v_slot
|
||||||
|
AND (sol.reservado_ate IS NULL OR sol.reservado_ate > v_agora)
|
||||||
|
) INTO v_ocupado;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
hora := v_slot;
|
||||||
|
disponivel := NOT v_ocupado;
|
||||||
|
RETURN NEXT;
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.agendador_slots_disponiveis(p_slug text, p_data date) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: auto_create_financial_record_from_session(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.auto_create_financial_record_from_session() RETURNS trigger
|
||||||
|
CREATE FUNCTION public.cancel_recurrence_from(p_recurrence_id uuid, p_from_date date) RETURNS void
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
UPDATE public.recurrence_rules
|
||||||
|
SET
|
||||||
|
end_date = p_from_date - INTERVAL '1 day',
|
||||||
|
open_ended = false,
|
||||||
|
status = CASE
|
||||||
|
WHEN p_from_date <= start_date THEN 'cancelado'
|
||||||
|
ELSE status
|
||||||
|
END,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE id = p_recurrence_id;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.cancel_recurrence_from(p_recurrence_id uuid, p_from_date date) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: cancel_subscription(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.cancel_subscription(p_subscription_id uuid) RETURNS public.subscriptions
|
||||||
|
CREATE FUNCTION public.cancelar_eventos_serie(p_serie_id uuid, p_a_partir_de timestamp with time zone DEFAULT now()) RETURNS integer
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_count integer;
|
||||||
|
BEGIN
|
||||||
|
UPDATE public.agenda_eventos
|
||||||
|
SET status = 'cancelado',
|
||||||
|
updated_at = now()
|
||||||
|
WHERE serie_id = p_serie_id
|
||||||
|
AND inicio_em >= p_a_partir_de
|
||||||
|
AND status NOT IN ('realizado', 'cancelado');
|
||||||
|
|
||||||
|
GET DIAGNOSTICS v_count = ROW_COUNT;
|
||||||
|
RETURN v_count;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.cancelar_eventos_serie(p_serie_id uuid, p_a_partir_de timestamp with time zone) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: FUNCTION cancelar_eventos_serie(p_serie_id uuid, p_a_partir_de timestamp with time zone); Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION public.cancelar_eventos_serie(p_serie_id uuid, p_a_partir_de timestamp with time zone) IS 'Cancela todos os eventos futuros de uma série a partir de p_a_partir_de (inclusive).
|
||||||
|
Não cancela eventos já realizados.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: change_subscription_plan(uuid, uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.change_subscription_plan(p_subscription_id uuid, p_new_plan_id uuid) RETURNS public.subscriptions
|
||||||
|
CREATE FUNCTION public.fn_agenda_regras_semanais_no_overlap() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
declare
|
||||||
|
v_count int;
|
||||||
|
begin
|
||||||
|
if new.ativo is false then
|
||||||
|
return new;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select count(*) into v_count
|
||||||
|
from public.agenda_regras_semanais r
|
||||||
|
where r.owner_id = new.owner_id
|
||||||
|
and r.dia_semana = new.dia_semana
|
||||||
|
and r.ativo is true
|
||||||
|
and (tg_op = 'INSERT' or r.id <> new.id)
|
||||||
|
and (new.hora_inicio < r.hora_fim and new.hora_fim > r.hora_inicio);
|
||||||
|
|
||||||
|
if v_count > 0 then
|
||||||
|
raise exception 'Janela sobreposta: já existe uma regra ativa nesse intervalo.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.fn_agenda_regras_semanais_no_overlap() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: get_financial_report(uuid, date, date, text); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.get_financial_report(p_owner_id uuid, p_start_date date, p_end_date date, p_group_by text DEFAULT 'month'::text) RETURNS TABLE(group_key text, group_label text, total_receitas numeric, total_despesas numeric, saldo numeric, total_pendente numeric, total_overdue numeric, count_records bigint)
|
||||||
|
CREATE FUNCTION public.set_updated_at_recurrence() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN NEW.updated_at = now(); RETURN NEW; END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.set_updated_at_recurrence() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: split_recurrence_at(uuid, date); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.split_recurrence_at(p_recurrence_id uuid, p_from_date date) RETURNS uuid
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_old public.recurrence_rules;
|
||||||
|
v_new_id uuid;
|
||||||
|
BEGIN
|
||||||
|
-- busca a regra original
|
||||||
|
SELECT * INTO v_old
|
||||||
|
FROM public.recurrence_rules
|
||||||
|
WHERE id = p_recurrence_id;
|
||||||
|
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RAISE EXCEPTION 'recurrence_rule % não encontrada', p_recurrence_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- encerra a regra antiga na data anterior
|
||||||
|
UPDATE public.recurrence_rules
|
||||||
|
SET
|
||||||
|
end_date = p_from_date - INTERVAL '1 day',
|
||||||
|
open_ended = false,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE id = p_recurrence_id;
|
||||||
|
|
||||||
|
-- cria nova regra a partir de p_from_date
|
||||||
|
INSERT INTO public.recurrence_rules (
|
||||||
|
tenant_id, owner_id, therapist_id, patient_id,
|
||||||
|
determined_commitment_id, type, interval, weekdays,
|
||||||
|
start_time, end_time, timezone, duration_min,
|
||||||
|
start_date, end_date, max_occurrences, open_ended,
|
||||||
|
modalidade, titulo_custom, observacoes, extra_fields, status
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
tenant_id, owner_id, therapist_id, patient_id,
|
||||||
|
determined_commitment_id, type, interval, weekdays,
|
||||||
|
start_time, end_time, timezone, duration_min,
|
||||||
|
p_from_date, v_old.end_date, v_old.max_occurrences, v_old.open_ended,
|
||||||
|
modalidade, titulo_custom, observacoes, extra_fields, status
|
||||||
|
FROM public.recurrence_rules
|
||||||
|
WHERE id = p_recurrence_id
|
||||||
|
RETURNING id INTO v_new_id;
|
||||||
|
|
||||||
|
RETURN v_new_id;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.split_recurrence_at(p_recurrence_id uuid, p_from_date date) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: subscription_intents_view_insert(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.subscription_intents_view_insert() RETURNS trigger
|
||||||
|
CREATE FUNCTION public.sync_busy_mirror_agenda_eventos() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
declare
|
||||||
|
clinic_tenant uuid;
|
||||||
|
is_personal boolean;
|
||||||
|
should_mirror boolean;
|
||||||
|
begin
|
||||||
|
-- Anti-recursão: espelho não espelha
|
||||||
|
if (tg_op <> 'DELETE') then
|
||||||
|
if new.mirror_of_event_id is not null then
|
||||||
|
return new;
|
||||||
|
end if;
|
||||||
|
else
|
||||||
|
if old.mirror_of_event_id is not null then
|
||||||
|
return old;
|
||||||
|
end if;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- Define se é pessoal e se deve espelhar
|
||||||
|
if (tg_op = 'DELETE') then
|
||||||
|
is_personal := (old.tenant_id = old.owner_id);
|
||||||
|
should_mirror := (old.visibility_scope in ('busy_only','private'));
|
||||||
|
else
|
||||||
|
is_personal := (new.tenant_id = new.owner_id);
|
||||||
|
should_mirror := (new.visibility_scope in ('busy_only','private'));
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- Se não é pessoal, não faz nada
|
||||||
|
if not is_personal then
|
||||||
|
if (tg_op = 'DELETE') then
|
||||||
|
return old;
|
||||||
|
end if;
|
||||||
|
return new;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- DELETE: remove espelhos existentes
|
||||||
|
if (tg_op = 'DELETE') then
|
||||||
|
delete from public.agenda_eventos e
|
||||||
|
where e.mirror_of_event_id = old.id
|
||||||
|
and e.mirror_source = 'personal_busy_mirror';
|
||||||
|
|
||||||
|
return old;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- INSERT/UPDATE:
|
||||||
|
-- Se não deve espelhar, remove espelhos e sai
|
||||||
|
if not should_mirror then
|
||||||
|
delete from public.agenda_eventos e
|
||||||
|
where e.mirror_of_event_id = new.id
|
||||||
|
and e.mirror_source = 'personal_busy_mirror';
|
||||||
|
|
||||||
|
return new;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- Para cada clínica onde o usuário é therapist active, cria/atualiza o "Ocupado"
|
||||||
|
for clinic_tenant in
|
||||||
|
select tm.tenant_id
|
||||||
|
from public.tenant_members tm
|
||||||
|
where tm.user_id = new.owner_id
|
||||||
|
and tm.role = 'therapist'
|
||||||
|
and tm.status = 'active'
|
||||||
|
and tm.tenant_id <> new.owner_id
|
||||||
|
loop
|
||||||
|
insert into public.agenda_eventos (
|
||||||
|
tenant_id,
|
||||||
|
owner_id,
|
||||||
|
terapeuta_id,
|
||||||
|
paciente_id,
|
||||||
|
tipo,
|
||||||
|
status,
|
||||||
|
titulo,
|
||||||
|
observacoes,
|
||||||
|
inicio_em,
|
||||||
|
fim_em,
|
||||||
|
mirror_of_event_id,
|
||||||
|
mirror_source,
|
||||||
|
visibility_scope,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) values (
|
||||||
|
clinic_tenant,
|
||||||
|
new.owner_id,
|
||||||
|
new.owner_id,
|
||||||
|
null,
|
||||||
|
'bloqueio'::public.tipo_evento_agenda,
|
||||||
|
'agendado'::public.status_evento_agenda,
|
||||||
|
'Ocupado',
|
||||||
|
null,
|
||||||
|
new.inicio_em,
|
||||||
|
new.fim_em,
|
||||||
|
new.id,
|
||||||
|
'personal_busy_mirror',
|
||||||
|
'public',
|
||||||
|
now(),
|
||||||
|
now()
|
||||||
|
)
|
||||||
|
on conflict (tenant_id, mirror_of_event_id) where mirror_of_event_id is not null
|
||||||
|
do update set
|
||||||
|
owner_id = excluded.owner_id,
|
||||||
|
terapeuta_id = excluded.terapeuta_id,
|
||||||
|
tipo = excluded.tipo,
|
||||||
|
status = excluded.status,
|
||||||
|
titulo = excluded.titulo,
|
||||||
|
observacoes = excluded.observacoes,
|
||||||
|
inicio_em = excluded.inicio_em,
|
||||||
|
fim_em = excluded.fim_em,
|
||||||
|
updated_at = now();
|
||||||
|
end loop;
|
||||||
|
|
||||||
|
-- Limpa espelhos de clínicas onde o vínculo therapist active não existe mais
|
||||||
|
delete from public.agenda_eventos e
|
||||||
|
where e.mirror_of_event_id = new.id
|
||||||
|
and e.mirror_source = 'personal_busy_mirror'
|
||||||
|
and not exists (
|
||||||
|
select 1
|
||||||
|
from public.tenant_members tm
|
||||||
|
where tm.user_id = new.owner_id
|
||||||
|
and tm.role = 'therapist'
|
||||||
|
and tm.status = 'active'
|
||||||
|
and tm.tenant_id = e.tenant_id
|
||||||
|
);
|
||||||
|
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.sync_busy_mirror_agenda_eventos() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: sync_overdue_financial_records(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.sync_overdue_financial_records() RETURNS integer
|
||||||
93
database-novo/schema/03_functions/auth.sql
Normal file
93
database-novo/schema/03_functions/auth.sql
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Functions — auth schema
|
||||||
|
-- auth.email(), auth.jwt(), auth.role(), auth.uid()
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: email(); Type: FUNCTION; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION auth.email() RETURNS text
|
||||||
|
LANGUAGE sql STABLE
|
||||||
|
AS $$
|
||||||
|
select
|
||||||
|
coalesce(
|
||||||
|
nullif(current_setting('request.jwt.claim.email', true), ''),
|
||||||
|
(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'email')
|
||||||
|
)::text
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION auth.email() OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: FUNCTION email(); Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION auth.email() IS 'Deprecated. Use auth.jwt() -> ''email'' instead.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: jwt(); Type: FUNCTION; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION auth.jwt() RETURNS jsonb
|
||||||
|
LANGUAGE sql STABLE
|
||||||
|
AS $$
|
||||||
|
select
|
||||||
|
coalesce(
|
||||||
|
nullif(current_setting('request.jwt.claim', true), ''),
|
||||||
|
nullif(current_setting('request.jwt.claims', true), '')
|
||||||
|
)::jsonb
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION auth.jwt() OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: role(); Type: FUNCTION; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION auth.role() RETURNS text
|
||||||
|
LANGUAGE sql STABLE
|
||||||
|
AS $$
|
||||||
|
select
|
||||||
|
coalesce(
|
||||||
|
nullif(current_setting('request.jwt.claim.role', true), ''),
|
||||||
|
(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'role')
|
||||||
|
)::text
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION auth.role() OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: FUNCTION role(); Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION auth.role() IS 'Deprecated. Use auth.jwt() -> ''role'' instead.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: uid(); Type: FUNCTION; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION auth.uid() RETURNS uuid
|
||||||
|
LANGUAGE sql STABLE
|
||||||
|
AS $$
|
||||||
|
select
|
||||||
|
coalesce(
|
||||||
|
nullif(current_setting('request.jwt.claim.sub', true), ''),
|
||||||
|
(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub')
|
||||||
|
)::uuid
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION auth.uid() OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: FUNCTION uid(); Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION auth.uid() IS 'Deprecated. Use auth.jwt() -> ''sub'' instead.';
|
||||||
|
|
||||||
1283
database-novo/schema/03_functions/billing.sql
Normal file
1283
database-novo/schema/03_functions/billing.sql
Normal file
File diff suppressed because it is too large
Load Diff
2350
database-novo/schema/03_functions/core.sql
Normal file
2350
database-novo/schema/03_functions/core.sql
Normal file
File diff suppressed because it is too large
Load Diff
818
database-novo/schema/03_functions/financial.sql
Normal file
818
database-novo/schema/03_functions/financial.sql
Normal file
@@ -0,0 +1,818 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Functions — Financeiro
|
||||||
|
-- =============================================================================
|
||||||
|
-- auto_create_financial_record_from_session, create_financial_record_for_session,
|
||||||
|
-- create_therapist_payout, get_financial_report, get_financial_summary,
|
||||||
|
-- list_financial_records, mark_as_paid, mark_payout_as_paid,
|
||||||
|
-- seed_default_financial_categories, sync_overdue_financial_records,
|
||||||
|
-- trg_fn_financial_records_auto_overdue, set_insurance/services_updated_at
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION public.auto_create_financial_record_from_session() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_price NUMERIC(10,2);
|
||||||
|
v_services_total NUMERIC(10,2);
|
||||||
|
v_already_billed BOOLEAN;
|
||||||
|
BEGIN
|
||||||
|
-- ── Guards de saída rápida ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Só processa quando o status muda PARA 'realizado'
|
||||||
|
IF NEW.status::TEXT <> 'realizado' THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Só processa quando houve mudança real de status
|
||||||
|
IF OLD.status IS NOT DISTINCT FROM NEW.status THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Só sessões (não bloqueios, feriados, etc.)
|
||||||
|
IF NEW.tipo::TEXT <> 'sessao' THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Paciente obrigatório para vincular a cobrança
|
||||||
|
IF NEW.patient_id IS NULL THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Sessões de pacote têm cobrança gerenciada por billing_contract
|
||||||
|
IF NEW.billing_contract_id IS NOT NULL THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Idempotência: já existe financial_record para este evento?
|
||||||
|
SELECT billed INTO v_already_billed
|
||||||
|
FROM public.agenda_eventos
|
||||||
|
WHERE id = NEW.id;
|
||||||
|
|
||||||
|
IF v_already_billed = TRUE THEN
|
||||||
|
-- Confirma no financial_records também (dupla verificação)
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM public.financial_records
|
||||||
|
WHERE agenda_evento_id = NEW.id AND deleted_at IS NULL
|
||||||
|
) THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ── Busca do preço ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
v_price := NULL;
|
||||||
|
|
||||||
|
-- Prioridade 1: soma dos serviços da regra de recorrência
|
||||||
|
IF NEW.recurrence_id IS NOT NULL THEN
|
||||||
|
SELECT COALESCE(SUM(rrs.final_price), 0)
|
||||||
|
INTO v_services_total
|
||||||
|
FROM public.recurrence_rule_services rrs
|
||||||
|
WHERE rrs.rule_id = NEW.recurrence_id;
|
||||||
|
|
||||||
|
IF v_services_total > 0 THEN
|
||||||
|
v_price := v_services_total;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Prioridade 2: price direto da regra (fallback se sem serviços)
|
||||||
|
IF v_price IS NULL OR v_price = 0 THEN
|
||||||
|
SELECT price INTO v_price
|
||||||
|
FROM public.recurrence_rules
|
||||||
|
WHERE id = NEW.recurrence_id;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Prioridade 3: price do próprio evento de agenda
|
||||||
|
IF v_price IS NULL OR v_price = 0 THEN
|
||||||
|
v_price := NEW.price;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Sem preço → não criar registro (não é erro, apenas skip silencioso)
|
||||||
|
IF v_price IS NULL OR v_price <= 0 THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ── Criação do financial_record ─────────────────────────────────────────
|
||||||
|
|
||||||
|
INSERT INTO public.financial_records (
|
||||||
|
owner_id,
|
||||||
|
tenant_id,
|
||||||
|
patient_id,
|
||||||
|
agenda_evento_id,
|
||||||
|
type,
|
||||||
|
amount,
|
||||||
|
discount_amount,
|
||||||
|
final_amount,
|
||||||
|
clinic_fee_pct,
|
||||||
|
clinic_fee_amount,
|
||||||
|
status,
|
||||||
|
due_date
|
||||||
|
-- payment_method: NULL até o momento do pagamento (mark_as_paid preenche)
|
||||||
|
) VALUES (
|
||||||
|
NEW.owner_id,
|
||||||
|
NEW.tenant_id,
|
||||||
|
NEW.patient_id,
|
||||||
|
NEW.id,
|
||||||
|
'receita',
|
||||||
|
v_price,
|
||||||
|
0,
|
||||||
|
v_price,
|
||||||
|
0, -- clinic_fee_pct: sem campo de configuração global no schema atual.
|
||||||
|
0, -- clinic_fee_amount: calculado manualmente ou via update posterior.
|
||||||
|
'pending',
|
||||||
|
(NEW.inicio_em::DATE + 7) -- vencimento padrão: 7 dias após a sessão
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ── Marca sessão como billed ────────────────────────────────────────────
|
||||||
|
-- UPDATE em billed (não em status) → não re-dispara este trigger
|
||||||
|
UPDATE public.agenda_eventos
|
||||||
|
SET billed = TRUE
|
||||||
|
WHERE id = NEW.id;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
-- Log silencioso: nunca bloquear a agenda por falha financeira
|
||||||
|
RAISE WARNING '[auto_create_financial_record_from_session] evento=% erro=%',
|
||||||
|
NEW.id, SQLERRM;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.auto_create_financial_record_from_session() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: FUNCTION auto_create_financial_record_from_session(); Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION public.auto_create_financial_record_from_session() IS 'Trigger que cria automaticamente um financial_record (receita, pending) quando uma sessão de agenda é marcada como realizada. Prioridade de preço: recurrence_rule_services > recurrence_rules.price > agenda_eventos.price. Skip silencioso se sem preço, pacote ou registro já existente.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: can_delete_patient(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.can_delete_patient(p_patient_id uuid) RETURNS boolean
|
||||||
|
CREATE FUNCTION public.create_financial_record_for_session(p_tenant_id uuid, p_owner_id uuid, p_patient_id uuid, p_agenda_evento_id uuid, p_amount numeric, p_due_date date) RETURNS SETOF public.financial_records
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_existing public.financial_records%ROWTYPE;
|
||||||
|
v_new public.financial_records%ROWTYPE;
|
||||||
|
BEGIN
|
||||||
|
-- Idempotência: retorna o registro existente se já foi criado
|
||||||
|
SELECT * INTO v_existing
|
||||||
|
FROM public.financial_records
|
||||||
|
WHERE agenda_evento_id = p_agenda_evento_id
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF FOUND THEN
|
||||||
|
RETURN NEXT v_existing;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Cria o novo registro
|
||||||
|
INSERT INTO public.financial_records (
|
||||||
|
tenant_id,
|
||||||
|
owner_id,
|
||||||
|
patient_id,
|
||||||
|
agenda_evento_id,
|
||||||
|
amount,
|
||||||
|
discount_amount,
|
||||||
|
final_amount,
|
||||||
|
status,
|
||||||
|
due_date
|
||||||
|
) VALUES (
|
||||||
|
p_tenant_id,
|
||||||
|
p_owner_id,
|
||||||
|
p_patient_id,
|
||||||
|
p_agenda_evento_id,
|
||||||
|
p_amount,
|
||||||
|
0,
|
||||||
|
p_amount,
|
||||||
|
'pending',
|
||||||
|
p_due_date
|
||||||
|
)
|
||||||
|
RETURNING * INTO v_new;
|
||||||
|
|
||||||
|
-- Marca o evento da agenda como billed = true
|
||||||
|
UPDATE public.agenda_eventos
|
||||||
|
SET billed = TRUE
|
||||||
|
WHERE id = p_agenda_evento_id;
|
||||||
|
|
||||||
|
RETURN NEXT v_new;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.create_financial_record_for_session(p_tenant_id uuid, p_owner_id uuid, p_patient_id uuid, p_agenda_evento_id uuid, p_amount numeric, p_due_date date) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: create_patient_intake_request(text, text, text, text, text, boolean); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.create_patient_intake_request(p_token text, p_name text, p_email text DEFAULT NULL::text, p_phone text DEFAULT NULL::text, p_notes text DEFAULT NULL::text, p_consent boolean DEFAULT false) RETURNS uuid
|
||||||
|
CREATE FUNCTION public.create_therapist_payout(p_tenant_id uuid, p_therapist_id uuid, p_period_start date, p_period_end date) RETURNS public.therapist_payouts
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_payout public.therapist_payouts%ROWTYPE;
|
||||||
|
v_total_sessions INTEGER;
|
||||||
|
v_gross NUMERIC(10,2);
|
||||||
|
v_clinic_fee NUMERIC(10,2);
|
||||||
|
v_net NUMERIC(10,2);
|
||||||
|
BEGIN
|
||||||
|
-- ── Verificação de permissão ────────────────────────────────────────────
|
||||||
|
-- Apenas o próprio terapeuta ou o tenant_admin pode criar o repasse
|
||||||
|
IF auth.uid() <> p_therapist_id AND NOT public.is_tenant_admin(p_tenant_id) THEN
|
||||||
|
RAISE EXCEPTION 'Sem permissão para criar repasse para este terapeuta.';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ── Verifica se já existe repasse para o mesmo período ─────────────────
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM public.therapist_payouts
|
||||||
|
WHERE owner_id = p_therapist_id
|
||||||
|
AND tenant_id = p_tenant_id
|
||||||
|
AND period_start = p_period_start
|
||||||
|
AND period_end = p_period_end
|
||||||
|
AND status <> 'cancelled'
|
||||||
|
) THEN
|
||||||
|
RAISE EXCEPTION
|
||||||
|
'Já existe um repasse ativo para o período % a % deste terapeuta.',
|
||||||
|
p_period_start, p_period_end;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ── Agrega os financial_records elegíveis ──────────────────────────────
|
||||||
|
-- Elegíveis: paid, receita, owner=terapeuta, tenant correto, paid_at no período,
|
||||||
|
-- não soft-deleted, ainda não vinculados a nenhum payout.
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS total_sessions,
|
||||||
|
COALESCE(SUM(amount), 0) AS gross_amount,
|
||||||
|
COALESCE(SUM(clinic_fee_amount), 0) AS clinic_fee_total,
|
||||||
|
COALESCE(SUM(net_amount), 0) AS net_amount
|
||||||
|
INTO
|
||||||
|
v_total_sessions, v_gross, v_clinic_fee, v_net
|
||||||
|
FROM public.financial_records fr
|
||||||
|
WHERE fr.owner_id = p_therapist_id
|
||||||
|
AND fr.tenant_id = p_tenant_id
|
||||||
|
AND fr.type = 'receita'
|
||||||
|
AND fr.status = 'paid'
|
||||||
|
AND fr.deleted_at IS NULL
|
||||||
|
AND fr.paid_at::DATE BETWEEN p_period_start AND p_period_end
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM public.therapist_payout_records tpr
|
||||||
|
WHERE tpr.financial_record_id = fr.id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Sem registros elegíveis → não criar payout vazio
|
||||||
|
IF v_total_sessions = 0 THEN
|
||||||
|
RAISE EXCEPTION
|
||||||
|
'Nenhum registro financeiro elegível encontrado para o período % a %.',
|
||||||
|
p_period_start, p_period_end;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ── Cria o repasse ─────────────────────────────────────────────────────
|
||||||
|
INSERT INTO public.therapist_payouts (
|
||||||
|
owner_id,
|
||||||
|
tenant_id,
|
||||||
|
period_start,
|
||||||
|
period_end,
|
||||||
|
total_sessions,
|
||||||
|
gross_amount,
|
||||||
|
clinic_fee_total,
|
||||||
|
net_amount,
|
||||||
|
status
|
||||||
|
) VALUES (
|
||||||
|
p_therapist_id,
|
||||||
|
p_tenant_id,
|
||||||
|
p_period_start,
|
||||||
|
p_period_end,
|
||||||
|
v_total_sessions,
|
||||||
|
v_gross,
|
||||||
|
v_clinic_fee,
|
||||||
|
v_net,
|
||||||
|
'pending'
|
||||||
|
)
|
||||||
|
RETURNING * INTO v_payout;
|
||||||
|
|
||||||
|
-- ── Vincula os financial_records ao repasse ────────────────────────────
|
||||||
|
INSERT INTO public.therapist_payout_records (payout_id, financial_record_id)
|
||||||
|
SELECT v_payout.id, fr.id
|
||||||
|
FROM public.financial_records fr
|
||||||
|
WHERE fr.owner_id = p_therapist_id
|
||||||
|
AND fr.tenant_id = p_tenant_id
|
||||||
|
AND fr.type = 'receita'
|
||||||
|
AND fr.status = 'paid'
|
||||||
|
AND fr.deleted_at IS NULL
|
||||||
|
AND fr.paid_at::DATE BETWEEN p_period_start AND p_period_end
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM public.therapist_payout_records tpr
|
||||||
|
WHERE tpr.financial_record_id = fr.id
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN v_payout;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.create_therapist_payout(p_tenant_id uuid, p_therapist_id uuid, p_period_start date, p_period_end date) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: FUNCTION create_therapist_payout(p_tenant_id uuid, p_therapist_id uuid, p_period_start date, p_period_end date); Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION public.create_therapist_payout(p_tenant_id uuid, p_therapist_id uuid, p_period_start date, p_period_end date) IS 'Cria um repasse para o terapeuta com todos os financial_records paid+receita do período que ainda não estejam vinculados a outro repasse. Lança exceção se não houver registros elegíveis ou se já houver repasse ativo no período.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: current_member_id(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.current_member_id(p_tenant_id uuid) RETURNS uuid
|
||||||
|
CREATE FUNCTION public.get_financial_report(p_owner_id uuid, p_start_date date, p_end_date date, p_group_by text DEFAULT 'month'::text) RETURNS TABLE(group_key text, group_label text, total_receitas numeric, total_despesas numeric, saldo numeric, total_pendente numeric, total_overdue numeric, count_records bigint)
|
||||||
|
LANGUAGE sql STABLE SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
|
||||||
|
-- ── Valida p_group_by antes de executar ──────────────────────────────────
|
||||||
|
-- (lança erro se valor inválido; plpgsql seria necessário para isso em SQL puro,
|
||||||
|
-- então usamos um CTE de validação com CASE WHEN para retornar vazio em vez de erro)
|
||||||
|
|
||||||
|
WITH base AS (
|
||||||
|
SELECT
|
||||||
|
fr.type,
|
||||||
|
fr.amount,
|
||||||
|
fr.final_amount,
|
||||||
|
fr.status,
|
||||||
|
fr.deleted_at,
|
||||||
|
-- Chave de agrupamento calculada conforme p_group_by
|
||||||
|
CASE p_group_by
|
||||||
|
WHEN 'month' THEN TO_CHAR(
|
||||||
|
COALESCE(fr.paid_at::DATE, fr.due_date, fr.created_at::DATE),
|
||||||
|
'YYYY-MM'
|
||||||
|
)
|
||||||
|
WHEN 'week' THEN TO_CHAR(
|
||||||
|
COALESCE(fr.paid_at::DATE, fr.due_date, fr.created_at::DATE),
|
||||||
|
'IYYY-"W"IW'
|
||||||
|
)
|
||||||
|
WHEN 'category' THEN COALESCE(fr.category_id::TEXT, fr.category, 'sem_categoria')
|
||||||
|
WHEN 'patient' THEN COALESCE(fr.patient_id::TEXT, 'sem_paciente')
|
||||||
|
ELSE NULL -- group_by inválido → group_key NULL → retorno vazio
|
||||||
|
END AS gkey,
|
||||||
|
-- Label legível (enriquecido via JOIN abaixo quando possível)
|
||||||
|
CASE p_group_by
|
||||||
|
WHEN 'month' THEN TO_CHAR(
|
||||||
|
COALESCE(fr.paid_at::DATE, fr.due_date, fr.created_at::DATE),
|
||||||
|
'YYYY-MM'
|
||||||
|
)
|
||||||
|
WHEN 'week' THEN TO_CHAR(
|
||||||
|
COALESCE(fr.paid_at::DATE, fr.due_date, fr.created_at::DATE),
|
||||||
|
'IYYY-"W"IW'
|
||||||
|
)
|
||||||
|
WHEN 'category' THEN COALESCE(fc.name, fr.category, 'Sem categoria')
|
||||||
|
WHEN 'patient' THEN COALESCE(p.nome_completo, fr.patient_id::TEXT, 'Sem paciente')
|
||||||
|
ELSE NULL
|
||||||
|
END AS glabel
|
||||||
|
FROM public.financial_records fr
|
||||||
|
LEFT JOIN public.financial_categories fc
|
||||||
|
ON fc.id = fr.category_id
|
||||||
|
LEFT JOIN public.patients p
|
||||||
|
ON p.id = fr.patient_id
|
||||||
|
WHERE fr.owner_id = p_owner_id
|
||||||
|
AND fr.deleted_at IS NULL
|
||||||
|
AND COALESCE(fr.paid_at::DATE, fr.due_date, fr.created_at::DATE)
|
||||||
|
BETWEEN p_start_date AND p_end_date
|
||||||
|
)
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
gkey AS group_key,
|
||||||
|
glabel AS group_label,
|
||||||
|
|
||||||
|
COALESCE(SUM(final_amount) FILTER (WHERE type = 'receita' AND status = 'paid'), 0)
|
||||||
|
AS total_receitas,
|
||||||
|
|
||||||
|
COALESCE(SUM(final_amount) FILTER (WHERE type = 'despesa' AND status = 'paid'), 0)
|
||||||
|
AS total_despesas,
|
||||||
|
|
||||||
|
COALESCE(SUM(final_amount) FILTER (WHERE type = 'receita' AND status = 'paid'), 0)
|
||||||
|
- COALESCE(SUM(final_amount) FILTER (WHERE type = 'despesa' AND status = 'paid'), 0)
|
||||||
|
AS saldo,
|
||||||
|
|
||||||
|
COALESCE(SUM(final_amount) FILTER (WHERE status = 'pending'), 0) AS total_pendente,
|
||||||
|
|
||||||
|
COALESCE(SUM(final_amount) FILTER (WHERE status = 'overdue'), 0) AS total_overdue,
|
||||||
|
|
||||||
|
COUNT(*) AS count_records
|
||||||
|
|
||||||
|
FROM base
|
||||||
|
WHERE gkey IS NOT NULL -- descarta p_group_by inválido
|
||||||
|
GROUP BY gkey, glabel
|
||||||
|
ORDER BY gkey ASC;
|
||||||
|
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.get_financial_report(p_owner_id uuid, p_start_date date, p_end_date date, p_group_by text) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: FUNCTION get_financial_report(p_owner_id uuid, p_start_date date, p_end_date date, p_group_by text); Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION public.get_financial_report(p_owner_id uuid, p_start_date date, p_end_date date, p_group_by text) IS 'Relatório financeiro agrupado por mês, semana ISO, categoria ou paciente. p_group_by aceita: ''month'' | ''week'' | ''category'' | ''patient''. Totais de receita/despesa consideram apenas registros com status=paid. total_pendente e total_overdue incluem todos os tipos (receita + despesa).';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: get_financial_summary(uuid, integer, integer); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.get_financial_summary(p_owner_id uuid, p_year integer, p_month integer) RETURNS TABLE(total_receitas numeric, total_despesas numeric, total_pendente numeric, saldo_liquido numeric, total_repasse numeric, count_receitas bigint, count_despesas bigint)
|
||||||
|
LANGUAGE sql STABLE SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
SELECT
|
||||||
|
-- Receitas pagas no período
|
||||||
|
COALESCE(SUM(amount) FILTER (
|
||||||
|
WHERE type = 'receita' AND status = 'paid'
|
||||||
|
), 0) AS total_receitas,
|
||||||
|
|
||||||
|
-- Despesas pagas no período
|
||||||
|
COALESCE(SUM(amount) FILTER (
|
||||||
|
WHERE type = 'despesa' AND status = 'paid'
|
||||||
|
), 0) AS total_despesas,
|
||||||
|
|
||||||
|
-- Tudo pendente ou vencido (receitas + despesas)
|
||||||
|
COALESCE(SUM(amount) FILTER (
|
||||||
|
WHERE status IN ('pending', 'overdue')
|
||||||
|
), 0) AS total_pendente,
|
||||||
|
|
||||||
|
-- Saldo líquido (receitas pagas − despesas pagas)
|
||||||
|
COALESCE(SUM(amount) FILTER (
|
||||||
|
WHERE type = 'receita' AND status = 'paid'
|
||||||
|
), 0)
|
||||||
|
- COALESCE(SUM(amount) FILTER (
|
||||||
|
WHERE type = 'despesa' AND status = 'paid'
|
||||||
|
), 0) AS saldo_liquido,
|
||||||
|
|
||||||
|
-- Total repassado à clínica (apenas receitas pagas)
|
||||||
|
COALESCE(SUM(clinic_fee_amount) FILTER (
|
||||||
|
WHERE type = 'receita' AND status = 'paid'
|
||||||
|
), 0) AS total_repasse,
|
||||||
|
|
||||||
|
-- Contadores (excluindo soft-deleted)
|
||||||
|
COUNT(*) FILTER (WHERE type = 'receita' AND deleted_at IS NULL) AS count_receitas,
|
||||||
|
COUNT(*) FILTER (WHERE type = 'despesa' AND deleted_at IS NULL) AS count_despesas
|
||||||
|
|
||||||
|
FROM public.financial_records
|
||||||
|
WHERE owner_id = p_owner_id
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
AND EXTRACT(YEAR FROM COALESCE(paid_at::DATE, due_date, created_at::DATE)) = p_year
|
||||||
|
AND EXTRACT(MONTH FROM COALESCE(paid_at::DATE, due_date, created_at::DATE)) = p_month;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.get_financial_summary(p_owner_id uuid, p_year integer, p_month integer) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: get_my_email(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.get_my_email() RETURNS text
|
||||||
|
CREATE FUNCTION public.list_financial_records(p_owner_id uuid, p_year integer DEFAULT NULL::integer, p_month integer DEFAULT NULL::integer, p_type text DEFAULT NULL::text, p_status text DEFAULT NULL::text, p_patient_id uuid DEFAULT NULL::uuid, p_limit integer DEFAULT 50, p_offset integer DEFAULT 0) RETURNS SETOF public.financial_records
|
||||||
|
LANGUAGE sql STABLE SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
SELECT *
|
||||||
|
FROM public.financial_records
|
||||||
|
WHERE owner_id = p_owner_id
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
AND (p_type IS NULL OR type::TEXT = p_type)
|
||||||
|
AND (p_status IS NULL OR status = p_status)
|
||||||
|
AND (p_patient_id IS NULL OR patient_id = p_patient_id)
|
||||||
|
AND (p_year IS NULL OR EXTRACT(YEAR FROM COALESCE(paid_at::DATE, due_date, created_at::DATE)) = p_year)
|
||||||
|
AND (p_month IS NULL OR EXTRACT(MONTH FROM COALESCE(paid_at::DATE, due_date, created_at::DATE)) = p_month)
|
||||||
|
ORDER BY COALESCE(paid_at, due_date::TIMESTAMPTZ, created_at) DESC
|
||||||
|
LIMIT p_limit
|
||||||
|
OFFSET p_offset;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.list_financial_records(p_owner_id uuid, p_year integer, p_month integer, p_type text, p_status text, p_patient_id uuid, p_limit integer, p_offset integer) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: mark_as_paid(uuid, text); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.mark_as_paid(p_financial_record_id uuid, p_payment_method text) RETURNS SETOF public.financial_records
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_record public.financial_records%ROWTYPE;
|
||||||
|
BEGIN
|
||||||
|
-- Garante que o registro pertence ao usuário autenticado (RLS não aplica em SECURITY DEFINER)
|
||||||
|
SELECT * INTO v_record
|
||||||
|
FROM public.financial_records
|
||||||
|
WHERE id = p_financial_record_id
|
||||||
|
AND owner_id = auth.uid()
|
||||||
|
AND deleted_at IS NULL;
|
||||||
|
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RAISE EXCEPTION 'Registro financeiro não encontrado ou sem permissão.';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_record.status NOT IN ('pending', 'overdue') THEN
|
||||||
|
RAISE EXCEPTION 'Apenas cobranças pendentes ou vencidas podem ser marcadas como pagas.';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
UPDATE public.financial_records
|
||||||
|
SET status = 'paid',
|
||||||
|
paid_at = NOW(),
|
||||||
|
payment_method = p_payment_method,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = p_financial_record_id
|
||||||
|
RETURNING * INTO v_record;
|
||||||
|
|
||||||
|
RETURN NEXT v_record;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.mark_as_paid(p_financial_record_id uuid, p_payment_method text) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: mark_payout_as_paid(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.mark_payout_as_paid(p_payout_id uuid) RETURNS public.therapist_payouts
|
||||||
|
CREATE FUNCTION public.mark_payout_as_paid(p_payout_id uuid) RETURNS public.therapist_payouts
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_payout public.therapist_payouts%ROWTYPE;
|
||||||
|
BEGIN
|
||||||
|
-- Busca o payout
|
||||||
|
SELECT * INTO v_payout
|
||||||
|
FROM public.therapist_payouts
|
||||||
|
WHERE id = p_payout_id;
|
||||||
|
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RAISE EXCEPTION 'Repasse não encontrado: %', p_payout_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Verifica permissão: apenas tenant_admin do tenant do repasse
|
||||||
|
IF NOT public.is_tenant_admin(v_payout.tenant_id) THEN
|
||||||
|
RAISE EXCEPTION 'Apenas o administrador da clínica pode marcar repasses como pagos.';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Verifica status
|
||||||
|
IF v_payout.status <> 'pending' THEN
|
||||||
|
RAISE EXCEPTION
|
||||||
|
'Repasse já está com status ''%''. Apenas repasses pendentes podem ser pagos.',
|
||||||
|
v_payout.status;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Atualiza
|
||||||
|
UPDATE public.therapist_payouts
|
||||||
|
SET
|
||||||
|
status = 'paid',
|
||||||
|
paid_at = NOW(),
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = p_payout_id
|
||||||
|
RETURNING * INTO v_payout;
|
||||||
|
|
||||||
|
RETURN v_payout;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.mark_payout_as_paid(p_payout_id uuid) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: FUNCTION mark_payout_as_paid(p_payout_id uuid); Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION public.mark_payout_as_paid(p_payout_id uuid) IS 'Marca um repasse de terapeuta como pago. Apenas o tenant_admin pode chamar. Apenas repasses com status=pending podem ser finalizados.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: my_tenants(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.my_tenants() RETURNS TABLE(tenant_id uuid, role text, status text, kind text)
|
||||||
|
CREATE FUNCTION public.seed_default_financial_categories(p_user_id uuid) RETURNS void
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO public.financial_categories (user_id, name, type, color, icon, sort_order)
|
||||||
|
VALUES
|
||||||
|
(p_user_id, 'Sessão', 'receita', '#22c55e', 'pi pi-heart', 1),
|
||||||
|
(p_user_id, 'Supervisão', 'receita', '#6366f1', 'pi pi-users', 2),
|
||||||
|
(p_user_id, 'Convênio', 'receita', '#3b82f6', 'pi pi-building', 3),
|
||||||
|
(p_user_id, 'Grupo terapêutico', 'receita', '#f59e0b', 'pi pi-sitemap', 4),
|
||||||
|
(p_user_id, 'Outro (receita)', 'receita', '#8b5cf6', 'pi pi-plus-circle', 5),
|
||||||
|
(p_user_id, 'Aluguel sala', 'despesa', '#ef4444', 'pi pi-home', 1),
|
||||||
|
(p_user_id, 'Plataforma/SaaS', 'despesa', '#f97316', 'pi pi-desktop', 2),
|
||||||
|
(p_user_id, 'Repasse clínica', 'despesa', '#64748b', 'pi pi-arrow-right-arrow-left', 3),
|
||||||
|
(p_user_id, 'Supervisão (custo)', 'despesa', '#6366f1', 'pi pi-users', 4),
|
||||||
|
(p_user_id, 'Outro (despesa)', 'despesa', '#94a3b8', 'pi pi-minus-circle', 5)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.seed_default_financial_categories(p_user_id uuid) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: seed_determined_commitments(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.seed_determined_commitments(p_tenant_id uuid) RETURNS void
|
||||||
|
CREATE FUNCTION public.set_insurance_plans_updated_at() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN NEW.updated_at = now(); RETURN NEW; END; $$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.set_insurance_plans_updated_at() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: set_owner_id(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.set_owner_id() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
begin
|
||||||
|
if new.owner_id is null then
|
||||||
|
new.owner_id := auth.uid();
|
||||||
|
end if;
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.set_owner_id() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: set_services_updated_at(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.set_services_updated_at() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = now();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.set_services_updated_at() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: set_tenant_feature_exception(uuid, text, boolean, text); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.set_tenant_feature_exception(p_tenant_id uuid, p_feature_key text, p_enabled boolean, p_reason text DEFAULT NULL::text) RETURNS void
|
||||||
|
CREATE FUNCTION public.set_updated_at() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = now();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.set_updated_at() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: set_updated_at_recurrence(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.set_updated_at_recurrence() RETURNS trigger
|
||||||
|
CREATE FUNCTION public.sync_overdue_financial_records() RETURNS integer
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_count integer;
|
||||||
|
BEGIN
|
||||||
|
UPDATE public.financial_records
|
||||||
|
SET
|
||||||
|
status = 'overdue',
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE status = 'pending'
|
||||||
|
AND due_date IS NOT NULL
|
||||||
|
AND due_date < CURRENT_DATE
|
||||||
|
AND deleted_at IS NULL;
|
||||||
|
|
||||||
|
GET DIAGNOSTICS v_count = ROW_COUNT;
|
||||||
|
RETURN v_count;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.sync_overdue_financial_records() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: FUNCTION sync_overdue_financial_records(); Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION public.sync_overdue_financial_records() IS 'Marca como overdue todos os financial_records pendentes com due_date vencido. Pode ser chamada manualmente, via pg_cron ou via Supabase Edge Function agendada.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: tenant_accept_invite(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.tenant_accept_invite(p_token uuid) RETURNS jsonb
|
||||||
|
CREATE FUNCTION public.trg_fn_financial_records_auto_overdue() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.status = 'pending'
|
||||||
|
AND NEW.due_date IS NOT NULL
|
||||||
|
AND NEW.due_date < CURRENT_DATE
|
||||||
|
THEN
|
||||||
|
NEW.status := 'overdue';
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.trg_fn_financial_records_auto_overdue() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: unstick_notification_queue(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.unstick_notification_queue() RETURNS integer
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_unstuck integer;
|
||||||
|
BEGIN
|
||||||
|
UPDATE public.notification_queue
|
||||||
|
SET status = 'pendente',
|
||||||
|
attempts = attempts + 1,
|
||||||
|
last_error = 'Timeout: preso em processando por >10min',
|
||||||
|
next_retry_at = now() + interval '2 minutes'
|
||||||
|
WHERE status = 'processando'
|
||||||
|
AND updated_at < now() - interval '10 minutes';
|
||||||
|
|
||||||
|
GET DIAGNOSTICS v_unstuck = ROW_COUNT;
|
||||||
|
RETURN v_unstuck;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.unstick_notification_queue() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: update_payment_settings_updated_at(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.update_payment_settings_updated_at() RETURNS trigger
|
||||||
|
CREATE FUNCTION public.update_payment_settings_updated_at() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = now();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.update_payment_settings_updated_at() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: update_professional_pricing_updated_at(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.update_professional_pricing_updated_at() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = now();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.update_professional_pricing_updated_at() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: user_has_feature(uuid, text); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.user_has_feature(_user_id uuid, _feature text) RETURNS boolean
|
||||||
316
database-novo/schema/03_functions/infra.sql
Normal file
316
database-novo/schema/03_functions/infra.sql
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Functions — infraestrutura
|
||||||
|
-- extensions.grant_pg_*, pgbouncer.get_auth, etc.
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION extensions.grant_pg_cron_access() RETURNS event_trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT
|
||||||
|
FROM pg_event_trigger_ddl_commands() AS ev
|
||||||
|
JOIN pg_extension AS ext
|
||||||
|
ON ev.objid = ext.oid
|
||||||
|
WHERE ext.extname = 'pg_cron'
|
||||||
|
)
|
||||||
|
THEN
|
||||||
|
grant usage on schema cron to postgres with grant option;
|
||||||
|
|
||||||
|
alter default privileges in schema cron grant all on tables to postgres with grant option;
|
||||||
|
alter default privileges in schema cron grant all on functions to postgres with grant option;
|
||||||
|
alter default privileges in schema cron grant all on sequences to postgres with grant option;
|
||||||
|
|
||||||
|
alter default privileges for user supabase_admin in schema cron grant all
|
||||||
|
on sequences to postgres with grant option;
|
||||||
|
alter default privileges for user supabase_admin in schema cron grant all
|
||||||
|
on tables to postgres with grant option;
|
||||||
|
alter default privileges for user supabase_admin in schema cron grant all
|
||||||
|
on functions to postgres with grant option;
|
||||||
|
|
||||||
|
grant all privileges on all tables in schema cron to postgres with grant option;
|
||||||
|
revoke all on table cron.job from postgres;
|
||||||
|
grant select on table cron.job to postgres with grant option;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION extensions.grant_pg_cron_access() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: FUNCTION grant_pg_cron_access(); Type: COMMENT; Schema: extensions; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION extensions.grant_pg_cron_access() IS 'Grants access to pg_cron';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: grant_pg_graphql_access(); Type: FUNCTION; Schema: extensions; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION extensions.grant_pg_graphql_access() RETURNS event_trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $_$
|
||||||
|
DECLARE
|
||||||
|
func_is_graphql_resolve bool;
|
||||||
|
BEGIN
|
||||||
|
func_is_graphql_resolve = (
|
||||||
|
SELECT n.proname = 'resolve'
|
||||||
|
FROM pg_event_trigger_ddl_commands() AS ev
|
||||||
|
LEFT JOIN pg_catalog.pg_proc AS n
|
||||||
|
ON ev.objid = n.oid
|
||||||
|
);
|
||||||
|
|
||||||
|
IF func_is_graphql_resolve
|
||||||
|
THEN
|
||||||
|
-- Update public wrapper to pass all arguments through to the pg_graphql resolve func
|
||||||
|
DROP FUNCTION IF EXISTS graphql_public.graphql;
|
||||||
|
create or replace function graphql_public.graphql(
|
||||||
|
"operationName" text default null,
|
||||||
|
query text default null,
|
||||||
|
variables jsonb default null,
|
||||||
|
extensions jsonb default null
|
||||||
|
)
|
||||||
|
returns jsonb
|
||||||
|
language sql
|
||||||
|
as $$
|
||||||
|
select graphql.resolve(
|
||||||
|
query := query,
|
||||||
|
variables := coalesce(variables, '{}'),
|
||||||
|
"operationName" := "operationName",
|
||||||
|
extensions := extensions
|
||||||
|
);
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- This hook executes when `graphql.resolve` is created. That is not necessarily the last
|
||||||
|
-- function in the extension so we need to grant permissions on existing entities AND
|
||||||
|
-- update default permissions to any others that are created after `graphql.resolve`
|
||||||
|
grant usage on schema graphql to postgres, anon, authenticated, service_role;
|
||||||
|
grant select on all tables in schema graphql to postgres, anon, authenticated, service_role;
|
||||||
|
grant execute on all functions in schema graphql to postgres, anon, authenticated, service_role;
|
||||||
|
grant all on all sequences in schema graphql to postgres, anon, authenticated, service_role;
|
||||||
|
alter default privileges in schema graphql grant all on tables to postgres, anon, authenticated, service_role;
|
||||||
|
alter default privileges in schema graphql grant all on functions to postgres, anon, authenticated, service_role;
|
||||||
|
alter default privileges in schema graphql grant all on sequences to postgres, anon, authenticated, service_role;
|
||||||
|
|
||||||
|
-- Allow postgres role to allow granting usage on graphql and graphql_public schemas to custom roles
|
||||||
|
grant usage on schema graphql_public to postgres with grant option;
|
||||||
|
grant usage on schema graphql to postgres with grant option;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
END;
|
||||||
|
$_$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION extensions.grant_pg_graphql_access() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: FUNCTION grant_pg_graphql_access(); Type: COMMENT; Schema: extensions; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION extensions.grant_pg_graphql_access() IS 'Grants access to pg_graphql';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: grant_pg_net_access(); Type: FUNCTION; Schema: extensions; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION extensions.grant_pg_net_access() RETURNS event_trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_event_trigger_ddl_commands() AS ev
|
||||||
|
JOIN pg_extension AS ext
|
||||||
|
ON ev.objid = ext.oid
|
||||||
|
WHERE ext.extname = 'pg_net'
|
||||||
|
)
|
||||||
|
THEN
|
||||||
|
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||||
|
|
||||||
|
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||||
|
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||||
|
|
||||||
|
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||||
|
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||||
|
|
||||||
|
REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||||
|
REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||||
|
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION extensions.grant_pg_net_access() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: FUNCTION grant_pg_net_access(); Type: COMMENT; Schema: extensions; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION extensions.grant_pg_net_access() IS 'Grants access to pg_net';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pgrst_ddl_watch(); Type: FUNCTION; Schema: extensions; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION extensions.pgrst_ddl_watch() RETURNS event_trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
cmd record;
|
||||||
|
BEGIN
|
||||||
|
FOR cmd IN SELECT * FROM pg_event_trigger_ddl_commands()
|
||||||
|
LOOP
|
||||||
|
IF cmd.command_tag IN (
|
||||||
|
'CREATE SCHEMA', 'ALTER SCHEMA'
|
||||||
|
, 'CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO', 'ALTER TABLE'
|
||||||
|
, 'CREATE FOREIGN TABLE', 'ALTER FOREIGN TABLE'
|
||||||
|
, 'CREATE VIEW', 'ALTER VIEW'
|
||||||
|
, 'CREATE MATERIALIZED VIEW', 'ALTER MATERIALIZED VIEW'
|
||||||
|
, 'CREATE FUNCTION', 'ALTER FUNCTION'
|
||||||
|
, 'CREATE TRIGGER'
|
||||||
|
, 'CREATE TYPE', 'ALTER TYPE'
|
||||||
|
, 'CREATE RULE'
|
||||||
|
, 'COMMENT'
|
||||||
|
)
|
||||||
|
-- don't notify in case of CREATE TEMP table or other objects created on pg_temp
|
||||||
|
AND cmd.schema_name is distinct from 'pg_temp'
|
||||||
|
THEN
|
||||||
|
NOTIFY pgrst, 'reload schema';
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END; $$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION extensions.pgrst_ddl_watch() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pgrst_drop_watch(); Type: FUNCTION; Schema: extensions; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION extensions.pgrst_drop_watch() RETURNS event_trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
obj record;
|
||||||
|
BEGIN
|
||||||
|
FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects()
|
||||||
|
LOOP
|
||||||
|
IF obj.object_type IN (
|
||||||
|
'schema'
|
||||||
|
, 'table'
|
||||||
|
, 'foreign table'
|
||||||
|
, 'view'
|
||||||
|
, 'materialized view'
|
||||||
|
, 'function'
|
||||||
|
, 'trigger'
|
||||||
|
, 'type'
|
||||||
|
, 'rule'
|
||||||
|
)
|
||||||
|
AND obj.is_temporary IS false -- no pg_temp objects
|
||||||
|
THEN
|
||||||
|
NOTIFY pgrst, 'reload schema';
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END; $$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION extensions.pgrst_drop_watch() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: set_graphql_placeholder(); Type: FUNCTION; Schema: extensions; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION extensions.set_graphql_placeholder() RETURNS event_trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $_$
|
||||||
|
DECLARE
|
||||||
|
graphql_is_dropped bool;
|
||||||
|
BEGIN
|
||||||
|
graphql_is_dropped = (
|
||||||
|
SELECT ev.schema_name = 'graphql_public'
|
||||||
|
FROM pg_event_trigger_dropped_objects() AS ev
|
||||||
|
WHERE ev.schema_name = 'graphql_public'
|
||||||
|
);
|
||||||
|
|
||||||
|
IF graphql_is_dropped
|
||||||
|
THEN
|
||||||
|
create or replace function graphql_public.graphql(
|
||||||
|
"operationName" text default null,
|
||||||
|
query text default null,
|
||||||
|
variables jsonb default null,
|
||||||
|
extensions jsonb default null
|
||||||
|
)
|
||||||
|
returns jsonb
|
||||||
|
language plpgsql
|
||||||
|
as $$
|
||||||
|
DECLARE
|
||||||
|
server_version float;
|
||||||
|
BEGIN
|
||||||
|
server_version = (SELECT (SPLIT_PART((select version()), ' ', 2))::float);
|
||||||
|
|
||||||
|
IF server_version >= 14 THEN
|
||||||
|
RETURN jsonb_build_object(
|
||||||
|
'errors', jsonb_build_array(
|
||||||
|
jsonb_build_object(
|
||||||
|
'message', 'pg_graphql extension is not enabled.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
ELSE
|
||||||
|
RETURN jsonb_build_object(
|
||||||
|
'errors', jsonb_build_array(
|
||||||
|
jsonb_build_object(
|
||||||
|
'message', 'pg_graphql is only available on projects running Postgres 14 onwards.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
END;
|
||||||
|
$_$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION extensions.set_graphql_placeholder() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: FUNCTION set_graphql_placeholder(); Type: COMMENT; Schema: extensions; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION extensions.set_graphql_placeholder() IS 'Reintroduces placeholder function for graphql_public.graphql';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: get_auth(text); Type: FUNCTION; Schema: pgbouncer; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION pgbouncer.get_auth(p_usename text) RETURNS TABLE(username text, password text)
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO ''
|
||||||
|
AS $_$
|
||||||
|
begin
|
||||||
|
raise debug 'PgBouncer auth request: %', p_usename;
|
||||||
|
|
||||||
|
return query
|
||||||
|
select
|
||||||
|
rolname::text,
|
||||||
|
case when rolvaliduntil < now()
|
||||||
|
then null
|
||||||
|
else rolpassword::text
|
||||||
|
end
|
||||||
|
from pg_authid
|
||||||
|
where rolname=$1 and rolcanlogin;
|
||||||
|
end;
|
||||||
|
$_$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION pgbouncer.get_auth(p_usename text) OWNER TO supabase_admin;
|
||||||
776
database-novo/schema/03_functions/misc.sql
Normal file
776
database-novo/schema/03_functions/misc.sql
Normal file
@@ -0,0 +1,776 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Functions — Compromissos, Suporte, SaaS
|
||||||
|
-- =============================================================================
|
||||||
|
-- seed_determined_commitments, delete_commitment_full,
|
||||||
|
-- delete_determined_commitment, guard_locked_commitment,
|
||||||
|
-- create_support_session, revoke_support_session, validate_support_session,
|
||||||
|
-- saas_votar_doc, faq_votar, notice_track_click/view,
|
||||||
|
-- sanitize_phone_br, create_clinic_tenant
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION public.create_clinic_tenant(p_name text) RETURNS uuid
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
declare
|
||||||
|
v_uid uuid;
|
||||||
|
v_tenant uuid;
|
||||||
|
v_name text;
|
||||||
|
begin
|
||||||
|
v_uid := auth.uid();
|
||||||
|
if v_uid is null then
|
||||||
|
raise exception 'Not authenticated';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
v_name := nullif(trim(coalesce(p_name, '')), '');
|
||||||
|
if v_name is null then
|
||||||
|
v_name := 'Clínica';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
insert into public.tenants (name, kind, created_at)
|
||||||
|
values (v_name, 'clinic', now())
|
||||||
|
returning id into v_tenant;
|
||||||
|
|
||||||
|
insert into public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||||
|
values (v_tenant, v_uid, 'tenant_admin', 'active', now());
|
||||||
|
|
||||||
|
return v_tenant;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.create_clinic_tenant(p_name text) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: financial_records; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.financial_records (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
tenant_id uuid,
|
||||||
|
type public.financial_record_type DEFAULT 'receita'::public.financial_record_type NOT NULL,
|
||||||
|
amount numeric(10,2) NOT NULL,
|
||||||
|
description text,
|
||||||
|
category text,
|
||||||
|
payment_method text,
|
||||||
|
paid_at timestamp with time zone,
|
||||||
|
due_date date,
|
||||||
|
installments smallint DEFAULT 1,
|
||||||
|
installment_number smallint DEFAULT 1,
|
||||||
|
installment_group uuid,
|
||||||
|
agenda_evento_id uuid,
|
||||||
|
patient_id uuid,
|
||||||
|
clinic_fee_pct numeric(5,2) DEFAULT 0,
|
||||||
|
clinic_fee_amount numeric(10,2) DEFAULT 0,
|
||||||
|
net_amount numeric(10,2) GENERATED ALWAYS AS ((amount - clinic_fee_amount)) STORED,
|
||||||
|
insurance_plan_id uuid,
|
||||||
|
notes text,
|
||||||
|
tags text[],
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
deleted_at timestamp with time zone,
|
||||||
|
discount_amount numeric(10,2) DEFAULT 0 NOT NULL,
|
||||||
|
final_amount numeric(10,2) DEFAULT 0 NOT NULL,
|
||||||
|
status text DEFAULT 'pending'::text NOT NULL,
|
||||||
|
category_id uuid,
|
||||||
|
CONSTRAINT financial_records_amount_check CHECK ((amount >= (0)::numeric)),
|
||||||
|
CONSTRAINT financial_records_clinic_fee_amount_check CHECK ((clinic_fee_amount >= (0)::numeric)),
|
||||||
|
CONSTRAINT financial_records_clinic_fee_pct_check CHECK (((clinic_fee_pct >= (0)::numeric) AND (clinic_fee_pct <= (100)::numeric))),
|
||||||
|
CONSTRAINT financial_records_discount_amount_check CHECK ((discount_amount >= (0)::numeric)),
|
||||||
|
CONSTRAINT financial_records_final_amount_check CHECK ((final_amount >= (0)::numeric)),
|
||||||
|
CONSTRAINT financial_records_installments_check CHECK ((installments >= 1)),
|
||||||
|
CONSTRAINT financial_records_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'paid'::text, 'partial'::text, 'overdue'::text, 'cancelled'::text, 'refunded'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.financial_records OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: create_financial_record_for_session(uuid, uuid, uuid, uuid, numeric, date); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.create_financial_record_for_session(p_tenant_id uuid, p_owner_id uuid, p_patient_id uuid, p_agenda_evento_id uuid, p_amount numeric, p_due_date date) RETURNS SETOF public.financial_records
|
||||||
|
CREATE FUNCTION public.create_support_session(p_tenant_id uuid, p_ttl_minutes integer DEFAULT 60) RETURNS json
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_admin_id uuid;
|
||||||
|
v_role text;
|
||||||
|
v_token text;
|
||||||
|
v_expires timestamp with time zone;
|
||||||
|
v_session support_sessions;
|
||||||
|
BEGIN
|
||||||
|
-- Verifica autenticação
|
||||||
|
v_admin_id := auth.uid();
|
||||||
|
IF v_admin_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'Não autenticado.' USING ERRCODE = 'P0001';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Verifica role saas_admin
|
||||||
|
SELECT role INTO v_role
|
||||||
|
FROM public.profiles
|
||||||
|
WHERE id = v_admin_id;
|
||||||
|
|
||||||
|
IF v_role <> 'saas_admin' THEN
|
||||||
|
RAISE EXCEPTION 'Acesso negado. Somente saas_admin pode criar sessões de suporte.'
|
||||||
|
USING ERRCODE = 'P0002';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Valida TTL (1 a 120 minutos)
|
||||||
|
IF p_ttl_minutes < 1 OR p_ttl_minutes > 120 THEN
|
||||||
|
RAISE EXCEPTION 'TTL inválido. Use entre 1 e 120 minutos.'
|
||||||
|
USING ERRCODE = 'P0003';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Valida tenant
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM public.tenants WHERE id = p_tenant_id) THEN
|
||||||
|
RAISE EXCEPTION 'Tenant não encontrado.'
|
||||||
|
USING ERRCODE = 'P0004';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Gera token único (64 chars hex, sem pgcrypto)
|
||||||
|
v_token := replace(gen_random_uuid()::text, '-', '') || replace(gen_random_uuid()::text, '-', '');
|
||||||
|
v_expires := now() + (p_ttl_minutes || ' minutes')::interval;
|
||||||
|
|
||||||
|
-- Insere sessão
|
||||||
|
INSERT INTO public.support_sessions (tenant_id, admin_id, token, expires_at)
|
||||||
|
VALUES (p_tenant_id, v_admin_id, v_token, v_expires)
|
||||||
|
RETURNING * INTO v_session;
|
||||||
|
|
||||||
|
RETURN json_build_object(
|
||||||
|
'token', v_session.token,
|
||||||
|
'expires_at', v_session.expires_at,
|
||||||
|
'session_id', v_session.id
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.create_support_session(p_tenant_id uuid, p_ttl_minutes integer) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: therapist_payouts; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.therapist_payouts (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
period_start date NOT NULL,
|
||||||
|
period_end date NOT NULL,
|
||||||
|
total_sessions integer DEFAULT 0 NOT NULL,
|
||||||
|
gross_amount numeric(10,2) DEFAULT 0 NOT NULL,
|
||||||
|
clinic_fee_total numeric(10,2) DEFAULT 0 NOT NULL,
|
||||||
|
net_amount numeric(10,2) DEFAULT 0 NOT NULL,
|
||||||
|
status text DEFAULT 'pending'::text NOT NULL,
|
||||||
|
paid_at timestamp with time zone,
|
||||||
|
notes text,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT therapist_payouts_clinic_fee_total_check CHECK ((clinic_fee_total >= (0)::numeric)),
|
||||||
|
CONSTRAINT therapist_payouts_gross_amount_check CHECK ((gross_amount >= (0)::numeric)),
|
||||||
|
CONSTRAINT therapist_payouts_net_amount_check CHECK ((net_amount >= (0)::numeric)),
|
||||||
|
CONSTRAINT therapist_payouts_period_chk CHECK ((period_end >= period_start)),
|
||||||
|
CONSTRAINT therapist_payouts_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'paid'::text, 'cancelled'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.therapist_payouts OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: create_therapist_payout(uuid, uuid, date, date); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.create_therapist_payout(p_tenant_id uuid, p_therapist_id uuid, p_period_start date, p_period_end date) RETURNS public.therapist_payouts
|
||||||
|
CREATE FUNCTION public.delete_commitment_full(p_tenant_id uuid, p_commitment_id uuid) RETURNS jsonb
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
declare
|
||||||
|
v_is_native boolean;
|
||||||
|
v_fields int := 0;
|
||||||
|
v_logs int := 0;
|
||||||
|
v_parent int := 0;
|
||||||
|
begin
|
||||||
|
if auth.uid() is null then
|
||||||
|
raise exception 'Not authenticated';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if not exists (
|
||||||
|
select 1
|
||||||
|
from public.tenant_members tm
|
||||||
|
where tm.tenant_id = p_tenant_id
|
||||||
|
and tm.user_id = auth.uid()
|
||||||
|
and tm.status = 'active'
|
||||||
|
) then
|
||||||
|
raise exception 'Not allowed';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select dc.is_native
|
||||||
|
into v_is_native
|
||||||
|
from public.determined_commitments dc
|
||||||
|
where dc.tenant_id = p_tenant_id
|
||||||
|
and dc.id = p_commitment_id;
|
||||||
|
|
||||||
|
if v_is_native is null then
|
||||||
|
raise exception 'Commitment not found';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if v_is_native = true then
|
||||||
|
raise exception 'Cannot delete native commitment';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
delete from public.determined_commitment_fields
|
||||||
|
where tenant_id = p_tenant_id
|
||||||
|
and commitment_id = p_commitment_id;
|
||||||
|
get diagnostics v_fields = row_count;
|
||||||
|
|
||||||
|
delete from public.commitment_time_logs
|
||||||
|
where tenant_id = p_tenant_id
|
||||||
|
and commitment_id = p_commitment_id;
|
||||||
|
get diagnostics v_logs = row_count;
|
||||||
|
|
||||||
|
delete from public.determined_commitments
|
||||||
|
where tenant_id = p_tenant_id
|
||||||
|
and id = p_commitment_id;
|
||||||
|
get diagnostics v_parent = row_count;
|
||||||
|
|
||||||
|
if v_parent <> 1 then
|
||||||
|
raise exception 'Parent not deleted (RLS/owner issue).';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
return jsonb_build_object(
|
||||||
|
'ok', true,
|
||||||
|
'deleted', jsonb_build_object(
|
||||||
|
'fields', v_fields,
|
||||||
|
'logs', v_logs,
|
||||||
|
'commitment', v_parent
|
||||||
|
)
|
||||||
|
);
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.delete_commitment_full(p_tenant_id uuid, p_commitment_id uuid) OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: delete_determined_commitment(uuid, uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.delete_determined_commitment(p_tenant_id uuid, p_commitment_id uuid) RETURNS jsonb
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
declare
|
||||||
|
v_is_native boolean;
|
||||||
|
v_fields_deleted int := 0;
|
||||||
|
v_logs_deleted int := 0;
|
||||||
|
v_commitment_deleted int := 0;
|
||||||
|
begin
|
||||||
|
if auth.uid() is null then
|
||||||
|
raise exception 'Not authenticated';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if not exists (
|
||||||
|
select 1
|
||||||
|
from public.tenant_members tm
|
||||||
|
where tm.tenant_id = p_tenant_id
|
||||||
|
and tm.user_id = auth.uid()
|
||||||
|
and tm.status = 'active'
|
||||||
|
) then
|
||||||
|
raise exception 'Not allowed';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select dc.is_native
|
||||||
|
into v_is_native
|
||||||
|
from public.determined_commitments dc
|
||||||
|
where dc.tenant_id = p_tenant_id
|
||||||
|
and dc.id = p_commitment_id;
|
||||||
|
|
||||||
|
if v_is_native is null then
|
||||||
|
raise exception 'Commitment not found for tenant';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if v_is_native = true then
|
||||||
|
raise exception 'Cannot delete native commitment';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
delete from public.determined_commitment_fields f
|
||||||
|
where f.tenant_id = p_tenant_id
|
||||||
|
and f.commitment_id = p_commitment_id;
|
||||||
|
get diagnostics v_fields_deleted = row_count;
|
||||||
|
|
||||||
|
delete from public.commitment_time_logs l
|
||||||
|
where l.tenant_id = p_tenant_id
|
||||||
|
and l.commitment_id = p_commitment_id;
|
||||||
|
get diagnostics v_logs_deleted = row_count;
|
||||||
|
|
||||||
|
delete from public.determined_commitments dc
|
||||||
|
where dc.tenant_id = p_tenant_id
|
||||||
|
and dc.id = p_commitment_id;
|
||||||
|
get diagnostics v_commitment_deleted = row_count;
|
||||||
|
|
||||||
|
if v_commitment_deleted <> 1 then
|
||||||
|
raise exception 'Delete did not remove the commitment (tenant mismatch?)';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
return jsonb_build_object(
|
||||||
|
'ok', true,
|
||||||
|
'tenant_id', p_tenant_id,
|
||||||
|
'commitment_id', p_commitment_id,
|
||||||
|
'deleted', jsonb_build_object(
|
||||||
|
'fields', v_fields_deleted,
|
||||||
|
'logs', v_logs_deleted,
|
||||||
|
'commitment', v_commitment_deleted
|
||||||
|
)
|
||||||
|
);
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.delete_determined_commitment(p_tenant_id uuid, p_commitment_id uuid) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: dev_list_auth_users(integer); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.dev_list_auth_users(p_limit integer DEFAULT 50) RETURNS TABLE(id uuid, email text, created_at timestamp with time zone)
|
||||||
|
CREATE FUNCTION public.faq_votar(faq_id uuid) RETURNS void
|
||||||
|
LANGUAGE sql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
update public.saas_faq
|
||||||
|
set votos = votos + 1,
|
||||||
|
updated_at = now()
|
||||||
|
where id = faq_id
|
||||||
|
and ativo = true;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.faq_votar(faq_id uuid) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: fix_all_subscription_mismatches(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.fix_all_subscription_mismatches() RETURNS void
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
declare
|
||||||
|
r record;
|
||||||
|
begin
|
||||||
|
for r in
|
||||||
|
select distinct s.user_id as owner_id
|
||||||
|
from public.subscriptions s
|
||||||
|
where s.status = 'active'
|
||||||
|
and s.user_id is not null
|
||||||
|
loop
|
||||||
|
perform public.rebuild_owner_entitlements(r.owner_id);
|
||||||
|
end loop;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.fix_all_subscription_mismatches() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: fn_agenda_regras_semanais_no_overlap(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.fn_agenda_regras_semanais_no_overlap() RETURNS trigger
|
||||||
|
CREATE FUNCTION public.guard_locked_commitment() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
begin
|
||||||
|
if (old.is_locked = true) then
|
||||||
|
if (tg_op = 'DELETE') then
|
||||||
|
raise exception 'Compromisso bloqueado não pode ser excluído.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if (tg_op = 'UPDATE') then
|
||||||
|
if (new.active = false) then
|
||||||
|
raise exception 'Compromisso bloqueado não pode ser desativado.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- trava renomear (mantém o "Sessão" sempre igual)
|
||||||
|
if (new.name is distinct from old.name) then
|
||||||
|
raise exception 'Compromisso bloqueado não pode ser renomeado.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- se quiser travar descrição também, descomente:
|
||||||
|
-- if (new.description is distinct from old.description) then
|
||||||
|
-- raise exception 'Compromisso bloqueado não pode alterar descrição.';
|
||||||
|
-- end if;
|
||||||
|
end if;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.guard_locked_commitment() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: guard_no_change_core_plan_key(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.guard_no_change_core_plan_key() RETURNS trigger
|
||||||
|
CREATE FUNCTION public.notice_track_click(p_notice_id uuid) RETURNS void
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
begin
|
||||||
|
update public.global_notices
|
||||||
|
set clicks_count = clicks_count + 1
|
||||||
|
where id = p_notice_id;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.notice_track_click(p_notice_id uuid) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: notice_track_view(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.notice_track_view(p_notice_id uuid) RETURNS void
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
begin
|
||||||
|
update public.global_notices
|
||||||
|
set views_count = views_count + 1
|
||||||
|
where id = p_notice_id;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.notice_track_view(p_notice_id uuid) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: notify_on_intake(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.notify_on_intake() RETURNS trigger
|
||||||
|
CREATE FUNCTION public.revoke_support_session(p_token text) RETURNS boolean
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_admin_id uuid;
|
||||||
|
v_role text;
|
||||||
|
BEGIN
|
||||||
|
v_admin_id := auth.uid();
|
||||||
|
IF v_admin_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'Não autenticado.' USING ERRCODE = 'P0001';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT role INTO v_role FROM public.profiles WHERE id = v_admin_id;
|
||||||
|
IF v_role <> 'saas_admin' THEN
|
||||||
|
RAISE EXCEPTION 'Acesso negado.' USING ERRCODE = 'P0002';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
DELETE FROM public.support_sessions
|
||||||
|
WHERE token = p_token
|
||||||
|
AND admin_id = v_admin_id;
|
||||||
|
|
||||||
|
RETURN FOUND;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.revoke_support_session(p_token text) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: rotate_patient_invite_token(text); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.rotate_patient_invite_token(p_new_token text) RETURNS uuid
|
||||||
|
CREATE FUNCTION public.saas_votar_doc(p_doc_id uuid, p_util boolean) RETURNS jsonb
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
declare
|
||||||
|
v_uid uuid := auth.uid();
|
||||||
|
v_voto_antigo boolean;
|
||||||
|
begin
|
||||||
|
if v_uid is null then
|
||||||
|
raise exception 'Não autenticado';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- Verifica se já votou
|
||||||
|
select util into v_voto_antigo
|
||||||
|
from public.saas_doc_votos
|
||||||
|
where doc_id = p_doc_id and user_id = v_uid;
|
||||||
|
|
||||||
|
if found then
|
||||||
|
-- Já votou igual → cancela o voto (toggle)
|
||||||
|
if v_voto_antigo = p_util then
|
||||||
|
delete from public.saas_doc_votos
|
||||||
|
where doc_id = p_doc_id and user_id = v_uid;
|
||||||
|
|
||||||
|
update public.saas_docs set
|
||||||
|
votos_util = greatest(0, votos_util - (case when p_util then 1 else 0 end)),
|
||||||
|
votos_nao_util = greatest(0, votos_nao_util - (case when not p_util then 1 else 0 end)),
|
||||||
|
updated_at = now()
|
||||||
|
where id = p_doc_id;
|
||||||
|
|
||||||
|
return jsonb_build_object('acao', 'removido', 'util', null);
|
||||||
|
else
|
||||||
|
-- Mudou de voto
|
||||||
|
update public.saas_doc_votos set util = p_util, updated_at = now()
|
||||||
|
where doc_id = p_doc_id and user_id = v_uid;
|
||||||
|
|
||||||
|
update public.saas_docs set
|
||||||
|
votos_util = greatest(0, votos_util + (case when p_util then 1 else -1 end)),
|
||||||
|
votos_nao_util = greatest(0, votos_nao_util + (case when not p_util then 1 else -1 end)),
|
||||||
|
updated_at = now()
|
||||||
|
where id = p_doc_id;
|
||||||
|
|
||||||
|
return jsonb_build_object('acao', 'atualizado', 'util', p_util);
|
||||||
|
end if;
|
||||||
|
else
|
||||||
|
-- Primeiro voto
|
||||||
|
insert into public.saas_doc_votos (doc_id, user_id, util)
|
||||||
|
values (p_doc_id, v_uid, p_util);
|
||||||
|
|
||||||
|
update public.saas_docs set
|
||||||
|
votos_util = votos_util + (case when p_util then 1 else 0 end),
|
||||||
|
votos_nao_util = votos_nao_util + (case when not p_util then 1 else 0 end),
|
||||||
|
updated_at = now()
|
||||||
|
where id = p_doc_id;
|
||||||
|
|
||||||
|
return jsonb_build_object('acao', 'registrado', 'util', p_util);
|
||||||
|
end if;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.saas_votar_doc(p_doc_id uuid, p_util boolean) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: safe_delete_patient(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.safe_delete_patient(p_patient_id uuid) RETURNS jsonb
|
||||||
|
CREATE FUNCTION public.sanitize_phone_br(raw_phone text) RETURNS text
|
||||||
|
LANGUAGE plpgsql IMMUTABLE
|
||||||
|
AS $$ DECLARE digits text;
|
||||||
|
BEGIN
|
||||||
|
digits := regexp_replace(COALESCE(raw_phone, ''), '[^0-9]', '', 'g');
|
||||||
|
IF digits = '' THEN RETURN ''; END IF;
|
||||||
|
IF length(digits) = 10 OR length(digits) = 11 THEN
|
||||||
|
digits := '55' || digits;
|
||||||
|
END IF;
|
||||||
|
RETURN digits;
|
||||||
|
END; $$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.sanitize_phone_br(raw_phone text) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: seed_default_financial_categories(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.seed_default_financial_categories(p_user_id uuid) RETURNS void
|
||||||
|
CREATE FUNCTION public.seed_determined_commitments(p_tenant_id uuid) RETURNS void
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
declare
|
||||||
|
v_id uuid;
|
||||||
|
begin
|
||||||
|
-- Sessão (locked + sempre ativa)
|
||||||
|
if not exists (
|
||||||
|
select 1 from public.determined_commitments
|
||||||
|
where tenant_id = p_tenant_id and is_native = true and native_key = 'session'
|
||||||
|
) then
|
||||||
|
insert into public.determined_commitments
|
||||||
|
(tenant_id, is_native, native_key, is_locked, active, name, description)
|
||||||
|
values
|
||||||
|
(p_tenant_id, true, 'session', true, true, 'Sessão', 'Sessão com paciente');
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- Leitura
|
||||||
|
if not exists (
|
||||||
|
select 1 from public.determined_commitments
|
||||||
|
where tenant_id = p_tenant_id and is_native = true and native_key = 'reading'
|
||||||
|
) then
|
||||||
|
insert into public.determined_commitments
|
||||||
|
(tenant_id, is_native, native_key, is_locked, active, name, description)
|
||||||
|
values
|
||||||
|
(p_tenant_id, true, 'reading', false, true, 'Leitura', 'Praticar leitura');
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- Supervisão
|
||||||
|
if not exists (
|
||||||
|
select 1 from public.determined_commitments
|
||||||
|
where tenant_id = p_tenant_id and is_native = true and native_key = 'supervision'
|
||||||
|
) then
|
||||||
|
insert into public.determined_commitments
|
||||||
|
(tenant_id, is_native, native_key, is_locked, active, name, description)
|
||||||
|
values
|
||||||
|
(p_tenant_id, true, 'supervision', false, true, 'Supervisão', 'Supervisão');
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- Aula ✅ (corrigido)
|
||||||
|
if not exists (
|
||||||
|
select 1 from public.determined_commitments
|
||||||
|
where tenant_id = p_tenant_id and is_native = true and native_key = 'class'
|
||||||
|
) then
|
||||||
|
insert into public.determined_commitments
|
||||||
|
(tenant_id, is_native, native_key, is_locked, active, name, description)
|
||||||
|
values
|
||||||
|
(p_tenant_id, true, 'class', false, false, 'Aula', 'Dar aula');
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- Análise pessoal
|
||||||
|
if not exists (
|
||||||
|
select 1 from public.determined_commitments
|
||||||
|
where tenant_id = p_tenant_id and is_native = true and native_key = 'analysis'
|
||||||
|
) then
|
||||||
|
insert into public.determined_commitments
|
||||||
|
(tenant_id, is_native, native_key, is_locked, active, name, description)
|
||||||
|
values
|
||||||
|
(p_tenant_id, true, 'analysis', false, true, 'Análise Pessoal', 'Minha análise pessoal');
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- -------------------------------------------------------
|
||||||
|
-- Campos padrão (idempotentes por (commitment_id, key))
|
||||||
|
-- -------------------------------------------------------
|
||||||
|
|
||||||
|
-- Leitura
|
||||||
|
select id into v_id
|
||||||
|
from public.determined_commitments
|
||||||
|
where tenant_id = p_tenant_id and is_native = true and native_key = 'reading'
|
||||||
|
limit 1;
|
||||||
|
|
||||||
|
if v_id is not null then
|
||||||
|
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'book') then
|
||||||
|
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
|
||||||
|
values (p_tenant_id, v_id, 'book', 'Livro', 'text', false, 10);
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'author') then
|
||||||
|
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
|
||||||
|
values (p_tenant_id, v_id, 'author', 'Autor', 'text', false, 20);
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'notes') then
|
||||||
|
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
|
||||||
|
values (p_tenant_id, v_id, 'notes', 'Observação', 'textarea', false, 30);
|
||||||
|
end if;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- Supervisão
|
||||||
|
select id into v_id
|
||||||
|
from public.determined_commitments
|
||||||
|
where tenant_id = p_tenant_id and is_native = true and native_key = 'supervision'
|
||||||
|
limit 1;
|
||||||
|
|
||||||
|
if v_id is not null then
|
||||||
|
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'supervisor') then
|
||||||
|
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
|
||||||
|
values (p_tenant_id, v_id, 'supervisor', 'Supervisor', 'text', false, 10);
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'topic') then
|
||||||
|
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
|
||||||
|
values (p_tenant_id, v_id, 'topic', 'Assunto', 'text', false, 20);
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'notes') then
|
||||||
|
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
|
||||||
|
values (p_tenant_id, v_id, 'notes', 'Observação', 'textarea', false, 30);
|
||||||
|
end if;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- Aula
|
||||||
|
select id into v_id
|
||||||
|
from public.determined_commitments
|
||||||
|
where tenant_id = p_tenant_id and is_native = true and native_key = 'class'
|
||||||
|
limit 1;
|
||||||
|
|
||||||
|
if v_id is not null then
|
||||||
|
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'theme') then
|
||||||
|
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
|
||||||
|
values (p_tenant_id, v_id, 'theme', 'Tema', 'text', false, 10);
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'group') then
|
||||||
|
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
|
||||||
|
values (p_tenant_id, v_id, 'group', 'Turma', 'text', false, 20);
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'notes') then
|
||||||
|
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
|
||||||
|
values (p_tenant_id, v_id, 'notes', 'Observação', 'textarea', false, 30);
|
||||||
|
end if;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- Análise
|
||||||
|
select id into v_id
|
||||||
|
from public.determined_commitments
|
||||||
|
where tenant_id = p_tenant_id and is_native = true and native_key = 'analysis'
|
||||||
|
limit 1;
|
||||||
|
|
||||||
|
if v_id is not null then
|
||||||
|
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'analyst') then
|
||||||
|
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
|
||||||
|
values (p_tenant_id, v_id, 'analyst', 'Analista', 'text', false, 10);
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'focus') then
|
||||||
|
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
|
||||||
|
values (p_tenant_id, v_id, 'focus', 'Foco', 'text', false, 20);
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'notes') then
|
||||||
|
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
|
||||||
|
values (p_tenant_id, v_id, 'notes', 'Observação', 'textarea', false, 30);
|
||||||
|
end if;
|
||||||
|
end if;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.seed_determined_commitments(p_tenant_id uuid) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: set_insurance_plans_updated_at(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.set_insurance_plans_updated_at() RETURNS trigger
|
||||||
|
CREATE FUNCTION public.validate_support_session(p_token text) RETURNS json
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_session support_sessions;
|
||||||
|
BEGIN
|
||||||
|
IF p_token IS NULL OR length(trim(p_token)) < 32 THEN
|
||||||
|
RETURN json_build_object('valid', false, 'tenant_id', null);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT * INTO v_session
|
||||||
|
FROM public.support_sessions
|
||||||
|
WHERE token = p_token
|
||||||
|
AND expires_at > now()
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RETURN json_build_object('valid', false, 'tenant_id', null);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN json_build_object(
|
||||||
|
'valid', true,
|
||||||
|
'tenant_id', v_session.tenant_id
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.validate_support_session(p_token text) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: whoami(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.whoami() RETURNS TABLE(uid uuid, role text)
|
||||||
404
database-novo/schema/03_functions/notifications.sql
Normal file
404
database-novo/schema/03_functions/notifications.sql
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Functions — Notificações
|
||||||
|
-- =============================================================================
|
||||||
|
-- cancel_notifications_on_opt_out, cancel_notifications_on_session_cancel,
|
||||||
|
-- cancel_patient_pending_notifications, cleanup_notification_queue,
|
||||||
|
-- notify_on_intake, notify_on_scheduling, notify_on_session_status,
|
||||||
|
-- populate_notification_queue, unstick_notification_queue
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION public.cancel_notifications_on_opt_out() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
-- WhatsApp opt-out
|
||||||
|
IF OLD.whatsapp_opt_in = true AND NEW.whatsapp_opt_in = false THEN
|
||||||
|
PERFORM public.cancel_patient_pending_notifications(
|
||||||
|
NEW.patient_id, 'whatsapp'
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
-- Email opt-out
|
||||||
|
IF OLD.email_opt_in = true AND NEW.email_opt_in = false THEN
|
||||||
|
PERFORM public.cancel_patient_pending_notifications(
|
||||||
|
NEW.patient_id, 'email'
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
-- SMS opt-out
|
||||||
|
IF OLD.sms_opt_in = true AND NEW.sms_opt_in = false THEN
|
||||||
|
PERFORM public.cancel_patient_pending_notifications(
|
||||||
|
NEW.patient_id, 'sms'
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.cancel_notifications_on_opt_out() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: cancel_notifications_on_session_cancel(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.cancel_notifications_on_session_cancel() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.status IN ('cancelado', 'excluido')
|
||||||
|
AND OLD.status NOT IN ('cancelado', 'excluido')
|
||||||
|
THEN
|
||||||
|
PERFORM public.cancel_patient_pending_notifications(
|
||||||
|
NEW.patient_id, NULL, NEW.id
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.cancel_notifications_on_session_cancel() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: cancel_patient_pending_notifications(uuid, text, uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.cancel_patient_pending_notifications(p_patient_id uuid, p_channel text DEFAULT NULL::text, p_evento_id uuid DEFAULT NULL::uuid) RETURNS integer
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_canceled integer;
|
||||||
|
BEGIN
|
||||||
|
UPDATE public.notification_queue
|
||||||
|
SET status = 'cancelado',
|
||||||
|
updated_at = now()
|
||||||
|
WHERE patient_id = p_patient_id
|
||||||
|
AND status IN ('pendente', 'processando')
|
||||||
|
AND (p_channel IS NULL OR channel = p_channel)
|
||||||
|
AND (p_evento_id IS NULL OR agenda_evento_id = p_evento_id);
|
||||||
|
|
||||||
|
GET DIAGNOSTICS v_canceled = ROW_COUNT;
|
||||||
|
RETURN v_canceled;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.cancel_patient_pending_notifications(p_patient_id uuid, p_channel text, p_evento_id uuid) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: cancel_recurrence_from(uuid, date); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.cancel_recurrence_from(p_recurrence_id uuid, p_from_date date) RETURNS void
|
||||||
|
CREATE FUNCTION public.cancel_patient_pending_notifications(p_patient_id uuid, p_channel text DEFAULT NULL::text, p_evento_id uuid DEFAULT NULL::uuid) RETURNS integer
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_canceled integer;
|
||||||
|
BEGIN
|
||||||
|
UPDATE public.notification_queue
|
||||||
|
SET status = 'cancelado',
|
||||||
|
updated_at = now()
|
||||||
|
WHERE patient_id = p_patient_id
|
||||||
|
AND status IN ('pendente', 'processando')
|
||||||
|
AND (p_channel IS NULL OR channel = p_channel)
|
||||||
|
AND (p_evento_id IS NULL OR agenda_evento_id = p_evento_id);
|
||||||
|
|
||||||
|
GET DIAGNOSTICS v_canceled = ROW_COUNT;
|
||||||
|
RETURN v_canceled;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.cancel_patient_pending_notifications(p_patient_id uuid, p_channel text, p_evento_id uuid) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: cancel_recurrence_from(uuid, date); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.cancel_recurrence_from(p_recurrence_id uuid, p_from_date date) RETURNS void
|
||||||
|
CREATE FUNCTION public.cleanup_notification_queue() RETURNS integer
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_deleted integer;
|
||||||
|
BEGIN
|
||||||
|
DELETE FROM public.notification_queue
|
||||||
|
WHERE status IN ('enviado', 'cancelado', 'ignorado')
|
||||||
|
AND created_at < now() - interval '90 days';
|
||||||
|
|
||||||
|
GET DIAGNOSTICS v_deleted = ROW_COUNT;
|
||||||
|
RETURN v_deleted;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.cleanup_notification_queue() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: create_clinic_tenant(text); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.create_clinic_tenant(p_name text) RETURNS uuid
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
declare
|
||||||
|
v_uid uuid;
|
||||||
|
v_tenant uuid;
|
||||||
|
v_name text;
|
||||||
|
begin
|
||||||
|
v_uid := auth.uid();
|
||||||
|
if v_uid is null then
|
||||||
|
raise exception 'Not authenticated';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
v_name := nullif(trim(coalesce(p_name, '')), '');
|
||||||
|
if v_name is null then
|
||||||
|
v_name := 'Clínica';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
insert into public.tenants (name, kind, created_at)
|
||||||
|
values (v_name, 'clinic', now())
|
||||||
|
returning id into v_tenant;
|
||||||
|
|
||||||
|
insert into public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||||
|
values (v_tenant, v_uid, 'tenant_admin', 'active', now());
|
||||||
|
|
||||||
|
return v_tenant;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.create_clinic_tenant(p_name text) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: financial_records; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.financial_records (
|
||||||
|
CREATE FUNCTION public.notify_on_intake() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.status = 'new' THEN
|
||||||
|
INSERT INTO public.notifications (
|
||||||
|
owner_id,
|
||||||
|
tenant_id,
|
||||||
|
type,
|
||||||
|
ref_id,
|
||||||
|
ref_table,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
NEW.owner_id,
|
||||||
|
NEW.tenant_id,
|
||||||
|
'new_patient',
|
||||||
|
NEW.id,
|
||||||
|
'patient_intake_requests',
|
||||||
|
jsonb_build_object(
|
||||||
|
'title', 'Novo cadastro externo',
|
||||||
|
'detail', COALESCE(NEW.nome_completo, 'Paciente'),
|
||||||
|
'deeplink', '/therapist/patients/cadastro/recebidos',
|
||||||
|
'avatar_initials', upper(left(COALESCE(NEW.nome_completo, '?'), 2))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.notify_on_intake() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: notify_on_scheduling(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.notify_on_scheduling() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
AS $$ BEGIN IF NEW.status = 'pendente' THEN
|
||||||
|
INSERT INTO public.notifications ( owner_id, tenant_id, type, ref_id, ref_table, payload ) VALUES (
|
||||||
|
NEW.owner_id, NEW.tenant_id,
|
||||||
|
'new_scheduling', NEW.id, 'agendador_solicitacoes', jsonb_build_object( 'title', 'Nova solicitação de agendamento', 'detail', COALESCE(NEW.paciente_nome, 'Paciente') || ' ' || COALESCE(NEW.paciente_sobrenome, '') || ' — ' || COALESCE(NEW.tipo, ''), 'deeplink', '/therapist/agendamentos-recebidos', 'avatar_initials', upper(left(COALESCE(NEW.paciente_nome, '?'), 1) || left(COALESCE(NEW.paciente_sobrenome, ''), 1)) ) ); END IF; RETURN NEW; END; $$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.notify_on_scheduling() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: notify_on_session_status(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.notify_on_session_status() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_nome text;
|
||||||
|
BEGIN
|
||||||
|
IF NEW.status IN ('faltou', 'cancelado') AND OLD.status IS DISTINCT FROM NEW.status THEN
|
||||||
|
|
||||||
|
SELECT nome_completo
|
||||||
|
INTO v_nome
|
||||||
|
FROM public.patients
|
||||||
|
WHERE id = NEW.patient_id
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
INSERT INTO public.notifications (
|
||||||
|
owner_id,
|
||||||
|
tenant_id,
|
||||||
|
type,
|
||||||
|
ref_id,
|
||||||
|
ref_table,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
NEW.owner_id,
|
||||||
|
NEW.tenant_id,
|
||||||
|
'session_status',
|
||||||
|
NEW.id,
|
||||||
|
'agenda_eventos',
|
||||||
|
jsonb_build_object(
|
||||||
|
'title', CASE WHEN NEW.status = 'faltou' THEN 'Paciente faltou' ELSE 'Sessão cancelada' END,
|
||||||
|
'detail', COALESCE(v_nome, 'Paciente') || ' — ' || to_char(NEW.inicio_em, 'DD/MM HH24:MI'),
|
||||||
|
'deeplink', '/therapist/agenda',
|
||||||
|
'avatar_initials', upper(left(COALESCE(v_nome, '?'), 2))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.notify_on_session_status() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: on_new_user_seed_patient_groups(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.on_new_user_seed_patient_groups() RETURNS trigger
|
||||||
|
CREATE FUNCTION public.populate_notification_queue() RETURNS void
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO public.notification_queue (
|
||||||
|
tenant_id, owner_id, agenda_evento_id, patient_id,
|
||||||
|
channel, template_key, schedule_key,
|
||||||
|
resolved_vars, recipient_address,
|
||||||
|
scheduled_at, idempotency_key
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
ae.tenant_id,
|
||||||
|
ae.owner_id,
|
||||||
|
ae.id AS agenda_evento_id,
|
||||||
|
ae.patient_id,
|
||||||
|
ch.channel,
|
||||||
|
'session.' || REPLACE(ns.event_type, '_sessao', '') || '.' || ch.channel,
|
||||||
|
ns.schedule_key,
|
||||||
|
jsonb_build_object(
|
||||||
|
'nome_paciente', COALESCE(p.nome_completo, 'Paciente'),
|
||||||
|
'data_sessao', TO_CHAR(ae.inicio_em AT TIME ZONE 'America/Sao_Paulo', 'DD/MM/YYYY'),
|
||||||
|
'hora_sessao', TO_CHAR(ae.inicio_em AT TIME ZONE 'America/Sao_Paulo', 'HH24:MI'),
|
||||||
|
'nome_terapeuta', COALESCE(prof.full_name, 'Terapeuta'),
|
||||||
|
'modalidade', COALESCE(ae.modalidade, 'Presencial'),
|
||||||
|
'titulo', COALESCE(ae.titulo, 'Sessão')
|
||||||
|
),
|
||||||
|
CASE ch.channel
|
||||||
|
WHEN 'whatsapp' THEN COALESCE(p.telefone, '')
|
||||||
|
WHEN 'sms' THEN COALESCE(p.telefone, '')
|
||||||
|
WHEN 'email' THEN COALESCE(p.email_principal, '')
|
||||||
|
END,
|
||||||
|
CASE
|
||||||
|
WHEN (ae.inicio_em - (ns.offset_minutes || ' minutes')::interval)::time
|
||||||
|
< ns.allowed_time_start
|
||||||
|
THEN DATE_TRUNC('day', ae.inicio_em - (ns.offset_minutes || ' minutes')::interval)
|
||||||
|
+ ns.allowed_time_start
|
||||||
|
WHEN (ae.inicio_em - (ns.offset_minutes || ' minutes')::interval)::time
|
||||||
|
> ns.allowed_time_end
|
||||||
|
THEN DATE_TRUNC('day', ae.inicio_em - (ns.offset_minutes || ' minutes')::interval)
|
||||||
|
+ ns.allowed_time_start
|
||||||
|
ELSE ae.inicio_em - (ns.offset_minutes || ' minutes')::interval
|
||||||
|
END,
|
||||||
|
ae.id::text || ':' || ns.schedule_key || ':' || ch.channel || ':'
|
||||||
|
|| ae.inicio_em::date::text
|
||||||
|
FROM public.agenda_eventos ae
|
||||||
|
JOIN public.patients p ON p.id = ae.patient_id
|
||||||
|
LEFT JOIN public.profiles prof ON prof.id = ae.owner_id
|
||||||
|
JOIN public.notification_schedules ns
|
||||||
|
ON ns.owner_id = ae.owner_id
|
||||||
|
AND ns.is_active = true
|
||||||
|
AND ns.deleted_at IS NULL
|
||||||
|
AND ns.trigger_type = 'before_event'
|
||||||
|
AND ns.event_type = 'lembrete_sessao'
|
||||||
|
JOIN public.notification_channels nc
|
||||||
|
ON nc.owner_id = ae.owner_id
|
||||||
|
AND nc.is_active = true
|
||||||
|
AND nc.deleted_at IS NULL
|
||||||
|
CROSS JOIN LATERAL (
|
||||||
|
SELECT 'whatsapp' AS channel WHERE ns.whatsapp_enabled AND nc.channel = 'whatsapp'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'email' AS channel WHERE ns.email_enabled AND nc.channel = 'email'
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'sms' AS channel WHERE ns.sms_enabled AND nc.channel = 'sms'
|
||||||
|
) ch
|
||||||
|
LEFT JOIN public.notification_preferences np
|
||||||
|
ON np.patient_id = ae.patient_id
|
||||||
|
AND np.owner_id = ae.owner_id
|
||||||
|
AND np.deleted_at IS NULL
|
||||||
|
WHERE
|
||||||
|
ae.inicio_em > now()
|
||||||
|
AND ae.inicio_em <= now() + interval '48 hours'
|
||||||
|
AND ae.status NOT IN ('cancelado', 'faltou')
|
||||||
|
AND CASE ch.channel
|
||||||
|
WHEN 'whatsapp' THEN COALESCE(p.telefone, '') != ''
|
||||||
|
WHEN 'sms' THEN COALESCE(p.telefone, '') != ''
|
||||||
|
WHEN 'email' THEN COALESCE(p.email_principal, '') != ''
|
||||||
|
END
|
||||||
|
AND CASE ch.channel
|
||||||
|
WHEN 'whatsapp' THEN COALESCE(np.whatsapp_opt_in, true)
|
||||||
|
WHEN 'email' THEN COALESCE(np.email_opt_in, true)
|
||||||
|
WHEN 'sms' THEN COALESCE(np.sms_opt_in, false)
|
||||||
|
END
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM public.profiles tp
|
||||||
|
WHERE tp.id = ae.owner_id
|
||||||
|
AND COALESCE(tp.notify_reminders, true) = true
|
||||||
|
)
|
||||||
|
ON CONFLICT (idempotency_key) DO NOTHING;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.populate_notification_queue() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: prevent_promoting_to_system(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.prevent_promoting_to_system() RETURNS trigger
|
||||||
|
CREATE FUNCTION public.unstick_notification_queue() RETURNS integer
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_unstuck integer;
|
||||||
|
BEGIN
|
||||||
|
UPDATE public.notification_queue
|
||||||
|
SET status = 'pendente',
|
||||||
|
attempts = attempts + 1,
|
||||||
|
last_error = 'Timeout: preso em processando por >10min',
|
||||||
|
next_retry_at = now() + interval '2 minutes'
|
||||||
|
WHERE status = 'processando'
|
||||||
|
AND updated_at < now() - interval '10 minutes';
|
||||||
|
|
||||||
|
GET DIAGNOSTICS v_unstuck = ROW_COUNT;
|
||||||
|
RETURN v_unstuck;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.unstick_notification_queue() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: update_payment_settings_updated_at(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.update_payment_settings_updated_at() RETURNS trigger
|
||||||
433
database-novo/schema/03_functions/patients.sql
Normal file
433
database-novo/schema/03_functions/patients.sql
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Functions — Pacientes
|
||||||
|
-- =============================================================================
|
||||||
|
-- can_delete_patient, safe_delete_patient, create_patient_intake_request,
|
||||||
|
-- create_patient_intake_request_v2, rotate_patient_invite_token,
|
||||||
|
-- patients_validate_member_consistency, prevent_system_group_changes,
|
||||||
|
-- seed_default_patient_groups
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION public.can_delete_patient(p_patient_id uuid) RETURNS boolean
|
||||||
|
LANGUAGE sql STABLE SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
SELECT NOT EXISTS (
|
||||||
|
SELECT 1 FROM public.agenda_eventos WHERE patient_id = p_patient_id
|
||||||
|
UNION ALL
|
||||||
|
SELECT 1 FROM public.recurrence_rules WHERE patient_id = p_patient_id
|
||||||
|
UNION ALL
|
||||||
|
SELECT 1 FROM public.billing_contracts WHERE patient_id = p_patient_id
|
||||||
|
);
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.can_delete_patient(p_patient_id uuid) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: cancel_notifications_on_opt_out(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.cancel_notifications_on_opt_out() RETURNS trigger
|
||||||
|
CREATE FUNCTION public.create_patient_intake_request(p_token text, p_name text, p_email text DEFAULT NULL::text, p_phone text DEFAULT NULL::text, p_notes text DEFAULT NULL::text, p_consent boolean DEFAULT false) RETURNS uuid
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
declare
|
||||||
|
v_owner uuid;
|
||||||
|
v_active boolean;
|
||||||
|
v_expires timestamptz;
|
||||||
|
v_max_uses int;
|
||||||
|
v_uses int;
|
||||||
|
v_id uuid;
|
||||||
|
begin
|
||||||
|
select owner_id, active, expires_at, max_uses, uses
|
||||||
|
into v_owner, v_active, v_expires, v_max_uses, v_uses
|
||||||
|
from public.patient_invites
|
||||||
|
where token = p_token
|
||||||
|
limit 1;
|
||||||
|
|
||||||
|
if v_owner is null then
|
||||||
|
raise exception 'Token inválido';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if v_active is not true then
|
||||||
|
raise exception 'Link desativado';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if v_expires is not null and now() > v_expires then
|
||||||
|
raise exception 'Link expirado';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if v_max_uses is not null and v_uses >= v_max_uses then
|
||||||
|
raise exception 'Limite de uso atingido';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if p_name is null or length(trim(p_name)) = 0 then
|
||||||
|
raise exception 'Nome é obrigatório';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
insert into public.patient_intake_requests
|
||||||
|
(owner_id, token, name, email, phone, notes, consent, status)
|
||||||
|
values
|
||||||
|
(v_owner, p_token, trim(p_name),
|
||||||
|
nullif(lower(trim(p_email)), ''),
|
||||||
|
nullif(trim(p_phone), ''),
|
||||||
|
nullif(trim(p_notes), ''),
|
||||||
|
coalesce(p_consent, false),
|
||||||
|
'new')
|
||||||
|
returning id into v_id;
|
||||||
|
|
||||||
|
update public.patient_invites
|
||||||
|
set uses = uses + 1
|
||||||
|
where token = p_token;
|
||||||
|
|
||||||
|
return v_id;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.create_patient_intake_request(p_token text, p_name text, p_email text, p_phone text, p_notes text, p_consent boolean) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: create_patient_intake_request_v2(text, jsonb); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.create_patient_intake_request_v2(p_token text, p_payload jsonb) RETURNS uuid
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $_$
|
||||||
|
declare
|
||||||
|
v_owner_id uuid;
|
||||||
|
v_intake_id uuid;
|
||||||
|
v_birth_raw text;
|
||||||
|
v_birth date;
|
||||||
|
begin
|
||||||
|
select owner_id
|
||||||
|
into v_owner_id
|
||||||
|
from public.patient_invites
|
||||||
|
where token = p_token;
|
||||||
|
|
||||||
|
if v_owner_id is null then
|
||||||
|
raise exception 'Token inválido ou expirado';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
v_birth_raw := nullif(trim(coalesce(
|
||||||
|
p_payload->>'data_nascimento',
|
||||||
|
''
|
||||||
|
)), '');
|
||||||
|
|
||||||
|
v_birth := case
|
||||||
|
when v_birth_raw is null then null
|
||||||
|
when v_birth_raw ~ '^\d{4}-\d{2}-\d{2}$' then v_birth_raw::date
|
||||||
|
when v_birth_raw ~ '^\d{2}-\d{2}-\d{4}$' then to_date(v_birth_raw, 'DD-MM-YYYY')
|
||||||
|
else null
|
||||||
|
end;
|
||||||
|
|
||||||
|
insert into public.patient_intake_requests (
|
||||||
|
owner_id,
|
||||||
|
token,
|
||||||
|
status,
|
||||||
|
consent,
|
||||||
|
|
||||||
|
nome_completo,
|
||||||
|
email_principal,
|
||||||
|
telefone,
|
||||||
|
|
||||||
|
avatar_url, -- 🔥 AQUI
|
||||||
|
|
||||||
|
data_nascimento,
|
||||||
|
cpf,
|
||||||
|
rg,
|
||||||
|
genero,
|
||||||
|
estado_civil,
|
||||||
|
profissao,
|
||||||
|
escolaridade,
|
||||||
|
nacionalidade,
|
||||||
|
naturalidade,
|
||||||
|
|
||||||
|
cep,
|
||||||
|
pais,
|
||||||
|
cidade,
|
||||||
|
estado,
|
||||||
|
endereco,
|
||||||
|
numero,
|
||||||
|
complemento,
|
||||||
|
bairro,
|
||||||
|
|
||||||
|
observacoes,
|
||||||
|
notas_internas,
|
||||||
|
|
||||||
|
encaminhado_por,
|
||||||
|
onde_nos_conheceu
|
||||||
|
)
|
||||||
|
values (
|
||||||
|
v_owner_id,
|
||||||
|
p_token,
|
||||||
|
'new',
|
||||||
|
coalesce((p_payload->>'consent')::boolean, false),
|
||||||
|
|
||||||
|
nullif(trim(p_payload->>'nome_completo'), ''),
|
||||||
|
nullif(trim(p_payload->>'email_principal'), ''),
|
||||||
|
nullif(regexp_replace(coalesce(p_payload->>'telefone',''), '\D', '', 'g'), ''),
|
||||||
|
|
||||||
|
nullif(trim(p_payload->>'avatar_url'), ''), -- 🔥 AQUI
|
||||||
|
|
||||||
|
v_birth,
|
||||||
|
nullif(regexp_replace(coalesce(p_payload->>'cpf',''), '\D', '', 'g'), ''),
|
||||||
|
nullif(trim(p_payload->>'rg'), ''),
|
||||||
|
nullif(trim(p_payload->>'genero'), ''),
|
||||||
|
nullif(trim(p_payload->>'estado_civil'), ''),
|
||||||
|
nullif(trim(p_payload->>'profissao'), ''),
|
||||||
|
nullif(trim(p_payload->>'escolaridade'), ''),
|
||||||
|
nullif(trim(p_payload->>'nacionalidade'), ''),
|
||||||
|
nullif(trim(p_payload->>'naturalidade'), ''),
|
||||||
|
|
||||||
|
nullif(regexp_replace(coalesce(p_payload->>'cep',''), '\D', '', 'g'), ''),
|
||||||
|
nullif(trim(p_payload->>'pais'), ''),
|
||||||
|
nullif(trim(p_payload->>'cidade'), ''),
|
||||||
|
nullif(trim(p_payload->>'estado'), ''),
|
||||||
|
nullif(trim(p_payload->>'endereco'), ''),
|
||||||
|
nullif(trim(p_payload->>'numero'), ''),
|
||||||
|
nullif(trim(p_payload->>'complemento'), ''),
|
||||||
|
nullif(trim(p_payload->>'bairro'), ''),
|
||||||
|
|
||||||
|
nullif(trim(p_payload->>'observacoes'), ''),
|
||||||
|
nullif(trim(p_payload->>'notas_internas'), ''),
|
||||||
|
|
||||||
|
nullif(trim(p_payload->>'encaminhado_por'), ''),
|
||||||
|
nullif(trim(p_payload->>'onde_nos_conheceu'), '')
|
||||||
|
)
|
||||||
|
returning id into v_intake_id;
|
||||||
|
|
||||||
|
return v_intake_id;
|
||||||
|
end;
|
||||||
|
$_$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.create_patient_intake_request_v2(p_token text, p_payload jsonb) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: create_support_session(uuid, integer); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.create_support_session(p_tenant_id uuid, p_ttl_minutes integer DEFAULT 60) RETURNS json
|
||||||
|
CREATE FUNCTION public.patients_validate_member_consistency() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_tenant_responsible uuid;
|
||||||
|
v_tenant_therapist uuid;
|
||||||
|
BEGIN
|
||||||
|
-- responsible_member sempre deve existir e ser do tenant
|
||||||
|
SELECT tenant_id INTO v_tenant_responsible
|
||||||
|
FROM public.tenant_members
|
||||||
|
WHERE id = NEW.responsible_member_id;
|
||||||
|
|
||||||
|
IF v_tenant_responsible IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'Responsible member not found';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NEW.tenant_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'tenant_id is required';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_tenant_responsible <> NEW.tenant_id THEN
|
||||||
|
RAISE EXCEPTION 'Responsible member must belong to the same tenant';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- therapist scope: therapist_member_id deve existir e ser do mesmo tenant
|
||||||
|
IF NEW.patient_scope = 'therapist' THEN
|
||||||
|
IF NEW.therapist_member_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'therapist_member_id is required when patient_scope=therapist';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT tenant_id INTO v_tenant_therapist
|
||||||
|
FROM public.tenant_members
|
||||||
|
WHERE id = NEW.therapist_member_id;
|
||||||
|
|
||||||
|
IF v_tenant_therapist IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'Therapist member not found';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_tenant_therapist <> NEW.tenant_id THEN
|
||||||
|
RAISE EXCEPTION 'Therapist member must belong to the same tenant';
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.patients_validate_member_consistency() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: patients_validate_responsible_member_tenant(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.patients_validate_responsible_member_tenant() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
declare
|
||||||
|
m_tenant uuid;
|
||||||
|
begin
|
||||||
|
select tenant_id into m_tenant
|
||||||
|
from public.tenant_members
|
||||||
|
where id = new.responsible_member_id;
|
||||||
|
|
||||||
|
if m_tenant is null then
|
||||||
|
raise exception 'Responsible member not found';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if new.tenant_id is null then
|
||||||
|
raise exception 'tenant_id is required';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if m_tenant <> new.tenant_id then
|
||||||
|
raise exception 'Responsible member must belong to the same tenant';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.patients_validate_responsible_member_tenant() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: populate_notification_queue(); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.populate_notification_queue() RETURNS void
|
||||||
|
CREATE FUNCTION public.prevent_system_group_changes() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
begin
|
||||||
|
-- Se for grupo do sistema, regras rígidas:
|
||||||
|
if old.is_system = true then
|
||||||
|
|
||||||
|
-- nunca pode deletar
|
||||||
|
if tg_op = 'DELETE' then
|
||||||
|
raise exception 'Grupos padrão do sistema não podem ser alterados ou excluídos.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if tg_op = 'UPDATE' then
|
||||||
|
-- permite SOMENTE mudar tenant_id e/ou updated_at
|
||||||
|
-- qualquer mudança de conteúdo permanece proibida
|
||||||
|
if
|
||||||
|
new.nome is distinct from old.nome or
|
||||||
|
new.descricao is distinct from old.descricao or
|
||||||
|
new.cor is distinct from old.cor or
|
||||||
|
new.is_active is distinct from old.is_active or
|
||||||
|
new.is_system is distinct from old.is_system or
|
||||||
|
new.owner_id is distinct from old.owner_id or
|
||||||
|
new.therapist_id is distinct from old.therapist_id or
|
||||||
|
new.created_at is distinct from old.created_at
|
||||||
|
then
|
||||||
|
raise exception 'Grupos padrão do sistema não podem ser alterados ou excluídos.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- chegou aqui: só tenant_id/updated_at mudaram -> ok
|
||||||
|
return new;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- não-system: deixa passar
|
||||||
|
if tg_op = 'DELETE' then
|
||||||
|
return old;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.prevent_system_group_changes() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: provision_account_tenant(uuid, text, text); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.provision_account_tenant(p_user_id uuid, p_kind text, p_name text DEFAULT NULL::text) RETURNS uuid
|
||||||
|
CREATE FUNCTION public.rotate_patient_invite_token(p_new_token text) RETURNS uuid
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
declare
|
||||||
|
v_uid uuid;
|
||||||
|
v_id uuid;
|
||||||
|
begin
|
||||||
|
-- pega o usuário logado
|
||||||
|
v_uid := auth.uid();
|
||||||
|
if v_uid is null then
|
||||||
|
raise exception 'Usuário não autenticado';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- desativa tokens antigos ativos do usuário
|
||||||
|
update public.patient_invites
|
||||||
|
set active = false
|
||||||
|
where owner_id = v_uid
|
||||||
|
and active = true;
|
||||||
|
|
||||||
|
-- cria novo token
|
||||||
|
insert into public.patient_invites (owner_id, token, active)
|
||||||
|
values (v_uid, p_new_token, true)
|
||||||
|
returning id into v_id;
|
||||||
|
|
||||||
|
return v_id;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.rotate_patient_invite_token(p_new_token text) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: saas_votar_doc(uuid, boolean); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.saas_votar_doc(p_doc_id uuid, p_util boolean) RETURNS jsonb
|
||||||
|
CREATE FUNCTION public.safe_delete_patient(p_patient_id uuid) RETURNS jsonb
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Bloqueia se houver histórico
|
||||||
|
IF NOT public.can_delete_patient(p_patient_id) THEN
|
||||||
|
RETURN jsonb_build_object(
|
||||||
|
'ok', false,
|
||||||
|
'error', 'has_history',
|
||||||
|
'message', 'Este paciente possui histórico clínico ou financeiro e não pode ser removido. Você pode desativar ou arquivar o paciente.'
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Verifica ownership via RLS (owner_id ou responsible_member_id)
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM public.patients
|
||||||
|
WHERE id = p_patient_id
|
||||||
|
AND (
|
||||||
|
owner_id = auth.uid()
|
||||||
|
OR responsible_member_id IN (
|
||||||
|
SELECT id FROM public.tenant_members WHERE user_id = auth.uid()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) THEN
|
||||||
|
RETURN jsonb_build_object(
|
||||||
|
'ok', false,
|
||||||
|
'error', 'forbidden',
|
||||||
|
'message', 'Sem permissão para excluir este paciente.'
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
DELETE FROM public.patients WHERE id = p_patient_id;
|
||||||
|
|
||||||
|
RETURN jsonb_build_object('ok', true);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION public.safe_delete_patient(p_patient_id uuid) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: sanitize_phone_br(text); Type: FUNCTION; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.sanitize_phone_br(raw_phone text) RETURNS text
|
||||||
721
database-novo/schema/03_functions/realtime.sql
Normal file
721
database-novo/schema/03_functions/realtime.sql
Normal file
@@ -0,0 +1,721 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Functions — realtime schema
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION realtime.apply_rls(wal jsonb, max_record_bytes integer DEFAULT (1024 * 1024)) RETURNS SETOF realtime.wal_rls
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
declare
|
||||||
|
-- Regclass of the table e.g. public.notes
|
||||||
|
entity_ regclass = (quote_ident(wal ->> 'schema') || '.' || quote_ident(wal ->> 'table'))::regclass;
|
||||||
|
|
||||||
|
-- I, U, D, T: insert, update ...
|
||||||
|
action realtime.action = (
|
||||||
|
case wal ->> 'action'
|
||||||
|
when 'I' then 'INSERT'
|
||||||
|
when 'U' then 'UPDATE'
|
||||||
|
when 'D' then 'DELETE'
|
||||||
|
else 'ERROR'
|
||||||
|
end
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Is row level security enabled for the table
|
||||||
|
is_rls_enabled bool = relrowsecurity from pg_class where oid = entity_;
|
||||||
|
|
||||||
|
subscriptions realtime.subscription[] = array_agg(subs)
|
||||||
|
from
|
||||||
|
realtime.subscription subs
|
||||||
|
where
|
||||||
|
subs.entity = entity_;
|
||||||
|
|
||||||
|
-- Subscription vars
|
||||||
|
roles regrole[] = array_agg(distinct us.claims_role::text)
|
||||||
|
from
|
||||||
|
unnest(subscriptions) us;
|
||||||
|
|
||||||
|
working_role regrole;
|
||||||
|
claimed_role regrole;
|
||||||
|
claims jsonb;
|
||||||
|
|
||||||
|
subscription_id uuid;
|
||||||
|
subscription_has_access bool;
|
||||||
|
visible_to_subscription_ids uuid[] = '{}';
|
||||||
|
|
||||||
|
-- structured info for wal's columns
|
||||||
|
columns realtime.wal_column[];
|
||||||
|
-- previous identity values for update/delete
|
||||||
|
old_columns realtime.wal_column[];
|
||||||
|
|
||||||
|
error_record_exceeds_max_size boolean = octet_length(wal::text) > max_record_bytes;
|
||||||
|
|
||||||
|
-- Primary jsonb output for record
|
||||||
|
output jsonb;
|
||||||
|
|
||||||
|
begin
|
||||||
|
perform set_config('role', null, true);
|
||||||
|
|
||||||
|
columns =
|
||||||
|
array_agg(
|
||||||
|
(
|
||||||
|
x->>'name',
|
||||||
|
x->>'type',
|
||||||
|
x->>'typeoid',
|
||||||
|
realtime.cast(
|
||||||
|
(x->'value') #>> '{}',
|
||||||
|
coalesce(
|
||||||
|
(x->>'typeoid')::regtype, -- null when wal2json version <= 2.4
|
||||||
|
(x->>'type')::regtype
|
||||||
|
)
|
||||||
|
),
|
||||||
|
(pks ->> 'name') is not null,
|
||||||
|
true
|
||||||
|
)::realtime.wal_column
|
||||||
|
)
|
||||||
|
from
|
||||||
|
jsonb_array_elements(wal -> 'columns') x
|
||||||
|
left join jsonb_array_elements(wal -> 'pk') pks
|
||||||
|
on (x ->> 'name') = (pks ->> 'name');
|
||||||
|
|
||||||
|
old_columns =
|
||||||
|
array_agg(
|
||||||
|
(
|
||||||
|
x->>'name',
|
||||||
|
x->>'type',
|
||||||
|
x->>'typeoid',
|
||||||
|
realtime.cast(
|
||||||
|
(x->'value') #>> '{}',
|
||||||
|
coalesce(
|
||||||
|
(x->>'typeoid')::regtype, -- null when wal2json version <= 2.4
|
||||||
|
(x->>'type')::regtype
|
||||||
|
)
|
||||||
|
),
|
||||||
|
(pks ->> 'name') is not null,
|
||||||
|
true
|
||||||
|
)::realtime.wal_column
|
||||||
|
)
|
||||||
|
from
|
||||||
|
jsonb_array_elements(wal -> 'identity') x
|
||||||
|
left join jsonb_array_elements(wal -> 'pk') pks
|
||||||
|
on (x ->> 'name') = (pks ->> 'name');
|
||||||
|
|
||||||
|
for working_role in select * from unnest(roles) loop
|
||||||
|
|
||||||
|
-- Update `is_selectable` for columns and old_columns
|
||||||
|
columns =
|
||||||
|
array_agg(
|
||||||
|
(
|
||||||
|
c.name,
|
||||||
|
c.type_name,
|
||||||
|
c.type_oid,
|
||||||
|
c.value,
|
||||||
|
c.is_pkey,
|
||||||
|
pg_catalog.has_column_privilege(working_role, entity_, c.name, 'SELECT')
|
||||||
|
)::realtime.wal_column
|
||||||
|
)
|
||||||
|
from
|
||||||
|
unnest(columns) c;
|
||||||
|
|
||||||
|
old_columns =
|
||||||
|
array_agg(
|
||||||
|
(
|
||||||
|
c.name,
|
||||||
|
c.type_name,
|
||||||
|
c.type_oid,
|
||||||
|
c.value,
|
||||||
|
c.is_pkey,
|
||||||
|
pg_catalog.has_column_privilege(working_role, entity_, c.name, 'SELECT')
|
||||||
|
)::realtime.wal_column
|
||||||
|
)
|
||||||
|
from
|
||||||
|
unnest(old_columns) c;
|
||||||
|
|
||||||
|
if action <> 'DELETE' and count(1) = 0 from unnest(columns) c where c.is_pkey then
|
||||||
|
return next (
|
||||||
|
jsonb_build_object(
|
||||||
|
'schema', wal ->> 'schema',
|
||||||
|
'table', wal ->> 'table',
|
||||||
|
'type', action
|
||||||
|
),
|
||||||
|
is_rls_enabled,
|
||||||
|
-- subscriptions is already filtered by entity
|
||||||
|
(select array_agg(s.subscription_id) from unnest(subscriptions) as s where claims_role = working_role),
|
||||||
|
array['Error 400: Bad Request, no primary key']
|
||||||
|
)::realtime.wal_rls;
|
||||||
|
|
||||||
|
-- The claims role does not have SELECT permission to the primary key of entity
|
||||||
|
elsif action <> 'DELETE' and sum(c.is_selectable::int) <> count(1) from unnest(columns) c where c.is_pkey then
|
||||||
|
return next (
|
||||||
|
jsonb_build_object(
|
||||||
|
'schema', wal ->> 'schema',
|
||||||
|
'table', wal ->> 'table',
|
||||||
|
'type', action
|
||||||
|
),
|
||||||
|
is_rls_enabled,
|
||||||
|
(select array_agg(s.subscription_id) from unnest(subscriptions) as s where claims_role = working_role),
|
||||||
|
array['Error 401: Unauthorized']
|
||||||
|
)::realtime.wal_rls;
|
||||||
|
|
||||||
|
else
|
||||||
|
output = jsonb_build_object(
|
||||||
|
'schema', wal ->> 'schema',
|
||||||
|
'table', wal ->> 'table',
|
||||||
|
'type', action,
|
||||||
|
'commit_timestamp', to_char(
|
||||||
|
((wal ->> 'timestamp')::timestamptz at time zone 'utc'),
|
||||||
|
'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
|
||||||
|
),
|
||||||
|
'columns', (
|
||||||
|
select
|
||||||
|
jsonb_agg(
|
||||||
|
jsonb_build_object(
|
||||||
|
'name', pa.attname,
|
||||||
|
'type', pt.typname
|
||||||
|
)
|
||||||
|
order by pa.attnum asc
|
||||||
|
)
|
||||||
|
from
|
||||||
|
pg_attribute pa
|
||||||
|
join pg_type pt
|
||||||
|
on pa.atttypid = pt.oid
|
||||||
|
where
|
||||||
|
attrelid = entity_
|
||||||
|
and attnum > 0
|
||||||
|
and pg_catalog.has_column_privilege(working_role, entity_, pa.attname, 'SELECT')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
-- Add "record" key for insert and update
|
||||||
|
|| case
|
||||||
|
when action in ('INSERT', 'UPDATE') then
|
||||||
|
jsonb_build_object(
|
||||||
|
'record',
|
||||||
|
(
|
||||||
|
select
|
||||||
|
jsonb_object_agg(
|
||||||
|
-- if unchanged toast, get column name and value from old record
|
||||||
|
coalesce((c).name, (oc).name),
|
||||||
|
case
|
||||||
|
when (c).name is null then (oc).value
|
||||||
|
else (c).value
|
||||||
|
end
|
||||||
|
)
|
||||||
|
from
|
||||||
|
unnest(columns) c
|
||||||
|
full outer join unnest(old_columns) oc
|
||||||
|
on (c).name = (oc).name
|
||||||
|
where
|
||||||
|
coalesce((c).is_selectable, (oc).is_selectable)
|
||||||
|
and ( not error_record_exceeds_max_size or (octet_length((c).value::text) <= 64))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else '{}'::jsonb
|
||||||
|
end
|
||||||
|
-- Add "old_record" key for update and delete
|
||||||
|
|| case
|
||||||
|
when action = 'UPDATE' then
|
||||||
|
jsonb_build_object(
|
||||||
|
'old_record',
|
||||||
|
(
|
||||||
|
select jsonb_object_agg((c).name, (c).value)
|
||||||
|
from unnest(old_columns) c
|
||||||
|
where
|
||||||
|
(c).is_selectable
|
||||||
|
and ( not error_record_exceeds_max_size or (octet_length((c).value::text) <= 64))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
when action = 'DELETE' then
|
||||||
|
jsonb_build_object(
|
||||||
|
'old_record',
|
||||||
|
(
|
||||||
|
select jsonb_object_agg((c).name, (c).value)
|
||||||
|
from unnest(old_columns) c
|
||||||
|
where
|
||||||
|
(c).is_selectable
|
||||||
|
and ( not error_record_exceeds_max_size or (octet_length((c).value::text) <= 64))
|
||||||
|
and ( not is_rls_enabled or (c).is_pkey ) -- if RLS enabled, we can't secure deletes so filter to pkey
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else '{}'::jsonb
|
||||||
|
end;
|
||||||
|
|
||||||
|
-- Create the prepared statement
|
||||||
|
if is_rls_enabled and action <> 'DELETE' then
|
||||||
|
if (select 1 from pg_prepared_statements where name = 'walrus_rls_stmt' limit 1) > 0 then
|
||||||
|
deallocate walrus_rls_stmt;
|
||||||
|
end if;
|
||||||
|
execute realtime.build_prepared_statement_sql('walrus_rls_stmt', entity_, columns);
|
||||||
|
end if;
|
||||||
|
|
||||||
|
visible_to_subscription_ids = '{}';
|
||||||
|
|
||||||
|
for subscription_id, claims in (
|
||||||
|
select
|
||||||
|
subs.subscription_id,
|
||||||
|
subs.claims
|
||||||
|
from
|
||||||
|
unnest(subscriptions) subs
|
||||||
|
where
|
||||||
|
subs.entity = entity_
|
||||||
|
and subs.claims_role = working_role
|
||||||
|
and (
|
||||||
|
realtime.is_visible_through_filters(columns, subs.filters)
|
||||||
|
or (
|
||||||
|
action = 'DELETE'
|
||||||
|
and realtime.is_visible_through_filters(old_columns, subs.filters)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) loop
|
||||||
|
|
||||||
|
if not is_rls_enabled or action = 'DELETE' then
|
||||||
|
visible_to_subscription_ids = visible_to_subscription_ids || subscription_id;
|
||||||
|
else
|
||||||
|
-- Check if RLS allows the role to see the record
|
||||||
|
perform
|
||||||
|
-- Trim leading and trailing quotes from working_role because set_config
|
||||||
|
-- doesn't recognize the role as valid if they are included
|
||||||
|
set_config('role', trim(both '"' from working_role::text), true),
|
||||||
|
set_config('request.jwt.claims', claims::text, true);
|
||||||
|
|
||||||
|
execute 'execute walrus_rls_stmt' into subscription_has_access;
|
||||||
|
|
||||||
|
if subscription_has_access then
|
||||||
|
visible_to_subscription_ids = visible_to_subscription_ids || subscription_id;
|
||||||
|
end if;
|
||||||
|
end if;
|
||||||
|
end loop;
|
||||||
|
|
||||||
|
perform set_config('role', null, true);
|
||||||
|
|
||||||
|
return next (
|
||||||
|
output,
|
||||||
|
is_rls_enabled,
|
||||||
|
visible_to_subscription_ids,
|
||||||
|
case
|
||||||
|
when error_record_exceeds_max_size then array['Error 413: Payload Too Large']
|
||||||
|
else '{}'
|
||||||
|
end
|
||||||
|
)::realtime.wal_rls;
|
||||||
|
|
||||||
|
end if;
|
||||||
|
end loop;
|
||||||
|
|
||||||
|
perform set_config('role', null, true);
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION realtime.apply_rls(wal jsonb, max_record_bytes integer) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: broadcast_changes(text, text, text, text, text, record, record, text); Type: FUNCTION; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION realtime.broadcast_changes(topic_name text, event_name text, operation text, table_name text, table_schema text, new record, old record, level text DEFAULT 'ROW'::text) RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
-- Declare a variable to hold the JSONB representation of the row
|
||||||
|
row_data jsonb := '{}'::jsonb;
|
||||||
|
BEGIN
|
||||||
|
IF level = 'STATEMENT' THEN
|
||||||
|
RAISE EXCEPTION 'function can only be triggered for each row, not for each statement';
|
||||||
|
END IF;
|
||||||
|
-- Check the operation type and handle accordingly
|
||||||
|
IF operation = 'INSERT' OR operation = 'UPDATE' OR operation = 'DELETE' THEN
|
||||||
|
row_data := jsonb_build_object('old_record', OLD, 'record', NEW, 'operation', operation, 'table', table_name, 'schema', table_schema);
|
||||||
|
PERFORM realtime.send (row_data, event_name, topic_name);
|
||||||
|
ELSE
|
||||||
|
RAISE EXCEPTION 'Unexpected operation type: %', operation;
|
||||||
|
END IF;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
RAISE EXCEPTION 'Failed to process the row: %', SQLERRM;
|
||||||
|
END;
|
||||||
|
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION realtime.broadcast_changes(topic_name text, event_name text, operation text, table_name text, table_schema text, new record, old record, level text) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: build_prepared_statement_sql(text, regclass, realtime.wal_column[]); Type: FUNCTION; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION realtime.build_prepared_statement_sql(prepared_statement_name text, entity regclass, columns realtime.wal_column[]) RETURNS text
|
||||||
|
LANGUAGE sql
|
||||||
|
AS $$
|
||||||
|
/*
|
||||||
|
Builds a sql string that, if executed, creates a prepared statement to
|
||||||
|
tests retrive a row from *entity* by its primary key columns.
|
||||||
|
Example
|
||||||
|
select realtime.build_prepared_statement_sql('public.notes', '{"id"}'::text[], '{"bigint"}'::text[])
|
||||||
|
*/
|
||||||
|
select
|
||||||
|
'prepare ' || prepared_statement_name || ' as
|
||||||
|
select
|
||||||
|
exists(
|
||||||
|
select
|
||||||
|
1
|
||||||
|
from
|
||||||
|
' || entity || '
|
||||||
|
where
|
||||||
|
' || string_agg(quote_ident(pkc.name) || '=' || quote_nullable(pkc.value #>> '{}') , ' and ') || '
|
||||||
|
)'
|
||||||
|
from
|
||||||
|
unnest(columns) pkc
|
||||||
|
where
|
||||||
|
pkc.is_pkey
|
||||||
|
group by
|
||||||
|
entity
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION realtime.build_prepared_statement_sql(prepared_statement_name text, entity regclass, columns realtime.wal_column[]) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: cast(text, regtype); Type: FUNCTION; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION realtime."cast"(val text, type_ regtype) RETURNS jsonb
|
||||||
|
LANGUAGE plpgsql IMMUTABLE
|
||||||
|
AS $$
|
||||||
|
declare
|
||||||
|
res jsonb;
|
||||||
|
begin
|
||||||
|
execute format('select to_jsonb(%L::'|| type_::text || ')', val) into res;
|
||||||
|
return res;
|
||||||
|
end
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION realtime."cast"(val text, type_ regtype) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: check_equality_op(realtime.equality_op, regtype, text, text); Type: FUNCTION; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION realtime.check_equality_op(op realtime.equality_op, type_ regtype, val_1 text, val_2 text) RETURNS boolean
|
||||||
|
LANGUAGE plpgsql IMMUTABLE
|
||||||
|
AS $$
|
||||||
|
/*
|
||||||
|
Casts *val_1* and *val_2* as type *type_* and check the *op* condition for truthiness
|
||||||
|
*/
|
||||||
|
declare
|
||||||
|
op_symbol text = (
|
||||||
|
case
|
||||||
|
when op = 'eq' then '='
|
||||||
|
when op = 'neq' then '!='
|
||||||
|
when op = 'lt' then '<'
|
||||||
|
when op = 'lte' then '<='
|
||||||
|
when op = 'gt' then '>'
|
||||||
|
when op = 'gte' then '>='
|
||||||
|
when op = 'in' then '= any'
|
||||||
|
else 'UNKNOWN OP'
|
||||||
|
end
|
||||||
|
);
|
||||||
|
res boolean;
|
||||||
|
begin
|
||||||
|
execute format(
|
||||||
|
'select %L::'|| type_::text || ' ' || op_symbol
|
||||||
|
|| ' ( %L::'
|
||||||
|
|| (
|
||||||
|
case
|
||||||
|
when op = 'in' then type_::text || '[]'
|
||||||
|
else type_::text end
|
||||||
|
)
|
||||||
|
|| ')', val_1, val_2) into res;
|
||||||
|
return res;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION realtime.check_equality_op(op realtime.equality_op, type_ regtype, val_1 text, val_2 text) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: is_visible_through_filters(realtime.wal_column[], realtime.user_defined_filter[]); Type: FUNCTION; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION realtime.is_visible_through_filters(columns realtime.wal_column[], filters realtime.user_defined_filter[]) RETURNS boolean
|
||||||
|
LANGUAGE sql IMMUTABLE
|
||||||
|
AS $_$
|
||||||
|
/*
|
||||||
|
Should the record be visible (true) or filtered out (false) after *filters* are applied
|
||||||
|
*/
|
||||||
|
select
|
||||||
|
-- Default to allowed when no filters present
|
||||||
|
$2 is null -- no filters. this should not happen because subscriptions has a default
|
||||||
|
or array_length($2, 1) is null -- array length of an empty array is null
|
||||||
|
or bool_and(
|
||||||
|
coalesce(
|
||||||
|
realtime.check_equality_op(
|
||||||
|
op:=f.op,
|
||||||
|
type_:=coalesce(
|
||||||
|
col.type_oid::regtype, -- null when wal2json version <= 2.4
|
||||||
|
col.type_name::regtype
|
||||||
|
),
|
||||||
|
-- cast jsonb to text
|
||||||
|
val_1:=col.value #>> '{}',
|
||||||
|
val_2:=f.value
|
||||||
|
),
|
||||||
|
false -- if null, filter does not match
|
||||||
|
)
|
||||||
|
)
|
||||||
|
from
|
||||||
|
unnest(filters) f
|
||||||
|
join unnest(columns) col
|
||||||
|
on f.column_name = col.name;
|
||||||
|
$_$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION realtime.is_visible_through_filters(columns realtime.wal_column[], filters realtime.user_defined_filter[]) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: list_changes(name, name, integer, integer); Type: FUNCTION; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION realtime.list_changes(publication name, slot_name name, max_changes integer, max_record_bytes integer) RETURNS SETOF realtime.wal_rls
|
||||||
|
LANGUAGE sql
|
||||||
|
SET log_min_messages TO 'fatal'
|
||||||
|
AS $$
|
||||||
|
with pub as (
|
||||||
|
select
|
||||||
|
concat_ws(
|
||||||
|
',',
|
||||||
|
case when bool_or(pubinsert) then 'insert' else null end,
|
||||||
|
case when bool_or(pubupdate) then 'update' else null end,
|
||||||
|
case when bool_or(pubdelete) then 'delete' else null end
|
||||||
|
) as w2j_actions,
|
||||||
|
coalesce(
|
||||||
|
string_agg(
|
||||||
|
realtime.quote_wal2json(format('%I.%I', schemaname, tablename)::regclass),
|
||||||
|
','
|
||||||
|
) filter (where ppt.tablename is not null and ppt.tablename not like '% %'),
|
||||||
|
''
|
||||||
|
) w2j_add_tables
|
||||||
|
from
|
||||||
|
pg_publication pp
|
||||||
|
left join pg_publication_tables ppt
|
||||||
|
on pp.pubname = ppt.pubname
|
||||||
|
where
|
||||||
|
pp.pubname = publication
|
||||||
|
group by
|
||||||
|
pp.pubname
|
||||||
|
limit 1
|
||||||
|
),
|
||||||
|
w2j as (
|
||||||
|
select
|
||||||
|
x.*, pub.w2j_add_tables
|
||||||
|
from
|
||||||
|
pub,
|
||||||
|
pg_logical_slot_get_changes(
|
||||||
|
slot_name, null, max_changes,
|
||||||
|
'include-pk', 'true',
|
||||||
|
'include-transaction', 'false',
|
||||||
|
'include-timestamp', 'true',
|
||||||
|
'include-type-oids', 'true',
|
||||||
|
'format-version', '2',
|
||||||
|
'actions', pub.w2j_actions,
|
||||||
|
'add-tables', pub.w2j_add_tables
|
||||||
|
) x
|
||||||
|
)
|
||||||
|
select
|
||||||
|
xyz.wal,
|
||||||
|
xyz.is_rls_enabled,
|
||||||
|
xyz.subscription_ids,
|
||||||
|
xyz.errors
|
||||||
|
from
|
||||||
|
w2j,
|
||||||
|
realtime.apply_rls(
|
||||||
|
wal := w2j.data::jsonb,
|
||||||
|
max_record_bytes := max_record_bytes
|
||||||
|
) xyz(wal, is_rls_enabled, subscription_ids, errors)
|
||||||
|
where
|
||||||
|
w2j.w2j_add_tables <> ''
|
||||||
|
and xyz.subscription_ids[1] is not null
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION realtime.list_changes(publication name, slot_name name, max_changes integer, max_record_bytes integer) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: quote_wal2json(regclass); Type: FUNCTION; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION realtime.quote_wal2json(entity regclass) RETURNS text
|
||||||
|
LANGUAGE sql IMMUTABLE STRICT
|
||||||
|
AS $$
|
||||||
|
select
|
||||||
|
(
|
||||||
|
select string_agg('' || ch,'')
|
||||||
|
from unnest(string_to_array(nsp.nspname::text, null)) with ordinality x(ch, idx)
|
||||||
|
where
|
||||||
|
not (x.idx = 1 and x.ch = '"')
|
||||||
|
and not (
|
||||||
|
x.idx = array_length(string_to_array(nsp.nspname::text, null), 1)
|
||||||
|
and x.ch = '"'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|| '.'
|
||||||
|
|| (
|
||||||
|
select string_agg('' || ch,'')
|
||||||
|
from unnest(string_to_array(pc.relname::text, null)) with ordinality x(ch, idx)
|
||||||
|
where
|
||||||
|
not (x.idx = 1 and x.ch = '"')
|
||||||
|
and not (
|
||||||
|
x.idx = array_length(string_to_array(nsp.nspname::text, null), 1)
|
||||||
|
and x.ch = '"'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
from
|
||||||
|
pg_class pc
|
||||||
|
join pg_namespace nsp
|
||||||
|
on pc.relnamespace = nsp.oid
|
||||||
|
where
|
||||||
|
pc.oid = entity
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION realtime.quote_wal2json(entity regclass) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: send(jsonb, text, text, boolean); Type: FUNCTION; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION realtime.send(payload jsonb, event text, topic text, private boolean DEFAULT true) RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
generated_id uuid;
|
||||||
|
final_payload jsonb;
|
||||||
|
BEGIN
|
||||||
|
BEGIN
|
||||||
|
-- Generate a new UUID for the id
|
||||||
|
generated_id := gen_random_uuid();
|
||||||
|
|
||||||
|
-- Check if payload has an 'id' key, if not, add the generated UUID
|
||||||
|
IF payload ? 'id' THEN
|
||||||
|
final_payload := payload;
|
||||||
|
ELSE
|
||||||
|
final_payload := jsonb_set(payload, '{id}', to_jsonb(generated_id));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Set the topic configuration
|
||||||
|
EXECUTE format('SET LOCAL realtime.topic TO %L', topic);
|
||||||
|
|
||||||
|
-- Attempt to insert the message
|
||||||
|
INSERT INTO realtime.messages (id, payload, event, topic, private, extension)
|
||||||
|
VALUES (generated_id, final_payload, event, topic, private, 'broadcast');
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
-- Capture and notify the error
|
||||||
|
RAISE WARNING 'ErrorSendingBroadcastMessage: %', SQLERRM;
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION realtime.send(payload jsonb, event text, topic text, private boolean) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: subscription_check_filters(); Type: FUNCTION; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION realtime.subscription_check_filters() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
/*
|
||||||
|
Validates that the user defined filters for a subscription:
|
||||||
|
- refer to valid columns that the claimed role may access
|
||||||
|
- values are coercable to the correct column type
|
||||||
|
*/
|
||||||
|
declare
|
||||||
|
col_names text[] = coalesce(
|
||||||
|
array_agg(c.column_name order by c.ordinal_position),
|
||||||
|
'{}'::text[]
|
||||||
|
)
|
||||||
|
from
|
||||||
|
information_schema.columns c
|
||||||
|
where
|
||||||
|
format('%I.%I', c.table_schema, c.table_name)::regclass = new.entity
|
||||||
|
and pg_catalog.has_column_privilege(
|
||||||
|
(new.claims ->> 'role'),
|
||||||
|
format('%I.%I', c.table_schema, c.table_name)::regclass,
|
||||||
|
c.column_name,
|
||||||
|
'SELECT'
|
||||||
|
);
|
||||||
|
filter realtime.user_defined_filter;
|
||||||
|
col_type regtype;
|
||||||
|
|
||||||
|
in_val jsonb;
|
||||||
|
begin
|
||||||
|
for filter in select * from unnest(new.filters) loop
|
||||||
|
-- Filtered column is valid
|
||||||
|
if not filter.column_name = any(col_names) then
|
||||||
|
raise exception 'invalid column for filter %', filter.column_name;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- Type is sanitized and safe for string interpolation
|
||||||
|
col_type = (
|
||||||
|
select atttypid::regtype
|
||||||
|
from pg_catalog.pg_attribute
|
||||||
|
where attrelid = new.entity
|
||||||
|
and attname = filter.column_name
|
||||||
|
);
|
||||||
|
if col_type is null then
|
||||||
|
raise exception 'failed to lookup type for column %', filter.column_name;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- Set maximum number of entries for in filter
|
||||||
|
if filter.op = 'in'::realtime.equality_op then
|
||||||
|
in_val = realtime.cast(filter.value, (col_type::text || '[]')::regtype);
|
||||||
|
if coalesce(jsonb_array_length(in_val), 0) > 100 then
|
||||||
|
raise exception 'too many values for `in` filter. Maximum 100';
|
||||||
|
end if;
|
||||||
|
else
|
||||||
|
-- raises an exception if value is not coercable to type
|
||||||
|
perform realtime.cast(filter.value, col_type);
|
||||||
|
end if;
|
||||||
|
|
||||||
|
end loop;
|
||||||
|
|
||||||
|
-- Apply consistent order to filters so the unique constraint on
|
||||||
|
-- (subscription_id, entity, filters) can't be tricked by a different filter order
|
||||||
|
new.filters = coalesce(
|
||||||
|
array_agg(f order by f.column_name, f.op, f.value),
|
||||||
|
'{}'
|
||||||
|
) from unnest(new.filters) f;
|
||||||
|
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION realtime.subscription_check_filters() OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: to_regrole(text); Type: FUNCTION; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION realtime.to_regrole(role_name text) RETURNS regrole
|
||||||
|
LANGUAGE sql IMMUTABLE
|
||||||
|
AS $$ select role_name::regrole $$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION realtime.to_regrole(role_name text) OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: topic(); Type: FUNCTION; Schema: realtime; Owner: supabase_realtime_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION realtime.topic() RETURNS text
|
||||||
|
LANGUAGE sql STABLE
|
||||||
|
AS $$
|
||||||
|
select nullif(current_setting('realtime.topic', true), '')::text;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION realtime.topic() OWNER TO supabase_realtime_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: can_insert_object(text, text, uuid, jsonb); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
877
database-novo/schema/03_functions/storage.sql
Normal file
877
database-novo/schema/03_functions/storage.sql
Normal file
@@ -0,0 +1,877 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Functions — storage schema
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION storage.can_insert_object(bucketid text, name text, owner uuid, metadata jsonb) RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO "storage"."objects" ("bucket_id", "name", "owner", "metadata") VALUES (bucketid, name, owner, metadata);
|
||||||
|
-- hack to rollback the successful insert
|
||||||
|
RAISE sqlstate 'PT200' using
|
||||||
|
message = 'ROLLBACK',
|
||||||
|
detail = 'rollback successful insert';
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION storage.can_insert_object(bucketid text, name text, owner uuid, metadata jsonb) OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: enforce_bucket_name_length(); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION storage.enforce_bucket_name_length() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
begin
|
||||||
|
if length(new.name) > 100 then
|
||||||
|
raise exception 'bucket name "%" is too long (% characters). Max is 100.', new.name, length(new.name);
|
||||||
|
end if;
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION storage.enforce_bucket_name_length() OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: extension(text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION storage.extension(name text) RETURNS text
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
_parts text[];
|
||||||
|
_filename text;
|
||||||
|
BEGIN
|
||||||
|
select string_to_array(name, '/') into _parts;
|
||||||
|
select _parts[array_length(_parts,1)] into _filename;
|
||||||
|
-- @todo return the last part instead of 2
|
||||||
|
return reverse(split_part(reverse(_filename), '.', 1));
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION storage.extension(name text) OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: filename(text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION storage.filename(name text) RETURNS text
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
_parts text[];
|
||||||
|
BEGIN
|
||||||
|
select string_to_array(name, '/') into _parts;
|
||||||
|
return _parts[array_length(_parts,1)];
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION storage.filename(name text) OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: foldername(text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION storage.foldername(name text) RETURNS text[]
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
_parts text[];
|
||||||
|
BEGIN
|
||||||
|
select string_to_array(name, '/') into _parts;
|
||||||
|
return _parts[1:array_length(_parts,1)-1];
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION storage.foldername(name text) OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: get_common_prefix(text, text, text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION storage.get_common_prefix(p_key text, p_prefix text, p_delimiter text) RETURNS text
|
||||||
|
LANGUAGE sql IMMUTABLE
|
||||||
|
AS $$
|
||||||
|
SELECT CASE
|
||||||
|
WHEN position(p_delimiter IN substring(p_key FROM length(p_prefix) + 1)) > 0
|
||||||
|
THEN left(p_key, length(p_prefix) + position(p_delimiter IN substring(p_key FROM length(p_prefix) + 1)))
|
||||||
|
ELSE NULL
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION storage.get_common_prefix(p_key text, p_prefix text, p_delimiter text) OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: get_size_by_bucket(); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION storage.get_size_by_bucket() RETURNS TABLE(size bigint, bucket_id text)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
return query
|
||||||
|
select sum((metadata->>'size')::int) as size, obj.bucket_id
|
||||||
|
from "storage".objects as obj
|
||||||
|
group by obj.bucket_id;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION storage.get_size_by_bucket() OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: list_multipart_uploads_with_delimiter(text, text, text, integer, text, text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION storage.list_multipart_uploads_with_delimiter(bucket_id text, prefix_param text, delimiter_param text, max_keys integer DEFAULT 100, next_key_token text DEFAULT ''::text, next_upload_token text DEFAULT ''::text) RETURNS TABLE(key text, id text, created_at timestamp with time zone)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $_$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY EXECUTE
|
||||||
|
'SELECT DISTINCT ON(key COLLATE "C") * from (
|
||||||
|
SELECT
|
||||||
|
CASE
|
||||||
|
WHEN position($2 IN substring(key from length($1) + 1)) > 0 THEN
|
||||||
|
substring(key from 1 for length($1) + position($2 IN substring(key from length($1) + 1)))
|
||||||
|
ELSE
|
||||||
|
key
|
||||||
|
END AS key, id, created_at
|
||||||
|
FROM
|
||||||
|
storage.s3_multipart_uploads
|
||||||
|
WHERE
|
||||||
|
bucket_id = $5 AND
|
||||||
|
key ILIKE $1 || ''%'' AND
|
||||||
|
CASE
|
||||||
|
WHEN $4 != '''' AND $6 = '''' THEN
|
||||||
|
CASE
|
||||||
|
WHEN position($2 IN substring(key from length($1) + 1)) > 0 THEN
|
||||||
|
substring(key from 1 for length($1) + position($2 IN substring(key from length($1) + 1))) COLLATE "C" > $4
|
||||||
|
ELSE
|
||||||
|
key COLLATE "C" > $4
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
true
|
||||||
|
END AND
|
||||||
|
CASE
|
||||||
|
WHEN $6 != '''' THEN
|
||||||
|
id COLLATE "C" > $6
|
||||||
|
ELSE
|
||||||
|
true
|
||||||
|
END
|
||||||
|
ORDER BY
|
||||||
|
key COLLATE "C" ASC, created_at ASC) as e order by key COLLATE "C" LIMIT $3'
|
||||||
|
USING prefix_param, delimiter_param, max_keys, next_key_token, bucket_id, next_upload_token;
|
||||||
|
END;
|
||||||
|
$_$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION storage.list_multipart_uploads_with_delimiter(bucket_id text, prefix_param text, delimiter_param text, max_keys integer, next_key_token text, next_upload_token text) OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: list_objects_with_delimiter(text, text, text, integer, text, text, text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION storage.list_objects_with_delimiter(_bucket_id text, prefix_param text, delimiter_param text, max_keys integer DEFAULT 100, start_after text DEFAULT ''::text, next_token text DEFAULT ''::text, sort_order text DEFAULT 'asc'::text) RETURNS TABLE(name text, id uuid, metadata jsonb, updated_at timestamp with time zone, created_at timestamp with time zone, last_accessed_at timestamp with time zone)
|
||||||
|
LANGUAGE plpgsql STABLE
|
||||||
|
AS $_$
|
||||||
|
DECLARE
|
||||||
|
v_peek_name TEXT;
|
||||||
|
v_current RECORD;
|
||||||
|
v_common_prefix TEXT;
|
||||||
|
|
||||||
|
-- Configuration
|
||||||
|
v_is_asc BOOLEAN;
|
||||||
|
v_prefix TEXT;
|
||||||
|
v_start TEXT;
|
||||||
|
v_upper_bound TEXT;
|
||||||
|
v_file_batch_size INT;
|
||||||
|
|
||||||
|
-- Seek state
|
||||||
|
v_next_seek TEXT;
|
||||||
|
v_count INT := 0;
|
||||||
|
|
||||||
|
-- Dynamic SQL for batch query only
|
||||||
|
v_batch_query TEXT;
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
-- ========================================================================
|
||||||
|
-- INITIALIZATION
|
||||||
|
-- ========================================================================
|
||||||
|
v_is_asc := lower(coalesce(sort_order, 'asc')) = 'asc';
|
||||||
|
v_prefix := coalesce(prefix_param, '');
|
||||||
|
v_start := CASE WHEN coalesce(next_token, '') <> '' THEN next_token ELSE coalesce(start_after, '') END;
|
||||||
|
v_file_batch_size := LEAST(GREATEST(max_keys * 2, 100), 1000);
|
||||||
|
|
||||||
|
-- Calculate upper bound for prefix filtering (bytewise, using COLLATE "C")
|
||||||
|
IF v_prefix = '' THEN
|
||||||
|
v_upper_bound := NULL;
|
||||||
|
ELSIF right(v_prefix, 1) = delimiter_param THEN
|
||||||
|
v_upper_bound := left(v_prefix, -1) || chr(ascii(delimiter_param) + 1);
|
||||||
|
ELSE
|
||||||
|
v_upper_bound := left(v_prefix, -1) || chr(ascii(right(v_prefix, 1)) + 1);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Build batch query (dynamic SQL - called infrequently, amortized over many rows)
|
||||||
|
IF v_is_asc THEN
|
||||||
|
IF v_upper_bound IS NOT NULL THEN
|
||||||
|
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
||||||
|
'FROM storage.objects o WHERE o.bucket_id = $1 AND o.name COLLATE "C" >= $2 ' ||
|
||||||
|
'AND o.name COLLATE "C" < $3 ORDER BY o.name COLLATE "C" ASC LIMIT $4';
|
||||||
|
ELSE
|
||||||
|
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
||||||
|
'FROM storage.objects o WHERE o.bucket_id = $1 AND o.name COLLATE "C" >= $2 ' ||
|
||||||
|
'ORDER BY o.name COLLATE "C" ASC LIMIT $4';
|
||||||
|
END IF;
|
||||||
|
ELSE
|
||||||
|
IF v_upper_bound IS NOT NULL THEN
|
||||||
|
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
||||||
|
'FROM storage.objects o WHERE o.bucket_id = $1 AND o.name COLLATE "C" < $2 ' ||
|
||||||
|
'AND o.name COLLATE "C" >= $3 ORDER BY o.name COLLATE "C" DESC LIMIT $4';
|
||||||
|
ELSE
|
||||||
|
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
||||||
|
'FROM storage.objects o WHERE o.bucket_id = $1 AND o.name COLLATE "C" < $2 ' ||
|
||||||
|
'ORDER BY o.name COLLATE "C" DESC LIMIT $4';
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ========================================================================
|
||||||
|
-- SEEK INITIALIZATION: Determine starting position
|
||||||
|
-- ========================================================================
|
||||||
|
IF v_start = '' THEN
|
||||||
|
IF v_is_asc THEN
|
||||||
|
v_next_seek := v_prefix;
|
||||||
|
ELSE
|
||||||
|
-- DESC without cursor: find the last item in range
|
||||||
|
IF v_upper_bound IS NOT NULL THEN
|
||||||
|
SELECT o.name INTO v_next_seek FROM storage.objects o
|
||||||
|
WHERE o.bucket_id = _bucket_id AND o.name COLLATE "C" >= v_prefix AND o.name COLLATE "C" < v_upper_bound
|
||||||
|
ORDER BY o.name COLLATE "C" DESC LIMIT 1;
|
||||||
|
ELSIF v_prefix <> '' THEN
|
||||||
|
SELECT o.name INTO v_next_seek FROM storage.objects o
|
||||||
|
WHERE o.bucket_id = _bucket_id AND o.name COLLATE "C" >= v_prefix
|
||||||
|
ORDER BY o.name COLLATE "C" DESC LIMIT 1;
|
||||||
|
ELSE
|
||||||
|
SELECT o.name INTO v_next_seek FROM storage.objects o
|
||||||
|
WHERE o.bucket_id = _bucket_id
|
||||||
|
ORDER BY o.name COLLATE "C" DESC LIMIT 1;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_next_seek IS NOT NULL THEN
|
||||||
|
v_next_seek := v_next_seek || delimiter_param;
|
||||||
|
ELSE
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
ELSE
|
||||||
|
-- Cursor provided: determine if it refers to a folder or leaf
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM storage.objects o
|
||||||
|
WHERE o.bucket_id = _bucket_id
|
||||||
|
AND o.name COLLATE "C" LIKE v_start || delimiter_param || '%'
|
||||||
|
LIMIT 1
|
||||||
|
) THEN
|
||||||
|
-- Cursor refers to a folder
|
||||||
|
IF v_is_asc THEN
|
||||||
|
v_next_seek := v_start || chr(ascii(delimiter_param) + 1);
|
||||||
|
ELSE
|
||||||
|
v_next_seek := v_start || delimiter_param;
|
||||||
|
END IF;
|
||||||
|
ELSE
|
||||||
|
-- Cursor refers to a leaf object
|
||||||
|
IF v_is_asc THEN
|
||||||
|
v_next_seek := v_start || delimiter_param;
|
||||||
|
ELSE
|
||||||
|
v_next_seek := v_start;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ========================================================================
|
||||||
|
-- MAIN LOOP: Hybrid peek-then-batch algorithm
|
||||||
|
-- Uses STATIC SQL for peek (hot path) and DYNAMIC SQL for batch
|
||||||
|
-- ========================================================================
|
||||||
|
LOOP
|
||||||
|
EXIT WHEN v_count >= max_keys;
|
||||||
|
|
||||||
|
-- STEP 1: PEEK using STATIC SQL (plan cached, very fast)
|
||||||
|
IF v_is_asc THEN
|
||||||
|
IF v_upper_bound IS NOT NULL THEN
|
||||||
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||||
|
WHERE o.bucket_id = _bucket_id AND o.name COLLATE "C" >= v_next_seek AND o.name COLLATE "C" < v_upper_bound
|
||||||
|
ORDER BY o.name COLLATE "C" ASC LIMIT 1;
|
||||||
|
ELSE
|
||||||
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||||
|
WHERE o.bucket_id = _bucket_id AND o.name COLLATE "C" >= v_next_seek
|
||||||
|
ORDER BY o.name COLLATE "C" ASC LIMIT 1;
|
||||||
|
END IF;
|
||||||
|
ELSE
|
||||||
|
IF v_upper_bound IS NOT NULL THEN
|
||||||
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||||
|
WHERE o.bucket_id = _bucket_id AND o.name COLLATE "C" < v_next_seek AND o.name COLLATE "C" >= v_prefix
|
||||||
|
ORDER BY o.name COLLATE "C" DESC LIMIT 1;
|
||||||
|
ELSIF v_prefix <> '' THEN
|
||||||
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||||
|
WHERE o.bucket_id = _bucket_id AND o.name COLLATE "C" < v_next_seek AND o.name COLLATE "C" >= v_prefix
|
||||||
|
ORDER BY o.name COLLATE "C" DESC LIMIT 1;
|
||||||
|
ELSE
|
||||||
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||||
|
WHERE o.bucket_id = _bucket_id AND o.name COLLATE "C" < v_next_seek
|
||||||
|
ORDER BY o.name COLLATE "C" DESC LIMIT 1;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
EXIT WHEN v_peek_name IS NULL;
|
||||||
|
|
||||||
|
-- STEP 2: Check if this is a FOLDER or FILE
|
||||||
|
v_common_prefix := storage.get_common_prefix(v_peek_name, v_prefix, delimiter_param);
|
||||||
|
|
||||||
|
IF v_common_prefix IS NOT NULL THEN
|
||||||
|
-- FOLDER: Emit and skip to next folder (no heap access needed)
|
||||||
|
name := rtrim(v_common_prefix, delimiter_param);
|
||||||
|
id := NULL;
|
||||||
|
updated_at := NULL;
|
||||||
|
created_at := NULL;
|
||||||
|
last_accessed_at := NULL;
|
||||||
|
metadata := NULL;
|
||||||
|
RETURN NEXT;
|
||||||
|
v_count := v_count + 1;
|
||||||
|
|
||||||
|
-- Advance seek past the folder range
|
||||||
|
IF v_is_asc THEN
|
||||||
|
v_next_seek := left(v_common_prefix, -1) || chr(ascii(delimiter_param) + 1);
|
||||||
|
ELSE
|
||||||
|
v_next_seek := v_common_prefix;
|
||||||
|
END IF;
|
||||||
|
ELSE
|
||||||
|
-- FILE: Batch fetch using DYNAMIC SQL (overhead amortized over many rows)
|
||||||
|
-- For ASC: upper_bound is the exclusive upper limit (< condition)
|
||||||
|
-- For DESC: prefix is the inclusive lower limit (>= condition)
|
||||||
|
FOR v_current IN EXECUTE v_batch_query USING _bucket_id, v_next_seek,
|
||||||
|
CASE WHEN v_is_asc THEN COALESCE(v_upper_bound, v_prefix) ELSE v_prefix END, v_file_batch_size
|
||||||
|
LOOP
|
||||||
|
v_common_prefix := storage.get_common_prefix(v_current.name, v_prefix, delimiter_param);
|
||||||
|
|
||||||
|
IF v_common_prefix IS NOT NULL THEN
|
||||||
|
-- Hit a folder: exit batch, let peek handle it
|
||||||
|
v_next_seek := v_current.name;
|
||||||
|
EXIT;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Emit file
|
||||||
|
name := v_current.name;
|
||||||
|
id := v_current.id;
|
||||||
|
updated_at := v_current.updated_at;
|
||||||
|
created_at := v_current.created_at;
|
||||||
|
last_accessed_at := v_current.last_accessed_at;
|
||||||
|
metadata := v_current.metadata;
|
||||||
|
RETURN NEXT;
|
||||||
|
v_count := v_count + 1;
|
||||||
|
|
||||||
|
-- Advance seek past this file
|
||||||
|
IF v_is_asc THEN
|
||||||
|
v_next_seek := v_current.name || delimiter_param;
|
||||||
|
ELSE
|
||||||
|
v_next_seek := v_current.name;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
EXIT WHEN v_count >= max_keys;
|
||||||
|
END LOOP;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$_$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION storage.list_objects_with_delimiter(_bucket_id text, prefix_param text, delimiter_param text, max_keys integer, start_after text, next_token text, sort_order text) OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: operation(); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION storage.operation() RETURNS text
|
||||||
|
LANGUAGE plpgsql STABLE
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN current_setting('storage.operation', true);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION storage.operation() OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: protect_delete(); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION storage.protect_delete() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Check if storage.allow_delete_query is set to 'true'
|
||||||
|
IF COALESCE(current_setting('storage.allow_delete_query', true), 'false') != 'true' THEN
|
||||||
|
RAISE EXCEPTION 'Direct deletion from storage tables is not allowed. Use the Storage API instead.'
|
||||||
|
USING HINT = 'This prevents accidental data loss from orphaned objects.',
|
||||||
|
ERRCODE = '42501';
|
||||||
|
END IF;
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION storage.protect_delete() OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: search(text, text, integer, integer, integer, text, text, text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION storage.search(prefix text, bucketname text, limits integer DEFAULT 100, levels integer DEFAULT 1, offsets integer DEFAULT 0, search text DEFAULT ''::text, sortcolumn text DEFAULT 'name'::text, sortorder text DEFAULT 'asc'::text) RETURNS TABLE(name text, id uuid, updated_at timestamp with time zone, created_at timestamp with time zone, last_accessed_at timestamp with time zone, metadata jsonb)
|
||||||
|
LANGUAGE plpgsql STABLE
|
||||||
|
AS $_$
|
||||||
|
DECLARE
|
||||||
|
v_peek_name TEXT;
|
||||||
|
v_current RECORD;
|
||||||
|
v_common_prefix TEXT;
|
||||||
|
v_delimiter CONSTANT TEXT := '/';
|
||||||
|
|
||||||
|
-- Configuration
|
||||||
|
v_limit INT;
|
||||||
|
v_prefix TEXT;
|
||||||
|
v_prefix_lower TEXT;
|
||||||
|
v_is_asc BOOLEAN;
|
||||||
|
v_order_by TEXT;
|
||||||
|
v_sort_order TEXT;
|
||||||
|
v_upper_bound TEXT;
|
||||||
|
v_file_batch_size INT;
|
||||||
|
|
||||||
|
-- Dynamic SQL for batch query only
|
||||||
|
v_batch_query TEXT;
|
||||||
|
|
||||||
|
-- Seek state
|
||||||
|
v_next_seek TEXT;
|
||||||
|
v_count INT := 0;
|
||||||
|
v_skipped INT := 0;
|
||||||
|
BEGIN
|
||||||
|
-- ========================================================================
|
||||||
|
-- INITIALIZATION
|
||||||
|
-- ========================================================================
|
||||||
|
v_limit := LEAST(coalesce(limits, 100), 1500);
|
||||||
|
v_prefix := coalesce(prefix, '') || coalesce(search, '');
|
||||||
|
v_prefix_lower := lower(v_prefix);
|
||||||
|
v_is_asc := lower(coalesce(sortorder, 'asc')) = 'asc';
|
||||||
|
v_file_batch_size := LEAST(GREATEST(v_limit * 2, 100), 1000);
|
||||||
|
|
||||||
|
-- Validate sort column
|
||||||
|
CASE lower(coalesce(sortcolumn, 'name'))
|
||||||
|
WHEN 'name' THEN v_order_by := 'name';
|
||||||
|
WHEN 'updated_at' THEN v_order_by := 'updated_at';
|
||||||
|
WHEN 'created_at' THEN v_order_by := 'created_at';
|
||||||
|
WHEN 'last_accessed_at' THEN v_order_by := 'last_accessed_at';
|
||||||
|
ELSE v_order_by := 'name';
|
||||||
|
END CASE;
|
||||||
|
|
||||||
|
v_sort_order := CASE WHEN v_is_asc THEN 'asc' ELSE 'desc' END;
|
||||||
|
|
||||||
|
-- ========================================================================
|
||||||
|
-- NON-NAME SORTING: Use path_tokens approach (unchanged)
|
||||||
|
-- ========================================================================
|
||||||
|
IF v_order_by != 'name' THEN
|
||||||
|
RETURN QUERY EXECUTE format(
|
||||||
|
$sql$
|
||||||
|
WITH folders AS (
|
||||||
|
SELECT path_tokens[$1] AS folder
|
||||||
|
FROM storage.objects
|
||||||
|
WHERE objects.name ILIKE $2 || '%%'
|
||||||
|
AND bucket_id = $3
|
||||||
|
AND array_length(objects.path_tokens, 1) <> $1
|
||||||
|
GROUP BY folder
|
||||||
|
ORDER BY folder %s
|
||||||
|
)
|
||||||
|
(SELECT folder AS "name",
|
||||||
|
NULL::uuid AS id,
|
||||||
|
NULL::timestamptz AS updated_at,
|
||||||
|
NULL::timestamptz AS created_at,
|
||||||
|
NULL::timestamptz AS last_accessed_at,
|
||||||
|
NULL::jsonb AS metadata FROM folders)
|
||||||
|
UNION ALL
|
||||||
|
(SELECT path_tokens[$1] AS "name",
|
||||||
|
id, updated_at, created_at, last_accessed_at, metadata
|
||||||
|
FROM storage.objects
|
||||||
|
WHERE objects.name ILIKE $2 || '%%'
|
||||||
|
AND bucket_id = $3
|
||||||
|
AND array_length(objects.path_tokens, 1) = $1
|
||||||
|
ORDER BY %I %s)
|
||||||
|
LIMIT $4 OFFSET $5
|
||||||
|
$sql$, v_sort_order, v_order_by, v_sort_order
|
||||||
|
) USING levels, v_prefix, bucketname, v_limit, offsets;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ========================================================================
|
||||||
|
-- NAME SORTING: Hybrid skip-scan with batch optimization
|
||||||
|
-- ========================================================================
|
||||||
|
|
||||||
|
-- Calculate upper bound for prefix filtering
|
||||||
|
IF v_prefix_lower = '' THEN
|
||||||
|
v_upper_bound := NULL;
|
||||||
|
ELSIF right(v_prefix_lower, 1) = v_delimiter THEN
|
||||||
|
v_upper_bound := left(v_prefix_lower, -1) || chr(ascii(v_delimiter) + 1);
|
||||||
|
ELSE
|
||||||
|
v_upper_bound := left(v_prefix_lower, -1) || chr(ascii(right(v_prefix_lower, 1)) + 1);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Build batch query (dynamic SQL - called infrequently, amortized over many rows)
|
||||||
|
IF v_is_asc THEN
|
||||||
|
IF v_upper_bound IS NOT NULL THEN
|
||||||
|
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
||||||
|
'FROM storage.objects o WHERE o.bucket_id = $1 AND lower(o.name) COLLATE "C" >= $2 ' ||
|
||||||
|
'AND lower(o.name) COLLATE "C" < $3 ORDER BY lower(o.name) COLLATE "C" ASC LIMIT $4';
|
||||||
|
ELSE
|
||||||
|
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
||||||
|
'FROM storage.objects o WHERE o.bucket_id = $1 AND lower(o.name) COLLATE "C" >= $2 ' ||
|
||||||
|
'ORDER BY lower(o.name) COLLATE "C" ASC LIMIT $4';
|
||||||
|
END IF;
|
||||||
|
ELSE
|
||||||
|
IF v_upper_bound IS NOT NULL THEN
|
||||||
|
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
||||||
|
'FROM storage.objects o WHERE o.bucket_id = $1 AND lower(o.name) COLLATE "C" < $2 ' ||
|
||||||
|
'AND lower(o.name) COLLATE "C" >= $3 ORDER BY lower(o.name) COLLATE "C" DESC LIMIT $4';
|
||||||
|
ELSE
|
||||||
|
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
||||||
|
'FROM storage.objects o WHERE o.bucket_id = $1 AND lower(o.name) COLLATE "C" < $2 ' ||
|
||||||
|
'ORDER BY lower(o.name) COLLATE "C" DESC LIMIT $4';
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Initialize seek position
|
||||||
|
IF v_is_asc THEN
|
||||||
|
v_next_seek := v_prefix_lower;
|
||||||
|
ELSE
|
||||||
|
-- DESC: find the last item in range first (static SQL)
|
||||||
|
IF v_upper_bound IS NOT NULL THEN
|
||||||
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||||
|
WHERE o.bucket_id = bucketname AND lower(o.name) COLLATE "C" >= v_prefix_lower AND lower(o.name) COLLATE "C" < v_upper_bound
|
||||||
|
ORDER BY lower(o.name) COLLATE "C" DESC LIMIT 1;
|
||||||
|
ELSIF v_prefix_lower <> '' THEN
|
||||||
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||||
|
WHERE o.bucket_id = bucketname AND lower(o.name) COLLATE "C" >= v_prefix_lower
|
||||||
|
ORDER BY lower(o.name) COLLATE "C" DESC LIMIT 1;
|
||||||
|
ELSE
|
||||||
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||||
|
WHERE o.bucket_id = bucketname
|
||||||
|
ORDER BY lower(o.name) COLLATE "C" DESC LIMIT 1;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_peek_name IS NOT NULL THEN
|
||||||
|
v_next_seek := lower(v_peek_name) || v_delimiter;
|
||||||
|
ELSE
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ========================================================================
|
||||||
|
-- MAIN LOOP: Hybrid peek-then-batch algorithm
|
||||||
|
-- Uses STATIC SQL for peek (hot path) and DYNAMIC SQL for batch
|
||||||
|
-- ========================================================================
|
||||||
|
LOOP
|
||||||
|
EXIT WHEN v_count >= v_limit;
|
||||||
|
|
||||||
|
-- STEP 1: PEEK using STATIC SQL (plan cached, very fast)
|
||||||
|
IF v_is_asc THEN
|
||||||
|
IF v_upper_bound IS NOT NULL THEN
|
||||||
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||||
|
WHERE o.bucket_id = bucketname AND lower(o.name) COLLATE "C" >= v_next_seek AND lower(o.name) COLLATE "C" < v_upper_bound
|
||||||
|
ORDER BY lower(o.name) COLLATE "C" ASC LIMIT 1;
|
||||||
|
ELSE
|
||||||
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||||
|
WHERE o.bucket_id = bucketname AND lower(o.name) COLLATE "C" >= v_next_seek
|
||||||
|
ORDER BY lower(o.name) COLLATE "C" ASC LIMIT 1;
|
||||||
|
END IF;
|
||||||
|
ELSE
|
||||||
|
IF v_upper_bound IS NOT NULL THEN
|
||||||
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||||
|
WHERE o.bucket_id = bucketname AND lower(o.name) COLLATE "C" < v_next_seek AND lower(o.name) COLLATE "C" >= v_prefix_lower
|
||||||
|
ORDER BY lower(o.name) COLLATE "C" DESC LIMIT 1;
|
||||||
|
ELSIF v_prefix_lower <> '' THEN
|
||||||
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||||
|
WHERE o.bucket_id = bucketname AND lower(o.name) COLLATE "C" < v_next_seek AND lower(o.name) COLLATE "C" >= v_prefix_lower
|
||||||
|
ORDER BY lower(o.name) COLLATE "C" DESC LIMIT 1;
|
||||||
|
ELSE
|
||||||
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||||
|
WHERE o.bucket_id = bucketname AND lower(o.name) COLLATE "C" < v_next_seek
|
||||||
|
ORDER BY lower(o.name) COLLATE "C" DESC LIMIT 1;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
EXIT WHEN v_peek_name IS NULL;
|
||||||
|
|
||||||
|
-- STEP 2: Check if this is a FOLDER or FILE
|
||||||
|
v_common_prefix := storage.get_common_prefix(lower(v_peek_name), v_prefix_lower, v_delimiter);
|
||||||
|
|
||||||
|
IF v_common_prefix IS NOT NULL THEN
|
||||||
|
-- FOLDER: Handle offset, emit if needed, skip to next folder
|
||||||
|
IF v_skipped < offsets THEN
|
||||||
|
v_skipped := v_skipped + 1;
|
||||||
|
ELSE
|
||||||
|
name := split_part(rtrim(v_common_prefix, v_delimiter), v_delimiter, levels);
|
||||||
|
id := NULL;
|
||||||
|
updated_at := NULL;
|
||||||
|
created_at := NULL;
|
||||||
|
last_accessed_at := NULL;
|
||||||
|
metadata := NULL;
|
||||||
|
RETURN NEXT;
|
||||||
|
v_count := v_count + 1;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Advance seek past the folder range
|
||||||
|
IF v_is_asc THEN
|
||||||
|
v_next_seek := lower(left(v_common_prefix, -1)) || chr(ascii(v_delimiter) + 1);
|
||||||
|
ELSE
|
||||||
|
v_next_seek := lower(v_common_prefix);
|
||||||
|
END IF;
|
||||||
|
ELSE
|
||||||
|
-- FILE: Batch fetch using DYNAMIC SQL (overhead amortized over many rows)
|
||||||
|
-- For ASC: upper_bound is the exclusive upper limit (< condition)
|
||||||
|
-- For DESC: prefix_lower is the inclusive lower limit (>= condition)
|
||||||
|
FOR v_current IN EXECUTE v_batch_query
|
||||||
|
USING bucketname, v_next_seek,
|
||||||
|
CASE WHEN v_is_asc THEN COALESCE(v_upper_bound, v_prefix_lower) ELSE v_prefix_lower END, v_file_batch_size
|
||||||
|
LOOP
|
||||||
|
v_common_prefix := storage.get_common_prefix(lower(v_current.name), v_prefix_lower, v_delimiter);
|
||||||
|
|
||||||
|
IF v_common_prefix IS NOT NULL THEN
|
||||||
|
-- Hit a folder: exit batch, let peek handle it
|
||||||
|
v_next_seek := lower(v_current.name);
|
||||||
|
EXIT;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Handle offset skipping
|
||||||
|
IF v_skipped < offsets THEN
|
||||||
|
v_skipped := v_skipped + 1;
|
||||||
|
ELSE
|
||||||
|
-- Emit file
|
||||||
|
name := split_part(v_current.name, v_delimiter, levels);
|
||||||
|
id := v_current.id;
|
||||||
|
updated_at := v_current.updated_at;
|
||||||
|
created_at := v_current.created_at;
|
||||||
|
last_accessed_at := v_current.last_accessed_at;
|
||||||
|
metadata := v_current.metadata;
|
||||||
|
RETURN NEXT;
|
||||||
|
v_count := v_count + 1;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Advance seek past this file
|
||||||
|
IF v_is_asc THEN
|
||||||
|
v_next_seek := lower(v_current.name) || v_delimiter;
|
||||||
|
ELSE
|
||||||
|
v_next_seek := lower(v_current.name);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
EXIT WHEN v_count >= v_limit;
|
||||||
|
END LOOP;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$_$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION storage.search(prefix text, bucketname text, limits integer, levels integer, offsets integer, search text, sortcolumn text, sortorder text) OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: search_by_timestamp(text, text, integer, integer, text, text, text, text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION storage.search_by_timestamp(p_prefix text, p_bucket_id text, p_limit integer, p_level integer, p_start_after text, p_sort_order text, p_sort_column text, p_sort_column_after text) RETURNS TABLE(key text, name text, id uuid, updated_at timestamp with time zone, created_at timestamp with time zone, last_accessed_at timestamp with time zone, metadata jsonb)
|
||||||
|
LANGUAGE plpgsql STABLE
|
||||||
|
AS $_$
|
||||||
|
DECLARE
|
||||||
|
v_cursor_op text;
|
||||||
|
v_query text;
|
||||||
|
v_prefix text;
|
||||||
|
BEGIN
|
||||||
|
v_prefix := coalesce(p_prefix, '');
|
||||||
|
|
||||||
|
IF p_sort_order = 'asc' THEN
|
||||||
|
v_cursor_op := '>';
|
||||||
|
ELSE
|
||||||
|
v_cursor_op := '<';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
v_query := format($sql$
|
||||||
|
WITH raw_objects AS (
|
||||||
|
SELECT
|
||||||
|
o.name AS obj_name,
|
||||||
|
o.id AS obj_id,
|
||||||
|
o.updated_at AS obj_updated_at,
|
||||||
|
o.created_at AS obj_created_at,
|
||||||
|
o.last_accessed_at AS obj_last_accessed_at,
|
||||||
|
o.metadata AS obj_metadata,
|
||||||
|
storage.get_common_prefix(o.name, $1, '/') AS common_prefix
|
||||||
|
FROM storage.objects o
|
||||||
|
WHERE o.bucket_id = $2
|
||||||
|
AND o.name COLLATE "C" LIKE $1 || '%%'
|
||||||
|
),
|
||||||
|
-- Aggregate common prefixes (folders)
|
||||||
|
-- Both created_at and updated_at use MIN(obj_created_at) to match the old prefixes table behavior
|
||||||
|
aggregated_prefixes AS (
|
||||||
|
SELECT
|
||||||
|
rtrim(common_prefix, '/') AS name,
|
||||||
|
NULL::uuid AS id,
|
||||||
|
MIN(obj_created_at) AS updated_at,
|
||||||
|
MIN(obj_created_at) AS created_at,
|
||||||
|
NULL::timestamptz AS last_accessed_at,
|
||||||
|
NULL::jsonb AS metadata,
|
||||||
|
TRUE AS is_prefix
|
||||||
|
FROM raw_objects
|
||||||
|
WHERE common_prefix IS NOT NULL
|
||||||
|
GROUP BY common_prefix
|
||||||
|
),
|
||||||
|
leaf_objects AS (
|
||||||
|
SELECT
|
||||||
|
obj_name AS name,
|
||||||
|
obj_id AS id,
|
||||||
|
obj_updated_at AS updated_at,
|
||||||
|
obj_created_at AS created_at,
|
||||||
|
obj_last_accessed_at AS last_accessed_at,
|
||||||
|
obj_metadata AS metadata,
|
||||||
|
FALSE AS is_prefix
|
||||||
|
FROM raw_objects
|
||||||
|
WHERE common_prefix IS NULL
|
||||||
|
),
|
||||||
|
combined AS (
|
||||||
|
SELECT * FROM aggregated_prefixes
|
||||||
|
UNION ALL
|
||||||
|
SELECT * FROM leaf_objects
|
||||||
|
),
|
||||||
|
filtered AS (
|
||||||
|
SELECT *
|
||||||
|
FROM combined
|
||||||
|
WHERE (
|
||||||
|
$5 = ''
|
||||||
|
OR ROW(
|
||||||
|
date_trunc('milliseconds', %I),
|
||||||
|
name COLLATE "C"
|
||||||
|
) %s ROW(
|
||||||
|
COALESCE(NULLIF($6, '')::timestamptz, 'epoch'::timestamptz),
|
||||||
|
$5
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
split_part(name, '/', $3) AS key,
|
||||||
|
name,
|
||||||
|
id,
|
||||||
|
updated_at,
|
||||||
|
created_at,
|
||||||
|
last_accessed_at,
|
||||||
|
metadata
|
||||||
|
FROM filtered
|
||||||
|
ORDER BY
|
||||||
|
COALESCE(date_trunc('milliseconds', %I), 'epoch'::timestamptz) %s,
|
||||||
|
name COLLATE "C" %s
|
||||||
|
LIMIT $4
|
||||||
|
$sql$,
|
||||||
|
p_sort_column,
|
||||||
|
v_cursor_op,
|
||||||
|
p_sort_column,
|
||||||
|
p_sort_order,
|
||||||
|
p_sort_order
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN QUERY EXECUTE v_query
|
||||||
|
USING v_prefix, p_bucket_id, p_level, p_limit, p_start_after, p_sort_column_after;
|
||||||
|
END;
|
||||||
|
$_$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION storage.search_by_timestamp(p_prefix text, p_bucket_id text, p_limit integer, p_level integer, p_start_after text, p_sort_order text, p_sort_column text, p_sort_column_after text) OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: search_v2(text, text, integer, integer, text, text, text, text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION storage.search_v2(prefix text, bucket_name text, limits integer DEFAULT 100, levels integer DEFAULT 1, start_after text DEFAULT ''::text, sort_order text DEFAULT 'asc'::text, sort_column text DEFAULT 'name'::text, sort_column_after text DEFAULT ''::text) RETURNS TABLE(key text, name text, id uuid, updated_at timestamp with time zone, created_at timestamp with time zone, last_accessed_at timestamp with time zone, metadata jsonb)
|
||||||
|
LANGUAGE plpgsql STABLE
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_sort_col text;
|
||||||
|
v_sort_ord text;
|
||||||
|
v_limit int;
|
||||||
|
BEGIN
|
||||||
|
-- Cap limit to maximum of 1500 records
|
||||||
|
v_limit := LEAST(coalesce(limits, 100), 1500);
|
||||||
|
|
||||||
|
-- Validate and normalize sort_order
|
||||||
|
v_sort_ord := lower(coalesce(sort_order, 'asc'));
|
||||||
|
IF v_sort_ord NOT IN ('asc', 'desc') THEN
|
||||||
|
v_sort_ord := 'asc';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Validate and normalize sort_column
|
||||||
|
v_sort_col := lower(coalesce(sort_column, 'name'));
|
||||||
|
IF v_sort_col NOT IN ('name', 'updated_at', 'created_at') THEN
|
||||||
|
v_sort_col := 'name';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Route to appropriate implementation
|
||||||
|
IF v_sort_col = 'name' THEN
|
||||||
|
-- Use list_objects_with_delimiter for name sorting (most efficient: O(k * log n))
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
split_part(l.name, '/', levels) AS key,
|
||||||
|
l.name AS name,
|
||||||
|
l.id,
|
||||||
|
l.updated_at,
|
||||||
|
l.created_at,
|
||||||
|
l.last_accessed_at,
|
||||||
|
l.metadata
|
||||||
|
FROM storage.list_objects_with_delimiter(
|
||||||
|
bucket_name,
|
||||||
|
coalesce(prefix, ''),
|
||||||
|
'/',
|
||||||
|
v_limit,
|
||||||
|
start_after,
|
||||||
|
'',
|
||||||
|
v_sort_ord
|
||||||
|
) l;
|
||||||
|
ELSE
|
||||||
|
-- Use aggregation approach for timestamp sorting
|
||||||
|
-- Not efficient for large datasets but supports correct pagination
|
||||||
|
RETURN QUERY SELECT * FROM storage.search_by_timestamp(
|
||||||
|
prefix, bucket_name, v_limit, levels, start_after,
|
||||||
|
v_sort_ord, v_sort_col, sort_column_after
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION storage.search_v2(prefix text, bucket_name text, limits integer, levels integer, start_after text, sort_order text, sort_column text, sort_column_after text) OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: update_updated_at_column(); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION storage.update_updated_at_column() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = now();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION storage.update_updated_at_column() OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: http_request(); Type: FUNCTION; Schema: supabase_functions; Owner: supabase_functions_admin
|
||||||
|
--
|
||||||
|
|
||||||
87
database-novo/schema/03_functions/supabase_functions.sql
Normal file
87
database-novo/schema/03_functions/supabase_functions.sql
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Functions — supabase_functions schema
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION supabase_functions.http_request() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
SET search_path TO 'supabase_functions'
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
request_id bigint;
|
||||||
|
payload jsonb;
|
||||||
|
url text := TG_ARGV[0]::text;
|
||||||
|
method text := TG_ARGV[1]::text;
|
||||||
|
headers jsonb DEFAULT '{}'::jsonb;
|
||||||
|
params jsonb DEFAULT '{}'::jsonb;
|
||||||
|
timeout_ms integer DEFAULT 1000;
|
||||||
|
BEGIN
|
||||||
|
IF url IS NULL OR url = 'null' THEN
|
||||||
|
RAISE EXCEPTION 'url argument is missing';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF method IS NULL OR method = 'null' THEN
|
||||||
|
RAISE EXCEPTION 'method argument is missing';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF TG_ARGV[2] IS NULL OR TG_ARGV[2] = 'null' THEN
|
||||||
|
headers = '{"Content-Type": "application/json"}'::jsonb;
|
||||||
|
ELSE
|
||||||
|
headers = TG_ARGV[2]::jsonb;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF TG_ARGV[3] IS NULL OR TG_ARGV[3] = 'null' THEN
|
||||||
|
params = '{}'::jsonb;
|
||||||
|
ELSE
|
||||||
|
params = TG_ARGV[3]::jsonb;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF TG_ARGV[4] IS NULL OR TG_ARGV[4] = 'null' THEN
|
||||||
|
timeout_ms = 1000;
|
||||||
|
ELSE
|
||||||
|
timeout_ms = TG_ARGV[4]::integer;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
CASE
|
||||||
|
WHEN method = 'GET' THEN
|
||||||
|
SELECT http_get INTO request_id FROM net.http_get(
|
||||||
|
url,
|
||||||
|
params,
|
||||||
|
headers,
|
||||||
|
timeout_ms
|
||||||
|
);
|
||||||
|
WHEN method = 'POST' THEN
|
||||||
|
payload = jsonb_build_object(
|
||||||
|
'old_record', OLD,
|
||||||
|
'record', NEW,
|
||||||
|
'type', TG_OP,
|
||||||
|
'table', TG_TABLE_NAME,
|
||||||
|
'schema', TG_TABLE_SCHEMA
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT http_post INTO request_id FROM net.http_post(
|
||||||
|
url,
|
||||||
|
payload,
|
||||||
|
params,
|
||||||
|
headers,
|
||||||
|
timeout_ms
|
||||||
|
);
|
||||||
|
ELSE
|
||||||
|
RAISE EXCEPTION 'method argument % is invalid', method;
|
||||||
|
END CASE;
|
||||||
|
|
||||||
|
INSERT INTO supabase_functions.hooks
|
||||||
|
(hook_table_id, hook_name, request_id)
|
||||||
|
VALUES
|
||||||
|
(TG_RELID, TG_NAME, request_id);
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER FUNCTION supabase_functions.http_request() OWNER TO supabase_functions_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: extensions; Type: TABLE; Schema: _realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
472
database-novo/schema/04_tables/agenda.sql
Normal file
472
database-novo/schema/04_tables/agenda.sql
Normal file
@@ -0,0 +1,472 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Tables — Agenda + Recorrências + Agendador Online
|
||||||
|
-- =============================================================================
|
||||||
|
-- agenda_bloqueios, agenda_configuracoes, agenda_eventos, agenda_excecoes,
|
||||||
|
-- agenda_online_slots, agenda_regras_semanais, agenda_slots_bloqueados_semanais,
|
||||||
|
-- agenda_slots_regras, recurrence_rules, recurrence_exceptions,
|
||||||
|
-- recurrence_rule_services, agendador_configuracoes, agendador_solicitacoes
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE public.agenda_bloqueios (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
tenant_id uuid,
|
||||||
|
tipo text NOT NULL,
|
||||||
|
titulo text NOT NULL,
|
||||||
|
data_inicio date NOT NULL,
|
||||||
|
data_fim date,
|
||||||
|
hora_inicio time without time zone,
|
||||||
|
hora_fim time without time zone,
|
||||||
|
recorrente boolean DEFAULT false NOT NULL,
|
||||||
|
dia_semana smallint,
|
||||||
|
observacao text,
|
||||||
|
origem text DEFAULT 'manual'::text NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT agenda_bloqueios_tipo_check CHECK ((tipo = ANY (ARRAY['feriado_nacional'::text, 'feriado_municipal'::text, 'bloqueio'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.agenda_bloqueios OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_configuracoes; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.agenda_configuracoes (
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
duracao_padrao_minutos integer DEFAULT 50 NOT NULL,
|
||||||
|
intervalo_padrao_minutos integer DEFAULT 0 NOT NULL,
|
||||||
|
timezone text DEFAULT 'America/Sao_Paulo'::text NOT NULL,
|
||||||
|
usar_horario_admin_custom boolean DEFAULT false NOT NULL,
|
||||||
|
admin_inicio_visualizacao time without time zone,
|
||||||
|
admin_fim_visualizacao time without time zone,
|
||||||
|
admin_slot_visual_minutos integer DEFAULT 30 NOT NULL,
|
||||||
|
online_ativo boolean DEFAULT false NOT NULL,
|
||||||
|
online_min_antecedencia_horas integer DEFAULT 24 NOT NULL,
|
||||||
|
online_max_dias_futuro integer DEFAULT 60 NOT NULL,
|
||||||
|
online_cancelar_ate_horas integer DEFAULT 12 NOT NULL,
|
||||||
|
online_reagendar_ate_horas integer DEFAULT 12 NOT NULL,
|
||||||
|
online_limite_agendamentos_futuros integer DEFAULT 1 NOT NULL,
|
||||||
|
online_modo text DEFAULT 'automatico'::text NOT NULL,
|
||||||
|
online_buffer_antes_min integer DEFAULT 0 NOT NULL,
|
||||||
|
online_buffer_depois_min integer DEFAULT 0 NOT NULL,
|
||||||
|
online_modalidade text DEFAULT 'ambos'::text NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
usar_granularidade_custom boolean DEFAULT false NOT NULL,
|
||||||
|
granularidade_min integer,
|
||||||
|
setup_concluido boolean DEFAULT false NOT NULL,
|
||||||
|
setup_concluido_em timestamp with time zone,
|
||||||
|
agenda_view_mode text DEFAULT 'full_24h'::text NOT NULL,
|
||||||
|
agenda_custom_start time without time zone,
|
||||||
|
agenda_custom_end time without time zone,
|
||||||
|
session_duration_min integer DEFAULT 50 NOT NULL,
|
||||||
|
session_break_min integer DEFAULT 10 NOT NULL,
|
||||||
|
pausas_semanais jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||||
|
setup_clinica_concluido boolean DEFAULT false NOT NULL,
|
||||||
|
setup_clinica_concluido_em timestamp with time zone,
|
||||||
|
tenant_id uuid,
|
||||||
|
jornada_igual_todos boolean DEFAULT true,
|
||||||
|
slot_mode text DEFAULT 'fixed'::text NOT NULL,
|
||||||
|
CONSTRAINT agenda_configuracoes_admin_slot_visual_minutos_check CHECK ((admin_slot_visual_minutos = ANY (ARRAY[5, 10, 15, 20, 30, 60]))),
|
||||||
|
CONSTRAINT agenda_configuracoes_check CHECK (((usar_horario_admin_custom = false) OR ((admin_inicio_visualizacao IS NOT NULL) AND (admin_fim_visualizacao IS NOT NULL) AND (admin_fim_visualizacao > admin_inicio_visualizacao)))),
|
||||||
|
CONSTRAINT agenda_configuracoes_duracao_padrao_minutos_check CHECK (((duracao_padrao_minutos >= 10) AND (duracao_padrao_minutos <= 240))),
|
||||||
|
CONSTRAINT agenda_configuracoes_granularidade_min_check CHECK (((granularidade_min IS NULL) OR (granularidade_min = ANY (ARRAY[5, 10, 15, 20, 30, 45, 50, 60])))),
|
||||||
|
CONSTRAINT agenda_configuracoes_intervalo_padrao_minutos_check CHECK (((intervalo_padrao_minutos >= 0) AND (intervalo_padrao_minutos <= 120))),
|
||||||
|
CONSTRAINT agenda_configuracoes_online_buffer_antes_min_check CHECK (((online_buffer_antes_min >= 0) AND (online_buffer_antes_min <= 120))),
|
||||||
|
CONSTRAINT agenda_configuracoes_online_buffer_depois_min_check CHECK (((online_buffer_depois_min >= 0) AND (online_buffer_depois_min <= 120))),
|
||||||
|
CONSTRAINT agenda_configuracoes_online_cancelar_ate_horas_check CHECK (((online_cancelar_ate_horas >= 0) AND (online_cancelar_ate_horas <= 720))),
|
||||||
|
CONSTRAINT agenda_configuracoes_online_limite_agendamentos_futuros_check CHECK (((online_limite_agendamentos_futuros >= 1) AND (online_limite_agendamentos_futuros <= 10))),
|
||||||
|
CONSTRAINT agenda_configuracoes_online_max_dias_futuro_check CHECK (((online_max_dias_futuro >= 1) AND (online_max_dias_futuro <= 365))),
|
||||||
|
CONSTRAINT agenda_configuracoes_online_min_antecedencia_horas_check CHECK (((online_min_antecedencia_horas >= 0) AND (online_min_antecedencia_horas <= 720))),
|
||||||
|
CONSTRAINT agenda_configuracoes_online_modalidade_check CHECK ((online_modalidade = ANY (ARRAY['online'::text, 'presencial'::text, 'ambos'::text]))),
|
||||||
|
CONSTRAINT agenda_configuracoes_online_modo_check CHECK ((online_modo = ANY (ARRAY['automatico'::text, 'aprovacao'::text]))),
|
||||||
|
CONSTRAINT agenda_configuracoes_online_reagendar_ate_horas_check CHECK (((online_reagendar_ate_horas >= 0) AND (online_reagendar_ate_horas <= 720))),
|
||||||
|
CONSTRAINT agenda_configuracoes_slot_mode_chk CHECK ((slot_mode = ANY (ARRAY['fixed'::text, 'dynamic'::text]))),
|
||||||
|
CONSTRAINT session_break_min_chk CHECK (((session_break_min >= 0) AND (session_break_min <= 60))),
|
||||||
|
CONSTRAINT session_duration_min_chk CHECK (((session_duration_min >= 10) AND (session_duration_min <= 240)))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.agenda_configuracoes OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_eventos; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.agenda_eventos (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
tipo public.tipo_evento_agenda DEFAULT 'sessao'::public.tipo_evento_agenda NOT NULL,
|
||||||
|
status public.status_evento_agenda DEFAULT 'agendado'::public.status_evento_agenda NOT NULL,
|
||||||
|
titulo text,
|
||||||
|
observacoes text,
|
||||||
|
inicio_em timestamp with time zone NOT NULL,
|
||||||
|
fim_em timestamp with time zone NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
terapeuta_id uuid,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
visibility_scope text DEFAULT 'public'::text NOT NULL,
|
||||||
|
mirror_of_event_id uuid,
|
||||||
|
mirror_source text,
|
||||||
|
patient_id uuid,
|
||||||
|
determined_commitment_id uuid,
|
||||||
|
link_online text,
|
||||||
|
titulo_custom text,
|
||||||
|
extra_fields jsonb,
|
||||||
|
recurrence_id uuid,
|
||||||
|
recurrence_date date,
|
||||||
|
modalidade text DEFAULT 'presencial'::text,
|
||||||
|
price numeric(10,2),
|
||||||
|
billing_contract_id uuid,
|
||||||
|
billed boolean DEFAULT false NOT NULL,
|
||||||
|
services_customized boolean DEFAULT false NOT NULL,
|
||||||
|
insurance_plan_id uuid,
|
||||||
|
insurance_guide_number text,
|
||||||
|
insurance_value numeric(10,2),
|
||||||
|
insurance_plan_service_id uuid,
|
||||||
|
CONSTRAINT agenda_eventos_check CHECK ((fim_em > inicio_em))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.agenda_eventos OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: COLUMN agenda_eventos.price; Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON COLUMN public.agenda_eventos.price IS 'Valor da sessão em BRL. Preenchido automaticamente pela tabela professional_pricing do profissional.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_excecoes; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.agenda_excecoes (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
data date NOT NULL,
|
||||||
|
hora_inicio time without time zone,
|
||||||
|
hora_fim time without time zone,
|
||||||
|
tipo public.tipo_excecao_agenda NOT NULL,
|
||||||
|
motivo text,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
status public.status_excecao_agenda DEFAULT 'ativo'::public.status_excecao_agenda NOT NULL,
|
||||||
|
fonte text DEFAULT 'manual'::text NOT NULL,
|
||||||
|
aplicavel_online boolean DEFAULT true NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
CONSTRAINT agenda_excecoes_check CHECK ((((hora_inicio IS NULL) AND (hora_fim IS NULL)) OR ((hora_inicio IS NOT NULL) AND (hora_fim IS NOT NULL) AND (hora_fim > hora_inicio)))),
|
||||||
|
CONSTRAINT agenda_excecoes_fonte_check CHECK ((fonte = ANY (ARRAY['manual'::text, 'feriado_google'::text, 'sistema'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.agenda_excecoes OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_online_slots; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.agenda_online_slots (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
weekday integer NOT NULL,
|
||||||
|
"time" time without time zone NOT NULL,
|
||||||
|
enabled boolean DEFAULT true NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
CONSTRAINT agenda_online_slots_weekday_check CHECK ((weekday = ANY (ARRAY[0, 1, 2, 3, 4, 5, 6])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.agenda_online_slots OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_online_slots_id_seq; Type: SEQUENCE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.agenda_online_slots_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.agenda_online_slots_id_seq OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_online_slots_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.agenda_online_slots_id_seq OWNED BY public.agenda_online_slots.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_regras_semanais; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.agenda_regras_semanais (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
dia_semana smallint NOT NULL,
|
||||||
|
hora_inicio time without time zone NOT NULL,
|
||||||
|
hora_fim time without time zone NOT NULL,
|
||||||
|
modalidade text DEFAULT 'ambos'::text NOT NULL,
|
||||||
|
ativo boolean DEFAULT true NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
CONSTRAINT agenda_regras_semanais_check CHECK ((hora_fim > hora_inicio)),
|
||||||
|
CONSTRAINT agenda_regras_semanais_dia_semana_check CHECK (((dia_semana >= 0) AND (dia_semana <= 6))),
|
||||||
|
CONSTRAINT agenda_regras_semanais_modalidade_check CHECK (((modalidade = ANY (ARRAY['online'::text, 'presencial'::text, 'ambos'::text])) OR (modalidade IS NULL)))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.agenda_regras_semanais OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_slots_bloqueados_semanais; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.agenda_slots_bloqueados_semanais (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
dia_semana smallint NOT NULL,
|
||||||
|
hora_inicio time without time zone NOT NULL,
|
||||||
|
motivo text,
|
||||||
|
ativo boolean DEFAULT true NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
CONSTRAINT agenda_slots_bloqueados_semanais_dia_semana_check CHECK (((dia_semana >= 0) AND (dia_semana <= 6)))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.agenda_slots_bloqueados_semanais OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_slots_regras; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.agenda_slots_regras (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
dia_semana smallint NOT NULL,
|
||||||
|
passo_minutos integer NOT NULL,
|
||||||
|
offset_minutos integer DEFAULT 0 NOT NULL,
|
||||||
|
buffer_antes_min integer DEFAULT 0 NOT NULL,
|
||||||
|
buffer_depois_min integer DEFAULT 0 NOT NULL,
|
||||||
|
min_antecedencia_horas integer DEFAULT 0 NOT NULL,
|
||||||
|
ativo boolean DEFAULT true NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
CONSTRAINT agenda_slots_regras_buffer_antes_min_check CHECK (((buffer_antes_min >= 0) AND (buffer_antes_min <= 240))),
|
||||||
|
CONSTRAINT agenda_slots_regras_buffer_depois_min_check CHECK (((buffer_depois_min >= 0) AND (buffer_depois_min <= 240))),
|
||||||
|
CONSTRAINT agenda_slots_regras_dia_semana_check CHECK (((dia_semana >= 0) AND (dia_semana <= 6))),
|
||||||
|
CONSTRAINT agenda_slots_regras_min_antecedencia_horas_check CHECK (((min_antecedencia_horas >= 0) AND (min_antecedencia_horas <= 720))),
|
||||||
|
CONSTRAINT agenda_slots_regras_offset_minutos_check CHECK (((offset_minutos >= 0) AND (offset_minutos <= 55))),
|
||||||
|
CONSTRAINT agenda_slots_regras_passo_minutos_check CHECK (((passo_minutos >= 5) AND (passo_minutos <= 240)))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.agenda_slots_regras OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agendador_configuracoes; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.agendador_configuracoes (
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
tenant_id uuid,
|
||||||
|
ativo boolean DEFAULT false NOT NULL,
|
||||||
|
link_slug text,
|
||||||
|
imagem_fundo_url text,
|
||||||
|
imagem_header_url text,
|
||||||
|
logomarca_url text,
|
||||||
|
cor_primaria text DEFAULT '#4b6bff'::text,
|
||||||
|
nome_exibicao text,
|
||||||
|
endereco text,
|
||||||
|
botao_como_chegar_ativo boolean DEFAULT true NOT NULL,
|
||||||
|
maps_url text,
|
||||||
|
modo_aprovacao text DEFAULT 'aprovacao'::text NOT NULL,
|
||||||
|
modalidade text DEFAULT 'presencial'::text NOT NULL,
|
||||||
|
tipos_habilitados jsonb DEFAULT '["primeira", "retorno"]'::jsonb NOT NULL,
|
||||||
|
duracao_sessao_min integer DEFAULT 50 NOT NULL,
|
||||||
|
antecedencia_minima_horas integer DEFAULT 24 NOT NULL,
|
||||||
|
prazo_resposta_horas integer DEFAULT 2 NOT NULL,
|
||||||
|
reserva_horas integer DEFAULT 2 NOT NULL,
|
||||||
|
pagamento_obrigatorio boolean DEFAULT false NOT NULL,
|
||||||
|
pix_chave text,
|
||||||
|
pix_countdown_minutos integer DEFAULT 20 NOT NULL,
|
||||||
|
triagem_motivo boolean DEFAULT true NOT NULL,
|
||||||
|
triagem_como_conheceu boolean DEFAULT false NOT NULL,
|
||||||
|
verificacao_email boolean DEFAULT false NOT NULL,
|
||||||
|
exigir_aceite_lgpd boolean DEFAULT true NOT NULL,
|
||||||
|
mensagem_boas_vindas text,
|
||||||
|
texto_como_se_preparar text,
|
||||||
|
texto_termos_lgpd text,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
pagamento_modo text DEFAULT 'sem_pagamento'::text NOT NULL,
|
||||||
|
pagamento_metodos_visiveis text[] DEFAULT '{}'::text[] NOT NULL,
|
||||||
|
CONSTRAINT agendador_configuracoes_antecedencia_check CHECK (((antecedencia_minima_horas >= 0) AND (antecedencia_minima_horas <= 720))),
|
||||||
|
CONSTRAINT agendador_configuracoes_duracao_check CHECK (((duracao_sessao_min >= 10) AND (duracao_sessao_min <= 240))),
|
||||||
|
CONSTRAINT agendador_configuracoes_modalidade_check CHECK ((modalidade = ANY (ARRAY['presencial'::text, 'online'::text, 'ambos'::text]))),
|
||||||
|
CONSTRAINT agendador_configuracoes_modo_check CHECK ((modo_aprovacao = ANY (ARRAY['automatico'::text, 'aprovacao'::text]))),
|
||||||
|
CONSTRAINT agendador_configuracoes_pix_countdown_check CHECK (((pix_countdown_minutos >= 5) AND (pix_countdown_minutos <= 120))),
|
||||||
|
CONSTRAINT agendador_configuracoes_prazo_check CHECK (((prazo_resposta_horas >= 1) AND (prazo_resposta_horas <= 72))),
|
||||||
|
CONSTRAINT agendador_configuracoes_reserva_check CHECK (((reserva_horas >= 1) AND (reserva_horas <= 48)))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.agendador_configuracoes OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: COLUMN agendador_configuracoes.pagamento_modo; Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON COLUMN public.agendador_configuracoes.pagamento_modo IS 'sem_pagamento | pagar_na_hora | pix_antecipado';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: COLUMN agendador_configuracoes.pagamento_metodos_visiveis; Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON COLUMN public.agendador_configuracoes.pagamento_metodos_visiveis IS 'Métodos exibidos ao paciente quando pagamento_modo = pagar_na_hora. Ex: {pix, deposito, dinheiro, cartao, convenio}';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agendador_solicitacoes; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.agendador_solicitacoes (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
tenant_id uuid,
|
||||||
|
paciente_nome text NOT NULL,
|
||||||
|
paciente_sobrenome text,
|
||||||
|
paciente_email text NOT NULL,
|
||||||
|
paciente_celular text,
|
||||||
|
paciente_cpf text,
|
||||||
|
tipo text NOT NULL,
|
||||||
|
modalidade text NOT NULL,
|
||||||
|
data_solicitada date NOT NULL,
|
||||||
|
hora_solicitada time without time zone NOT NULL,
|
||||||
|
reservado_ate timestamp with time zone,
|
||||||
|
motivo text,
|
||||||
|
como_conheceu text,
|
||||||
|
pix_status text DEFAULT 'pendente'::text,
|
||||||
|
pix_pago_em timestamp with time zone,
|
||||||
|
status text DEFAULT 'pendente'::text NOT NULL,
|
||||||
|
recusado_motivo text,
|
||||||
|
autorizado_em timestamp with time zone,
|
||||||
|
autorizado_por uuid,
|
||||||
|
user_id uuid,
|
||||||
|
patient_id uuid,
|
||||||
|
evento_id uuid,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT agendador_sol_modalidade_check CHECK ((modalidade = ANY (ARRAY['presencial'::text, 'online'::text]))),
|
||||||
|
CONSTRAINT agendador_sol_pix_check CHECK (((pix_status IS NULL) OR (pix_status = ANY (ARRAY['pendente'::text, 'pago'::text, 'expirado'::text])))),
|
||||||
|
CONSTRAINT agendador_sol_status_check CHECK ((status = ANY (ARRAY['pendente'::text, 'autorizado'::text, 'recusado'::text, 'expirado'::text, 'convertido'::text]))),
|
||||||
|
CONSTRAINT agendador_sol_tipo_check CHECK ((tipo = ANY (ARRAY['primeira'::text, 'retorno'::text, 'reagendar'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.agendador_solicitacoes OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: billing_contracts; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.recurrence_exceptions (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
recurrence_id uuid NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
original_date date NOT NULL,
|
||||||
|
type public.recurrence_exception_type NOT NULL,
|
||||||
|
new_date date,
|
||||||
|
new_start_time time without time zone,
|
||||||
|
new_end_time time without time zone,
|
||||||
|
modalidade text,
|
||||||
|
observacoes text,
|
||||||
|
titulo_custom text,
|
||||||
|
extra_fields jsonb,
|
||||||
|
reason text,
|
||||||
|
agenda_evento_id uuid,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.recurrence_exceptions OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: recurrence_rule_services; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.recurrence_rule_services (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
rule_id uuid NOT NULL,
|
||||||
|
service_id uuid NOT NULL,
|
||||||
|
quantity integer DEFAULT 1 NOT NULL,
|
||||||
|
unit_price numeric(10,2) NOT NULL,
|
||||||
|
discount_pct numeric(5,2) DEFAULT 0,
|
||||||
|
discount_flat numeric(10,2) DEFAULT 0,
|
||||||
|
final_price numeric(10,2) NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
CONSTRAINT recurrence_rule_services_disc_flat_chk CHECK ((discount_flat >= (0)::numeric)),
|
||||||
|
CONSTRAINT recurrence_rule_services_disc_pct_chk CHECK (((discount_pct >= (0)::numeric) AND (discount_pct <= (100)::numeric))),
|
||||||
|
CONSTRAINT recurrence_rule_services_final_price_chk CHECK ((final_price >= (0)::numeric)),
|
||||||
|
CONSTRAINT recurrence_rule_services_quantity_chk CHECK ((quantity > 0))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.recurrence_rule_services OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: recurrence_rules; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.recurrence_rules (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
therapist_id uuid,
|
||||||
|
patient_id uuid,
|
||||||
|
determined_commitment_id uuid,
|
||||||
|
type public.recurrence_type DEFAULT 'weekly'::public.recurrence_type NOT NULL,
|
||||||
|
"interval" smallint DEFAULT 1 NOT NULL,
|
||||||
|
weekdays smallint[] DEFAULT '{}'::smallint[] NOT NULL,
|
||||||
|
start_time time without time zone NOT NULL,
|
||||||
|
end_time time without time zone NOT NULL,
|
||||||
|
timezone text DEFAULT 'America/Sao_Paulo'::text NOT NULL,
|
||||||
|
duration_min smallint DEFAULT 50 NOT NULL,
|
||||||
|
start_date date NOT NULL,
|
||||||
|
end_date date,
|
||||||
|
max_occurrences integer,
|
||||||
|
open_ended boolean DEFAULT true NOT NULL,
|
||||||
|
modalidade text DEFAULT 'presencial'::text,
|
||||||
|
titulo_custom text,
|
||||||
|
observacoes text,
|
||||||
|
extra_fields jsonb,
|
||||||
|
status text DEFAULT 'ativo'::text NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
price numeric(10,2),
|
||||||
|
insurance_plan_id uuid,
|
||||||
|
insurance_guide_number text,
|
||||||
|
insurance_value numeric(10,2),
|
||||||
|
insurance_plan_service_id uuid,
|
||||||
|
CONSTRAINT recurrence_rules_dates_chk CHECK (((end_date IS NULL) OR (end_date >= start_date))),
|
||||||
|
CONSTRAINT recurrence_rules_interval_chk CHECK (("interval" >= 1)),
|
||||||
|
CONSTRAINT recurrence_rules_status_check CHECK ((status = ANY (ARRAY['ativo'::text, 'pausado'::text, 'cancelado'::text]))),
|
||||||
|
CONSTRAINT recurrence_rules_times_chk CHECK ((end_time > start_time))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
608
database-novo/schema/04_tables/auth.sql
Normal file
608
database-novo/schema/04_tables/auth.sql
Normal file
@@ -0,0 +1,608 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Tables — auth schema (Supabase GoTrue)
|
||||||
|
-- =============================================================================
|
||||||
|
-- auth.users, auth.identities, auth.sessions, auth.refresh_tokens,
|
||||||
|
-- auth.mfa_*, auth.saml_*, auth.sso_*, auth.flow_state, etc.
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE auth.audit_log_entries (
|
||||||
|
instance_id uuid,
|
||||||
|
id uuid NOT NULL,
|
||||||
|
payload json,
|
||||||
|
created_at timestamp with time zone,
|
||||||
|
ip_address character varying(64) DEFAULT ''::character varying NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE auth.audit_log_entries OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: TABLE audit_log_entries; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON TABLE auth.audit_log_entries IS 'Auth: Audit trail for user actions.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: flow_state; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE auth.flow_state (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
user_id uuid,
|
||||||
|
auth_code text,
|
||||||
|
code_challenge_method auth.code_challenge_method,
|
||||||
|
code_challenge text,
|
||||||
|
provider_type text NOT NULL,
|
||||||
|
provider_access_token text,
|
||||||
|
provider_refresh_token text,
|
||||||
|
created_at timestamp with time zone,
|
||||||
|
updated_at timestamp with time zone,
|
||||||
|
authentication_method text NOT NULL,
|
||||||
|
auth_code_issued_at timestamp with time zone,
|
||||||
|
invite_token text,
|
||||||
|
referrer text,
|
||||||
|
oauth_client_state_id uuid,
|
||||||
|
linking_target_id uuid,
|
||||||
|
email_optional boolean DEFAULT false NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE auth.flow_state OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: TABLE flow_state; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON TABLE auth.flow_state IS 'Stores metadata for all OAuth/SSO login flows';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: identities; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE auth.identities (
|
||||||
|
provider_id text NOT NULL,
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
identity_data jsonb NOT NULL,
|
||||||
|
provider text NOT NULL,
|
||||||
|
last_sign_in_at timestamp with time zone,
|
||||||
|
created_at timestamp with time zone,
|
||||||
|
updated_at timestamp with time zone,
|
||||||
|
email text GENERATED ALWAYS AS (lower((identity_data ->> 'email'::text))) STORED,
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE auth.identities OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: TABLE identities; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON TABLE auth.identities IS 'Auth: Stores identities associated to a user.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: COLUMN identities.email; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON COLUMN auth.identities.email IS 'Auth: Email is a generated column that references the optional email property in the identity_data';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: instances; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE auth.instances (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
uuid uuid,
|
||||||
|
raw_base_config text,
|
||||||
|
created_at timestamp with time zone,
|
||||||
|
updated_at timestamp with time zone
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE auth.instances OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: TABLE instances; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON TABLE auth.instances IS 'Auth: Manages users across multiple sites.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: mfa_amr_claims; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE auth.mfa_amr_claims (
|
||||||
|
session_id uuid NOT NULL,
|
||||||
|
created_at timestamp with time zone NOT NULL,
|
||||||
|
updated_at timestamp with time zone NOT NULL,
|
||||||
|
authentication_method text NOT NULL,
|
||||||
|
id uuid NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE auth.mfa_amr_claims OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: TABLE mfa_amr_claims; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON TABLE auth.mfa_amr_claims IS 'auth: stores authenticator method reference claims for multi factor authentication';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: mfa_challenges; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE auth.mfa_challenges (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
factor_id uuid NOT NULL,
|
||||||
|
created_at timestamp with time zone NOT NULL,
|
||||||
|
verified_at timestamp with time zone,
|
||||||
|
ip_address inet NOT NULL,
|
||||||
|
otp_code text,
|
||||||
|
web_authn_session_data jsonb
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE auth.mfa_challenges OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: TABLE mfa_challenges; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON TABLE auth.mfa_challenges IS 'auth: stores metadata about challenge requests made';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: mfa_factors; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE auth.mfa_factors (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
friendly_name text,
|
||||||
|
factor_type auth.factor_type NOT NULL,
|
||||||
|
status auth.factor_status NOT NULL,
|
||||||
|
created_at timestamp with time zone NOT NULL,
|
||||||
|
updated_at timestamp with time zone NOT NULL,
|
||||||
|
secret text,
|
||||||
|
phone text,
|
||||||
|
last_challenged_at timestamp with time zone,
|
||||||
|
web_authn_credential jsonb,
|
||||||
|
web_authn_aaguid uuid,
|
||||||
|
last_webauthn_challenge_data jsonb
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE auth.mfa_factors OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: TABLE mfa_factors; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON TABLE auth.mfa_factors IS 'auth: stores metadata about factors';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: COLUMN mfa_factors.last_webauthn_challenge_data; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON COLUMN auth.mfa_factors.last_webauthn_challenge_data IS 'Stores the latest WebAuthn challenge data including attestation/assertion for customer verification';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: oauth_authorizations; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE auth.oauth_authorizations (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
authorization_id text NOT NULL,
|
||||||
|
client_id uuid NOT NULL,
|
||||||
|
user_id uuid,
|
||||||
|
redirect_uri text NOT NULL,
|
||||||
|
scope text NOT NULL,
|
||||||
|
state text,
|
||||||
|
resource text,
|
||||||
|
code_challenge text,
|
||||||
|
code_challenge_method auth.code_challenge_method,
|
||||||
|
response_type auth.oauth_response_type DEFAULT 'code'::auth.oauth_response_type NOT NULL,
|
||||||
|
status auth.oauth_authorization_status DEFAULT 'pending'::auth.oauth_authorization_status NOT NULL,
|
||||||
|
authorization_code text,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
expires_at timestamp with time zone DEFAULT (now() + '00:03:00'::interval) NOT NULL,
|
||||||
|
approved_at timestamp with time zone,
|
||||||
|
nonce text,
|
||||||
|
CONSTRAINT oauth_authorizations_authorization_code_length CHECK ((char_length(authorization_code) <= 255)),
|
||||||
|
CONSTRAINT oauth_authorizations_code_challenge_length CHECK ((char_length(code_challenge) <= 128)),
|
||||||
|
CONSTRAINT oauth_authorizations_expires_at_future CHECK ((expires_at > created_at)),
|
||||||
|
CONSTRAINT oauth_authorizations_nonce_length CHECK ((char_length(nonce) <= 255)),
|
||||||
|
CONSTRAINT oauth_authorizations_redirect_uri_length CHECK ((char_length(redirect_uri) <= 2048)),
|
||||||
|
CONSTRAINT oauth_authorizations_resource_length CHECK ((char_length(resource) <= 2048)),
|
||||||
|
CONSTRAINT oauth_authorizations_scope_length CHECK ((char_length(scope) <= 4096)),
|
||||||
|
CONSTRAINT oauth_authorizations_state_length CHECK ((char_length(state) <= 4096))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE auth.oauth_authorizations OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: oauth_client_states; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE auth.oauth_client_states (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
provider_type text NOT NULL,
|
||||||
|
code_verifier text,
|
||||||
|
created_at timestamp with time zone NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE auth.oauth_client_states OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: TABLE oauth_client_states; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON TABLE auth.oauth_client_states IS 'Stores OAuth states for third-party provider authentication flows where Supabase acts as the OAuth client.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: oauth_clients; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE auth.oauth_clients (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
client_secret_hash text,
|
||||||
|
registration_type auth.oauth_registration_type NOT NULL,
|
||||||
|
redirect_uris text NOT NULL,
|
||||||
|
grant_types text NOT NULL,
|
||||||
|
client_name text,
|
||||||
|
client_uri text,
|
||||||
|
logo_uri text,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
deleted_at timestamp with time zone,
|
||||||
|
client_type auth.oauth_client_type DEFAULT 'confidential'::auth.oauth_client_type NOT NULL,
|
||||||
|
token_endpoint_auth_method text NOT NULL,
|
||||||
|
CONSTRAINT oauth_clients_client_name_length CHECK ((char_length(client_name) <= 1024)),
|
||||||
|
CONSTRAINT oauth_clients_client_uri_length CHECK ((char_length(client_uri) <= 2048)),
|
||||||
|
CONSTRAINT oauth_clients_logo_uri_length CHECK ((char_length(logo_uri) <= 2048)),
|
||||||
|
CONSTRAINT oauth_clients_token_endpoint_auth_method_check CHECK ((token_endpoint_auth_method = ANY (ARRAY['client_secret_basic'::text, 'client_secret_post'::text, 'none'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE auth.oauth_clients OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: oauth_consents; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE auth.oauth_consents (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
client_id uuid NOT NULL,
|
||||||
|
scopes text NOT NULL,
|
||||||
|
granted_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
revoked_at timestamp with time zone,
|
||||||
|
CONSTRAINT oauth_consents_revoked_after_granted CHECK (((revoked_at IS NULL) OR (revoked_at >= granted_at))),
|
||||||
|
CONSTRAINT oauth_consents_scopes_length CHECK ((char_length(scopes) <= 2048)),
|
||||||
|
CONSTRAINT oauth_consents_scopes_not_empty CHECK ((char_length(TRIM(BOTH FROM scopes)) > 0))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE auth.oauth_consents OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: one_time_tokens; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE auth.one_time_tokens (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
token_type auth.one_time_token_type NOT NULL,
|
||||||
|
token_hash text NOT NULL,
|
||||||
|
relates_to text NOT NULL,
|
||||||
|
created_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT one_time_tokens_token_hash_check CHECK ((char_length(token_hash) > 0))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE auth.one_time_tokens OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: refresh_tokens; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE auth.refresh_tokens (
|
||||||
|
instance_id uuid,
|
||||||
|
id bigint NOT NULL,
|
||||||
|
token character varying(255),
|
||||||
|
user_id character varying(255),
|
||||||
|
revoked boolean,
|
||||||
|
created_at timestamp with time zone,
|
||||||
|
updated_at timestamp with time zone,
|
||||||
|
parent character varying(255),
|
||||||
|
session_id uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE auth.refresh_tokens OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: TABLE refresh_tokens; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON TABLE auth.refresh_tokens IS 'Auth: Store of tokens used to refresh JWT tokens once they expire.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: refresh_tokens_id_seq; Type: SEQUENCE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE auth.refresh_tokens_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER SEQUENCE auth.refresh_tokens_id_seq OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: refresh_tokens_id_seq; Type: SEQUENCE OWNED BY; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE auth.refresh_tokens_id_seq OWNED BY auth.refresh_tokens.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: saml_providers; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE auth.saml_providers (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
sso_provider_id uuid NOT NULL,
|
||||||
|
entity_id text NOT NULL,
|
||||||
|
metadata_xml text NOT NULL,
|
||||||
|
metadata_url text,
|
||||||
|
attribute_mapping jsonb,
|
||||||
|
created_at timestamp with time zone,
|
||||||
|
updated_at timestamp with time zone,
|
||||||
|
name_id_format text,
|
||||||
|
CONSTRAINT "entity_id not empty" CHECK ((char_length(entity_id) > 0)),
|
||||||
|
CONSTRAINT "metadata_url not empty" CHECK (((metadata_url = NULL::text) OR (char_length(metadata_url) > 0))),
|
||||||
|
CONSTRAINT "metadata_xml not empty" CHECK ((char_length(metadata_xml) > 0))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE auth.saml_providers OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: TABLE saml_providers; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON TABLE auth.saml_providers IS 'Auth: Manages SAML Identity Provider connections.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: saml_relay_states; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE auth.saml_relay_states (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
sso_provider_id uuid NOT NULL,
|
||||||
|
request_id text NOT NULL,
|
||||||
|
for_email text,
|
||||||
|
redirect_to text,
|
||||||
|
created_at timestamp with time zone,
|
||||||
|
updated_at timestamp with time zone,
|
||||||
|
flow_state_id uuid,
|
||||||
|
CONSTRAINT "request_id not empty" CHECK ((char_length(request_id) > 0))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE auth.saml_relay_states OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: TABLE saml_relay_states; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON TABLE auth.saml_relay_states IS 'Auth: Contains SAML Relay State information for each Service Provider initiated login.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: schema_migrations; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE auth.schema_migrations (
|
||||||
|
version character varying(255) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE auth.schema_migrations OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: TABLE schema_migrations; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON TABLE auth.schema_migrations IS 'Auth: Manages updates to the auth system.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: sessions; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE auth.sessions (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
created_at timestamp with time zone,
|
||||||
|
updated_at timestamp with time zone,
|
||||||
|
factor_id uuid,
|
||||||
|
aal auth.aal_level,
|
||||||
|
not_after timestamp with time zone,
|
||||||
|
refreshed_at timestamp without time zone,
|
||||||
|
user_agent text,
|
||||||
|
ip inet,
|
||||||
|
tag text,
|
||||||
|
oauth_client_id uuid,
|
||||||
|
refresh_token_hmac_key text,
|
||||||
|
refresh_token_counter bigint,
|
||||||
|
scopes text,
|
||||||
|
CONSTRAINT sessions_scopes_length CHECK ((char_length(scopes) <= 4096))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE auth.sessions OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: TABLE sessions; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON TABLE auth.sessions IS 'Auth: Stores session data associated to a user.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: COLUMN sessions.not_after; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON COLUMN auth.sessions.not_after IS 'Auth: Not after is a nullable column that contains a timestamp after which the session should be regarded as expired.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: COLUMN sessions.refresh_token_hmac_key; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON COLUMN auth.sessions.refresh_token_hmac_key IS 'Holds a HMAC-SHA256 key used to sign refresh tokens for this session.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: COLUMN sessions.refresh_token_counter; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON COLUMN auth.sessions.refresh_token_counter IS 'Holds the ID (counter) of the last issued refresh token.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: sso_domains; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE auth.sso_domains (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
sso_provider_id uuid NOT NULL,
|
||||||
|
domain text NOT NULL,
|
||||||
|
created_at timestamp with time zone,
|
||||||
|
updated_at timestamp with time zone,
|
||||||
|
CONSTRAINT "domain not empty" CHECK ((char_length(domain) > 0))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE auth.sso_domains OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: TABLE sso_domains; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON TABLE auth.sso_domains IS 'Auth: Manages SSO email address domain mapping to an SSO Identity Provider.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: sso_providers; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE auth.sso_providers (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
resource_id text,
|
||||||
|
created_at timestamp with time zone,
|
||||||
|
updated_at timestamp with time zone,
|
||||||
|
disabled boolean,
|
||||||
|
CONSTRAINT "resource_id not empty" CHECK (((resource_id = NULL::text) OR (char_length(resource_id) > 0)))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE auth.sso_providers OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: TABLE sso_providers; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON TABLE auth.sso_providers IS 'Auth: Manages SSO identity provider information; see saml_providers for SAML.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: COLUMN sso_providers.resource_id; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON COLUMN auth.sso_providers.resource_id IS 'Auth: Uniquely identifies a SSO provider according to a user-chosen resource ID (case insensitive), useful in infrastructure as code.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: users; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE auth.users (
|
||||||
|
instance_id uuid,
|
||||||
|
id uuid NOT NULL,
|
||||||
|
aud character varying(255),
|
||||||
|
role character varying(255),
|
||||||
|
email character varying(255),
|
||||||
|
encrypted_password character varying(255),
|
||||||
|
email_confirmed_at timestamp with time zone,
|
||||||
|
invited_at timestamp with time zone,
|
||||||
|
confirmation_token character varying(255),
|
||||||
|
confirmation_sent_at timestamp with time zone,
|
||||||
|
recovery_token character varying(255),
|
||||||
|
recovery_sent_at timestamp with time zone,
|
||||||
|
email_change_token_new character varying(255),
|
||||||
|
email_change character varying(255),
|
||||||
|
email_change_sent_at timestamp with time zone,
|
||||||
|
last_sign_in_at timestamp with time zone,
|
||||||
|
raw_app_meta_data jsonb,
|
||||||
|
raw_user_meta_data jsonb,
|
||||||
|
is_super_admin boolean,
|
||||||
|
created_at timestamp with time zone,
|
||||||
|
updated_at timestamp with time zone,
|
||||||
|
phone text DEFAULT NULL::character varying,
|
||||||
|
phone_confirmed_at timestamp with time zone,
|
||||||
|
phone_change text DEFAULT ''::character varying,
|
||||||
|
phone_change_token character varying(255) DEFAULT ''::character varying,
|
||||||
|
phone_change_sent_at timestamp with time zone,
|
||||||
|
confirmed_at timestamp with time zone GENERATED ALWAYS AS (LEAST(email_confirmed_at, phone_confirmed_at)) STORED,
|
||||||
|
email_change_token_current character varying(255) DEFAULT ''::character varying,
|
||||||
|
email_change_confirm_status smallint DEFAULT 0,
|
||||||
|
banned_until timestamp with time zone,
|
||||||
|
reauthentication_token character varying(255) DEFAULT ''::character varying,
|
||||||
|
reauthentication_sent_at timestamp with time zone,
|
||||||
|
is_sso_user boolean DEFAULT false NOT NULL,
|
||||||
|
deleted_at timestamp with time zone,
|
||||||
|
is_anonymous boolean DEFAULT false NOT NULL,
|
||||||
|
CONSTRAINT users_email_change_confirm_status_check CHECK (((email_change_confirm_status >= 0) AND (email_change_confirm_status <= 2)))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE auth.users OWNER TO supabase_auth_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: TABLE users; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON TABLE auth.users IS 'Auth: Stores user login data within a secure schema.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: COLUMN users.is_sso_user; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON COLUMN auth.users.is_sso_user IS 'Auth: Set this column to true when the account comes from SSO. These accounts can have duplicate emails.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: addon_credits; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
77
database-novo/schema/04_tables/commitments.sql
Normal file
77
database-novo/schema/04_tables/commitments.sql
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Tables — Compromissos Determinados
|
||||||
|
-- =============================================================================
|
||||||
|
-- determined_commitments, determined_commitment_fields,
|
||||||
|
-- commitment_services, commitment_time_logs
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE public.determined_commitments (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
created_by uuid,
|
||||||
|
is_native boolean DEFAULT false NOT NULL,
|
||||||
|
native_key text,
|
||||||
|
is_locked boolean DEFAULT false NOT NULL,
|
||||||
|
active boolean DEFAULT true NOT NULL,
|
||||||
|
name text NOT NULL,
|
||||||
|
description text,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
bg_color text,
|
||||||
|
text_color text
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.determined_commitment_fields (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
commitment_id uuid NOT NULL,
|
||||||
|
key text NOT NULL,
|
||||||
|
label text NOT NULL,
|
||||||
|
field_type public.determined_field_type DEFAULT 'text'::public.determined_field_type NOT NULL,
|
||||||
|
required boolean DEFAULT false NOT NULL,
|
||||||
|
sort_order integer DEFAULT 0 NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.commitment_services (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
commitment_id uuid NOT NULL,
|
||||||
|
service_id uuid NOT NULL,
|
||||||
|
quantity integer DEFAULT 1 NOT NULL,
|
||||||
|
unit_price numeric(10,2) NOT NULL,
|
||||||
|
discount_pct numeric(5,2) DEFAULT 0,
|
||||||
|
discount_flat numeric(10,2) DEFAULT 0,
|
||||||
|
final_price numeric(10,2) NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
CONSTRAINT commitment_services_disc_flat_chk CHECK ((discount_flat >= (0)::numeric)),
|
||||||
|
CONSTRAINT commitment_services_disc_pct_chk CHECK (((discount_pct >= (0)::numeric) AND (discount_pct <= (100)::numeric))),
|
||||||
|
CONSTRAINT commitment_services_final_price_chk CHECK ((final_price >= (0)::numeric)),
|
||||||
|
CONSTRAINT commitment_services_quantity_chk CHECK ((quantity > 0))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.commitment_services OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: commitment_time_logs; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.commitment_time_logs (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
commitment_id uuid NOT NULL,
|
||||||
|
calendar_event_id uuid,
|
||||||
|
source public.commitment_log_source DEFAULT 'manual'::public.commitment_log_source NOT NULL,
|
||||||
|
started_at timestamp with time zone NOT NULL,
|
||||||
|
ended_at timestamp with time zone NOT NULL,
|
||||||
|
minutes integer NOT NULL,
|
||||||
|
created_by uuid,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
171
database-novo/schema/04_tables/core.sql
Normal file
171
database-novo/schema/04_tables/core.sql
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Tables — Core
|
||||||
|
-- =============================================================================
|
||||||
|
-- profiles, tenants, tenant_members, tenant_invites, tenant_features,
|
||||||
|
-- tenant_feature_exceptions_log, saas_admins, owner_users, user_settings,
|
||||||
|
-- company_profiles, dev_user_credentials
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE public.profiles (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
role text DEFAULT 'tenant_member'::text NOT NULL,
|
||||||
|
full_name text,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
avatar_url text,
|
||||||
|
phone text,
|
||||||
|
bio text,
|
||||||
|
language text DEFAULT 'pt-BR'::text NOT NULL,
|
||||||
|
timezone text DEFAULT 'America/Sao_Paulo'::text NOT NULL,
|
||||||
|
notify_system_email boolean DEFAULT true NOT NULL,
|
||||||
|
notify_reminders boolean DEFAULT true NOT NULL,
|
||||||
|
notify_news boolean DEFAULT false NOT NULL,
|
||||||
|
account_type text DEFAULT 'free'::text NOT NULL,
|
||||||
|
platform_roles text[] DEFAULT '{}'::text[] NOT NULL,
|
||||||
|
nickname text,
|
||||||
|
work_description text,
|
||||||
|
work_description_other text,
|
||||||
|
site_url text,
|
||||||
|
social_instagram text,
|
||||||
|
social_youtube text,
|
||||||
|
social_facebook text,
|
||||||
|
social_x text,
|
||||||
|
social_custom jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||||
|
CONSTRAINT profiles_account_type_check CHECK ((account_type = ANY (ARRAY['free'::text, 'patient'::text, 'therapist'::text, 'clinic'::text]))),
|
||||||
|
CONSTRAINT profiles_role_check CHECK ((role = ANY (ARRAY['saas_admin'::text, 'tenant_member'::text, 'portal_user'::text, 'patient'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.tenants (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
name text,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
kind text DEFAULT 'saas'::text NOT NULL,
|
||||||
|
CONSTRAINT tenants_kind_check CHECK ((kind = ANY (ARRAY['therapist'::text, 'clinic_coworking'::text, 'clinic_reception'::text, 'clinic_full'::text, 'clinic'::text, 'saas'::text, 'supervisor'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.tenant_members (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
role text NOT NULL,
|
||||||
|
status text DEFAULT 'active'::text NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.tenant_invites (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
email text NOT NULL,
|
||||||
|
role text NOT NULL,
|
||||||
|
token uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
invited_by uuid,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
expires_at timestamp with time zone DEFAULT (now() + '7 days'::interval) NOT NULL,
|
||||||
|
accepted_at timestamp with time zone,
|
||||||
|
accepted_by uuid,
|
||||||
|
revoked_at timestamp with time zone,
|
||||||
|
revoked_by uuid,
|
||||||
|
CONSTRAINT tenant_invites_role_check CHECK ((role = ANY (ARRAY['therapist'::text, 'secretary'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.tenant_features (
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
feature_key text NOT NULL,
|
||||||
|
enabled boolean DEFAULT false NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.tenant_feature_exceptions_log (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
feature_key text NOT NULL,
|
||||||
|
enabled boolean NOT NULL,
|
||||||
|
reason text,
|
||||||
|
created_by uuid,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.saas_admins (
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.owner_users (
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
role text DEFAULT 'admin'::text NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.user_settings (
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
theme_mode text DEFAULT 'dark'::text NOT NULL,
|
||||||
|
preset text DEFAULT 'Aura'::text NOT NULL,
|
||||||
|
primary_color text DEFAULT 'noir'::text NOT NULL,
|
||||||
|
surface_color text DEFAULT 'slate'::text NOT NULL,
|
||||||
|
menu_mode text DEFAULT 'static'::text NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
layout_variant text DEFAULT 'classic'::text NOT NULL,
|
||||||
|
CONSTRAINT user_settings_layout_variant_check CHECK ((layout_variant = ANY (ARRAY['classic'::text, 'rail'::text]))),
|
||||||
|
CONSTRAINT user_settings_menu_mode_check CHECK ((menu_mode = ANY (ARRAY['static'::text, 'overlay'::text]))),
|
||||||
|
CONSTRAINT user_settings_theme_mode_check CHECK ((theme_mode = ANY (ARRAY['light'::text, 'dark'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.company_profiles (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
nome_fantasia text,
|
||||||
|
razao_social text,
|
||||||
|
tipo_empresa text,
|
||||||
|
cnpj text,
|
||||||
|
ie text,
|
||||||
|
im text,
|
||||||
|
cep text,
|
||||||
|
logradouro text,
|
||||||
|
numero text,
|
||||||
|
complemento text,
|
||||||
|
bairro text,
|
||||||
|
cidade text,
|
||||||
|
estado text,
|
||||||
|
email text,
|
||||||
|
telefone text,
|
||||||
|
site text,
|
||||||
|
logo_url text,
|
||||||
|
redes_sociais jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.dev_user_credentials (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
user_id uuid,
|
||||||
|
email text NOT NULL,
|
||||||
|
password_dev text NOT NULL,
|
||||||
|
kind text DEFAULT 'custom'::text NOT NULL,
|
||||||
|
note text,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
199
database-novo/schema/04_tables/financial.sql
Normal file
199
database-novo/schema/04_tables/financial.sql
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Tables — Financeiro
|
||||||
|
-- =============================================================================
|
||||||
|
-- financial_records, financial_categories, financial_exceptions,
|
||||||
|
-- payment_settings, professional_pricing, therapist_payouts,
|
||||||
|
-- therapist_payout_records, services, insurance_plans, insurance_plan_services
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE public.financial_records (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
tenant_id uuid,
|
||||||
|
type public.financial_record_type DEFAULT 'receita'::public.financial_record_type NOT NULL,
|
||||||
|
amount numeric(10,2) NOT NULL,
|
||||||
|
description text,
|
||||||
|
category text,
|
||||||
|
payment_method text,
|
||||||
|
paid_at timestamp with time zone,
|
||||||
|
due_date date,
|
||||||
|
installments smallint DEFAULT 1,
|
||||||
|
installment_number smallint DEFAULT 1,
|
||||||
|
installment_group uuid,
|
||||||
|
agenda_evento_id uuid,
|
||||||
|
patient_id uuid,
|
||||||
|
clinic_fee_pct numeric(5,2) DEFAULT 0,
|
||||||
|
clinic_fee_amount numeric(10,2) DEFAULT 0,
|
||||||
|
net_amount numeric(10,2) GENERATED ALWAYS AS ((amount - clinic_fee_amount)) STORED,
|
||||||
|
insurance_plan_id uuid,
|
||||||
|
notes text,
|
||||||
|
tags text[],
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
deleted_at timestamp with time zone,
|
||||||
|
discount_amount numeric(10,2) DEFAULT 0 NOT NULL,
|
||||||
|
final_amount numeric(10,2) DEFAULT 0 NOT NULL,
|
||||||
|
status text DEFAULT 'pending'::text NOT NULL,
|
||||||
|
category_id uuid,
|
||||||
|
CONSTRAINT financial_records_amount_check CHECK ((amount >= (0)::numeric)),
|
||||||
|
CONSTRAINT financial_records_clinic_fee_amount_check CHECK ((clinic_fee_amount >= (0)::numeric)),
|
||||||
|
CONSTRAINT financial_records_clinic_fee_pct_check CHECK (((clinic_fee_pct >= (0)::numeric) AND (clinic_fee_pct <= (100)::numeric))),
|
||||||
|
CONSTRAINT financial_records_discount_amount_check CHECK ((discount_amount >= (0)::numeric)),
|
||||||
|
CONSTRAINT financial_records_final_amount_check CHECK ((final_amount >= (0)::numeric)),
|
||||||
|
CONSTRAINT financial_records_installments_check CHECK ((installments >= 1)),
|
||||||
|
CONSTRAINT financial_records_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'paid'::text, 'partial'::text, 'overdue'::text, 'cancelled'::text, 'refunded'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.financial_categories (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
name text NOT NULL,
|
||||||
|
type public.financial_record_type DEFAULT 'receita'::public.financial_record_type NOT NULL,
|
||||||
|
color text DEFAULT '#6366f1'::text,
|
||||||
|
icon text DEFAULT 'pi pi-tag'::text,
|
||||||
|
sort_order integer DEFAULT 0,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.financial_categories OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: financial_exceptions; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.financial_exceptions (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
exception_type text NOT NULL,
|
||||||
|
charge_mode text NOT NULL,
|
||||||
|
charge_value numeric(10,2),
|
||||||
|
charge_pct numeric(5,2),
|
||||||
|
min_hours_notice integer,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
updated_at timestamp with time zone DEFAULT now(),
|
||||||
|
CONSTRAINT financial_exceptions_charge_chk CHECK ((charge_mode = ANY (ARRAY['none'::text, 'full'::text, 'fixed_fee'::text, 'percentage'::text]))),
|
||||||
|
CONSTRAINT financial_exceptions_type_chk CHECK ((exception_type = ANY (ARRAY['patient_no_show'::text, 'patient_cancellation'::text, 'professional_cancellation'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.payment_settings (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
tenant_id uuid,
|
||||||
|
pix_ativo boolean DEFAULT false NOT NULL,
|
||||||
|
pix_tipo text DEFAULT 'cpf'::text NOT NULL,
|
||||||
|
pix_chave text DEFAULT ''::text NOT NULL,
|
||||||
|
pix_nome_titular text DEFAULT ''::text NOT NULL,
|
||||||
|
deposito_ativo boolean DEFAULT false NOT NULL,
|
||||||
|
deposito_banco text DEFAULT ''::text NOT NULL,
|
||||||
|
deposito_agencia text DEFAULT ''::text NOT NULL,
|
||||||
|
deposito_conta text DEFAULT ''::text NOT NULL,
|
||||||
|
deposito_tipo_conta text DEFAULT 'corrente'::text NOT NULL,
|
||||||
|
deposito_titular text DEFAULT ''::text NOT NULL,
|
||||||
|
deposito_cpf_cnpj text DEFAULT ''::text NOT NULL,
|
||||||
|
dinheiro_ativo boolean DEFAULT false NOT NULL,
|
||||||
|
cartao_ativo boolean DEFAULT false NOT NULL,
|
||||||
|
cartao_instrucao text DEFAULT ''::text NOT NULL,
|
||||||
|
convenio_ativo boolean DEFAULT false NOT NULL,
|
||||||
|
convenio_lista text DEFAULT ''::text NOT NULL,
|
||||||
|
observacoes_pagamento text DEFAULT ''::text NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
updated_at timestamp with time zone DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.professional_pricing (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
determined_commitment_id uuid,
|
||||||
|
price numeric(10,2) NOT NULL,
|
||||||
|
notes text,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
updated_at timestamp with time zone DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.therapist_payouts (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
period_start date NOT NULL,
|
||||||
|
period_end date NOT NULL,
|
||||||
|
total_sessions integer DEFAULT 0 NOT NULL,
|
||||||
|
gross_amount numeric(10,2) DEFAULT 0 NOT NULL,
|
||||||
|
clinic_fee_total numeric(10,2) DEFAULT 0 NOT NULL,
|
||||||
|
net_amount numeric(10,2) DEFAULT 0 NOT NULL,
|
||||||
|
status text DEFAULT 'pending'::text NOT NULL,
|
||||||
|
paid_at timestamp with time zone,
|
||||||
|
notes text,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT therapist_payouts_clinic_fee_total_check CHECK ((clinic_fee_total >= (0)::numeric)),
|
||||||
|
CONSTRAINT therapist_payouts_gross_amount_check CHECK ((gross_amount >= (0)::numeric)),
|
||||||
|
CONSTRAINT therapist_payouts_net_amount_check CHECK ((net_amount >= (0)::numeric)),
|
||||||
|
CONSTRAINT therapist_payouts_period_chk CHECK ((period_end >= period_start)),
|
||||||
|
CONSTRAINT therapist_payouts_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'paid'::text, 'cancelled'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.therapist_payout_records (
|
||||||
|
payout_id uuid NOT NULL,
|
||||||
|
financial_record_id uuid NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.services (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
name text NOT NULL,
|
||||||
|
description text,
|
||||||
|
price numeric(10,2) NOT NULL,
|
||||||
|
duration_min integer,
|
||||||
|
active boolean DEFAULT true NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
updated_at timestamp with time zone DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.insurance_plan_services (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
insurance_plan_id uuid NOT NULL,
|
||||||
|
name text NOT NULL,
|
||||||
|
value numeric(10,2) NOT NULL,
|
||||||
|
active boolean DEFAULT true NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
updated_at timestamp with time zone DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.insurance_plan_services OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: insurance_plans; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.insurance_plans (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
name text NOT NULL,
|
||||||
|
notes text,
|
||||||
|
default_value numeric(10,2),
|
||||||
|
active boolean DEFAULT true NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
updated_at timestamp with time zone DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
500
database-novo/schema/04_tables/infra.sql
Normal file
500
database-novo/schema/04_tables/infra.sql
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Tables — Infraestrutura (realtime, storage, supabase_functions)
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE _realtime.extensions (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
type text,
|
||||||
|
settings jsonb,
|
||||||
|
tenant_external_id text,
|
||||||
|
inserted_at timestamp(0) without time zone NOT NULL,
|
||||||
|
updated_at timestamp(0) without time zone NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE _realtime.extensions OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: schema_migrations; Type: TABLE; Schema: _realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE _realtime.schema_migrations (
|
||||||
|
version bigint NOT NULL,
|
||||||
|
inserted_at timestamp(0) without time zone
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE _realtime.schema_migrations OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: tenants; Type: TABLE; Schema: _realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE _realtime.tenants (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
name text,
|
||||||
|
external_id text,
|
||||||
|
jwt_secret text,
|
||||||
|
max_concurrent_users integer DEFAULT 200 NOT NULL,
|
||||||
|
inserted_at timestamp(0) without time zone NOT NULL,
|
||||||
|
updated_at timestamp(0) without time zone NOT NULL,
|
||||||
|
max_events_per_second integer DEFAULT 100 NOT NULL,
|
||||||
|
postgres_cdc_default text DEFAULT 'postgres_cdc_rls'::text,
|
||||||
|
max_bytes_per_second integer DEFAULT 100000 NOT NULL,
|
||||||
|
max_channels_per_client integer DEFAULT 100 NOT NULL,
|
||||||
|
max_joins_per_second integer DEFAULT 500 NOT NULL,
|
||||||
|
suspend boolean DEFAULT false,
|
||||||
|
jwt_jwks jsonb,
|
||||||
|
notify_private_alpha boolean DEFAULT false,
|
||||||
|
private_only boolean DEFAULT false NOT NULL,
|
||||||
|
migrations_ran integer DEFAULT 0,
|
||||||
|
broadcast_adapter character varying(255) DEFAULT 'gen_rpc'::character varying,
|
||||||
|
max_presence_events_per_second integer DEFAULT 1000,
|
||||||
|
max_payload_size_in_kb integer DEFAULT 3000,
|
||||||
|
CONSTRAINT jwt_secret_or_jwt_jwks_required CHECK (((jwt_secret IS NOT NULL) OR (jwt_jwks IS NOT NULL)))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE _realtime.tenants OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: audit_log_entries; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE realtime.messages (
|
||||||
|
topic text NOT NULL,
|
||||||
|
extension text NOT NULL,
|
||||||
|
payload jsonb,
|
||||||
|
event text,
|
||||||
|
private boolean DEFAULT false,
|
||||||
|
updated_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||||
|
inserted_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL
|
||||||
|
)
|
||||||
|
PARTITION BY RANGE (inserted_at);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE realtime.messages OWNER TO supabase_realtime_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: messages_2026_03_20; Type: TABLE; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE realtime.messages_2026_03_20 (
|
||||||
|
topic text NOT NULL,
|
||||||
|
extension text NOT NULL,
|
||||||
|
payload jsonb,
|
||||||
|
event text,
|
||||||
|
private boolean DEFAULT false,
|
||||||
|
updated_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||||
|
inserted_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE realtime.messages_2026_03_20 OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: messages_2026_03_21; Type: TABLE; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE realtime.messages_2026_03_21 (
|
||||||
|
topic text NOT NULL,
|
||||||
|
extension text NOT NULL,
|
||||||
|
payload jsonb,
|
||||||
|
event text,
|
||||||
|
private boolean DEFAULT false,
|
||||||
|
updated_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||||
|
inserted_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE realtime.messages_2026_03_21 OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: messages_2026_03_22; Type: TABLE; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE realtime.messages_2026_03_22 (
|
||||||
|
topic text NOT NULL,
|
||||||
|
extension text NOT NULL,
|
||||||
|
payload jsonb,
|
||||||
|
event text,
|
||||||
|
private boolean DEFAULT false,
|
||||||
|
updated_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||||
|
inserted_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE realtime.messages_2026_03_22 OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: messages_2026_03_23; Type: TABLE; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE realtime.messages_2026_03_23 (
|
||||||
|
topic text NOT NULL,
|
||||||
|
extension text NOT NULL,
|
||||||
|
payload jsonb,
|
||||||
|
event text,
|
||||||
|
private boolean DEFAULT false,
|
||||||
|
updated_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||||
|
inserted_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE realtime.messages_2026_03_23 OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: messages_2026_03_24; Type: TABLE; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE realtime.messages_2026_03_24 (
|
||||||
|
topic text NOT NULL,
|
||||||
|
extension text NOT NULL,
|
||||||
|
payload jsonb,
|
||||||
|
event text,
|
||||||
|
private boolean DEFAULT false,
|
||||||
|
updated_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||||
|
inserted_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE realtime.messages_2026_03_24 OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: messages_2026_03_25; Type: TABLE; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE realtime.messages_2026_03_25 (
|
||||||
|
topic text NOT NULL,
|
||||||
|
extension text NOT NULL,
|
||||||
|
payload jsonb,
|
||||||
|
event text,
|
||||||
|
private boolean DEFAULT false,
|
||||||
|
updated_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||||
|
inserted_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE realtime.messages_2026_03_25 OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: messages_2026_03_26; Type: TABLE; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE realtime.messages_2026_03_26 (
|
||||||
|
topic text NOT NULL,
|
||||||
|
extension text NOT NULL,
|
||||||
|
payload jsonb,
|
||||||
|
event text,
|
||||||
|
private boolean DEFAULT false,
|
||||||
|
updated_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||||
|
inserted_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE realtime.messages_2026_03_26 OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: schema_migrations; Type: TABLE; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE realtime.schema_migrations (
|
||||||
|
version bigint NOT NULL,
|
||||||
|
inserted_at timestamp(0) without time zone
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE realtime.schema_migrations OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: subscription; Type: TABLE; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE realtime.subscription (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
subscription_id uuid NOT NULL,
|
||||||
|
entity regclass NOT NULL,
|
||||||
|
filters realtime.user_defined_filter[] DEFAULT '{}'::realtime.user_defined_filter[] NOT NULL,
|
||||||
|
claims jsonb NOT NULL,
|
||||||
|
claims_role regrole GENERATED ALWAYS AS (realtime.to_regrole((claims ->> 'role'::text))) STORED NOT NULL,
|
||||||
|
created_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE realtime.subscription OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: subscription_id_seq; Type: SEQUENCE; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE realtime.subscription ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (
|
||||||
|
SEQUENCE NAME realtime.subscription_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: buckets; Type: TABLE; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE storage.buckets (
|
||||||
|
id text NOT NULL,
|
||||||
|
name text NOT NULL,
|
||||||
|
owner uuid,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
updated_at timestamp with time zone DEFAULT now(),
|
||||||
|
public boolean DEFAULT false,
|
||||||
|
avif_autodetection boolean DEFAULT false,
|
||||||
|
file_size_limit bigint,
|
||||||
|
allowed_mime_types text[],
|
||||||
|
owner_id text,
|
||||||
|
type storage.buckettype DEFAULT 'STANDARD'::storage.buckettype NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE storage.buckets OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: COLUMN buckets.owner; Type: COMMENT; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON COLUMN storage.buckets.owner IS 'Field is deprecated, use owner_id instead';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: buckets_analytics; Type: TABLE; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE storage.buckets_analytics (
|
||||||
|
name text NOT NULL,
|
||||||
|
type storage.buckettype DEFAULT 'ANALYTICS'::storage.buckettype NOT NULL,
|
||||||
|
format text DEFAULT 'ICEBERG'::text NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
deleted_at timestamp with time zone
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE storage.buckets_analytics OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: buckets_vectors; Type: TABLE; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE storage.buckets_vectors (
|
||||||
|
id text NOT NULL,
|
||||||
|
type storage.buckettype DEFAULT 'VECTOR'::storage.buckettype NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE storage.buckets_vectors OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: iceberg_namespaces; Type: TABLE; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE storage.iceberg_namespaces (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
bucket_name text NOT NULL,
|
||||||
|
name text NOT NULL COLLATE pg_catalog."C",
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
|
catalog_id uuid NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE storage.iceberg_namespaces OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: iceberg_tables; Type: TABLE; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE storage.iceberg_tables (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
namespace_id uuid NOT NULL,
|
||||||
|
bucket_name text NOT NULL,
|
||||||
|
name text NOT NULL COLLATE pg_catalog."C",
|
||||||
|
location text NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
remote_table_id text,
|
||||||
|
shard_key text,
|
||||||
|
shard_id text,
|
||||||
|
catalog_id uuid NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE storage.iceberg_tables OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: migrations; Type: TABLE; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE storage.migrations (
|
||||||
|
id integer NOT NULL,
|
||||||
|
name character varying(100) NOT NULL,
|
||||||
|
hash character varying(40) NOT NULL,
|
||||||
|
executed_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE storage.migrations OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: objects; Type: TABLE; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE storage.objects (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
bucket_id text,
|
||||||
|
name text,
|
||||||
|
owner uuid,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
updated_at timestamp with time zone DEFAULT now(),
|
||||||
|
last_accessed_at timestamp with time zone DEFAULT now(),
|
||||||
|
metadata jsonb,
|
||||||
|
path_tokens text[] GENERATED ALWAYS AS (string_to_array(name, '/'::text)) STORED,
|
||||||
|
version text,
|
||||||
|
owner_id text,
|
||||||
|
user_metadata jsonb
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE storage.objects OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: COLUMN objects.owner; Type: COMMENT; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON COLUMN storage.objects.owner IS 'Field is deprecated, use owner_id instead';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: s3_multipart_uploads; Type: TABLE; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE storage.s3_multipart_uploads (
|
||||||
|
id text NOT NULL,
|
||||||
|
in_progress_size bigint DEFAULT 0 NOT NULL,
|
||||||
|
upload_signature text NOT NULL,
|
||||||
|
bucket_id text NOT NULL,
|
||||||
|
key text NOT NULL COLLATE pg_catalog."C",
|
||||||
|
version text NOT NULL,
|
||||||
|
owner_id text,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
user_metadata jsonb
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE storage.s3_multipart_uploads OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: s3_multipart_uploads_parts; Type: TABLE; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE storage.s3_multipart_uploads_parts (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
upload_id text NOT NULL,
|
||||||
|
size bigint DEFAULT 0 NOT NULL,
|
||||||
|
part_number integer NOT NULL,
|
||||||
|
bucket_id text NOT NULL,
|
||||||
|
key text NOT NULL COLLATE pg_catalog."C",
|
||||||
|
etag text NOT NULL,
|
||||||
|
owner_id text,
|
||||||
|
version text NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE storage.s3_multipart_uploads_parts OWNER TO supabase_storage_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: vector_indexes; Type: TABLE; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE storage.vector_indexes (
|
||||||
|
id text DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
name text NOT NULL COLLATE pg_catalog."C",
|
||||||
|
bucket_id text NOT NULL,
|
||||||
|
data_type text NOT NULL,
|
||||||
|
dimension integer NOT NULL,
|
||||||
|
distance_metric text NOT NULL,
|
||||||
|
metadata_configuration jsonb,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE supabase_functions.hooks (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
hook_table_id integer NOT NULL,
|
||||||
|
hook_name text NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
request_id bigint
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE supabase_functions.hooks OWNER TO supabase_functions_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: TABLE hooks; Type: COMMENT; Schema: supabase_functions; Owner: supabase_functions_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON TABLE supabase_functions.hooks IS 'Supabase Functions Hooks: Audit trail for triggered hooks.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: hooks_id_seq; Type: SEQUENCE; Schema: supabase_functions; Owner: supabase_functions_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE supabase_functions.hooks_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER SEQUENCE supabase_functions.hooks_id_seq OWNER TO supabase_functions_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: hooks_id_seq; Type: SEQUENCE OWNED BY; Schema: supabase_functions; Owner: supabase_functions_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE supabase_functions.hooks_id_seq OWNED BY supabase_functions.hooks.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: migrations; Type: TABLE; Schema: supabase_functions; Owner: supabase_functions_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE supabase_functions.migrations (
|
||||||
|
version text NOT NULL,
|
||||||
|
inserted_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE supabase_functions.migrations OWNER TO supabase_functions_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: messages_2026_03_20; Type: TABLE ATTACH; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
267
database-novo/schema/04_tables/notifications.sql
Normal file
267
database-novo/schema/04_tables/notifications.sql
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Tables — Notificações + Email Templates
|
||||||
|
-- =============================================================================
|
||||||
|
-- notification_channels, notification_logs, notification_preferences,
|
||||||
|
-- notification_queue, notification_schedules, notification_templates,
|
||||||
|
-- notifications, email_templates_global, email_templates_tenant,
|
||||||
|
-- email_layout_config
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE public.notification_channels (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
channel text NOT NULL,
|
||||||
|
provider text NOT NULL,
|
||||||
|
is_active boolean DEFAULT false NOT NULL,
|
||||||
|
display_name text,
|
||||||
|
sender_address text,
|
||||||
|
credentials jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
|
connection_status text DEFAULT 'disconnected'::text,
|
||||||
|
last_health_check timestamp with time zone,
|
||||||
|
metadata jsonb DEFAULT '{}'::jsonb,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
deleted_at timestamp with time zone,
|
||||||
|
CONSTRAINT notification_channels_channel_check CHECK ((channel = ANY (ARRAY['whatsapp'::text, 'email'::text, 'sms'::text]))),
|
||||||
|
CONSTRAINT notification_channels_connection_status_check CHECK ((connection_status = ANY (ARRAY['connected'::text, 'disconnected'::text, 'connecting'::text, 'qr_pending'::text, 'error'::text]))),
|
||||||
|
CONSTRAINT notification_channels_provider_check CHECK ((provider = ANY (ARRAY['evolution_api'::text, 'meta_official'::text, 'twilio'::text, 'zenvia'::text, 'sendgrid'::text, 'resend'::text, 'smtp'::text, 'zapi'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.notification_channels OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: notification_logs; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.notification_logs (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
queue_id uuid,
|
||||||
|
agenda_evento_id uuid,
|
||||||
|
patient_id uuid NOT NULL,
|
||||||
|
channel text NOT NULL,
|
||||||
|
template_key text NOT NULL,
|
||||||
|
schedule_key text,
|
||||||
|
recipient_address text NOT NULL,
|
||||||
|
resolved_message text,
|
||||||
|
resolved_vars jsonb,
|
||||||
|
status text NOT NULL,
|
||||||
|
provider text,
|
||||||
|
provider_message_id text,
|
||||||
|
provider_status text,
|
||||||
|
provider_response jsonb,
|
||||||
|
sent_at timestamp with time zone,
|
||||||
|
delivered_at timestamp with time zone,
|
||||||
|
read_at timestamp with time zone,
|
||||||
|
failed_at timestamp with time zone,
|
||||||
|
failure_reason text,
|
||||||
|
estimated_cost_brl numeric(8,4) DEFAULT 0,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT notification_logs_status_check CHECK ((status = ANY (ARRAY['sent'::text, 'delivered'::text, 'read'::text, 'failed'::text, 'bounced'::text, 'opted_out'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.notification_logs OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: notification_preferences; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.notification_preferences (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
patient_id uuid NOT NULL,
|
||||||
|
whatsapp_opt_in boolean DEFAULT true NOT NULL,
|
||||||
|
email_opt_in boolean DEFAULT true NOT NULL,
|
||||||
|
sms_opt_in boolean DEFAULT false NOT NULL,
|
||||||
|
preferred_time_start time without time zone DEFAULT '08:00:00'::time without time zone,
|
||||||
|
preferred_time_end time without time zone DEFAULT '20:00:00'::time without time zone,
|
||||||
|
lgpd_consent_given boolean DEFAULT false NOT NULL,
|
||||||
|
lgpd_consent_date timestamp with time zone,
|
||||||
|
lgpd_consent_version text,
|
||||||
|
lgpd_consent_ip inet,
|
||||||
|
lgpd_opt_out_date timestamp with time zone,
|
||||||
|
lgpd_opt_out_reason text,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
deleted_at timestamp with time zone
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.notification_preferences OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: notification_queue; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.notification_queue (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
agenda_evento_id uuid,
|
||||||
|
patient_id uuid NOT NULL,
|
||||||
|
channel text NOT NULL,
|
||||||
|
template_key text NOT NULL,
|
||||||
|
schedule_key text NOT NULL,
|
||||||
|
resolved_vars jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
|
recipient_address text NOT NULL,
|
||||||
|
status text DEFAULT 'pendente'::text NOT NULL,
|
||||||
|
scheduled_at timestamp with time zone NOT NULL,
|
||||||
|
sent_at timestamp with time zone,
|
||||||
|
next_retry_at timestamp with time zone,
|
||||||
|
attempts integer DEFAULT 0 NOT NULL,
|
||||||
|
max_attempts integer DEFAULT 5 NOT NULL,
|
||||||
|
last_error text,
|
||||||
|
idempotency_key text NOT NULL,
|
||||||
|
provider_message_id text,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT notification_queue_channel_check CHECK ((channel = ANY (ARRAY['whatsapp'::text, 'email'::text, 'sms'::text]))),
|
||||||
|
CONSTRAINT notification_queue_status_check CHECK ((status = ANY (ARRAY['pendente'::text, 'processando'::text, 'enviado'::text, 'falhou'::text, 'cancelado'::text, 'ignorado'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.notification_queue OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: notification_schedules; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.notification_schedules (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
schedule_key text NOT NULL,
|
||||||
|
event_type text NOT NULL,
|
||||||
|
trigger_type text NOT NULL,
|
||||||
|
offset_minutes integer DEFAULT 0,
|
||||||
|
whatsapp_enabled boolean DEFAULT true NOT NULL,
|
||||||
|
email_enabled boolean DEFAULT true NOT NULL,
|
||||||
|
sms_enabled boolean DEFAULT false NOT NULL,
|
||||||
|
allowed_time_start time without time zone DEFAULT '08:00:00'::time without time zone,
|
||||||
|
allowed_time_end time without time zone DEFAULT '20:00:00'::time without time zone,
|
||||||
|
skip_weekends boolean DEFAULT false,
|
||||||
|
skip_holidays boolean DEFAULT false,
|
||||||
|
is_active boolean DEFAULT true NOT NULL,
|
||||||
|
sort_order integer DEFAULT 0,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
deleted_at timestamp with time zone,
|
||||||
|
CONSTRAINT notification_schedules_event_type_check CHECK ((event_type = ANY (ARRAY['lembrete_sessao'::text, 'confirmacao_sessao'::text, 'cancelamento_sessao'::text, 'reagendamento'::text, 'cobranca_pendente'::text, 'boas_vindas_paciente'::text]))),
|
||||||
|
CONSTRAINT notification_schedules_trigger_type_check CHECK ((trigger_type = ANY (ARRAY['before_event'::text, 'after_event'::text, 'immediate'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.notification_schedules OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: notification_templates; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.notification_templates (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
tenant_id uuid,
|
||||||
|
owner_id uuid,
|
||||||
|
key text NOT NULL,
|
||||||
|
domain text NOT NULL,
|
||||||
|
channel text NOT NULL,
|
||||||
|
event_type text NOT NULL,
|
||||||
|
body_text text NOT NULL,
|
||||||
|
meta_template_name text,
|
||||||
|
meta_template_namespace text,
|
||||||
|
meta_components jsonb,
|
||||||
|
meta_status text DEFAULT 'draft'::text,
|
||||||
|
variables jsonb DEFAULT '[]'::jsonb,
|
||||||
|
version integer DEFAULT 1 NOT NULL,
|
||||||
|
is_active boolean DEFAULT true NOT NULL,
|
||||||
|
is_default boolean DEFAULT false NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
deleted_at timestamp with time zone,
|
||||||
|
CONSTRAINT notification_templates_channel_check CHECK ((channel = ANY (ARRAY['whatsapp'::text, 'sms'::text]))),
|
||||||
|
CONSTRAINT notification_templates_domain_check CHECK ((domain = ANY (ARRAY['session'::text, 'intake'::text, 'billing'::text, 'system'::text]))),
|
||||||
|
CONSTRAINT notification_templates_event_type_check CHECK ((event_type = ANY (ARRAY['lembrete_sessao'::text, 'confirmacao_sessao'::text, 'cancelamento_sessao'::text, 'reagendamento'::text, 'cobranca_pendente'::text, 'boas_vindas_paciente'::text, 'intake_recebido'::text, 'intake_aprovado'::text, 'intake_rejeitado'::text]))),
|
||||||
|
CONSTRAINT notification_templates_meta_status_check CHECK ((meta_status = ANY (ARRAY['draft'::text, 'pending_approval'::text, 'approved'::text, 'rejected'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.notification_templates OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: notifications; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.notifications (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
tenant_id uuid,
|
||||||
|
type text NOT NULL,
|
||||||
|
ref_id uuid,
|
||||||
|
ref_table text,
|
||||||
|
payload jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
|
read_at timestamp with time zone,
|
||||||
|
archived boolean DEFAULT false NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT notifications_type_check CHECK ((type = ANY (ARRAY['new_scheduling'::text, 'new_patient'::text, 'recurrence_alert'::text, 'session_status'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.email_layout_config (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
header_config jsonb DEFAULT '{"layout": null, "content": "", "enabled": false}'::jsonb NOT NULL,
|
||||||
|
footer_config jsonb DEFAULT '{"layout": null, "content": "", "enabled": false}'::jsonb NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.email_layout_config OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: email_templates_global; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.email_templates_global (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
key text NOT NULL,
|
||||||
|
domain text NOT NULL,
|
||||||
|
channel text DEFAULT 'email'::text NOT NULL,
|
||||||
|
subject text NOT NULL,
|
||||||
|
body_html text NOT NULL,
|
||||||
|
body_text text,
|
||||||
|
version integer DEFAULT 1 NOT NULL,
|
||||||
|
is_active boolean DEFAULT true NOT NULL,
|
||||||
|
variables jsonb,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.email_templates_global OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: email_templates_tenant; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.email_templates_tenant (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
owner_id uuid,
|
||||||
|
template_key text NOT NULL,
|
||||||
|
subject text,
|
||||||
|
body_html text,
|
||||||
|
body_text text,
|
||||||
|
enabled boolean DEFAULT true NOT NULL,
|
||||||
|
synced_version integer,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
185
database-novo/schema/04_tables/patients.sql
Normal file
185
database-novo/schema/04_tables/patients.sql
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Tables — Pacientes
|
||||||
|
-- =============================================================================
|
||||||
|
-- patients, patient_groups, patient_group_patient, patient_tags,
|
||||||
|
-- patient_patient_tag, patient_intake_requests, patient_invites,
|
||||||
|
-- patient_discounts
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE public.patients (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
nome_completo text NOT NULL,
|
||||||
|
email_principal text,
|
||||||
|
telefone text,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
owner_id uuid,
|
||||||
|
avatar_url text,
|
||||||
|
status text DEFAULT 'Ativo'::text,
|
||||||
|
last_attended_at timestamp with time zone,
|
||||||
|
is_native boolean DEFAULT false,
|
||||||
|
naturalidade text,
|
||||||
|
data_nascimento date,
|
||||||
|
rg text,
|
||||||
|
cpf text,
|
||||||
|
identification_color text,
|
||||||
|
genero text,
|
||||||
|
estado_civil text,
|
||||||
|
email_alternativo text,
|
||||||
|
pais text DEFAULT 'Brasil'::text,
|
||||||
|
cep text,
|
||||||
|
cidade text,
|
||||||
|
estado text,
|
||||||
|
endereco text,
|
||||||
|
numero text,
|
||||||
|
bairro text,
|
||||||
|
complemento text,
|
||||||
|
escolaridade text,
|
||||||
|
profissao text,
|
||||||
|
nome_parente text,
|
||||||
|
grau_parentesco text,
|
||||||
|
telefone_alternativo text,
|
||||||
|
onde_nos_conheceu text,
|
||||||
|
encaminhado_por text,
|
||||||
|
nome_responsavel text,
|
||||||
|
telefone_responsavel text,
|
||||||
|
cpf_responsavel text,
|
||||||
|
observacao_responsavel text,
|
||||||
|
cobranca_no_responsavel boolean DEFAULT false,
|
||||||
|
observacoes text,
|
||||||
|
notas_internas text,
|
||||||
|
updated_at timestamp with time zone DEFAULT now(),
|
||||||
|
telefone_parente text,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
responsible_member_id uuid NOT NULL,
|
||||||
|
user_id uuid,
|
||||||
|
patient_scope text DEFAULT 'clinic'::text NOT NULL,
|
||||||
|
therapist_member_id uuid,
|
||||||
|
CONSTRAINT cpf_responsavel_format_check CHECK (((cpf_responsavel IS NULL) OR (cpf_responsavel ~ '^\d{11}$'::text))),
|
||||||
|
CONSTRAINT patients_cpf_format_check CHECK (((cpf IS NULL) OR (cpf ~ '^\d{11}$'::text))),
|
||||||
|
CONSTRAINT patients_patient_scope_check CHECK ((patient_scope = ANY (ARRAY['clinic'::text, 'therapist'::text]))),
|
||||||
|
CONSTRAINT patients_status_check CHECK ((status = ANY (ARRAY['Ativo'::text, 'Inativo'::text, 'Alta'::text, 'Encaminhado'::text, 'Arquivado'::text]))),
|
||||||
|
CONSTRAINT patients_therapist_scope_consistency CHECK ((((patient_scope = 'clinic'::text) AND (therapist_member_id IS NULL)) OR ((patient_scope = 'therapist'::text) AND (therapist_member_id IS NOT NULL))))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.patient_groups (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
nome text NOT NULL,
|
||||||
|
descricao text,
|
||||||
|
cor text,
|
||||||
|
is_active boolean DEFAULT true NOT NULL,
|
||||||
|
is_system boolean DEFAULT false NOT NULL,
|
||||||
|
owner_id uuid DEFAULT auth.uid() NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
therapist_id uuid,
|
||||||
|
tenant_id uuid NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.patient_group_patient (
|
||||||
|
patient_group_id uuid NOT NULL,
|
||||||
|
patient_id uuid NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
tenant_id uuid NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.patient_tags (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
nome text NOT NULL,
|
||||||
|
cor text,
|
||||||
|
is_padrao boolean DEFAULT false NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone,
|
||||||
|
tenant_id uuid NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.patient_patient_tag (
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
patient_id uuid NOT NULL,
|
||||||
|
tag_id uuid NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.patient_intake_requests (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
token text NOT NULL,
|
||||||
|
consent boolean DEFAULT false NOT NULL,
|
||||||
|
status text DEFAULT 'new'::text NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
converted_patient_id uuid,
|
||||||
|
rejected_reason text,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
cpf text,
|
||||||
|
rg text,
|
||||||
|
cep text,
|
||||||
|
nome_completo text,
|
||||||
|
email_principal text,
|
||||||
|
telefone text,
|
||||||
|
pais text,
|
||||||
|
cidade text,
|
||||||
|
estado text,
|
||||||
|
endereco text,
|
||||||
|
numero text,
|
||||||
|
bairro text,
|
||||||
|
complemento text,
|
||||||
|
data_nascimento date,
|
||||||
|
naturalidade text,
|
||||||
|
genero text,
|
||||||
|
estado_civil text,
|
||||||
|
onde_nos_conheceu text,
|
||||||
|
encaminhado_por text,
|
||||||
|
observacoes text,
|
||||||
|
notas_internas text,
|
||||||
|
email_alternativo text,
|
||||||
|
telefone_alternativo text,
|
||||||
|
profissao text,
|
||||||
|
escolaridade text,
|
||||||
|
nacionalidade text,
|
||||||
|
avatar_url text,
|
||||||
|
tenant_id uuid,
|
||||||
|
CONSTRAINT chk_intakes_status CHECK ((status = ANY (ARRAY['new'::text, 'converted'::text, 'rejected'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.patient_invites (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
token text NOT NULL,
|
||||||
|
active boolean DEFAULT true NOT NULL,
|
||||||
|
expires_at timestamp with time zone,
|
||||||
|
max_uses integer,
|
||||||
|
uses integer DEFAULT 0 NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
tenant_id uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.patient_discounts (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
patient_id uuid NOT NULL,
|
||||||
|
discount_pct numeric(5,2) DEFAULT 0,
|
||||||
|
discount_flat numeric(10,2) DEFAULT 0,
|
||||||
|
reason text,
|
||||||
|
active boolean DEFAULT true NOT NULL,
|
||||||
|
active_from timestamp with time zone DEFAULT now(),
|
||||||
|
active_to timestamp with time zone,
|
||||||
|
created_at timestamp with time zone DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
371
database-novo/schema/04_tables/plans_billing.sql
Normal file
371
database-novo/schema/04_tables/plans_billing.sql
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Tables — Plans, Billing, Subscriptions
|
||||||
|
-- =============================================================================
|
||||||
|
-- plans, plan_prices, plan_features, plan_public, plan_public_bullets,
|
||||||
|
-- features, entitlements_invalidation, subscriptions, subscription_events,
|
||||||
|
-- subscription_intents_personal, subscription_intents_tenant,
|
||||||
|
-- subscription_intents_legacy, billing_contracts,
|
||||||
|
-- addon_credits, addon_products, addon_transactions,
|
||||||
|
-- modules, module_features, tenant_modules
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE public.plans (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
key text NOT NULL,
|
||||||
|
name text NOT NULL,
|
||||||
|
description text,
|
||||||
|
is_active boolean DEFAULT true NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
price_cents integer DEFAULT 0 NOT NULL,
|
||||||
|
currency text DEFAULT 'BRL'::text NOT NULL,
|
||||||
|
billing_interval text DEFAULT 'month'::text NOT NULL,
|
||||||
|
target text,
|
||||||
|
max_supervisees integer,
|
||||||
|
CONSTRAINT plans_target_check CHECK ((target = ANY (ARRAY['patient'::text, 'therapist'::text, 'clinic'::text, 'supervisor'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.plan_prices (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
plan_id uuid NOT NULL,
|
||||||
|
currency text DEFAULT 'BRL'::text NOT NULL,
|
||||||
|
"interval" text NOT NULL,
|
||||||
|
amount_cents integer NOT NULL,
|
||||||
|
is_active boolean DEFAULT true NOT NULL,
|
||||||
|
active_from timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
active_to timestamp with time zone,
|
||||||
|
source text DEFAULT 'manual'::text NOT NULL,
|
||||||
|
provider text,
|
||||||
|
provider_price_id text,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT plan_prices_amount_cents_check CHECK ((amount_cents >= 0)),
|
||||||
|
CONSTRAINT plan_prices_interval_check CHECK (("interval" = ANY (ARRAY['month'::text, 'year'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.plan_features (
|
||||||
|
plan_id uuid NOT NULL,
|
||||||
|
feature_id uuid NOT NULL,
|
||||||
|
enabled boolean DEFAULT true NOT NULL,
|
||||||
|
limits jsonb,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.plan_public (
|
||||||
|
plan_id uuid NOT NULL,
|
||||||
|
public_name text DEFAULT ''::text NOT NULL,
|
||||||
|
public_description text DEFAULT ''::text NOT NULL,
|
||||||
|
badge text,
|
||||||
|
is_featured boolean DEFAULT false NOT NULL,
|
||||||
|
is_visible boolean DEFAULT true NOT NULL,
|
||||||
|
sort_order integer DEFAULT 0 NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.plan_public_bullets (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
plan_id uuid NOT NULL,
|
||||||
|
text text NOT NULL,
|
||||||
|
sort_order integer DEFAULT 0 NOT NULL,
|
||||||
|
highlight boolean DEFAULT false NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.features (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
key text NOT NULL,
|
||||||
|
description text,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
descricao text DEFAULT ''::text NOT NULL,
|
||||||
|
name text DEFAULT ''::text NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.features OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: COLUMN features.descricao; Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON COLUMN public.features.descricao IS 'Descrição humana da feature (exibição no admin e documentação).';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: feriados; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.entitlements_invalidation (
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
changed_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.subscriptions (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
user_id uuid,
|
||||||
|
plan_id uuid NOT NULL,
|
||||||
|
status text DEFAULT 'active'::text NOT NULL,
|
||||||
|
current_period_start timestamp with time zone,
|
||||||
|
current_period_end timestamp with time zone,
|
||||||
|
cancel_at_period_end boolean DEFAULT false NOT NULL,
|
||||||
|
provider text DEFAULT 'manual'::text NOT NULL,
|
||||||
|
provider_customer_id text,
|
||||||
|
provider_subscription_id text,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
tenant_id uuid,
|
||||||
|
plan_key text,
|
||||||
|
"interval" text,
|
||||||
|
source text DEFAULT 'manual'::text NOT NULL,
|
||||||
|
started_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
canceled_at timestamp with time zone,
|
||||||
|
activated_at timestamp with time zone,
|
||||||
|
past_due_since timestamp with time zone,
|
||||||
|
suspended_at timestamp with time zone,
|
||||||
|
suspended_reason text,
|
||||||
|
cancelled_at timestamp with time zone,
|
||||||
|
cancel_reason text,
|
||||||
|
expired_at timestamp with time zone,
|
||||||
|
CONSTRAINT subscriptions_interval_check CHECK (("interval" = ANY (ARRAY['month'::text, 'year'::text]))),
|
||||||
|
CONSTRAINT subscriptions_owner_xor CHECK ((((tenant_id IS NOT NULL) AND (user_id IS NULL)) OR ((tenant_id IS NULL) AND (user_id IS NOT NULL)))),
|
||||||
|
CONSTRAINT subscriptions_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'active'::text, 'past_due'::text, 'suspended'::text, 'cancelled'::text, 'expired'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE public.subscription_events (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
subscription_id uuid NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
event_type text NOT NULL,
|
||||||
|
old_plan_id uuid,
|
||||||
|
new_plan_id uuid,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
created_by uuid,
|
||||||
|
source text DEFAULT 'admin_ui'::text,
|
||||||
|
reason text,
|
||||||
|
metadata jsonb,
|
||||||
|
owner_type text NOT NULL,
|
||||||
|
owner_ref uuid NOT NULL,
|
||||||
|
CONSTRAINT subscription_events_owner_ref_consistency_chk CHECK ((owner_id = owner_ref)),
|
||||||
|
CONSTRAINT subscription_events_owner_type_chk CHECK (((owner_type IS NULL) OR (owner_type = ANY (ARRAY['clinic'::text, 'therapist'::text]))))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.subscription_intents_personal (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
created_by_user_id uuid,
|
||||||
|
email text NOT NULL,
|
||||||
|
plan_id uuid NOT NULL,
|
||||||
|
plan_key text,
|
||||||
|
"interval" text,
|
||||||
|
amount_cents integer,
|
||||||
|
currency text,
|
||||||
|
status text DEFAULT 'new'::text NOT NULL,
|
||||||
|
source text DEFAULT 'manual'::text NOT NULL,
|
||||||
|
notes text,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
paid_at timestamp with time zone,
|
||||||
|
subscription_id uuid,
|
||||||
|
CONSTRAINT sint_personal_interval_check CHECK ((("interval" IS NULL) OR ("interval" = ANY (ARRAY['month'::text, 'year'::text])))),
|
||||||
|
CONSTRAINT sint_personal_status_check CHECK ((status = ANY (ARRAY['new'::text, 'waiting_payment'::text, 'paid'::text, 'canceled'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.subscription_intents_tenant (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
created_by_user_id uuid,
|
||||||
|
email text NOT NULL,
|
||||||
|
plan_id uuid NOT NULL,
|
||||||
|
plan_key text,
|
||||||
|
"interval" text,
|
||||||
|
amount_cents integer,
|
||||||
|
currency text,
|
||||||
|
status text DEFAULT 'new'::text NOT NULL,
|
||||||
|
source text DEFAULT 'manual'::text NOT NULL,
|
||||||
|
notes text,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
paid_at timestamp with time zone,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
subscription_id uuid,
|
||||||
|
CONSTRAINT sint_tenant_interval_check CHECK ((("interval" IS NULL) OR ("interval" = ANY (ARRAY['month'::text, 'year'::text])))),
|
||||||
|
CONSTRAINT sint_tenant_status_check CHECK ((status = ANY (ARRAY['new'::text, 'waiting_payment'::text, 'paid'::text, 'canceled'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.subscription_intents_legacy (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
user_id uuid,
|
||||||
|
email text,
|
||||||
|
plan_key text NOT NULL,
|
||||||
|
"interval" text NOT NULL,
|
||||||
|
amount_cents integer NOT NULL,
|
||||||
|
currency text DEFAULT 'BRL'::text NOT NULL,
|
||||||
|
status text DEFAULT 'new'::text NOT NULL,
|
||||||
|
source text DEFAULT 'landing'::text NOT NULL,
|
||||||
|
notes text,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
paid_at timestamp with time zone,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
created_by_user_id uuid,
|
||||||
|
CONSTRAINT subscription_intents_interval_check CHECK (("interval" = ANY (ARRAY['month'::text, 'year'::text]))),
|
||||||
|
CONSTRAINT subscription_intents_status_check CHECK ((status = ANY (ARRAY['new'::text, 'waiting_payment'::text, 'paid'::text, 'canceled'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.billing_contracts (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
patient_id uuid NOT NULL,
|
||||||
|
type text NOT NULL,
|
||||||
|
total_sessions integer,
|
||||||
|
sessions_used integer DEFAULT 0,
|
||||||
|
package_price numeric(10,2),
|
||||||
|
amount numeric(10,2),
|
||||||
|
billing_interval text,
|
||||||
|
active_from timestamp with time zone DEFAULT now(),
|
||||||
|
active_to timestamp with time zone,
|
||||||
|
status text DEFAULT 'active'::text NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
CONSTRAINT billing_contracts_interval_chk CHECK (((billing_interval IS NULL) OR (billing_interval = ANY (ARRAY['monthly'::text, 'weekly'::text])))),
|
||||||
|
CONSTRAINT billing_contracts_sess_used_chk CHECK (((sessions_used IS NULL) OR (sessions_used >= 0))),
|
||||||
|
CONSTRAINT billing_contracts_status_chk CHECK ((status = ANY (ARRAY['active'::text, 'completed'::text, 'cancelled'::text]))),
|
||||||
|
CONSTRAINT billing_contracts_total_sess_chk CHECK (((total_sessions IS NULL) OR (total_sessions > 0))),
|
||||||
|
CONSTRAINT billing_contracts_type_chk CHECK ((type = ANY (ARRAY['per_session'::text, 'package'::text, 'subscription'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.addon_credits (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
owner_id uuid,
|
||||||
|
addon_type text NOT NULL,
|
||||||
|
balance integer DEFAULT 0 NOT NULL,
|
||||||
|
total_purchased integer DEFAULT 0 NOT NULL,
|
||||||
|
total_consumed integer DEFAULT 0 NOT NULL,
|
||||||
|
low_balance_threshold integer DEFAULT 10,
|
||||||
|
low_balance_notified boolean DEFAULT false,
|
||||||
|
daily_limit integer,
|
||||||
|
hourly_limit integer,
|
||||||
|
daily_used integer DEFAULT 0,
|
||||||
|
hourly_used integer DEFAULT 0,
|
||||||
|
daily_reset_at timestamp with time zone,
|
||||||
|
hourly_reset_at timestamp with time zone,
|
||||||
|
from_number_override text,
|
||||||
|
expires_at timestamp with time zone,
|
||||||
|
is_active boolean DEFAULT true,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
updated_at timestamp with time zone DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.addon_products (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
slug text NOT NULL,
|
||||||
|
name text NOT NULL,
|
||||||
|
description text,
|
||||||
|
addon_type text NOT NULL,
|
||||||
|
icon text DEFAULT 'pi pi-box'::text,
|
||||||
|
credits_amount integer DEFAULT 0,
|
||||||
|
price_cents integer DEFAULT 0 NOT NULL,
|
||||||
|
currency text DEFAULT 'BRL'::text,
|
||||||
|
is_active boolean DEFAULT true,
|
||||||
|
is_visible boolean DEFAULT true,
|
||||||
|
sort_order integer DEFAULT 0,
|
||||||
|
metadata jsonb DEFAULT '{}'::jsonb,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
updated_at timestamp with time zone DEFAULT now(),
|
||||||
|
deleted_at timestamp with time zone
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.addon_transactions (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
owner_id uuid,
|
||||||
|
addon_type text NOT NULL,
|
||||||
|
type text NOT NULL,
|
||||||
|
amount integer NOT NULL,
|
||||||
|
balance_before integer DEFAULT 0 NOT NULL,
|
||||||
|
balance_after integer DEFAULT 0 NOT NULL,
|
||||||
|
product_id uuid,
|
||||||
|
queue_id uuid,
|
||||||
|
description text,
|
||||||
|
admin_user_id uuid,
|
||||||
|
payment_method text,
|
||||||
|
payment_reference text,
|
||||||
|
price_cents integer,
|
||||||
|
currency text DEFAULT 'BRL'::text,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
metadata jsonb DEFAULT '{}'::jsonb
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.addon_transactions OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: TABLE addon_transactions; Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON TABLE public.addon_transactions IS 'Histórico de todas as transações de créditos: compras, consumo, ajustes, reembolsos.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_bloqueios; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.modules (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
key text NOT NULL,
|
||||||
|
name text NOT NULL,
|
||||||
|
description text,
|
||||||
|
is_active boolean DEFAULT true NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.module_features (
|
||||||
|
module_id uuid NOT NULL,
|
||||||
|
feature_id uuid NOT NULL,
|
||||||
|
enabled boolean DEFAULT true NOT NULL,
|
||||||
|
limits jsonb,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.tenant_modules (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
owner_id uuid NOT NULL,
|
||||||
|
module_id uuid NOT NULL,
|
||||||
|
status text DEFAULT 'active'::text NOT NULL,
|
||||||
|
settings jsonb,
|
||||||
|
provider text DEFAULT 'manual'::text NOT NULL,
|
||||||
|
provider_item_id text,
|
||||||
|
installed_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.tenant_modules OWNER TO supabase_admin;
|
||||||
204
database-novo/schema/04_tables/saas_admin.sql
Normal file
204
database-novo/schema/04_tables/saas_admin.sql
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Tables — SaaS Admin, FAQ, Docs, UI
|
||||||
|
-- =============================================================================
|
||||||
|
-- saas_docs, saas_doc_votos, saas_faq, saas_faq_itens,
|
||||||
|
-- feriados, global_notices, login_carousel_slides, notice_dismissals,
|
||||||
|
-- support_sessions
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE public.saas_doc_votos (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
doc_id uuid NOT NULL,
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
util boolean NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.saas_doc_votos OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: saas_docs; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.saas_docs (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
titulo text NOT NULL,
|
||||||
|
conteudo text DEFAULT ''::text NOT NULL,
|
||||||
|
medias jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||||
|
tipo_acesso text DEFAULT 'usuario'::text NOT NULL,
|
||||||
|
pagina_path text NOT NULL,
|
||||||
|
docs_relacionados uuid[] DEFAULT '{}'::uuid[] NOT NULL,
|
||||||
|
ativo boolean DEFAULT true NOT NULL,
|
||||||
|
ordem integer DEFAULT 0 NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
categoria text,
|
||||||
|
exibir_no_faq boolean DEFAULT false NOT NULL,
|
||||||
|
votos_util integer DEFAULT 0 NOT NULL,
|
||||||
|
votos_nao_util integer DEFAULT 0 NOT NULL,
|
||||||
|
CONSTRAINT saas_docs_tipo_acesso_check CHECK ((tipo_acesso = ANY (ARRAY['admin'::text, 'usuario'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.saas_docs OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: COLUMN saas_docs.categoria; Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON COLUMN public.saas_docs.categoria IS 'Agrupa docs no portal FAQ (ex: Conta, Agenda, Pagamentos)';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: COLUMN saas_docs.exibir_no_faq; Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON COLUMN public.saas_docs.exibir_no_faq IS 'Se true, a doc e seus itens FAQ aparecem no portal de FAQ';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: saas_faq; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.saas_faq (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
pergunta text NOT NULL,
|
||||||
|
categoria text,
|
||||||
|
publico boolean DEFAULT false NOT NULL,
|
||||||
|
votos integer DEFAULT 0 NOT NULL,
|
||||||
|
titulo text,
|
||||||
|
conteudo text,
|
||||||
|
tipo_acesso text DEFAULT 'usuario'::text NOT NULL,
|
||||||
|
pagina_path text NOT NULL,
|
||||||
|
pagina_label text,
|
||||||
|
medias jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||||
|
faqs_relacionados uuid[] DEFAULT '{}'::uuid[] NOT NULL,
|
||||||
|
ativo boolean DEFAULT true NOT NULL,
|
||||||
|
ordem integer DEFAULT 0 NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.saas_faq OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: saas_faq_itens; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.saas_faq_itens (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
doc_id uuid NOT NULL,
|
||||||
|
pergunta text NOT NULL,
|
||||||
|
resposta text,
|
||||||
|
ordem integer DEFAULT 0 NOT NULL,
|
||||||
|
ativo boolean DEFAULT true NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.saas_faq_itens OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: TABLE saas_faq_itens; Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON TABLE public.saas_faq_itens IS 'Pares pergunta/resposta vinculados a um documento de ajuda';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: services; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.feriados (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
tenant_id uuid,
|
||||||
|
owner_id uuid,
|
||||||
|
tipo text DEFAULT 'municipal'::text NOT NULL,
|
||||||
|
nome text NOT NULL,
|
||||||
|
data date NOT NULL,
|
||||||
|
cidade text,
|
||||||
|
estado text,
|
||||||
|
observacao text,
|
||||||
|
bloqueia_sessoes boolean DEFAULT false NOT NULL,
|
||||||
|
criado_em timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT feriados_tipo_check CHECK ((tipo = ANY (ARRAY['municipal'::text, 'personalizado'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.global_notices (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
title text,
|
||||||
|
message text DEFAULT ''::text NOT NULL,
|
||||||
|
variant text DEFAULT 'info'::text NOT NULL,
|
||||||
|
roles text[] DEFAULT '{}'::text[] NOT NULL,
|
||||||
|
contexts text[] DEFAULT '{}'::text[] NOT NULL,
|
||||||
|
starts_at timestamp with time zone,
|
||||||
|
ends_at timestamp with time zone,
|
||||||
|
is_active boolean DEFAULT true NOT NULL,
|
||||||
|
priority integer DEFAULT 0 NOT NULL,
|
||||||
|
dismissible boolean DEFAULT true NOT NULL,
|
||||||
|
persist_dismiss boolean DEFAULT true NOT NULL,
|
||||||
|
dismiss_scope text DEFAULT 'device'::text NOT NULL,
|
||||||
|
show_once boolean DEFAULT false NOT NULL,
|
||||||
|
max_views integer,
|
||||||
|
cooldown_minutes integer,
|
||||||
|
version integer DEFAULT 1 NOT NULL,
|
||||||
|
action_type text DEFAULT 'none'::text NOT NULL,
|
||||||
|
action_label text,
|
||||||
|
action_url text,
|
||||||
|
action_route text,
|
||||||
|
views_count integer DEFAULT 0 NOT NULL,
|
||||||
|
clicks_count integer DEFAULT 0 NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
created_by uuid,
|
||||||
|
content_align text DEFAULT 'left'::text NOT NULL,
|
||||||
|
link_target text DEFAULT '_blank'::text NOT NULL,
|
||||||
|
CONSTRAINT global_notices_action_type_check CHECK ((action_type = ANY (ARRAY['none'::text, 'internal'::text, 'external'::text]))),
|
||||||
|
CONSTRAINT global_notices_content_align_check CHECK ((content_align = ANY (ARRAY['left'::text, 'center'::text, 'right'::text, 'justify'::text]))),
|
||||||
|
CONSTRAINT global_notices_dismiss_scope_check CHECK ((dismiss_scope = ANY (ARRAY['session'::text, 'device'::text, 'user'::text]))),
|
||||||
|
CONSTRAINT global_notices_link_target_check CHECK ((link_target = ANY (ARRAY['_blank'::text, '_self'::text, '_parent'::text, '_top'::text]))),
|
||||||
|
CONSTRAINT global_notices_variant_check CHECK ((variant = ANY (ARRAY['info'::text, 'success'::text, 'warning'::text, 'error'::text])))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.login_carousel_slides (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
title text NOT NULL,
|
||||||
|
body text NOT NULL,
|
||||||
|
icon text DEFAULT 'pi-star'::text NOT NULL,
|
||||||
|
ordem integer DEFAULT 0 NOT NULL,
|
||||||
|
ativo boolean DEFAULT true NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
updated_at timestamp with time zone DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.notice_dismissals (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
notice_id uuid NOT NULL,
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
version integer DEFAULT 1 NOT NULL,
|
||||||
|
dismissed_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.support_sessions (
|
||||||
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
admin_id uuid NOT NULL,
|
||||||
|
token text DEFAULT encode(extensions.gen_random_bytes(32), 'hex'::text) NOT NULL,
|
||||||
|
expires_at timestamp with time zone DEFAULT (now() + '01:00:00'::interval) NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
596
database-novo/schema/05_views/views.sql
Normal file
596
database-novo/schema/05_views/views.sql
Normal file
@@ -0,0 +1,596 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Views
|
||||||
|
-- =============================================================================
|
||||||
|
-- current_tenant_id, owner_feature_entitlements, subscription_intents,
|
||||||
|
-- v_auth_users_public, v_cashflow_projection, v_commitment_totals,
|
||||||
|
-- v_patient_groups_with_counts, v_plan_active_prices, v_public_pricing,
|
||||||
|
-- v_subscription_feature_mismatch, v_subscription_health, v_subscription_health_v2,
|
||||||
|
-- v_tag_patient_counts, v_tenant_active_subscription, v_tenant_entitlements,
|
||||||
|
-- v_tenant_entitlements_full, v_tenant_entitlements_json,
|
||||||
|
-- v_tenant_feature_exceptions, v_tenant_feature_mismatch,
|
||||||
|
-- v_tenant_members_with_profiles, v_tenant_people, v_tenant_staff,
|
||||||
|
-- v_user_active_subscription, v_user_entitlements
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE VIEW public.current_tenant_id AS
|
||||||
|
SELECT current_setting('request.jwt.claim.tenant_id'::text, true) AS current_setting;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.current_tenant_id OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: determined_commitment_fields; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
|
||||||
|
CREATE VIEW public.owner_feature_entitlements AS
|
||||||
|
WITH base AS (
|
||||||
|
SELECT s.user_id AS owner_id,
|
||||||
|
f.key AS feature_key,
|
||||||
|
pf.limits,
|
||||||
|
'plan'::text AS source
|
||||||
|
FROM ((public.subscriptions s
|
||||||
|
JOIN public.plan_features pf ON (((pf.plan_id = s.plan_id) AND (pf.enabled = true))))
|
||||||
|
JOIN public.features f ON ((f.id = pf.feature_id)))
|
||||||
|
WHERE ((s.status = 'active'::text) AND (s.user_id IS NOT NULL))
|
||||||
|
UNION ALL
|
||||||
|
SELECT tm.owner_id,
|
||||||
|
f.key AS feature_key,
|
||||||
|
mf.limits,
|
||||||
|
'module'::text AS source
|
||||||
|
FROM (((public.tenant_modules tm
|
||||||
|
JOIN public.modules m ON (((m.id = tm.module_id) AND (m.is_active = true))))
|
||||||
|
JOIN public.module_features mf ON (((mf.module_id = m.id) AND (mf.enabled = true))))
|
||||||
|
JOIN public.features f ON ((f.id = mf.feature_id)))
|
||||||
|
WHERE ((tm.status = 'active'::text) AND (tm.owner_id IS NOT NULL))
|
||||||
|
)
|
||||||
|
SELECT owner_id,
|
||||||
|
feature_key,
|
||||||
|
array_agg(DISTINCT source) AS sources,
|
||||||
|
jsonb_agg(limits) FILTER (WHERE (limits IS NOT NULL)) AS limits_list
|
||||||
|
FROM base
|
||||||
|
GROUP BY owner_id, feature_key;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.owner_feature_entitlements OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: owner_users; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
|
||||||
|
CREATE VIEW public.subscription_intents AS
|
||||||
|
SELECT t.id,
|
||||||
|
t.user_id,
|
||||||
|
t.created_by_user_id,
|
||||||
|
t.email,
|
||||||
|
t.plan_id,
|
||||||
|
t.plan_key,
|
||||||
|
t."interval",
|
||||||
|
t.amount_cents,
|
||||||
|
t.currency,
|
||||||
|
t.status,
|
||||||
|
t.source,
|
||||||
|
t.notes,
|
||||||
|
t.created_at,
|
||||||
|
t.paid_at,
|
||||||
|
t.tenant_id,
|
||||||
|
t.subscription_id,
|
||||||
|
'clinic'::text AS plan_target
|
||||||
|
FROM public.subscription_intents_tenant t
|
||||||
|
UNION ALL
|
||||||
|
SELECT p.id,
|
||||||
|
p.user_id,
|
||||||
|
p.created_by_user_id,
|
||||||
|
p.email,
|
||||||
|
p.plan_id,
|
||||||
|
p.plan_key,
|
||||||
|
p."interval",
|
||||||
|
p.amount_cents,
|
||||||
|
p.currency,
|
||||||
|
p.status,
|
||||||
|
p.source,
|
||||||
|
p.notes,
|
||||||
|
p.created_at,
|
||||||
|
p.paid_at,
|
||||||
|
NULL::uuid AS tenant_id,
|
||||||
|
p.subscription_id,
|
||||||
|
'therapist'::text AS plan_target
|
||||||
|
FROM public.subscription_intents_personal p;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.subscription_intents OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: subscription_intents_legacy; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
|
||||||
|
CREATE VIEW public.v_auth_users_public AS
|
||||||
|
SELECT id AS user_id,
|
||||||
|
email,
|
||||||
|
created_at,
|
||||||
|
last_sign_in_at
|
||||||
|
FROM auth.users u;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.v_auth_users_public OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: v_cashflow_projection; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE VIEW public.v_cashflow_projection WITH (security_invoker='on') AS
|
||||||
|
SELECT gs.mes,
|
||||||
|
to_char(gs.mes, 'YYYY-MM'::text) AS mes_label,
|
||||||
|
COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'receita'::public.financial_record_type) AND (fr.status = ANY (ARRAY['pending'::text, 'overdue'::text])))), (0)::numeric) AS receitas_projetadas,
|
||||||
|
COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'despesa'::public.financial_record_type) AND (fr.status = ANY (ARRAY['pending'::text, 'overdue'::text])))), (0)::numeric) AS despesas_projetadas,
|
||||||
|
COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'receita'::public.financial_record_type) AND (fr.status = 'pending'::text))), (0)::numeric) AS receitas_pendentes,
|
||||||
|
COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'receita'::public.financial_record_type) AND (fr.status = 'overdue'::text))), (0)::numeric) AS receitas_vencidas,
|
||||||
|
COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'despesa'::public.financial_record_type) AND (fr.status = 'pending'::text))), (0)::numeric) AS despesas_pendentes,
|
||||||
|
COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'despesa'::public.financial_record_type) AND (fr.status = 'overdue'::text))), (0)::numeric) AS despesas_vencidas,
|
||||||
|
(COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'receita'::public.financial_record_type) AND (fr.status = ANY (ARRAY['pending'::text, 'overdue'::text])))), (0)::numeric) - COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'despesa'::public.financial_record_type) AND (fr.status = ANY (ARRAY['pending'::text, 'overdue'::text])))), (0)::numeric)) AS saldo_projetado,
|
||||||
|
count(fr.id) FILTER (WHERE (fr.status = ANY (ARRAY['pending'::text, 'overdue'::text]))) AS count_registros
|
||||||
|
FROM (generate_series(((date_trunc('month'::text, (CURRENT_DATE)::timestamp with time zone))::date)::timestamp with time zone, (((date_trunc('month'::text, (CURRENT_DATE)::timestamp with time zone) + '5 mons'::interval))::date)::timestamp with time zone, '1 mon'::interval) gs(mes)
|
||||||
|
LEFT JOIN public.financial_records fr ON (((fr.deleted_at IS NULL) AND (fr.status = ANY (ARRAY['pending'::text, 'overdue'::text])) AND ((date_trunc('month'::text, (fr.due_date)::timestamp with time zone))::date = gs.mes))))
|
||||||
|
GROUP BY gs.mes
|
||||||
|
ORDER BY gs.mes;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.v_cashflow_projection OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: VIEW v_cashflow_projection; Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON VIEW public.v_cashflow_projection IS 'Fluxo de caixa projetado: próximos 6 meses com totais de pending+overdue por due_date. Usa security_invoker=on — filtra automaticamente pelo auth.uid() via RLS de financial_records.';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: v_commitment_totals; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE VIEW public.v_commitment_totals AS
|
||||||
|
SELECT c.tenant_id,
|
||||||
|
c.id AS commitment_id,
|
||||||
|
(COALESCE(sum(l.minutes), (0)::bigint))::integer AS total_minutes
|
||||||
|
FROM (public.determined_commitments c
|
||||||
|
LEFT JOIN public.commitment_time_logs l ON ((l.commitment_id = c.id)))
|
||||||
|
GROUP BY c.tenant_id, c.id;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.v_commitment_totals OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: v_patient_groups_with_counts; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE VIEW public.v_patient_groups_with_counts AS
|
||||||
|
SELECT pg.id,
|
||||||
|
pg.nome,
|
||||||
|
pg.cor,
|
||||||
|
pg.owner_id,
|
||||||
|
pg.is_system,
|
||||||
|
pg.is_active,
|
||||||
|
pg.created_at,
|
||||||
|
pg.updated_at,
|
||||||
|
(COALESCE(count(pgp.patient_id), (0)::bigint))::integer AS patients_count
|
||||||
|
FROM (public.patient_groups pg
|
||||||
|
LEFT JOIN public.patient_group_patient pgp ON ((pgp.patient_group_id = pg.id)))
|
||||||
|
GROUP BY pg.id, pg.nome, pg.cor, pg.owner_id, pg.is_system, pg.is_active, pg.created_at, pg.updated_at;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.v_patient_groups_with_counts OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: v_plan_active_prices; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE VIEW public.v_plan_active_prices AS
|
||||||
|
SELECT plan_id,
|
||||||
|
max(
|
||||||
|
CASE
|
||||||
|
WHEN (("interval" = 'month'::text) AND is_active) THEN amount_cents
|
||||||
|
ELSE NULL::integer
|
||||||
|
END) AS monthly_cents,
|
||||||
|
max(
|
||||||
|
CASE
|
||||||
|
WHEN (("interval" = 'year'::text) AND is_active) THEN amount_cents
|
||||||
|
ELSE NULL::integer
|
||||||
|
END) AS yearly_cents,
|
||||||
|
max(
|
||||||
|
CASE
|
||||||
|
WHEN (("interval" = 'month'::text) AND is_active) THEN currency
|
||||||
|
ELSE NULL::text
|
||||||
|
END) AS monthly_currency,
|
||||||
|
max(
|
||||||
|
CASE
|
||||||
|
WHEN (("interval" = 'year'::text) AND is_active) THEN currency
|
||||||
|
ELSE NULL::text
|
||||||
|
END) AS yearly_currency
|
||||||
|
FROM public.plan_prices
|
||||||
|
GROUP BY plan_id;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.v_plan_active_prices OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: v_public_pricing; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE VIEW public.v_public_pricing AS
|
||||||
|
SELECT p.id AS plan_id,
|
||||||
|
p.key AS plan_key,
|
||||||
|
p.name AS plan_name,
|
||||||
|
COALESCE(pp.public_name, ''::text) AS public_name,
|
||||||
|
COALESCE(pp.public_description, ''::text) AS public_description,
|
||||||
|
pp.badge,
|
||||||
|
COALESCE(pp.is_featured, false) AS is_featured,
|
||||||
|
COALESCE(pp.is_visible, true) AS is_visible,
|
||||||
|
COALESCE(pp.sort_order, 0) AS sort_order,
|
||||||
|
ap.monthly_cents,
|
||||||
|
ap.yearly_cents,
|
||||||
|
ap.monthly_currency,
|
||||||
|
ap.yearly_currency,
|
||||||
|
COALESCE(( SELECT jsonb_agg(jsonb_build_object('id', b.id, 'text', b.text, 'highlight', b.highlight, 'sort_order', b.sort_order) ORDER BY b.sort_order, b.created_at) AS jsonb_agg
|
||||||
|
FROM public.plan_public_bullets b
|
||||||
|
WHERE (b.plan_id = p.id)), '[]'::jsonb) AS bullets,
|
||||||
|
p.target AS plan_target
|
||||||
|
FROM ((public.plans p
|
||||||
|
LEFT JOIN public.plan_public pp ON ((pp.plan_id = p.id)))
|
||||||
|
LEFT JOIN public.v_plan_active_prices ap ON ((ap.plan_id = p.id)))
|
||||||
|
ORDER BY COALESCE(pp.sort_order, 0), p.key;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.v_public_pricing OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: v_subscription_feature_mismatch; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE VIEW public.v_subscription_feature_mismatch AS
|
||||||
|
WITH expected AS (
|
||||||
|
SELECT s.user_id AS owner_id,
|
||||||
|
f.key AS feature_key
|
||||||
|
FROM ((public.subscriptions s
|
||||||
|
JOIN public.plan_features pf ON (((pf.plan_id = s.plan_id) AND (pf.enabled = true))))
|
||||||
|
JOIN public.features f ON ((f.id = pf.feature_id)))
|
||||||
|
WHERE ((s.status = 'active'::text) AND (s.tenant_id IS NULL) AND (s.user_id IS NOT NULL))
|
||||||
|
), actual AS (
|
||||||
|
SELECT e.owner_id,
|
||||||
|
e.feature_key
|
||||||
|
FROM public.owner_feature_entitlements e
|
||||||
|
)
|
||||||
|
SELECT COALESCE(expected.owner_id, actual.owner_id) AS owner_id,
|
||||||
|
COALESCE(expected.feature_key, actual.feature_key) AS feature_key,
|
||||||
|
CASE
|
||||||
|
WHEN ((expected.feature_key IS NOT NULL) AND (actual.feature_key IS NULL)) THEN 'missing_entitlement'::text
|
||||||
|
WHEN ((expected.feature_key IS NULL) AND (actual.feature_key IS NOT NULL)) THEN 'unexpected_entitlement'::text
|
||||||
|
ELSE NULL::text
|
||||||
|
END AS mismatch_type
|
||||||
|
FROM (expected
|
||||||
|
FULL JOIN actual ON (((expected.owner_id = actual.owner_id) AND (expected.feature_key = actual.feature_key))))
|
||||||
|
WHERE ((expected.feature_key IS NULL) OR (actual.feature_key IS NULL));
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.v_subscription_feature_mismatch OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: v_subscription_health; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE VIEW public.v_subscription_health AS
|
||||||
|
SELECT s.id AS subscription_id,
|
||||||
|
s.user_id AS owner_id,
|
||||||
|
s.status,
|
||||||
|
s.plan_id,
|
||||||
|
p.key AS plan_key,
|
||||||
|
s.current_period_start,
|
||||||
|
s.current_period_end,
|
||||||
|
s.updated_at,
|
||||||
|
CASE
|
||||||
|
WHEN (s.plan_id IS NULL) THEN 'missing_plan'::text
|
||||||
|
WHEN (p.id IS NULL) THEN 'invalid_plan'::text
|
||||||
|
WHEN ((s.status = 'active'::text) AND (s.current_period_end IS NOT NULL) AND (s.current_period_end < now())) THEN 'expired_but_active'::text
|
||||||
|
WHEN ((s.status = 'canceled'::text) AND (s.current_period_end > now())) THEN 'canceled_but_still_in_period'::text
|
||||||
|
ELSE 'ok'::text
|
||||||
|
END AS health_status,
|
||||||
|
CASE
|
||||||
|
WHEN (s.tenant_id IS NOT NULL) THEN 'clinic'::text
|
||||||
|
ELSE 'therapist'::text
|
||||||
|
END AS owner_type,
|
||||||
|
COALESCE(s.tenant_id, s.user_id) AS owner_ref
|
||||||
|
FROM (public.subscriptions s
|
||||||
|
LEFT JOIN public.plans p ON ((p.id = s.plan_id)));
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.v_subscription_health OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: v_subscription_health_v2; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE VIEW public.v_subscription_health_v2 AS
|
||||||
|
SELECT s.id AS subscription_id,
|
||||||
|
s.user_id AS owner_id,
|
||||||
|
CASE
|
||||||
|
WHEN (s.tenant_id IS NOT NULL) THEN 'clinic'::text
|
||||||
|
ELSE 'therapist'::text
|
||||||
|
END AS owner_type,
|
||||||
|
COALESCE(s.tenant_id, s.user_id) AS owner_ref,
|
||||||
|
s.status,
|
||||||
|
s.plan_id,
|
||||||
|
p.key AS plan_key,
|
||||||
|
s.current_period_start,
|
||||||
|
s.current_period_end,
|
||||||
|
s.updated_at,
|
||||||
|
CASE
|
||||||
|
WHEN (s.plan_id IS NULL) THEN 'missing_plan'::text
|
||||||
|
WHEN (p.id IS NULL) THEN 'invalid_plan'::text
|
||||||
|
WHEN ((s.status = 'active'::text) AND (s.current_period_end IS NOT NULL) AND (s.current_period_end < now())) THEN 'expired_but_active'::text
|
||||||
|
WHEN ((s.status = 'canceled'::text) AND (s.current_period_end > now())) THEN 'canceled_but_still_in_period'::text
|
||||||
|
ELSE 'ok'::text
|
||||||
|
END AS health_status
|
||||||
|
FROM (public.subscriptions s
|
||||||
|
LEFT JOIN public.plans p ON ((p.id = s.plan_id)));
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.v_subscription_health_v2 OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: v_tag_patient_counts; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE VIEW public.v_tag_patient_counts AS
|
||||||
|
SELECT t.id,
|
||||||
|
t.owner_id,
|
||||||
|
t.nome,
|
||||||
|
t.cor,
|
||||||
|
t.is_padrao,
|
||||||
|
t.created_at,
|
||||||
|
t.updated_at,
|
||||||
|
(COALESCE(count(ppt.patient_id), (0)::bigint))::integer AS pacientes_count,
|
||||||
|
(COALESCE(count(ppt.patient_id), (0)::bigint))::integer AS patient_count
|
||||||
|
FROM (public.patient_tags t
|
||||||
|
LEFT JOIN public.patient_patient_tag ppt ON (((ppt.tag_id = t.id) AND (ppt.owner_id = t.owner_id))))
|
||||||
|
GROUP BY t.id, t.owner_id, t.nome, t.cor, t.is_padrao, t.created_at, t.updated_at;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.v_tag_patient_counts OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: v_tenant_active_subscription; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE VIEW public.v_tenant_active_subscription AS
|
||||||
|
SELECT DISTINCT ON (tenant_id) tenant_id,
|
||||||
|
plan_id,
|
||||||
|
plan_key,
|
||||||
|
"interval",
|
||||||
|
status,
|
||||||
|
current_period_start,
|
||||||
|
current_period_end,
|
||||||
|
created_at
|
||||||
|
FROM public.subscriptions s
|
||||||
|
WHERE ((tenant_id IS NOT NULL) AND (status = 'active'::text) AND ((current_period_end IS NULL) OR (current_period_end > now())))
|
||||||
|
ORDER BY tenant_id, created_at DESC;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.v_tenant_active_subscription OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: v_tenant_entitlements; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE VIEW public.v_tenant_entitlements AS
|
||||||
|
SELECT a.tenant_id,
|
||||||
|
f.key AS feature_key,
|
||||||
|
true AS allowed
|
||||||
|
FROM ((public.v_tenant_active_subscription a
|
||||||
|
JOIN public.plan_features pf ON (((pf.plan_id = a.plan_id) AND (pf.enabled = true))))
|
||||||
|
JOIN public.features f ON ((f.id = pf.feature_id)));
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.v_tenant_entitlements OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: v_tenant_entitlements_full; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE VIEW public.v_tenant_entitlements_full AS
|
||||||
|
SELECT a.tenant_id,
|
||||||
|
f.key AS feature_key,
|
||||||
|
(pf.enabled = true) AS allowed,
|
||||||
|
pf.limits,
|
||||||
|
a.plan_id,
|
||||||
|
p.key AS plan_key
|
||||||
|
FROM (((public.v_tenant_active_subscription a
|
||||||
|
JOIN public.plan_features pf ON ((pf.plan_id = a.plan_id)))
|
||||||
|
JOIN public.features f ON ((f.id = pf.feature_id)))
|
||||||
|
JOIN public.plans p ON ((p.id = a.plan_id)));
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.v_tenant_entitlements_full OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: v_tenant_entitlements_json; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE VIEW public.v_tenant_entitlements_json AS
|
||||||
|
SELECT tenant_id,
|
||||||
|
max(plan_key) AS plan_key,
|
||||||
|
jsonb_object_agg(feature_key, jsonb_build_object('allowed', allowed, 'limits', COALESCE(limits, '{}'::jsonb)) ORDER BY feature_key) AS entitlements
|
||||||
|
FROM public.v_tenant_entitlements_full
|
||||||
|
GROUP BY tenant_id;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.v_tenant_entitlements_json OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: v_tenant_feature_exceptions; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE VIEW public.v_tenant_feature_exceptions AS
|
||||||
|
SELECT tf.tenant_id,
|
||||||
|
a.plan_key,
|
||||||
|
tf.feature_key,
|
||||||
|
'commercial_exception'::text AS exception_type
|
||||||
|
FROM ((public.tenant_features tf
|
||||||
|
JOIN public.v_tenant_active_subscription a ON ((a.tenant_id = tf.tenant_id)))
|
||||||
|
LEFT JOIN public.v_tenant_entitlements_full v ON (((v.tenant_id = tf.tenant_id) AND (v.feature_key = tf.feature_key))))
|
||||||
|
WHERE ((tf.enabled = true) AND (COALESCE(v.allowed, false) = false));
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.v_tenant_feature_exceptions OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: v_tenant_feature_mismatch; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE VIEW public.v_tenant_feature_mismatch AS
|
||||||
|
WITH plan_allowed AS (
|
||||||
|
SELECT v.tenant_id,
|
||||||
|
v.feature_key,
|
||||||
|
v.allowed
|
||||||
|
FROM public.v_tenant_entitlements_full v
|
||||||
|
), overrides AS (
|
||||||
|
SELECT tf.tenant_id,
|
||||||
|
tf.feature_key,
|
||||||
|
tf.enabled
|
||||||
|
FROM public.tenant_features tf
|
||||||
|
)
|
||||||
|
SELECT o.tenant_id,
|
||||||
|
o.feature_key,
|
||||||
|
CASE
|
||||||
|
WHEN ((o.enabled = true) AND (COALESCE(p.allowed, false) = false)) THEN 'unexpected_override'::text
|
||||||
|
ELSE NULL::text
|
||||||
|
END AS mismatch_type
|
||||||
|
FROM (overrides o
|
||||||
|
LEFT JOIN plan_allowed p ON (((p.tenant_id = o.tenant_id) AND (p.feature_key = o.feature_key))))
|
||||||
|
WHERE ((o.enabled = true) AND (COALESCE(p.allowed, false) = false));
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.v_tenant_feature_mismatch OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: v_tenant_members_with_profiles; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE VIEW public.v_tenant_members_with_profiles AS
|
||||||
|
SELECT tm.id AS tenant_member_id,
|
||||||
|
tm.tenant_id,
|
||||||
|
tm.user_id,
|
||||||
|
tm.role,
|
||||||
|
tm.status,
|
||||||
|
tm.created_at,
|
||||||
|
p.full_name,
|
||||||
|
au.email
|
||||||
|
FROM ((public.tenant_members tm
|
||||||
|
LEFT JOIN public.profiles p ON ((p.id = tm.user_id)))
|
||||||
|
LEFT JOIN auth.users au ON ((au.id = tm.user_id)));
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.v_tenant_members_with_profiles OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: v_tenant_people; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE VIEW public.v_tenant_people AS
|
||||||
|
SELECT 'member'::text AS type,
|
||||||
|
m.tenant_id,
|
||||||
|
m.user_id,
|
||||||
|
u.email,
|
||||||
|
m.role,
|
||||||
|
m.status,
|
||||||
|
NULL::uuid AS invite_token,
|
||||||
|
NULL::timestamp with time zone AS expires_at
|
||||||
|
FROM (public.tenant_members m
|
||||||
|
JOIN auth.users u ON ((u.id = m.user_id)))
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'invite'::text AS type,
|
||||||
|
i.tenant_id,
|
||||||
|
NULL::uuid AS user_id,
|
||||||
|
i.email,
|
||||||
|
i.role,
|
||||||
|
'invited'::text AS status,
|
||||||
|
i.token AS invite_token,
|
||||||
|
i.expires_at
|
||||||
|
FROM public.tenant_invites i
|
||||||
|
WHERE ((i.accepted_at IS NULL) AND (i.revoked_at IS NULL));
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.v_tenant_people OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: v_tenant_staff; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE VIEW public.v_tenant_staff AS
|
||||||
|
SELECT ('m_'::text || (tm.id)::text) AS row_id,
|
||||||
|
tm.tenant_id,
|
||||||
|
tm.user_id,
|
||||||
|
tm.role,
|
||||||
|
tm.status,
|
||||||
|
tm.created_at,
|
||||||
|
p.full_name,
|
||||||
|
au.email,
|
||||||
|
NULL::uuid AS invite_token
|
||||||
|
FROM ((public.tenant_members tm
|
||||||
|
LEFT JOIN public.profiles p ON ((p.id = tm.user_id)))
|
||||||
|
LEFT JOIN auth.users au ON ((au.id = tm.user_id)))
|
||||||
|
UNION ALL
|
||||||
|
SELECT ('i_'::text || (ti.id)::text) AS row_id,
|
||||||
|
ti.tenant_id,
|
||||||
|
NULL::uuid AS user_id,
|
||||||
|
ti.role,
|
||||||
|
'invited'::text AS status,
|
||||||
|
ti.created_at,
|
||||||
|
NULL::text AS full_name,
|
||||||
|
ti.email,
|
||||||
|
ti.token AS invite_token
|
||||||
|
FROM public.tenant_invites ti
|
||||||
|
WHERE ((ti.accepted_at IS NULL) AND (ti.revoked_at IS NULL) AND (ti.expires_at > now()));
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.v_tenant_staff OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: v_user_active_subscription; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE VIEW public.v_user_active_subscription AS
|
||||||
|
SELECT DISTINCT ON (user_id) user_id,
|
||||||
|
plan_id,
|
||||||
|
plan_key,
|
||||||
|
"interval",
|
||||||
|
status,
|
||||||
|
current_period_start,
|
||||||
|
current_period_end,
|
||||||
|
created_at
|
||||||
|
FROM public.subscriptions s
|
||||||
|
WHERE ((tenant_id IS NULL) AND (user_id IS NOT NULL) AND (status = 'active'::text) AND ((current_period_end IS NULL) OR (current_period_end > now())))
|
||||||
|
ORDER BY user_id, created_at DESC;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.v_user_active_subscription OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: v_user_entitlements; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE VIEW public.v_user_entitlements AS
|
||||||
|
SELECT a.user_id,
|
||||||
|
f.key AS feature_key,
|
||||||
|
true AS allowed
|
||||||
|
FROM ((public.v_user_active_subscription a
|
||||||
|
JOIN public.plan_features pf ON (((pf.plan_id = a.plan_id) AND (pf.enabled = true))))
|
||||||
|
JOIN public.features f ON ((f.id = pf.feature_id)));
|
||||||
|
|
||||||
|
|
||||||
|
ALTER VIEW public.v_user_entitlements OWNER TO supabase_admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: messages; Type: TABLE; Schema: realtime; Owner: supabase_realtime_admin
|
||||||
|
--
|
||||||
|
|
||||||
2049
database-novo/schema/06_indexes/indexes.sql
Normal file
2049
database-novo/schema/06_indexes/indexes.sql
Normal file
File diff suppressed because it is too large
Load Diff
2629
database-novo/schema/07_foreign_keys/constraints.sql
Normal file
2629
database-novo/schema/07_foreign_keys/constraints.sql
Normal file
File diff suppressed because it is too large
Load Diff
481
database-novo/schema/08_triggers/triggers.sql
Normal file
481
database-novo/schema/08_triggers/triggers.sql
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- AgenciaPsi — Triggers
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: users trg_seed_patient_groups; Type: TRIGGER; Schema: auth; Owner: supabase_auth_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_seed_patient_groups AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.on_new_user_seed_patient_groups();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_bloqueios agenda_bloqueios_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER agenda_bloqueios_updated_at BEFORE UPDATE ON public.agenda_bloqueios FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agendador_configuracoes agendador_slug_trigger; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER agendador_slug_trigger BEFORE INSERT OR UPDATE ON public.agendador_configuracoes FOR EACH ROW EXECUTE FUNCTION public.agendador_gerar_slug();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: tenant_members prevent_saas_membership_trigger; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER prevent_saas_membership_trigger BEFORE INSERT ON public.tenant_members FOR EACH ROW EXECUTE FUNCTION public.prevent_saas_membership();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: insurance_plan_services set_insurance_plan_services_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER set_insurance_plan_services_updated_at BEFORE UPDATE ON public.insurance_plan_services FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: user_settings t_user_settings_set_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER t_user_settings_set_updated_at BEFORE UPDATE ON public.user_settings FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_configuracoes tg_agenda_configuracoes_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER tg_agenda_configuracoes_updated_at BEFORE UPDATE ON public.agenda_configuracoes FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_eventos tg_agenda_eventos_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER tg_agenda_eventos_updated_at BEFORE UPDATE ON public.agenda_eventos FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_excecoes tg_agenda_excecoes_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER tg_agenda_excecoes_updated_at BEFORE UPDATE ON public.agenda_excecoes FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_regras_semanais tg_agenda_regras_semanais_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER tg_agenda_regras_semanais_updated_at BEFORE UPDATE ON public.agenda_regras_semanais FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: recurrence_rules tg_recurrence_rules_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER tg_recurrence_rules_updated_at BEFORE UPDATE ON public.recurrence_rules FOR EACH ROW EXECUTE FUNCTION public.set_updated_at_recurrence();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: plan_public tr_plan_public_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER tr_plan_public_updated_at BEFORE UPDATE ON public.plan_public FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: profiles trg_account_type_immutable; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_account_type_immutable BEFORE UPDATE OF account_type ON public.profiles FOR EACH ROW EXECUTE FUNCTION public.guard_account_type_immutable();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_configuracoes trg_agenda_cfg_sync; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_agenda_cfg_sync BEFORE INSERT OR UPDATE ON public.agenda_configuracoes FOR EACH ROW EXECUTE FUNCTION public.agenda_cfg_sync();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_eventos trg_agenda_eventos_busy_mirror_del; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_agenda_eventos_busy_mirror_del AFTER DELETE ON public.agenda_eventos FOR EACH ROW WHEN (((old.mirror_of_event_id IS NULL) AND (old.tenant_id = old.owner_id))) EXECUTE FUNCTION public.sync_busy_mirror_agenda_eventos();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_eventos trg_agenda_eventos_busy_mirror_ins; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_agenda_eventos_busy_mirror_ins AFTER INSERT ON public.agenda_eventos FOR EACH ROW WHEN (((new.mirror_of_event_id IS NULL) AND (new.tenant_id = new.owner_id) AND (new.visibility_scope = ANY (ARRAY['busy_only'::text, 'private'::text])))) EXECUTE FUNCTION public.sync_busy_mirror_agenda_eventos();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_eventos trg_agenda_eventos_busy_mirror_upd; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_agenda_eventos_busy_mirror_upd AFTER UPDATE ON public.agenda_eventos FOR EACH ROW WHEN (((new.mirror_of_event_id IS NULL) AND (new.tenant_id = new.owner_id) AND ((new.visibility_scope IS DISTINCT FROM old.visibility_scope) OR (new.inicio_em IS DISTINCT FROM old.inicio_em) OR (new.fim_em IS DISTINCT FROM old.fim_em) OR (new.owner_id IS DISTINCT FROM old.owner_id) OR (new.tenant_id IS DISTINCT FROM old.tenant_id)))) EXECUTE FUNCTION public.sync_busy_mirror_agenda_eventos();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_regras_semanais trg_agenda_regras_semanais_no_overlap; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_agenda_regras_semanais_no_overlap BEFORE INSERT OR UPDATE ON public.agenda_regras_semanais FOR EACH ROW EXECUTE FUNCTION public.fn_agenda_regras_semanais_no_overlap();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_eventos trg_auto_financial_from_session; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_auto_financial_from_session AFTER UPDATE OF status ON public.agenda_eventos FOR EACH ROW EXECUTE FUNCTION public.auto_create_financial_record_from_session();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: notification_preferences trg_cancel_notifs_on_opt_out; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_cancel_notifs_on_opt_out AFTER UPDATE ON public.notification_preferences FOR EACH ROW EXECUTE FUNCTION public.cancel_notifications_on_opt_out();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_eventos trg_cancel_notifs_on_session_cancel; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_cancel_notifs_on_session_cancel AFTER UPDATE ON public.agenda_eventos FOR EACH ROW WHEN ((new.status IS DISTINCT FROM old.status)) EXECUTE FUNCTION public.cancel_notifications_on_session_cancel();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: company_profiles trg_company_profiles_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_company_profiles_updated_at BEFORE UPDATE ON public.company_profiles FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: determined_commitment_fields trg_determined_commitment_fields_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_determined_commitment_fields_updated_at BEFORE UPDATE ON public.determined_commitment_fields FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: determined_commitments trg_determined_commitments_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_determined_commitments_updated_at BEFORE UPDATE ON public.determined_commitments FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: email_layout_config trg_email_layout_config_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_email_layout_config_updated_at BEFORE UPDATE ON public.email_layout_config FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: email_templates_global trg_email_templates_global_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_email_templates_global_updated_at BEFORE UPDATE ON public.email_templates_global FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: email_templates_tenant trg_email_templates_tenant_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_email_templates_tenant_updated_at BEFORE UPDATE ON public.email_templates_tenant FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: financial_exceptions trg_financial_exceptions_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_financial_exceptions_updated_at BEFORE UPDATE ON public.financial_exceptions FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: financial_records trg_financial_records_auto_overdue; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_financial_records_auto_overdue BEFORE UPDATE ON public.financial_records FOR EACH ROW EXECUTE FUNCTION public.trg_fn_financial_records_auto_overdue();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: financial_records trg_financial_records_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_financial_records_updated_at BEFORE UPDATE ON public.financial_records FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: global_notices trg_global_notices_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_global_notices_updated_at BEFORE UPDATE ON public.global_notices FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: insurance_plans trg_insurance_plans_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_insurance_plans_updated_at BEFORE UPDATE ON public.insurance_plans FOR EACH ROW EXECUTE FUNCTION public.set_insurance_plans_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: plans trg_no_change_core_plan_key; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_no_change_core_plan_key BEFORE UPDATE ON public.plans FOR EACH ROW EXECUTE FUNCTION public.guard_no_change_core_plan_key();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: plans trg_no_change_plan_target; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_no_change_plan_target BEFORE UPDATE ON public.plans FOR EACH ROW EXECUTE FUNCTION public.guard_no_change_plan_target();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: plans trg_no_delete_core_plans; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_no_delete_core_plans BEFORE DELETE ON public.plans FOR EACH ROW EXECUTE FUNCTION public.guard_no_delete_core_plans();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: notification_channels trg_notification_channels_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_notification_channels_updated_at BEFORE UPDATE ON public.notification_channels FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: notification_logs trg_notification_logs_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_notification_logs_updated_at BEFORE UPDATE ON public.notification_logs FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: notification_preferences trg_notification_preferences_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_notification_preferences_updated_at BEFORE UPDATE ON public.notification_preferences FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: notification_queue trg_notification_queue_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_notification_queue_updated_at BEFORE UPDATE ON public.notification_queue FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: notification_schedules trg_notification_schedules_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_notification_schedules_updated_at BEFORE UPDATE ON public.notification_schedules FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: notification_templates trg_notification_templates_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_notification_templates_updated_at BEFORE UPDATE ON public.notification_templates FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: patient_intake_requests trg_notify_on_intake; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_notify_on_intake AFTER INSERT ON public.patient_intake_requests FOR EACH ROW EXECUTE FUNCTION public.notify_on_intake();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agendador_solicitacoes trg_notify_on_scheduling; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_notify_on_scheduling AFTER INSERT ON public.agendador_solicitacoes FOR EACH ROW EXECUTE FUNCTION public.notify_on_scheduling();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: agenda_eventos trg_notify_on_session_status; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_notify_on_session_status AFTER UPDATE OF status ON public.agenda_eventos FOR EACH ROW EXECUTE FUNCTION public.notify_on_session_status();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: tenant_members trg_patient_cannot_own_tenant; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_patient_cannot_own_tenant BEFORE INSERT OR UPDATE ON public.tenant_members FOR EACH ROW EXECUTE FUNCTION public.guard_patient_cannot_own_tenant();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: patient_groups trg_patient_groups_set_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_patient_groups_set_updated_at BEFORE UPDATE ON public.patient_groups FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: patient_intake_requests trg_patient_intake_requests_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_patient_intake_requests_updated_at BEFORE UPDATE ON public.patient_intake_requests FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: patient_tags trg_patient_tags_set_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_patient_tags_set_updated_at BEFORE UPDATE ON public.patient_tags FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: patients trg_patients_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_patients_updated_at BEFORE UPDATE ON public.patients FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: patients trg_patients_validate_members; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_patients_validate_members BEFORE INSERT OR UPDATE OF tenant_id, responsible_member_id, patient_scope, therapist_member_id ON public.patients FOR EACH ROW EXECUTE FUNCTION public.patients_validate_member_consistency();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: payment_settings trg_payment_settings_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_payment_settings_updated_at BEFORE UPDATE ON public.payment_settings FOR EACH ROW EXECUTE FUNCTION public.update_payment_settings_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: patient_groups trg_prevent_promoting_to_system; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_prevent_promoting_to_system BEFORE UPDATE ON public.patient_groups FOR EACH ROW EXECUTE FUNCTION public.prevent_promoting_to_system();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: patient_groups trg_prevent_system_group_changes; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_prevent_system_group_changes BEFORE DELETE OR UPDATE ON public.patient_groups FOR EACH ROW EXECUTE FUNCTION public.prevent_system_group_changes();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: professional_pricing trg_professional_pricing_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_professional_pricing_updated_at BEFORE UPDATE ON public.professional_pricing FOR EACH ROW EXECUTE FUNCTION public.update_professional_pricing_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: profiles trg_profiles_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_profiles_updated_at BEFORE UPDATE ON public.profiles FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: services trg_services_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_services_updated_at BEFORE UPDATE ON public.services FOR EACH ROW EXECUTE FUNCTION public.set_services_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: subscription_intents trg_subscription_intents_view_insert; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_subscription_intents_view_insert INSTEAD OF INSERT ON public.subscription_intents FOR EACH ROW EXECUTE FUNCTION public.subscription_intents_view_insert();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: subscriptions trg_subscriptions_validate_scope; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_subscriptions_validate_scope BEFORE INSERT OR UPDATE ON public.subscriptions FOR EACH ROW EXECUTE FUNCTION public.subscriptions_validate_scope();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: tenant_features trg_tenant_features_guard_with_plan; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_tenant_features_guard_with_plan BEFORE INSERT OR UPDATE ON public.tenant_features FOR EACH ROW EXECUTE FUNCTION public.tenant_features_guard_with_plan();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: tenant_features trg_tenant_features_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_tenant_features_updated_at BEFORE UPDATE ON public.tenant_features FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: tenants trg_tenant_kind_immutable; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_tenant_kind_immutable BEFORE UPDATE OF kind ON public.tenants FOR EACH ROW EXECUTE FUNCTION public.guard_tenant_kind_immutable();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: therapist_payouts trg_therapist_payouts_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_therapist_payouts_updated_at BEFORE UPDATE ON public.therapist_payouts FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: user_settings trg_user_settings_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_user_settings_updated_at BEFORE UPDATE ON public.user_settings FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: subscription tr_check_filters; Type: TRIGGER; Schema: realtime; Owner: supabase_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER tr_check_filters BEFORE INSERT OR UPDATE ON realtime.subscription FOR EACH ROW EXECUTE FUNCTION realtime.subscription_check_filters();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: buckets enforce_bucket_name_length_trigger; Type: TRIGGER; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER enforce_bucket_name_length_trigger BEFORE INSERT OR UPDATE OF name ON storage.buckets FOR EACH ROW EXECUTE FUNCTION storage.enforce_bucket_name_length();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: buckets protect_buckets_delete; Type: TRIGGER; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER protect_buckets_delete BEFORE DELETE ON storage.buckets FOR EACH STATEMENT EXECUTE FUNCTION storage.protect_delete();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: objects protect_objects_delete; Type: TRIGGER; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER protect_objects_delete BEFORE DELETE ON storage.objects FOR EACH STATEMENT EXECUTE FUNCTION storage.protect_delete();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: objects update_objects_updated_at; Type: TRIGGER; Schema: storage; Owner: supabase_storage_admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER update_objects_updated_at BEFORE UPDATE ON storage.objects FOR EACH ROW EXECUTE FUNCTION storage.update_updated_at_column();
|
||||||
2313
database-novo/schema/09_policies/policies.sql
Normal file
2313
database-novo/schema/09_policies/policies.sql
Normal file
File diff suppressed because it is too large
Load Diff
6534
database-novo/schema/10_grants/grants.sql
Normal file
6534
database-novo/schema/10_grants/grants.sql
Normal file
File diff suppressed because it is too large
Load Diff
47
database-novo/seeds/run_all_seeds.sh
Normal file
47
database-novo/seeds/run_all_seeds.sh
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# Executa todos os seeds na ordem correta
|
||||||
|
# =============================================================================
|
||||||
|
# Uso: bash run_all_seeds.sh
|
||||||
|
#
|
||||||
|
# Pré-requisitos:
|
||||||
|
# - Container supabase_db_agenciapsi-primesakai rodando
|
||||||
|
# - Schema já aplicado (schema/00_full/schema.sql)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
CONTAINER="supabase_db_agenciapsi-primesakai"
|
||||||
|
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== Aplicando seeds — Usuários ==="
|
||||||
|
|
||||||
|
for seed in \
|
||||||
|
seed_001_fixed.sql \
|
||||||
|
seed_002.sql \
|
||||||
|
seed_003.sql
|
||||||
|
do
|
||||||
|
echo ""
|
||||||
|
echo ">>> $seed"
|
||||||
|
docker exec -i "$CONTAINER" psql -U postgres -d postgres < "$DIR/$seed"
|
||||||
|
echo "<<< OK"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Aplicando seeds — Sistema (planos, features, subscriptions) ==="
|
||||||
|
|
||||||
|
for seed in \
|
||||||
|
seed_010_plans.sql \
|
||||||
|
seed_011_features.sql \
|
||||||
|
seed_012_plan_features.sql \
|
||||||
|
seed_013_subscriptions.sql \
|
||||||
|
seed_014_global_data.sql
|
||||||
|
do
|
||||||
|
echo ""
|
||||||
|
echo ">>> $seed"
|
||||||
|
docker exec -i "$CONTAINER" psql -U postgres -d postgres < "$DIR/$seed"
|
||||||
|
echo "<<< OK"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Todos os seeds aplicados com sucesso ==="
|
||||||
327
database-novo/seeds/seed_001_fixed.sql
Normal file
327
database-novo/seeds/seed_001_fixed.sql
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- SEED — Usuários fictícios para teste
|
||||||
|
-- =============================================================================
|
||||||
|
-- IMPORTANTE: Execute APÓS a migration_001.sql
|
||||||
|
-- IMPORTANTE: Requer extensão pgcrypto (já ativa no Supabase)
|
||||||
|
--
|
||||||
|
-- Cria os seguintes usuários de teste:
|
||||||
|
--
|
||||||
|
-- paciente@agenciapsi.com.br senha: Teste@123 → paciente
|
||||||
|
-- terapeuta@agenciapsi.com.br senha: Teste@123 → terapeuta solo
|
||||||
|
-- clinica1@agenciapsi.com.br senha: Teste@123 → clínica coworking
|
||||||
|
-- clinica2@agenciapsi.com.br senha: Teste@123 → clínica com secretaria
|
||||||
|
-- clinica3@agenciapsi.com.br senha: Teste@123 → clínica full
|
||||||
|
-- saas@agenciapsi.com.br senha: Teste@123 → admin da plataforma
|
||||||
|
--
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Limpa seeds anteriores se existirem
|
||||||
|
DELETE FROM auth.users
|
||||||
|
WHERE email IN (
|
||||||
|
'paciente@agenciapsi.com.br',
|
||||||
|
'terapeuta@agenciapsi.com.br',
|
||||||
|
'clinica1@agenciapsi.com.br',
|
||||||
|
'clinica2@agenciapsi.com.br',
|
||||||
|
'clinica3@agenciapsi.com.br',
|
||||||
|
'saas@agenciapsi.com.br'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. Cria usuários no auth.users
|
||||||
|
-- NOTA: confirmed_at é coluna gerada — removida do INSERT
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO auth.users (
|
||||||
|
instance_id,
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
encrypted_password,
|
||||||
|
email_confirmed_at,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
raw_user_meta_data,
|
||||||
|
raw_app_meta_data,
|
||||||
|
role,
|
||||||
|
aud,
|
||||||
|
is_sso_user,
|
||||||
|
is_anonymous,
|
||||||
|
confirmation_token,
|
||||||
|
recovery_token,
|
||||||
|
email_change_token_new,
|
||||||
|
email_change_token_current,
|
||||||
|
email_change
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
-- Paciente
|
||||||
|
(
|
||||||
|
'00000000-0000-0000-0000-000000000000',
|
||||||
|
'aaaaaaaa-0001-0001-0001-000000000001',
|
||||||
|
'paciente@agenciapsi.com.br',
|
||||||
|
crypt('Teste@123', gen_salt('bf')),
|
||||||
|
now(), now(), now(),
|
||||||
|
'{"name": "Ana Paciente"}'::jsonb,
|
||||||
|
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||||
|
'authenticated', 'authenticated', false, false, '', '', '', '', ''
|
||||||
|
),
|
||||||
|
-- Terapeuta
|
||||||
|
(
|
||||||
|
'00000000-0000-0000-0000-000000000000',
|
||||||
|
'aaaaaaaa-0002-0002-0002-000000000002',
|
||||||
|
'terapeuta@agenciapsi.com.br',
|
||||||
|
crypt('Teste@123', gen_salt('bf')),
|
||||||
|
now(), now(), now(),
|
||||||
|
'{"name": "Bruno Terapeuta"}'::jsonb,
|
||||||
|
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||||
|
'authenticated', 'authenticated', false, false, '', '', '', '', ''
|
||||||
|
),
|
||||||
|
-- Clínica 1 — Coworking
|
||||||
|
(
|
||||||
|
'00000000-0000-0000-0000-000000000000',
|
||||||
|
'aaaaaaaa-0003-0003-0003-000000000003',
|
||||||
|
'clinica1@agenciapsi.com.br',
|
||||||
|
crypt('Teste@123', gen_salt('bf')),
|
||||||
|
now(), now(), now(),
|
||||||
|
'{"name": "Clínica Espaço Psi"}'::jsonb,
|
||||||
|
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||||
|
'authenticated', 'authenticated', false, false, '', '', '', '', ''
|
||||||
|
),
|
||||||
|
-- Clínica 2 — Recepção
|
||||||
|
(
|
||||||
|
'00000000-0000-0000-0000-000000000000',
|
||||||
|
'aaaaaaaa-0004-0004-0004-000000000004',
|
||||||
|
'clinica2@agenciapsi.com.br',
|
||||||
|
crypt('Teste@123', gen_salt('bf')),
|
||||||
|
now(), now(), now(),
|
||||||
|
'{"name": "Clínica Mente sã"}'::jsonb,
|
||||||
|
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||||
|
'authenticated', 'authenticated', false, false, '', '', '', '', ''
|
||||||
|
),
|
||||||
|
-- Clínica 3 — Full
|
||||||
|
(
|
||||||
|
'00000000-0000-0000-0000-000000000000',
|
||||||
|
'aaaaaaaa-0005-0005-0005-000000000005',
|
||||||
|
'clinica3@agenciapsi.com.br',
|
||||||
|
crypt('Teste@123', gen_salt('bf')),
|
||||||
|
now(), now(), now(),
|
||||||
|
'{"name": "Clínica Bem Estar"}'::jsonb,
|
||||||
|
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||||
|
'authenticated', 'authenticated', false, false, '', '', '', '', ''
|
||||||
|
),
|
||||||
|
-- SaaS Admin
|
||||||
|
(
|
||||||
|
'00000000-0000-0000-0000-000000000000',
|
||||||
|
'aaaaaaaa-0006-0006-0006-000000000006',
|
||||||
|
'saas@agenciapsi.com.br',
|
||||||
|
crypt('Teste@123', gen_salt('bf')),
|
||||||
|
now(), now(), now(),
|
||||||
|
'{"name": "Admin Plataforma"}'::jsonb,
|
||||||
|
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||||
|
'authenticated', 'authenticated', false, false, '', '', '', '', ''
|
||||||
|
);
|
||||||
|
|
||||||
|
-- auth.identities (obrigatório para GoTrue reconhecer login email/senha)
|
||||||
|
INSERT INTO auth.identities (id, user_id, provider_id, provider, identity_data, created_at, updated_at, last_sign_in_at)
|
||||||
|
VALUES
|
||||||
|
(gen_random_uuid(), 'aaaaaaaa-0001-0001-0001-000000000001', 'paciente@agenciapsi.com.br', 'email', '{"sub": "aaaaaaaa-0001-0001-0001-000000000001", "email": "paciente@agenciapsi.com.br", "email_verified": true}'::jsonb, now(), now(), now()),
|
||||||
|
(gen_random_uuid(), 'aaaaaaaa-0002-0002-0002-000000000002', 'terapeuta@agenciapsi.com.br', 'email', '{"sub": "aaaaaaaa-0002-0002-0002-000000000002", "email": "terapeuta@agenciapsi.com.br", "email_verified": true}'::jsonb, now(), now(), now()),
|
||||||
|
(gen_random_uuid(), 'aaaaaaaa-0003-0003-0003-000000000003', 'clinica1@agenciapsi.com.br', 'email', '{"sub": "aaaaaaaa-0003-0003-0003-000000000003", "email": "clinica1@agenciapsi.com.br", "email_verified": true}'::jsonb, now(), now(), now()),
|
||||||
|
(gen_random_uuid(), 'aaaaaaaa-0004-0004-0004-000000000004', 'clinica2@agenciapsi.com.br', 'email', '{"sub": "aaaaaaaa-0004-0004-0004-000000000004", "email": "clinica2@agenciapsi.com.br", "email_verified": true}'::jsonb, now(), now(), now()),
|
||||||
|
(gen_random_uuid(), 'aaaaaaaa-0005-0005-0005-000000000005', 'clinica3@agenciapsi.com.br', 'email', '{"sub": "aaaaaaaa-0005-0005-0005-000000000005", "email": "clinica3@agenciapsi.com.br", "email_verified": true}'::jsonb, now(), now(), now()),
|
||||||
|
(gen_random_uuid(), 'aaaaaaaa-0006-0006-0006-000000000006', 'saas@agenciapsi.com.br', 'email', '{"sub": "aaaaaaaa-0006-0006-0006-000000000006", "email": "saas@agenciapsi.com.br", "email_verified": true}'::jsonb, now(), now(), now())
|
||||||
|
ON CONFLICT (provider, provider_id) DO NOTHING;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. Profiles
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO public.profiles (id, role, account_type, full_name)
|
||||||
|
VALUES
|
||||||
|
('aaaaaaaa-0001-0001-0001-000000000001', 'portal_user', 'patient', 'Ana Paciente'),
|
||||||
|
('aaaaaaaa-0002-0002-0002-000000000002', 'tenant_member', 'therapist', 'Bruno Terapeuta'),
|
||||||
|
('aaaaaaaa-0003-0003-0003-000000000003', 'tenant_member', 'clinic', 'Clínica Espaço Psi'),
|
||||||
|
('aaaaaaaa-0004-0004-0004-000000000004', 'tenant_member', 'clinic', 'Clínica Mente sã'),
|
||||||
|
('aaaaaaaa-0005-0005-0005-000000000005', 'tenant_member', 'clinic', 'Clínica Bem Estar'),
|
||||||
|
('aaaaaaaa-0006-0006-0006-000000000006', 'saas_admin', 'free', 'Admin Plataforma')
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
role = EXCLUDED.role,
|
||||||
|
account_type = EXCLUDED.account_type,
|
||||||
|
full_name = EXCLUDED.full_name;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. SaaS Admin na tabela saas_admins
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO public.saas_admins (user_id, created_at)
|
||||||
|
VALUES ('aaaaaaaa-0006-0006-0006-000000000006', now())
|
||||||
|
ON CONFLICT (user_id) DO NOTHING;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. Tenant do terapeuta
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO public.tenants (id, name, kind, created_at)
|
||||||
|
VALUES ('bbbbbbbb-0002-0002-0002-000000000002', 'Bruno Terapeuta', 'therapist', now())
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||||
|
VALUES ('bbbbbbbb-0002-0002-0002-000000000002', 'aaaaaaaa-0002-0002-0002-000000000002', 'tenant_admin', 'active', now())
|
||||||
|
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||||
|
|
||||||
|
DO $$ BEGIN PERFORM public.seed_determined_commitments('bbbbbbbb-0002-0002-0002-000000000002'); END $$;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 5. Tenant da Clínica 1 — Coworking
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO public.tenants (id, name, kind, created_at)
|
||||||
|
VALUES ('bbbbbbbb-0003-0003-0003-000000000003', 'Clínica Espaço Psi', 'clinic_coworking', now())
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||||
|
VALUES ('bbbbbbbb-0003-0003-0003-000000000003', 'aaaaaaaa-0003-0003-0003-000000000003', 'tenant_admin', 'active', now())
|
||||||
|
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||||
|
|
||||||
|
DO $$ BEGIN PERFORM public.seed_determined_commitments('bbbbbbbb-0003-0003-0003-000000000003'); END $$;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 6. Tenant da Clínica 2 — Recepção
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO public.tenants (id, name, kind, created_at)
|
||||||
|
VALUES ('bbbbbbbb-0004-0004-0004-000000000004', 'Clínica Mente sã', 'clinic_reception', now())
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||||
|
VALUES ('bbbbbbbb-0004-0004-0004-000000000004', 'aaaaaaaa-0004-0004-0004-000000000004', 'tenant_admin', 'active', now())
|
||||||
|
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||||
|
|
||||||
|
DO $$ BEGIN PERFORM public.seed_determined_commitments('bbbbbbbb-0004-0004-0004-000000000004'); END $$;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 7. Tenant da Clínica 3 — Full
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO public.tenants (id, name, kind, created_at)
|
||||||
|
VALUES ('bbbbbbbb-0005-0005-0005-000000000005', 'Clínica Bem Estar', 'clinic_full', now())
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||||
|
VALUES ('bbbbbbbb-0005-0005-0005-000000000005', 'aaaaaaaa-0005-0005-0005-000000000005', 'tenant_admin', 'active', now())
|
||||||
|
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||||
|
|
||||||
|
DO $$ BEGIN PERFORM public.seed_determined_commitments('bbbbbbbb-0005-0005-0005-000000000005'); END $$;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 8. Subscriptions ativas para cada conta
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Terapeuta → plano therapist_free
|
||||||
|
INSERT INTO public.subscriptions (
|
||||||
|
user_id, plan_id, plan_key, status, interval,
|
||||||
|
current_period_start, current_period_end,
|
||||||
|
source, started_at, activated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'aaaaaaaa-0002-0002-0002-000000000002',
|
||||||
|
p.id, p.key, 'active', 'month',
|
||||||
|
now(), now() + interval '30 days',
|
||||||
|
'seed', now(), now()
|
||||||
|
FROM public.plans p WHERE p.key = 'therapist_free';
|
||||||
|
|
||||||
|
-- Clínica 1 → plano clinic_free
|
||||||
|
INSERT INTO public.subscriptions (
|
||||||
|
tenant_id, plan_id, plan_key, status, interval,
|
||||||
|
current_period_start, current_period_end,
|
||||||
|
source, started_at, activated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'bbbbbbbb-0003-0003-0003-000000000003',
|
||||||
|
p.id, p.key, 'active', 'month',
|
||||||
|
now(), now() + interval '30 days',
|
||||||
|
'seed', now(), now()
|
||||||
|
FROM public.plans p WHERE p.key = 'clinic_free';
|
||||||
|
|
||||||
|
-- Clínica 2 → plano clinic_free
|
||||||
|
INSERT INTO public.subscriptions (
|
||||||
|
tenant_id, plan_id, plan_key, status, interval,
|
||||||
|
current_period_start, current_period_end,
|
||||||
|
source, started_at, activated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'bbbbbbbb-0004-0004-0004-000000000004',
|
||||||
|
p.id, p.key, 'active', 'month',
|
||||||
|
now(), now() + interval '30 days',
|
||||||
|
'seed', now(), now()
|
||||||
|
FROM public.plans p WHERE p.key = 'clinic_free';
|
||||||
|
|
||||||
|
-- Clínica 3 → plano clinic_free
|
||||||
|
INSERT INTO public.subscriptions (
|
||||||
|
tenant_id, plan_id, plan_key, status, interval,
|
||||||
|
current_period_start, current_period_end,
|
||||||
|
source, started_at, activated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'bbbbbbbb-0005-0005-0005-000000000005',
|
||||||
|
p.id, p.key, 'active', 'month',
|
||||||
|
now(), now() + interval '30 days',
|
||||||
|
'seed', now(), now()
|
||||||
|
FROM public.plans p WHERE p.key = 'clinic_free';
|
||||||
|
|
||||||
|
-- Paciente → plano patient_free
|
||||||
|
INSERT INTO public.subscriptions (
|
||||||
|
user_id, plan_id, plan_key, status, interval,
|
||||||
|
current_period_start, current_period_end,
|
||||||
|
source, started_at, activated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'aaaaaaaa-0001-0001-0001-000000000001',
|
||||||
|
p.id, p.key, 'active', 'month',
|
||||||
|
now(), now() + interval '30 days',
|
||||||
|
'seed', now(), now()
|
||||||
|
FROM public.plans p WHERE p.key = 'patient_free';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 9. Vincula terapeuta à Clínica 3 (full) como exemplo
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||||
|
VALUES (
|
||||||
|
'bbbbbbbb-0005-0005-0005-000000000005',
|
||||||
|
'aaaaaaaa-0002-0002-0002-000000000002',
|
||||||
|
'therapist',
|
||||||
|
'active',
|
||||||
|
now()
|
||||||
|
)
|
||||||
|
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 10. Confirma
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
RAISE NOTICE '✅ Seed aplicado com sucesso.';
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE ' Usuários criados:';
|
||||||
|
RAISE NOTICE ' paciente@agenciapsi.com.br → patient';
|
||||||
|
RAISE NOTICE ' terapeuta@agenciapsi.com.br → therapist (tenant próprio + vinculado à Clínica 3)';
|
||||||
|
RAISE NOTICE ' clinica1@agenciapsi.com.br → clinic_coworking';
|
||||||
|
RAISE NOTICE ' clinica2@agenciapsi.com.br → clinic_reception';
|
||||||
|
RAISE NOTICE ' clinica3@agenciapsi.com.br → clinic_full';
|
||||||
|
RAISE NOTICE ' saas@agenciapsi.com.br → saas_admin';
|
||||||
|
RAISE NOTICE ' Senha de todos: Teste@123';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
199
database-novo/seeds/seed_002.sql
Normal file
199
database-novo/seeds/seed_002.sql
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- SEED 002 — Supervisor e Editor
|
||||||
|
-- =============================================================================
|
||||||
|
-- Execute APÓS seed_001.sql
|
||||||
|
-- Requer: pgcrypto (já ativo no Supabase)
|
||||||
|
--
|
||||||
|
-- Cria os seguintes usuários de teste:
|
||||||
|
--
|
||||||
|
-- supervisor@agenciapsi.com.br senha: Teste@123 → supervisor da Clínica 3
|
||||||
|
-- editor@agenciapsi.com.br senha: Teste@123 → editor de conteúdo (plataforma)
|
||||||
|
--
|
||||||
|
-- UUIDs reservados:
|
||||||
|
-- Supervisor → aaaaaaaa-0007-0007-0007-000000000007
|
||||||
|
-- Editor → aaaaaaaa-0008-0008-0008-000000000008
|
||||||
|
--
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 0. Migration: adiciona platform_roles em profiles (se não existir)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
ALTER TABLE public.profiles
|
||||||
|
ADD COLUMN IF NOT EXISTS platform_roles text[] NOT NULL DEFAULT '{}';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN public.profiles.platform_roles IS
|
||||||
|
'Papéis globais de plataforma, independentes de tenant. Ex: editor de microlearning. Atribuído pelo saas_admin.';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. Remove seeds anteriores (idempotente)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
DELETE FROM auth.users
|
||||||
|
WHERE email IN (
|
||||||
|
'supervisor@agenciapsi.com.br',
|
||||||
|
'editor@agenciapsi.com.br'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. Cria usuários no auth.users
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO auth.users (
|
||||||
|
instance_id,
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
encrypted_password,
|
||||||
|
email_confirmed_at,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
raw_user_meta_data,
|
||||||
|
raw_app_meta_data,
|
||||||
|
role,
|
||||||
|
aud,
|
||||||
|
is_sso_user,
|
||||||
|
is_anonymous,
|
||||||
|
confirmation_token,
|
||||||
|
recovery_token,
|
||||||
|
email_change_token_new,
|
||||||
|
email_change_token_current,
|
||||||
|
email_change
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
-- Supervisor
|
||||||
|
(
|
||||||
|
'00000000-0000-0000-0000-000000000000',
|
||||||
|
'aaaaaaaa-0007-0007-0007-000000000007',
|
||||||
|
'supervisor@agenciapsi.com.br',
|
||||||
|
crypt('Teste@123', gen_salt('bf')),
|
||||||
|
now(), now(), now(),
|
||||||
|
'{"name": "Carlos Supervisor"}'::jsonb,
|
||||||
|
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||||
|
'authenticated', 'authenticated', false, false, '', '', '', '', ''
|
||||||
|
),
|
||||||
|
-- Editor de Conteúdo
|
||||||
|
(
|
||||||
|
'00000000-0000-0000-0000-000000000000',
|
||||||
|
'aaaaaaaa-0008-0008-0008-000000000008',
|
||||||
|
'editor@agenciapsi.com.br',
|
||||||
|
crypt('Teste@123', gen_salt('bf')),
|
||||||
|
now(), now(), now(),
|
||||||
|
'{"name": "Diana Editora"}'::jsonb,
|
||||||
|
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||||
|
'authenticated', 'authenticated', false, false, '', '', '', '', ''
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. auth.identities (obrigatório para GoTrue reconhecer login)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO auth.identities (id, user_id, provider_id, provider, identity_data, created_at, updated_at, last_sign_in_at)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
gen_random_uuid(),
|
||||||
|
'aaaaaaaa-0007-0007-0007-000000000007',
|
||||||
|
'supervisor@agenciapsi.com.br',
|
||||||
|
'email',
|
||||||
|
'{"sub": "aaaaaaaa-0007-0007-0007-000000000007", "email": "supervisor@agenciapsi.com.br", "email_verified": true}'::jsonb,
|
||||||
|
now(), now(), now()
|
||||||
|
),
|
||||||
|
(
|
||||||
|
gen_random_uuid(),
|
||||||
|
'aaaaaaaa-0008-0008-0008-000000000008',
|
||||||
|
'editor@agenciapsi.com.br',
|
||||||
|
'email',
|
||||||
|
'{"sub": "aaaaaaaa-0008-0008-0008-000000000008", "email": "editor@agenciapsi.com.br", "email_verified": true}'::jsonb,
|
||||||
|
now(), now(), now()
|
||||||
|
)
|
||||||
|
ON CONFLICT (provider, provider_id) DO NOTHING;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. Profiles
|
||||||
|
-- Supervisor → tenant_member (papel no tenant via tenant_members.role)
|
||||||
|
-- Editor → tenant_member + platform_roles = '{editor}'
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO public.profiles (id, role, account_type, full_name, platform_roles)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
'aaaaaaaa-0007-0007-0007-000000000007',
|
||||||
|
'tenant_member',
|
||||||
|
'therapist',
|
||||||
|
'Carlos Supervisor',
|
||||||
|
'{}'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'aaaaaaaa-0008-0008-0008-000000000008',
|
||||||
|
'tenant_member',
|
||||||
|
'therapist',
|
||||||
|
'Diana Editora',
|
||||||
|
'{editor}' -- permissão de plataforma: acesso à área do editor
|
||||||
|
)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
role = EXCLUDED.role,
|
||||||
|
account_type = EXCLUDED.account_type,
|
||||||
|
full_name = EXCLUDED.full_name,
|
||||||
|
platform_roles = EXCLUDED.platform_roles;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 5. Vincula Supervisor à Clínica 3 (Full) com role 'supervisor'
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||||
|
VALUES (
|
||||||
|
'bbbbbbbb-0005-0005-0005-000000000005', -- Clínica Bem Estar (Full)
|
||||||
|
'aaaaaaaa-0007-0007-0007-000000000007', -- Carlos Supervisor
|
||||||
|
'supervisor',
|
||||||
|
'active',
|
||||||
|
now()
|
||||||
|
)
|
||||||
|
ON CONFLICT (tenant_id, user_id) DO UPDATE SET
|
||||||
|
role = EXCLUDED.role,
|
||||||
|
status = EXCLUDED.status;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 6. Vincula Editor à Clínica 3 como terapeuta
|
||||||
|
-- (contexto de tenant para o editor poder usar /therapist também,
|
||||||
|
-- se necessário. O papel de editor vem de platform_roles.)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||||
|
VALUES (
|
||||||
|
'bbbbbbbb-0005-0005-0005-000000000005', -- Clínica Bem Estar (Full)
|
||||||
|
'aaaaaaaa-0008-0008-0008-000000000008', -- Diana Editora
|
||||||
|
'therapist',
|
||||||
|
'active',
|
||||||
|
now()
|
||||||
|
)
|
||||||
|
ON CONFLICT (tenant_id, user_id) DO UPDATE SET
|
||||||
|
role = EXCLUDED.role,
|
||||||
|
status = EXCLUDED.status;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 7. Confirma
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
RAISE NOTICE '✅ Seed 002 aplicado com sucesso.';
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE ' Migration aplicada:';
|
||||||
|
RAISE NOTICE ' → profiles.platform_roles text[] adicionada (se não existia)';
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE ' Usuários criados:';
|
||||||
|
RAISE NOTICE ' supervisor@agenciapsi.com.br → supervisor da Clínica Bem Estar (Full)';
|
||||||
|
RAISE NOTICE ' editor@agenciapsi.com.br → editor de conteúdo (platform_roles = {editor})';
|
||||||
|
RAISE NOTICE ' Senha de todos: Teste@123';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
283
database-novo/seeds/seed_003.sql
Normal file
283
database-novo/seeds/seed_003.sql
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- SEED 003 — Terapeuta 2, Terapeuta 3 e Secretária
|
||||||
|
-- =============================================================================
|
||||||
|
-- Execute APÓS seed_001.sql (e seed_002.sql se quiser todos os seeds)
|
||||||
|
-- Requer: pgcrypto (já ativo no Supabase)
|
||||||
|
--
|
||||||
|
-- Cria os seguintes usuários de teste:
|
||||||
|
--
|
||||||
|
-- therapist2@agenciapsi.com.br senha: Teste@123 → terapeuta 2 (tenant próprio + Clínica 3)
|
||||||
|
-- therapist3@agenciapsi.com.br senha: Teste@123 → terapeuta 3 (tenant próprio + Clínica 3)
|
||||||
|
-- secretary@agenciapsi.com.br senha: Teste@123 → clinic_admin na Clínica 2 (Mente sã)
|
||||||
|
--
|
||||||
|
-- UUIDs reservados:
|
||||||
|
-- Terapeuta 2 → aaaaaaaa-0009-0009-0009-000000000009
|
||||||
|
-- Terapeuta 3 → aaaaaaaa-0010-0010-0010-000000000010
|
||||||
|
-- Secretária → aaaaaaaa-0011-0011-0011-000000000011
|
||||||
|
-- Tenant Terapeuta 2 → bbbbbbbb-0009-0009-0009-000000000009
|
||||||
|
-- Tenant Terapeuta 3 → bbbbbbbb-0010-0010-0010-000000000010
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. Remove seeds anteriores (idempotente)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
DELETE FROM auth.users
|
||||||
|
WHERE email IN (
|
||||||
|
'therapist2@agenciapsi.com.br',
|
||||||
|
'therapist3@agenciapsi.com.br',
|
||||||
|
'secretary@agenciapsi.com.br'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. Cria usuários no auth.users
|
||||||
|
-- ⚠️ confirmed_at é coluna gerada — NÃO incluir na lista
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO auth.users (
|
||||||
|
instance_id,
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
encrypted_password,
|
||||||
|
email_confirmed_at,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
raw_user_meta_data,
|
||||||
|
raw_app_meta_data,
|
||||||
|
role,
|
||||||
|
aud,
|
||||||
|
is_sso_user,
|
||||||
|
is_anonymous,
|
||||||
|
confirmation_token,
|
||||||
|
recovery_token,
|
||||||
|
email_change_token_new,
|
||||||
|
email_change_token_current,
|
||||||
|
email_change
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
-- Terapeuta 2
|
||||||
|
(
|
||||||
|
'00000000-0000-0000-0000-000000000000',
|
||||||
|
'aaaaaaaa-0009-0009-0009-000000000009',
|
||||||
|
'therapist2@agenciapsi.com.br',
|
||||||
|
crypt('Teste@123', gen_salt('bf')),
|
||||||
|
now(), now(), now(),
|
||||||
|
'{"name": "Eva Terapeuta"}'::jsonb,
|
||||||
|
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||||
|
'authenticated', 'authenticated', false, false, '', '', '', '', ''
|
||||||
|
),
|
||||||
|
-- Terapeuta 3
|
||||||
|
(
|
||||||
|
'00000000-0000-0000-0000-000000000000',
|
||||||
|
'aaaaaaaa-0010-0010-0010-000000000010',
|
||||||
|
'therapist3@agenciapsi.com.br',
|
||||||
|
crypt('Teste@123', gen_salt('bf')),
|
||||||
|
now(), now(), now(),
|
||||||
|
'{"name": "Felipe Terapeuta"}'::jsonb,
|
||||||
|
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||||
|
'authenticated', 'authenticated', false, false, '', '', '', '', ''
|
||||||
|
),
|
||||||
|
-- Secretária
|
||||||
|
(
|
||||||
|
'00000000-0000-0000-0000-000000000000',
|
||||||
|
'aaaaaaaa-0011-0011-0011-000000000011',
|
||||||
|
'secretary@agenciapsi.com.br',
|
||||||
|
crypt('Teste@123', gen_salt('bf')),
|
||||||
|
now(), now(), now(),
|
||||||
|
'{"name": "Gabriela Secretária"}'::jsonb,
|
||||||
|
'{"provider": "email", "providers": ["email"]}'::jsonb,
|
||||||
|
'authenticated', 'authenticated', false, false, '', '', '', '', ''
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. auth.identities (obrigatório para GoTrue reconhecer login)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO auth.identities (id, user_id, provider_id, provider, identity_data, created_at, updated_at, last_sign_in_at)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
gen_random_uuid(),
|
||||||
|
'aaaaaaaa-0009-0009-0009-000000000009',
|
||||||
|
'therapist2@agenciapsi.com.br',
|
||||||
|
'email',
|
||||||
|
'{"sub": "aaaaaaaa-0009-0009-0009-000000000009", "email": "therapist2@agenciapsi.com.br", "email_verified": true}'::jsonb,
|
||||||
|
now(), now(), now()
|
||||||
|
),
|
||||||
|
(
|
||||||
|
gen_random_uuid(),
|
||||||
|
'aaaaaaaa-0010-0010-0010-000000000010',
|
||||||
|
'therapist3@agenciapsi.com.br',
|
||||||
|
'email',
|
||||||
|
'{"sub": "aaaaaaaa-0010-0010-0010-000000000010", "email": "therapist3@agenciapsi.com.br", "email_verified": true}'::jsonb,
|
||||||
|
now(), now(), now()
|
||||||
|
),
|
||||||
|
(
|
||||||
|
gen_random_uuid(),
|
||||||
|
'aaaaaaaa-0011-0011-0011-000000000011',
|
||||||
|
'secretary@agenciapsi.com.br',
|
||||||
|
'email',
|
||||||
|
'{"sub": "aaaaaaaa-0011-0011-0011-000000000011", "email": "secretary@agenciapsi.com.br", "email_verified": true}'::jsonb,
|
||||||
|
now(), now(), now()
|
||||||
|
)
|
||||||
|
ON CONFLICT (provider, provider_id) DO NOTHING;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. Profiles
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO public.profiles (id, role, account_type, full_name)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
'aaaaaaaa-0009-0009-0009-000000000009',
|
||||||
|
'tenant_member',
|
||||||
|
'therapist',
|
||||||
|
'Eva Terapeuta'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'aaaaaaaa-0010-0010-0010-000000000010',
|
||||||
|
'tenant_member',
|
||||||
|
'therapist',
|
||||||
|
'Felipe Terapeuta'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'aaaaaaaa-0011-0011-0011-000000000011',
|
||||||
|
'tenant_member',
|
||||||
|
'therapist',
|
||||||
|
'Gabriela Secretária'
|
||||||
|
)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
role = EXCLUDED.role,
|
||||||
|
account_type = EXCLUDED.account_type,
|
||||||
|
full_name = EXCLUDED.full_name;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 5. Tenants pessoais dos Terapeutas 2 e 3
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO public.tenants (id, name, kind, created_at)
|
||||||
|
VALUES
|
||||||
|
('bbbbbbbb-0009-0009-0009-000000000009', 'Eva Terapeuta', 'therapist', now()),
|
||||||
|
('bbbbbbbb-0010-0010-0010-000000000010', 'Felipe Terapeuta', 'therapist', now())
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- Terapeuta 2 → tenant_admin do próprio tenant
|
||||||
|
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||||
|
VALUES (
|
||||||
|
'bbbbbbbb-0009-0009-0009-000000000009',
|
||||||
|
'aaaaaaaa-0009-0009-0009-000000000009',
|
||||||
|
'tenant_admin', 'active', now()
|
||||||
|
)
|
||||||
|
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||||
|
|
||||||
|
-- Terapeuta 3 → tenant_admin do próprio tenant
|
||||||
|
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||||
|
VALUES (
|
||||||
|
'bbbbbbbb-0010-0010-0010-000000000010',
|
||||||
|
'aaaaaaaa-0010-0010-0010-000000000010',
|
||||||
|
'tenant_admin', 'active', now()
|
||||||
|
)
|
||||||
|
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 6. Vincula Terapeutas 2 e 3 à Clínica 3 — Full
|
||||||
|
-- (mesmo padrão de terapeuta@agenciapsi.com.br no seed_001)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
'bbbbbbbb-0005-0005-0005-000000000005', -- Clínica Bem Estar (Full)
|
||||||
|
'aaaaaaaa-0009-0009-0009-000000000009', -- Eva Terapeuta
|
||||||
|
'therapist', 'active', now()
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'bbbbbbbb-0005-0005-0005-000000000005', -- Clínica Bem Estar (Full)
|
||||||
|
'aaaaaaaa-0010-0010-0010-000000000010', -- Felipe Terapeuta
|
||||||
|
'therapist', 'active', now()
|
||||||
|
)
|
||||||
|
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 7. Vincula Secretária à Clínica 2 (Recepção) como clinic_admin
|
||||||
|
-- A secretária gerencia a recepção/agenda da clínica.
|
||||||
|
-- Acessa a área /admin com o mesmo contexto de clinic_admin.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||||
|
VALUES (
|
||||||
|
'bbbbbbbb-0004-0004-0004-000000000004', -- Clínica Mente sã (Recepção)
|
||||||
|
'aaaaaaaa-0011-0011-0011-000000000011', -- Gabriela Secretária
|
||||||
|
'clinic_admin', 'active', now()
|
||||||
|
)
|
||||||
|
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 8. Subscriptions
|
||||||
|
-- Terapeutas 2 e 3 → therapist_free (escopo: user_id)
|
||||||
|
-- Secretária → sem assinatura própria (usa o plano da Clínica 2)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Terapeuta 2 → therapist_free
|
||||||
|
INSERT INTO public.subscriptions (
|
||||||
|
user_id, plan_id, plan_key, status, interval,
|
||||||
|
current_period_start, current_period_end,
|
||||||
|
source, started_at, activated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'aaaaaaaa-0009-0009-0009-000000000009',
|
||||||
|
p.id, p.key, 'active', 'month',
|
||||||
|
now(), now() + interval '30 days',
|
||||||
|
'seed', now(), now()
|
||||||
|
FROM public.plans p WHERE p.key = 'therapist_free'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM public.subscriptions s
|
||||||
|
WHERE s.user_id = 'aaaaaaaa-0009-0009-0009-000000000009' AND s.status = 'active'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Terapeuta 3 → therapist_free
|
||||||
|
INSERT INTO public.subscriptions (
|
||||||
|
user_id, plan_id, plan_key, status, interval,
|
||||||
|
current_period_start, current_period_end,
|
||||||
|
source, started_at, activated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'aaaaaaaa-0010-0010-0010-000000000010',
|
||||||
|
p.id, p.key, 'active', 'month',
|
||||||
|
now(), now() + interval '30 days',
|
||||||
|
'seed', now(), now()
|
||||||
|
FROM public.plans p WHERE p.key = 'therapist_free'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM public.subscriptions s
|
||||||
|
WHERE s.user_id = 'aaaaaaaa-0010-0010-0010-000000000010' AND s.status = 'active'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Nota: a Secretária não tem assinatura própria.
|
||||||
|
-- O acesso vem do plano da Clínica 2 (tenant_id = bbbbbbbb-0004-0004-0004-000000000004).
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 9. Confirma
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
RAISE NOTICE '✅ Seed 003 aplicado com sucesso.';
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE ' Usuários criados:';
|
||||||
|
RAISE NOTICE ' therapist2@agenciapsi.com.br → tenant próprio (bbbbbbbb-0009) + Clínica 3 como therapist';
|
||||||
|
RAISE NOTICE ' therapist3@agenciapsi.com.br → tenant próprio (bbbbbbbb-0010) + Clínica 3 como therapist';
|
||||||
|
RAISE NOTICE ' secretary@agenciapsi.com.br → clinic_admin na Clínica 2 Mente sã (bbbbbbbb-0004)';
|
||||||
|
RAISE NOTICE ' Senha de todos: Teste@123';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
74
database-novo/seeds/seed_010_plans.sql
Normal file
74
database-novo/seeds/seed_010_plans.sql
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- SEED 010 — Planos e Preços
|
||||||
|
-- =============================================================================
|
||||||
|
-- Idempotente: ON CONFLICT (id) DO UPDATE
|
||||||
|
--
|
||||||
|
-- Planos:
|
||||||
|
-- patient_free → pacientes (R$0)
|
||||||
|
-- therapist_free → terapeutas solo (R$0)
|
||||||
|
-- therapist_pro → terapeutas PRO (R$49/mês, R$490/ano)
|
||||||
|
-- clinic_free → clínicas (R$0)
|
||||||
|
-- clinic_pro → clínicas PRO (R$149/mês, R$1490/ano)
|
||||||
|
-- supervisor_free → supervisores (R$0, até 3 supervisionados)
|
||||||
|
-- supervisor_pro → supervisores PRO (R$0, até 20 supervisionados)
|
||||||
|
--
|
||||||
|
-- UUIDs preservados do data_dump original para consistência.
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. Plans
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO public.plans (id, key, name, description, is_active, created_at, price_cents, currency, billing_interval, target, max_supervisees)
|
||||||
|
VALUES
|
||||||
|
('984c1f29-a975-4208-93ac-2118ed1039b7', 'patient_free', 'Paciente Free', 'Plano gratuito para pacientes', true, '2026-03-03 22:40:11.413107+00', 0, 'BRL', 'month', 'patient', NULL),
|
||||||
|
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', 'therapist_free', 'THERAPIST FREE', 'Plano gratuito para terapeutas.', true, '2026-03-01 09:40:48.439668+00', 0, 'BRL', 'month', 'therapist', NULL),
|
||||||
|
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', 'therapist_pro', 'THERAPIST PRO', 'Plano completo para terapeutas.', true, '2026-03-01 09:25:03.878498+00', 4900, 'BRL', 'month', 'therapist', NULL),
|
||||||
|
('01a5867f-0705-4714-ac97-a23470949157', 'clinic_free', 'CLINIC FREE', 'Plano gratuito para clínicas iniciarem.', true, '2026-03-01 09:25:03.878498+00', 0, 'BRL', 'month', 'clinic', NULL),
|
||||||
|
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', 'clinic_pro', 'CLINIC PRO', 'Plano completo para clínicas.', true, '2026-03-01 09:30:06.50975+00', 14900, 'BRL', 'month', 'clinic', NULL),
|
||||||
|
('8c4895a3-e12d-48de-a078-efb8a4ea2eb2', 'supervisor_free', 'Supervisor Free', 'Plano gratuito de supervisão. Até 3 terapeutas supervisionados.', true, '2026-03-05 00:58:17.218326+00', 0, 'BRL', 'month', 'supervisor', 3),
|
||||||
|
('ca28e46c-0687-45d5-9406-0a0f56a5b625', 'supervisor_pro', 'Supervisor PRO', 'Plano profissional de supervisão. Até 20 terapeutas supervisionados.', true, '2026-03-05 00:58:17.218326+00', 0, 'BRL', 'month', 'supervisor', 20)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
name = EXCLUDED.name,
|
||||||
|
description = EXCLUDED.description,
|
||||||
|
is_active = EXCLUDED.is_active,
|
||||||
|
price_cents = EXCLUDED.price_cents,
|
||||||
|
currency = EXCLUDED.currency,
|
||||||
|
billing_interval = EXCLUDED.billing_interval,
|
||||||
|
max_supervisees = EXCLUDED.max_supervisees;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. Plan Prices (somente planos pagos)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO public.plan_prices (id, plan_id, currency, "interval", amount_cents, is_active, active_from, active_to, source, provider, provider_price_id, created_at)
|
||||||
|
VALUES
|
||||||
|
-- therapist_pro: R$49/mês
|
||||||
|
('37510504-4617-4421-9979-4249778bd5ae', '82067ba7-16f0-4803-b36f-4c4e8919d4b4', 'BRL', 'month', 4900, true, '2026-03-01 09:25:03.878498+00', NULL, 'manual', NULL, NULL, '2026-03-01 09:25:03.878498+00'),
|
||||||
|
-- therapist_pro: R$490/ano
|
||||||
|
('225afd5a-9f30-46bc-a0df-5eb8f91660cb', '82067ba7-16f0-4803-b36f-4c4e8919d4b4', 'BRL', 'year', 49000, true, '2026-03-01 09:25:03.878498+00', NULL, 'manual', NULL, NULL, '2026-03-01 09:25:03.878498+00'),
|
||||||
|
-- clinic_pro: R$149/mês
|
||||||
|
('124779b4-362d-4890-9631-747021ecc1c0', 'a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', 'BRL', 'month', 14900, true, '2026-03-01 09:30:06.50975+00', NULL, 'manual', NULL, NULL, '2026-03-01 09:30:06.50975+00'),
|
||||||
|
-- clinic_pro: R$1490/ano
|
||||||
|
('73908784-6299-45c8-b547-e1556b45c292', 'a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', 'BRL', 'year', 149000, true, '2026-03-01 09:30:06.50975+00', NULL, 'manual', NULL, NULL, '2026-03-01 09:30:06.50975+00')
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
amount_cents = EXCLUDED.amount_cents,
|
||||||
|
is_active = EXCLUDED.is_active,
|
||||||
|
active_from = EXCLUDED.active_from,
|
||||||
|
active_to = EXCLUDED.active_to;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. Confirma
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
RAISE NOTICE 'seed_010_plans: 7 planos e 4 preços inseridos/atualizados.';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
85
database-novo/seeds/seed_011_features.sql
Normal file
85
database-novo/seeds/seed_011_features.sql
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- SEED 011 — Features (capacidades do sistema)
|
||||||
|
-- =============================================================================
|
||||||
|
-- Idempotente: ON CONFLICT (id) DO UPDATE
|
||||||
|
-- UUIDs preservados do data_dump original.
|
||||||
|
--
|
||||||
|
-- 26 features organizadas por domínio:
|
||||||
|
-- agenda.* → visualizar, gerenciar agenda
|
||||||
|
-- patients.* → módulo de pacientes
|
||||||
|
-- online_scheduling.* → agendamento online
|
||||||
|
-- supervisor.* → supervisão clínica
|
||||||
|
-- reports / reminders → relatórios e lembretes
|
||||||
|
-- clinic / branding → funcionalidades de clínica
|
||||||
|
-- intake → triagem / cadastro externo
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
INSERT INTO public.features (id, key, description, created_at, descricao, name)
|
||||||
|
VALUES
|
||||||
|
-- ── Agenda ──
|
||||||
|
('5e539124-630f-4c2a-a9de-7999317085e6', 'agenda.view', NULL, '2026-02-21 02:36:01.562728+00', 'Visualizar agenda', 'Visualizar Agenda'),
|
||||||
|
('a74fef14-c9d9-4884-ba45-f81c60e0783a', 'agenda.manage', NULL, '2026-02-21 02:35:50.629667+00', 'Adicionar Compromissos na agenda', 'Gerenciar Agenda'),
|
||||||
|
|
||||||
|
-- ── Pacientes ──
|
||||||
|
('a56482a1-0787-49da-90a7-e1857488734a', 'patients', 'Patients module', '2026-03-02 12:35:19.955748+00', 'Pacientes', 'Pacientes'),
|
||||||
|
('57f631a1-9ebe-480b-a2cb-144ad32ff5f0', 'patients.view', NULL, '2026-02-28 11:15:14.275572+00', 'Visualizar pacientes', 'Visualizar Pacientes'),
|
||||||
|
('4e5bc50b-e339-42fe-9d91-61e8555f83e7', 'patients.manage', NULL, '2026-02-28 11:15:14.275572+00', 'Gerenciar pacientes', 'Gerenciar Pacientes'),
|
||||||
|
|
||||||
|
-- ── Agendamento Online ──
|
||||||
|
('53a48c3b-0617-4618-adf8-f3a255c51ee4', 'online_scheduling', 'Online scheduling capability', '2026-03-01 09:59:15.432733+00', 'Agendamento online', 'Agendamento Online (Público)'),
|
||||||
|
('5739aa27-b089-4b15-b149-31b13d768825', 'online_scheduling.manage', 'Gerenciar agendamento online (config/admin)', '2026-02-15 21:50:02.056357+00', 'Gerenciar agendamento online (admin)', 'Agendamento Online'),
|
||||||
|
('0bfe0b1c-8c3d-4c0c-af29-2ddc24f31bc7', 'online_scheduling.public', 'Página pública de agendamento', '2026-02-15 21:50:02.056357+00', 'Página pública de agendamento', 'Página Pública de Agendamento'),
|
||||||
|
|
||||||
|
-- ── Supervisão ──
|
||||||
|
('9ab8bdbb-838b-4946-aa5d-fd9cfdd257b3', 'supervisor.access', NULL, '2026-03-05 00:58:17.218326+00', 'Acesso básico ao espaço de supervisão (sala, lista de supervisionados).', 'Acesso à Supervisão'),
|
||||||
|
('1167b54a-0e93-43a2-94d7-c12e64eb56de', 'supervisor.invite', NULL, '2026-03-05 00:58:17.218326+00', 'Permite convidar terapeutas para participar da sala de supervisão.', 'Convidar Supervisionados'),
|
||||||
|
('761e4495-b46a-4791-9519-86ffe48dc47f', 'supervisor.sessions', NULL, '2026-03-05 00:58:17.218326+00', 'Agendamento e registro de sessões de supervisão.', 'Sessões de Supervisão'),
|
||||||
|
('7e82ee01-44f6-4b3f-9861-840c58e13f58', 'supervisor.reports', NULL, '2026-03-05 00:58:17.218326+00', 'Relatórios avançados de progresso e evolução dos supervisionados.', 'Relatórios de Supervisão'),
|
||||||
|
|
||||||
|
-- ── Relatórios ──
|
||||||
|
('b3efa25d-60a4-4974-8153-6ec098b3d477', 'reports_basic', 'Basic reports', '2026-03-01 09:59:15.432733+00', 'Relatórios básicos', 'Relatórios Básicos'),
|
||||||
|
('bf133ad1-da8e-4ea9-bd66-21901cb50075', 'reports_advanced', 'Advanced reports', '2026-03-01 09:59:15.432733+00', 'Relatórios avançados', 'Relatórios Avançados (v2)'),
|
||||||
|
('a830e45b-3bb4-4b17-812d-fe83777a2377', 'advanced_reports', 'Relatórios avançados (KPIs, filtros e exportações)', '2026-02-15 23:29:55.845638+00', 'Relatórios avançados', 'Relatórios Avançados'),
|
||||||
|
|
||||||
|
-- ── Lembretes ──
|
||||||
|
('f5d66212-fd73-4472-a306-07928e5deaec', 'reminders', 'Notifications/reminders capability', '2026-03-01 09:59:15.432733+00', 'Lembretes', 'Lembretes'),
|
||||||
|
('8cc81988-d02a-4542-9cb2-ce2ed7c18d60', 'sms_reminder', 'Lembretes por SMS (confirmações e avisos)', '2026-02-15 23:29:55.845638+00', 'Lembretes por SMS', 'Lembretes por SMS'),
|
||||||
|
|
||||||
|
-- ── Clínica ──
|
||||||
|
('9b36c65d-b3b3-4bed-b6d5-f7ee8c087c80', 'clinic_calendar', 'Clinic calendar / admin view', '2026-03-01 09:59:15.432733+00', 'Agenda da clínica', 'Agenda da Clínica'),
|
||||||
|
('336aeeba-b18e-4e68-8303-d42ba09f4b20', 'secretary', 'Secretary features (clinic)', '2026-03-01 09:59:15.432733+00', 'Secretaria', 'Secretaria'),
|
||||||
|
('30c9cdd5-7c8c-44d9-8c0b-614165bb9496', 'shared_reception', 'Shared reception / secretary', '2026-03-02 12:35:19.955748+00', 'Recepção / Secretária', 'Recepção Compartilhada'),
|
||||||
|
('74fc1321-4d17-49c3-b72e-db3a7f4be451', 'rooms', 'Rooms / coworking', '2026-03-02 12:35:19.955748+00', 'Salas / Coworking', 'Salas / Coworking'),
|
||||||
|
|
||||||
|
-- ── Intake / Cadastro externo ──
|
||||||
|
('c109ad27-0edf-4774-91a7-94dac4faab49', 'intake_public', 'Public intake link', '2026-03-02 12:35:19.955748+00', 'Link externo de cadastro', 'Link Externo de Cadastro'),
|
||||||
|
('90e92108-8124-40ee-88a0-f0ecafb57d76', 'intakes_pro', 'Cadastros/Intakes PRO (fluxos avançados e automações)', '2026-02-15 23:29:55.845638+00', 'Formulários PRO', 'Formulários PRO'),
|
||||||
|
|
||||||
|
-- ── 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'),
|
||||||
|
|
||||||
|
-- ── 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,
|
||||||
|
description = EXCLUDED.description,
|
||||||
|
descricao = EXCLUDED.descricao,
|
||||||
|
name = EXCLUDED.name;
|
||||||
|
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
RAISE NOTICE 'seed_011_features: 31 features inseridas/atualizadas.';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
215
database-novo/seeds/seed_012_plan_features.sql
Normal file
215
database-novo/seeds/seed_012_plan_features.sql
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- SEED 012 — Plan Features (vínculo plano ↔ feature com limites)
|
||||||
|
-- =============================================================================
|
||||||
|
-- Idempotente: ON CONFLICT (plan_id, feature_id) DO UPDATE
|
||||||
|
--
|
||||||
|
-- Legenda:
|
||||||
|
-- enabled=true → feature ativa no plano
|
||||||
|
-- enabled=false → feature aparece como "PRO" / upsell na UI
|
||||||
|
-- limits jsonb → limites quantitativos (sessions_per_month, etc.)
|
||||||
|
--
|
||||||
|
-- IMPORTANTE: therapist_free e clinic_free incluem features básicas
|
||||||
|
-- necessárias para o sistema funcionar (agenda, pacientes, etc.).
|
||||||
|
-- O data_dump de 11/03 estava incompleto para esses planos.
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Limpa plan_features existentes para reinserir de forma limpa
|
||||||
|
DELETE FROM public.plan_features;
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════
|
||||||
|
-- CLINIC PRO (a74bc2d4) — todas as features habilitadas
|
||||||
|
-- ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
INSERT INTO public.plan_features (plan_id, feature_id, enabled, limits) VALUES
|
||||||
|
-- Agenda
|
||||||
|
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', '5e539124-630f-4c2a-a9de-7999317085e6', true, NULL), -- agenda.view
|
||||||
|
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', 'a74fef14-c9d9-4884-ba45-f81c60e0783a', true, NULL), -- agenda.manage
|
||||||
|
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', '9b36c65d-b3b3-4bed-b6d5-f7ee8c087c80', true, '{"max_patients": 9999, "max_therapists": 999}'), -- clinic_calendar
|
||||||
|
-- Pacientes
|
||||||
|
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', 'a56482a1-0787-49da-90a7-e1857488734a', true, NULL), -- patients
|
||||||
|
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', '57f631a1-9ebe-480b-a2cb-144ad32ff5f0', true, NULL), -- patients.view
|
||||||
|
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', '4e5bc50b-e339-42fe-9d91-61e8555f83e7', true, NULL), -- patients.manage
|
||||||
|
-- Agendamento online
|
||||||
|
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', '53a48c3b-0617-4618-adf8-f3a255c51ee4', true, '{"sessions_per_month": 9999}'), -- online_scheduling
|
||||||
|
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', '5739aa27-b089-4b15-b149-31b13d768825', true, NULL), -- online_scheduling.manage
|
||||||
|
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', '0bfe0b1c-8c3d-4c0c-af29-2ddc24f31bc7', true, NULL), -- online_scheduling.public
|
||||||
|
-- Relatórios
|
||||||
|
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', 'b3efa25d-60a4-4974-8153-6ec098b3d477', true, NULL), -- reports_basic
|
||||||
|
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', 'bf133ad1-da8e-4ea9-bd66-21901cb50075', true, NULL), -- reports_advanced
|
||||||
|
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', 'a830e45b-3bb4-4b17-812d-fe83777a2377', true, NULL), -- advanced_reports
|
||||||
|
-- Lembretes
|
||||||
|
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', 'f5d66212-fd73-4472-a306-07928e5deaec', true, '{"reminders_per_month": 9999}'), -- reminders
|
||||||
|
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', '8cc81988-d02a-4542-9cb2-ce2ed7c18d60', true, NULL), -- sms_reminder
|
||||||
|
-- Clínica
|
||||||
|
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', '336aeeba-b18e-4e68-8303-d42ba09f4b20', true, NULL), -- secretary
|
||||||
|
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', '30c9cdd5-7c8c-44d9-8c0b-614165bb9496', true, NULL), -- shared_reception
|
||||||
|
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', '74fc1321-4d17-49c3-b72e-db3a7f4be451', true, NULL), -- rooms
|
||||||
|
-- Intake
|
||||||
|
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', 'c109ad27-0edf-4774-91a7-94dac4faab49', true, NULL), -- intake_public
|
||||||
|
('a74bc2d4-88c6-4cc6-b004-ef2bcb1b5145', '90e92108-8124-40ee-88a0-f0ecafb57d76', true, NULL), -- intakes_pro
|
||||||
|
-- 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
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════
|
||||||
|
-- CLINIC FREE (01a5867f) — features básicas para funcionar
|
||||||
|
-- ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
INSERT INTO public.plan_features (plan_id, feature_id, enabled, limits) VALUES
|
||||||
|
-- Agenda
|
||||||
|
('01a5867f-0705-4714-ac97-a23470949157', '5e539124-630f-4c2a-a9de-7999317085e6', true, NULL), -- agenda.view
|
||||||
|
('01a5867f-0705-4714-ac97-a23470949157', 'a74fef14-c9d9-4884-ba45-f81c60e0783a', true, NULL), -- agenda.manage
|
||||||
|
('01a5867f-0705-4714-ac97-a23470949157', '9b36c65d-b3b3-4bed-b6d5-f7ee8c087c80', true, '{"max_patients": 30, "max_therapists": 5}'), -- clinic_calendar
|
||||||
|
-- Pacientes
|
||||||
|
('01a5867f-0705-4714-ac97-a23470949157', 'a56482a1-0787-49da-90a7-e1857488734a', true, NULL), -- patients
|
||||||
|
('01a5867f-0705-4714-ac97-a23470949157', '57f631a1-9ebe-480b-a2cb-144ad32ff5f0', true, NULL), -- patients.view
|
||||||
|
('01a5867f-0705-4714-ac97-a23470949157', '4e5bc50b-e339-42fe-9d91-61e8555f83e7', true, NULL), -- patients.manage
|
||||||
|
-- Agendamento online (com limites)
|
||||||
|
('01a5867f-0705-4714-ac97-a23470949157', '53a48c3b-0617-4618-adf8-f3a255c51ee4', true, '{"sessions_per_month": 40}'), -- online_scheduling
|
||||||
|
('01a5867f-0705-4714-ac97-a23470949157', '5739aa27-b089-4b15-b149-31b13d768825', true, NULL), -- online_scheduling.manage
|
||||||
|
('01a5867f-0705-4714-ac97-a23470949157', '0bfe0b1c-8c3d-4c0c-af29-2ddc24f31bc7', true, NULL), -- online_scheduling.public
|
||||||
|
-- Relatórios (básico sim, avançado não)
|
||||||
|
('01a5867f-0705-4714-ac97-a23470949157', 'b3efa25d-60a4-4974-8153-6ec098b3d477', true, NULL), -- reports_basic
|
||||||
|
('01a5867f-0705-4714-ac97-a23470949157', 'bf133ad1-da8e-4ea9-bd66-21901cb50075', false, NULL), -- reports_advanced (PRO)
|
||||||
|
('01a5867f-0705-4714-ac97-a23470949157', 'a830e45b-3bb4-4b17-812d-fe83777a2377', false, NULL), -- advanced_reports (PRO)
|
||||||
|
-- Lembretes (com limites)
|
||||||
|
('01a5867f-0705-4714-ac97-a23470949157', 'f5d66212-fd73-4472-a306-07928e5deaec', true, '{"reminders_per_month": 50}'), -- reminders
|
||||||
|
('01a5867f-0705-4714-ac97-a23470949157', '8cc81988-d02a-4542-9cb2-ce2ed7c18d60', false, NULL), -- sms_reminder (PRO)
|
||||||
|
-- Intake
|
||||||
|
('01a5867f-0705-4714-ac97-a23470949157', 'c109ad27-0edf-4774-91a7-94dac4faab49', true, NULL), -- intake_public
|
||||||
|
('01a5867f-0705-4714-ac97-a23470949157', '90e92108-8124-40ee-88a0-f0ecafb57d76', false, NULL), -- intakes_pro (PRO)
|
||||||
|
-- PRO-only (desabilitado)
|
||||||
|
('01a5867f-0705-4714-ac97-a23470949157', '336aeeba-b18e-4e68-8303-d42ba09f4b20', false, NULL), -- secretary (PRO)
|
||||||
|
('01a5867f-0705-4714-ac97-a23470949157', '30c9cdd5-7c8c-44d9-8c0b-614165bb9496', false, NULL), -- shared_reception (PRO)
|
||||||
|
('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)
|
||||||
|
-- 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)
|
||||||
|
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════
|
||||||
|
-- THERAPIST PRO (82067ba7) — todas as features habilitadas
|
||||||
|
-- ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
INSERT INTO public.plan_features (plan_id, feature_id, enabled, limits) VALUES
|
||||||
|
-- Agenda
|
||||||
|
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', '5e539124-630f-4c2a-a9de-7999317085e6', true, NULL), -- agenda.view
|
||||||
|
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', 'a74fef14-c9d9-4884-ba45-f81c60e0783a', true, NULL), -- agenda.manage
|
||||||
|
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', '9b36c65d-b3b3-4bed-b6d5-f7ee8c087c80', true, NULL), -- clinic_calendar
|
||||||
|
-- Pacientes
|
||||||
|
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', '57f631a1-9ebe-480b-a2cb-144ad32ff5f0', true, NULL), -- patients.view
|
||||||
|
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', '4e5bc50b-e339-42fe-9d91-61e8555f83e7', true, NULL), -- patients.manage
|
||||||
|
-- Agendamento online
|
||||||
|
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', '53a48c3b-0617-4618-adf8-f3a255c51ee4', true, '{"sessions_per_month": 9999}'), -- online_scheduling
|
||||||
|
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', '5739aa27-b089-4b15-b149-31b13d768825', true, NULL), -- online_scheduling.manage
|
||||||
|
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', '0bfe0b1c-8c3d-4c0c-af29-2ddc24f31bc7', true, NULL), -- online_scheduling.public
|
||||||
|
-- Relatórios
|
||||||
|
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', 'b3efa25d-60a4-4974-8153-6ec098b3d477', true, NULL), -- reports_basic
|
||||||
|
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', 'bf133ad1-da8e-4ea9-bd66-21901cb50075', true, NULL), -- reports_advanced
|
||||||
|
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', 'a830e45b-3bb4-4b17-812d-fe83777a2377', true, NULL), -- advanced_reports
|
||||||
|
-- Lembretes
|
||||||
|
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', 'f5d66212-fd73-4472-a306-07928e5deaec', true, '{"reminders_per_month": 9999}'), -- reminders
|
||||||
|
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', '8cc81988-d02a-4542-9cb2-ce2ed7c18d60', true, NULL), -- sms_reminder
|
||||||
|
-- Clínica
|
||||||
|
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', '336aeeba-b18e-4e68-8303-d42ba09f4b20', true, NULL), -- secretary
|
||||||
|
-- Intake
|
||||||
|
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', 'c109ad27-0edf-4774-91a7-94dac4faab49', true, NULL), -- intake_public
|
||||||
|
('82067ba7-16f0-4803-b36f-4c4e8919d4b4', '90e92108-8124-40ee-88a0-f0ecafb57d76', true, NULL), -- intakes_pro
|
||||||
|
-- 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
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════
|
||||||
|
-- THERAPIST FREE (c56fe2a8) — features básicas para funcionar
|
||||||
|
-- ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
INSERT INTO public.plan_features (plan_id, feature_id, enabled, limits) VALUES
|
||||||
|
-- Agenda
|
||||||
|
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', '5e539124-630f-4c2a-a9de-7999317085e6', true, NULL), -- agenda.view
|
||||||
|
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', 'a74fef14-c9d9-4884-ba45-f81c60e0783a', true, NULL), -- agenda.manage
|
||||||
|
-- Pacientes
|
||||||
|
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', '57f631a1-9ebe-480b-a2cb-144ad32ff5f0', true, NULL), -- patients.view
|
||||||
|
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', '4e5bc50b-e339-42fe-9d91-61e8555f83e7', true, NULL), -- patients.manage
|
||||||
|
-- Agendamento online (com limites)
|
||||||
|
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', '53a48c3b-0617-4618-adf8-f3a255c51ee4', true, '{"sessions_per_month": 40}'), -- online_scheduling
|
||||||
|
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', '5739aa27-b089-4b15-b149-31b13d768825', true, NULL), -- online_scheduling.manage
|
||||||
|
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', '0bfe0b1c-8c3d-4c0c-af29-2ddc24f31bc7', true, NULL), -- online_scheduling.public
|
||||||
|
-- Relatórios (básico sim, avançado não)
|
||||||
|
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', 'b3efa25d-60a4-4974-8153-6ec098b3d477', true, NULL), -- reports_basic
|
||||||
|
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', 'bf133ad1-da8e-4ea9-bd66-21901cb50075', false, NULL), -- reports_advanced (PRO)
|
||||||
|
-- Lembretes (com limites)
|
||||||
|
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', 'f5d66212-fd73-4472-a306-07928e5deaec', true, '{"reminders_per_month": 50}'), -- reminders
|
||||||
|
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', '8cc81988-d02a-4542-9cb2-ce2ed7c18d60', false, NULL), -- sms_reminder (PRO)
|
||||||
|
-- Intake
|
||||||
|
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', 'c109ad27-0edf-4774-91a7-94dac4faab49', true, NULL), -- intake_public
|
||||||
|
('c56fe2a8-2c17-4048-adc7-ff7fbd89461a', '90e92108-8124-40ee-88a0-f0ecafb57d76', false, NULL), -- intakes_pro (PRO)
|
||||||
|
-- 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)
|
||||||
|
-- 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)
|
||||||
|
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════
|
||||||
|
-- SUPERVISOR FREE (8c4895a3) — acesso + convite
|
||||||
|
-- ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
INSERT INTO public.plan_features (plan_id, feature_id, enabled, limits) VALUES
|
||||||
|
('8c4895a3-e12d-48de-a078-efb8a4ea2eb2', '9ab8bdbb-838b-4946-aa5d-fd9cfdd257b3', true, NULL), -- supervisor.access
|
||||||
|
('8c4895a3-e12d-48de-a078-efb8a4ea2eb2', '1167b54a-0e93-43a2-94d7-c12e64eb56de', true, NULL); -- supervisor.invite
|
||||||
|
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════
|
||||||
|
-- SUPERVISOR PRO (ca28e46c) — acesso + convite + sessões + relatórios
|
||||||
|
-- ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
INSERT INTO public.plan_features (plan_id, feature_id, enabled, limits) VALUES
|
||||||
|
('ca28e46c-0687-45d5-9406-0a0f56a5b625', '9ab8bdbb-838b-4946-aa5d-fd9cfdd257b3', true, NULL), -- supervisor.access
|
||||||
|
('ca28e46c-0687-45d5-9406-0a0f56a5b625', '1167b54a-0e93-43a2-94d7-c12e64eb56de', true, NULL), -- supervisor.invite
|
||||||
|
('ca28e46c-0687-45d5-9406-0a0f56a5b625', '761e4495-b46a-4791-9519-86ffe48dc47f', true, NULL), -- supervisor.sessions
|
||||||
|
('ca28e46c-0687-45d5-9406-0a0f56a5b625', '7e82ee01-44f6-4b3f-9861-840c58e13f58', true, NULL); -- supervisor.reports
|
||||||
|
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════
|
||||||
|
-- PATIENT FREE (984c1f29) — sem features de plano (acesso via portal)
|
||||||
|
-- ════════════════════════════════════════════════════════════════
|
||||||
|
-- Pacientes acessam via portal_user, não precisam de features de plano.
|
||||||
|
-- O acesso é controlado pelo role 'portal_user' no profile.
|
||||||
|
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
RAISE NOTICE 'seed_012_plan_features: plan_features inseridos para todos os 7 planos.';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
184
database-novo/seeds/seed_013_subscriptions.sql
Normal file
184
database-novo/seeds/seed_013_subscriptions.sql
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- SEED 013 — Subscriptions + Determined Commitments
|
||||||
|
-- =============================================================================
|
||||||
|
-- Execute APÓS: seed_010 (plans), seed_011 (features), seed_012 (plan_features)
|
||||||
|
-- E APÓS: seed_001, seed_002, seed_003 (usuários e tenants)
|
||||||
|
--
|
||||||
|
-- Cria subscriptions para todos os usuários de teste e chama
|
||||||
|
-- seed_determined_commitments() para tenants que ainda não têm.
|
||||||
|
--
|
||||||
|
-- Mapeamento:
|
||||||
|
-- paciente@ (user_id) → patient_free
|
||||||
|
-- terapeuta@ (user_id) → therapist_free
|
||||||
|
-- clinica1@ (tenant_id) → clinic_free (coworking)
|
||||||
|
-- clinica2@ (tenant_id) → clinic_free (recepção)
|
||||||
|
-- clinica3@ (tenant_id) → clinic_free (full)
|
||||||
|
-- supervisor@ (user_id) → supervisor_free
|
||||||
|
-- editor@ (user_id) → therapist_free (acesso via platform_roles)
|
||||||
|
-- therapist2@ (user_id) → therapist_free
|
||||||
|
-- therapist3@ (user_id) → therapist_free
|
||||||
|
-- secretary@ → sem plano próprio (usa plano da Clínica 2)
|
||||||
|
-- saas@ → saas_admin (sem plano)
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Limpa subscriptions de seed anteriores
|
||||||
|
DELETE FROM public.subscriptions WHERE source = 'seed';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. Subscriptions — user_id scope (therapist, patient, supervisor)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Paciente → patient_free
|
||||||
|
INSERT INTO public.subscriptions (
|
||||||
|
user_id, plan_id, plan_key, status, "interval",
|
||||||
|
current_period_start, current_period_end,
|
||||||
|
source, started_at, activated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'aaaaaaaa-0001-0001-0001-000000000001',
|
||||||
|
p.id, p.key, 'active', 'month',
|
||||||
|
now(), now() + interval '1 year',
|
||||||
|
'seed', now(), now()
|
||||||
|
FROM public.plans p WHERE p.key = 'patient_free';
|
||||||
|
|
||||||
|
-- Terapeuta → therapist_free
|
||||||
|
INSERT INTO public.subscriptions (
|
||||||
|
user_id, plan_id, plan_key, status, "interval",
|
||||||
|
current_period_start, current_period_end,
|
||||||
|
source, started_at, activated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'aaaaaaaa-0002-0002-0002-000000000002',
|
||||||
|
p.id, p.key, 'active', 'month',
|
||||||
|
now(), now() + interval '1 year',
|
||||||
|
'seed', now(), now()
|
||||||
|
FROM public.plans p WHERE p.key = 'therapist_free';
|
||||||
|
|
||||||
|
-- Supervisor → supervisor_free
|
||||||
|
INSERT INTO public.subscriptions (
|
||||||
|
user_id, plan_id, plan_key, status, "interval",
|
||||||
|
current_period_start, current_period_end,
|
||||||
|
source, started_at, activated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'aaaaaaaa-0007-0007-0007-000000000007',
|
||||||
|
p.id, p.key, 'active', 'month',
|
||||||
|
now(), now() + interval '1 year',
|
||||||
|
'seed', now(), now()
|
||||||
|
FROM public.plans p WHERE p.key = 'supervisor_free';
|
||||||
|
|
||||||
|
-- Editor → therapist_free (acesso editor vem de platform_roles)
|
||||||
|
INSERT INTO public.subscriptions (
|
||||||
|
user_id, plan_id, plan_key, status, "interval",
|
||||||
|
current_period_start, current_period_end,
|
||||||
|
source, started_at, activated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'aaaaaaaa-0008-0008-0008-000000000008',
|
||||||
|
p.id, p.key, 'active', 'month',
|
||||||
|
now(), now() + interval '1 year',
|
||||||
|
'seed', now(), now()
|
||||||
|
FROM public.plans p WHERE p.key = 'therapist_free';
|
||||||
|
|
||||||
|
-- Terapeuta 2 → therapist_free
|
||||||
|
INSERT INTO public.subscriptions (
|
||||||
|
user_id, plan_id, plan_key, status, "interval",
|
||||||
|
current_period_start, current_period_end,
|
||||||
|
source, started_at, activated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'aaaaaaaa-0009-0009-0009-000000000009',
|
||||||
|
p.id, p.key, 'active', 'month',
|
||||||
|
now(), now() + interval '1 year',
|
||||||
|
'seed', now(), now()
|
||||||
|
FROM public.plans p WHERE p.key = 'therapist_free';
|
||||||
|
|
||||||
|
-- Terapeuta 3 → therapist_free
|
||||||
|
INSERT INTO public.subscriptions (
|
||||||
|
user_id, plan_id, plan_key, status, "interval",
|
||||||
|
current_period_start, current_period_end,
|
||||||
|
source, started_at, activated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'aaaaaaaa-0010-0010-0010-000000000010',
|
||||||
|
p.id, p.key, 'active', 'month',
|
||||||
|
now(), now() + interval '1 year',
|
||||||
|
'seed', now(), now()
|
||||||
|
FROM public.plans p WHERE p.key = 'therapist_free';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. Subscriptions — tenant_id scope (clinic)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Clínica 1 (Espaço Psi / Coworking) → clinic_free
|
||||||
|
INSERT INTO public.subscriptions (
|
||||||
|
tenant_id, plan_id, plan_key, status, "interval",
|
||||||
|
current_period_start, current_period_end,
|
||||||
|
source, started_at, activated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'bbbbbbbb-0003-0003-0003-000000000003',
|
||||||
|
p.id, p.key, 'active', 'month',
|
||||||
|
now(), now() + interval '1 year',
|
||||||
|
'seed', now(), now()
|
||||||
|
FROM public.plans p WHERE p.key = 'clinic_free';
|
||||||
|
|
||||||
|
-- Clínica 2 (Mente sã / Recepção) → clinic_free
|
||||||
|
INSERT INTO public.subscriptions (
|
||||||
|
tenant_id, plan_id, plan_key, status, "interval",
|
||||||
|
current_period_start, current_period_end,
|
||||||
|
source, started_at, activated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'bbbbbbbb-0004-0004-0004-000000000004',
|
||||||
|
p.id, p.key, 'active', 'month',
|
||||||
|
now(), now() + interval '1 year',
|
||||||
|
'seed', now(), now()
|
||||||
|
FROM public.plans p WHERE p.key = 'clinic_free';
|
||||||
|
|
||||||
|
-- Clínica 3 (Bem Estar / Full) → clinic_free
|
||||||
|
INSERT INTO public.subscriptions (
|
||||||
|
tenant_id, plan_id, plan_key, status, "interval",
|
||||||
|
current_period_start, current_period_end,
|
||||||
|
source, started_at, activated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'bbbbbbbb-0005-0005-0005-000000000005',
|
||||||
|
p.id, p.key, 'active', 'month',
|
||||||
|
now(), now() + interval '1 year',
|
||||||
|
'seed', now(), now()
|
||||||
|
FROM public.plans p WHERE p.key = 'clinic_free';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. Determined Commitments (idempotente por design da função)
|
||||||
|
-- ============================================================
|
||||||
|
-- Chama para todos os tenants. A função usa ON CONFLICT internamente.
|
||||||
|
|
||||||
|
DO $$ BEGIN PERFORM public.seed_determined_commitments('bbbbbbbb-0002-0002-0002-000000000002'); END $$; -- Terapeuta
|
||||||
|
DO $$ BEGIN PERFORM public.seed_determined_commitments('bbbbbbbb-0003-0003-0003-000000000003'); END $$; -- Clínica 1
|
||||||
|
DO $$ BEGIN PERFORM public.seed_determined_commitments('bbbbbbbb-0004-0004-0004-000000000004'); END $$; -- Clínica 2
|
||||||
|
DO $$ BEGIN PERFORM public.seed_determined_commitments('bbbbbbbb-0005-0005-0005-000000000005'); END $$; -- Clínica 3
|
||||||
|
DO $$ BEGIN PERFORM public.seed_determined_commitments('bbbbbbbb-0009-0009-0009-000000000009'); END $$; -- Terapeuta 2
|
||||||
|
DO $$ BEGIN PERFORM public.seed_determined_commitments('bbbbbbbb-0010-0010-0010-000000000010'); END $$; -- Terapeuta 3
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. Confirma
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
v_count integer;
|
||||||
|
BEGIN
|
||||||
|
SELECT count(*) INTO v_count FROM public.subscriptions WHERE source = 'seed';
|
||||||
|
RAISE NOTICE 'seed_013_subscriptions: % subscriptions ativas criadas.', v_count;
|
||||||
|
SELECT count(*) INTO v_count FROM public.determined_commitments WHERE is_native = true;
|
||||||
|
RAISE NOTICE 'seed_013_subscriptions: % determined_commitments nativos existentes.', v_count;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
427
database-novo/seeds/seed_014_global_data.sql
Normal file
427
database-novo/seeds/seed_014_global_data.sql
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- SEED 014 — Dados Globais (Email Templates, Notification Templates, Carousel)
|
||||||
|
-- =============================================================================
|
||||||
|
-- Idempotente: ON CONFLICT DO UPDATE / DO NOTHING
|
||||||
|
--
|
||||||
|
-- Cria:
|
||||||
|
-- 11 email_templates_global (templates de email do sistema)
|
||||||
|
-- 7 notification_templates WhatsApp (default)
|
||||||
|
-- 6 notification_templates SMS (default)
|
||||||
|
-- 3 login_carousel_slides (slides da página de login)
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. Email Templates Globais
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO public.email_templates_global
|
||||||
|
(key, domain, channel, subject, body_html, body_text, version, is_active, variables)
|
||||||
|
VALUES
|
||||||
|
|
||||||
|
-- session.reminder.email
|
||||||
|
(
|
||||||
|
'session.reminder.email',
|
||||||
|
'session',
|
||||||
|
'email',
|
||||||
|
'Lembrete: sua sessão amanhã às {{session_time}}',
|
||||||
|
'
|
||||||
|
<p>Olá, <strong>{{patient_name}}</strong>!</p>
|
||||||
|
<p>Este é um lembrete da sua sessão agendada para <strong>{{session_date}}</strong> às <strong>{{session_time}}</strong>.</p>
|
||||||
|
<p>Modalidade: <strong>{{session_modality}}</strong></p>
|
||||||
|
{{#if session_link}}
|
||||||
|
<p><a href="{{session_link}}">Clique aqui para entrar na sessão online</a></p>
|
||||||
|
{{/if}}
|
||||||
|
<p>Em caso de necessidade de cancelamento, entre em contato com antecedência.</p>
|
||||||
|
<p>Até logo,<br><strong>{{therapist_name}}</strong></p>
|
||||||
|
',
|
||||||
|
'Olá, {{patient_name}}! Lembrete da sua sessão: {{session_date}} às {{session_time}} ({{session_modality}}).',
|
||||||
|
2,
|
||||||
|
true,
|
||||||
|
'{"patient_name":"Nome completo do paciente","session_date":"Data da sessão (ex: 20/03/2026)","session_time":"Horário da sessão (ex: 14:00)","session_modality":"Presencial ou Online","session_link":"Link da videochamada (apenas online)","therapist_name":"Nome do terapeuta"}'
|
||||||
|
),
|
||||||
|
|
||||||
|
-- session.confirmation.email
|
||||||
|
(
|
||||||
|
'session.confirmation.email',
|
||||||
|
'session',
|
||||||
|
'email',
|
||||||
|
'Sessão confirmada — {{session_date}} às {{session_time}}',
|
||||||
|
'
|
||||||
|
<p>Olá, <strong>{{patient_name}}</strong>!</p>
|
||||||
|
<p>Sua sessão foi confirmada com sucesso.</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Data:</strong> {{session_date}}</li>
|
||||||
|
<li><strong>Horário:</strong> {{session_time}}</li>
|
||||||
|
<li><strong>Modalidade:</strong> {{session_modality}}</li>
|
||||||
|
{{#if session_address}}<li><strong>Local:</strong> {{session_address}}</li>{{/if}}
|
||||||
|
</ul>
|
||||||
|
<p>Até lá,<br><strong>{{therapist_name}}</strong></p>
|
||||||
|
',
|
||||||
|
'Sessão confirmada: {{session_date}} às {{session_time}} ({{session_modality}}).',
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
'{"patient_name":"Nome do paciente","session_date":"Data da sessão","session_time":"Horário da sessão","session_modality":"Presencial ou Online","session_address":"Endereço (apenas presencial)","therapist_name":"Nome do terapeuta"}'
|
||||||
|
),
|
||||||
|
|
||||||
|
-- session.cancellation.email
|
||||||
|
(
|
||||||
|
'session.cancellation.email',
|
||||||
|
'session',
|
||||||
|
'email',
|
||||||
|
'Sessão cancelada — {{session_date}}',
|
||||||
|
'
|
||||||
|
<p>Olá, <strong>{{patient_name}}</strong>!</p>
|
||||||
|
<p>Informamos que sua sessão do dia <strong>{{session_date}}</strong> às <strong>{{session_time}}</strong> foi cancelada.</p>
|
||||||
|
{{#if cancellation_reason}}<p>Motivo: {{cancellation_reason}}</p>{{/if}}
|
||||||
|
<p>Entre em contato para reagendar.</p>
|
||||||
|
<p><strong>{{therapist_name}}</strong></p>
|
||||||
|
',
|
||||||
|
NULL,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
'{"patient_name":"Nome do paciente","session_date":"Data cancelada","session_time":"Horário cancelado","cancellation_reason":"Motivo do cancelamento (opcional)","therapist_name":"Nome do terapeuta"}'
|
||||||
|
),
|
||||||
|
|
||||||
|
-- session.rescheduled.email
|
||||||
|
(
|
||||||
|
'session.rescheduled.email',
|
||||||
|
'session',
|
||||||
|
'email',
|
||||||
|
'Sessão reagendada — {{session_date}} às {{session_time}}',
|
||||||
|
'
|
||||||
|
<p>Olá, <strong>{{patient_name}}</strong>!</p>
|
||||||
|
<p>Sua sessão foi reagendada para <strong>{{session_date}}</strong> às <strong>{{session_time}}</strong>.</p>
|
||||||
|
<p>Modalidade: <strong>{{session_modality}}</strong></p>
|
||||||
|
<p>Até lá,<br><strong>{{therapist_name}}</strong></p>
|
||||||
|
',
|
||||||
|
NULL,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
'{"patient_name":"Nome do paciente","session_date":"Nova data","session_time":"Novo horário","session_modality":"Presencial ou Online","therapist_name":"Nome do terapeuta"}'
|
||||||
|
),
|
||||||
|
|
||||||
|
-- intake.received.email
|
||||||
|
(
|
||||||
|
'intake.received.email',
|
||||||
|
'intake',
|
||||||
|
'email',
|
||||||
|
'Recebemos seu cadastro — {{clinic_name}}',
|
||||||
|
'
|
||||||
|
<p>Olá, <strong>{{patient_name}}</strong>!</p>
|
||||||
|
<p>Recebemos seu cadastro com sucesso. Nossa equipe entrará em contato em breve para dar continuidade ao processo.</p>
|
||||||
|
<p>Obrigado pela confiança,<br><strong>{{clinic_name}}</strong></p>
|
||||||
|
',
|
||||||
|
NULL,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
'{"patient_name":"Nome do solicitante","clinic_name":"Nome da clínica ou terapeuta"}'
|
||||||
|
),
|
||||||
|
|
||||||
|
-- intake.approved.email
|
||||||
|
(
|
||||||
|
'intake.approved.email',
|
||||||
|
'intake',
|
||||||
|
'email',
|
||||||
|
'Cadastro aprovado — bem-vindo(a)!',
|
||||||
|
'
|
||||||
|
<p>Olá, <strong>{{patient_name}}</strong>!</p>
|
||||||
|
<p>Seu cadastro foi aprovado. Você já pode acessar o portal e agendar sua primeira sessão.</p>
|
||||||
|
<p><a href="{{portal_link}}">Acessar portal →</a></p>
|
||||||
|
<p>Qualquer dúvida, estamos à disposição.<br><strong>{{therapist_name}}</strong></p>
|
||||||
|
',
|
||||||
|
NULL,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
'{"patient_name":"Nome do paciente","therapist_name":"Nome do terapeuta","portal_link":"Link do portal do paciente"}'
|
||||||
|
),
|
||||||
|
|
||||||
|
-- intake.rejected.email
|
||||||
|
(
|
||||||
|
'intake.rejected.email',
|
||||||
|
'intake',
|
||||||
|
'email',
|
||||||
|
'Atualização sobre seu cadastro — {{clinic_name}}',
|
||||||
|
'
|
||||||
|
<p>Olá, <strong>{{patient_name}}</strong>!</p>
|
||||||
|
<p>Agradecemos seu interesse. Infelizmente não será possível dar continuidade ao seu cadastro no momento.</p>
|
||||||
|
{{#if rejection_reason}}<p>{{rejection_reason}}</p>{{/if}}
|
||||||
|
<p><strong>{{clinic_name}}</strong></p>
|
||||||
|
',
|
||||||
|
NULL,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
'{"patient_name":"Nome do paciente","clinic_name":"Nome da clínica","rejection_reason":"Motivo (opcional)"}'
|
||||||
|
),
|
||||||
|
|
||||||
|
-- scheduler.request_accepted.email
|
||||||
|
(
|
||||||
|
'scheduler.request_accepted.email',
|
||||||
|
'session',
|
||||||
|
'email',
|
||||||
|
'Sua solicitação foi aceita — {{session_date}} às {{session_time}}',
|
||||||
|
'
|
||||||
|
<p>Olá, <strong>{{patient_name}}</strong>!</p>
|
||||||
|
<p>Sua solicitação de agendamento foi aceita.</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Data:</strong> {{session_date}}</li>
|
||||||
|
<li><strong>Horário:</strong> {{session_time}}</li>
|
||||||
|
<li><strong>Tipo:</strong> {{session_type}}</li>
|
||||||
|
<li><strong>Modalidade:</strong> {{session_modality}}</li>
|
||||||
|
</ul>
|
||||||
|
<p>Até logo,<br><strong>{{therapist_name}}</strong></p>
|
||||||
|
',
|
||||||
|
NULL,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
'{"patient_name":"Nome do paciente","session_date":"Data confirmada","session_time":"Horário confirmado","session_type":"Primeira consulta / Retorno","session_modality":"Presencial ou Online","therapist_name":"Nome do terapeuta"}'
|
||||||
|
),
|
||||||
|
|
||||||
|
-- scheduler.request_rejected.email
|
||||||
|
(
|
||||||
|
'scheduler.request_rejected.email',
|
||||||
|
'session',
|
||||||
|
'email',
|
||||||
|
'Atualização sobre sua solicitação de agendamento',
|
||||||
|
'
|
||||||
|
<p>Olá, <strong>{{patient_name}}</strong>!</p>
|
||||||
|
<p>Infelizmente não foi possível confirmar sua solicitação de agendamento para <strong>{{session_date}}</strong>.</p>
|
||||||
|
{{#if rejection_reason}}<p>Motivo: {{rejection_reason}}</p>{{/if}}
|
||||||
|
<p>Entre em contato para verificar outros horários disponíveis.</p>
|
||||||
|
<p><strong>{{therapist_name}}</strong></p>
|
||||||
|
',
|
||||||
|
NULL,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
'{"patient_name":"Nome do paciente","session_date":"Data solicitada","rejection_reason":"Motivo (opcional)","therapist_name":"Nome do terapeuta"}'
|
||||||
|
),
|
||||||
|
|
||||||
|
-- system.welcome.email
|
||||||
|
(
|
||||||
|
'system.welcome.email',
|
||||||
|
'system',
|
||||||
|
'email',
|
||||||
|
'Bem-vindo(a) ao {{clinic_name}}!',
|
||||||
|
'
|
||||||
|
<p>Olá, <strong>{{patient_name}}</strong>!</p>
|
||||||
|
<p>Seja bem-vindo(a)! Sua conta foi criada com sucesso.</p>
|
||||||
|
<p><a href="{{portal_link}}">Acessar minha área →</a></p>
|
||||||
|
',
|
||||||
|
NULL,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
'{"patient_name":"Nome do paciente","clinic_name":"Nome da clínica","portal_link":"Link do portal"}'
|
||||||
|
),
|
||||||
|
|
||||||
|
-- system.password_reset.email
|
||||||
|
(
|
||||||
|
'system.password_reset.email',
|
||||||
|
'system',
|
||||||
|
'email',
|
||||||
|
'Redefinição de senha',
|
||||||
|
'
|
||||||
|
<p>Olá, <strong>{{patient_name}}</strong>!</p>
|
||||||
|
<p>Recebemos uma solicitação para redefinir sua senha.</p>
|
||||||
|
<p><a href="{{reset_link}}">Clique aqui para redefinir sua senha →</a></p>
|
||||||
|
<p>Se você não solicitou a redefinição, ignore este e-mail.</p>
|
||||||
|
',
|
||||||
|
NULL,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
'{"patient_name":"Nome do usuário","reset_link":"Link de redefinição de senha"}'
|
||||||
|
)
|
||||||
|
|
||||||
|
ON CONFLICT (key) DO UPDATE SET
|
||||||
|
domain = EXCLUDED.domain,
|
||||||
|
channel = EXCLUDED.channel,
|
||||||
|
subject = EXCLUDED.subject,
|
||||||
|
body_html = EXCLUDED.body_html,
|
||||||
|
body_text = EXCLUDED.body_text,
|
||||||
|
version = EXCLUDED.version,
|
||||||
|
is_active = EXCLUDED.is_active,
|
||||||
|
variables = EXCLUDED.variables,
|
||||||
|
updated_at = now();
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. Notification Templates — WhatsApp (default)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO public.notification_templates
|
||||||
|
(tenant_id, owner_id, key, domain, channel, event_type, body_text, variables, is_default, is_active)
|
||||||
|
VALUES
|
||||||
|
-- Lembrete 24h
|
||||||
|
(
|
||||||
|
NULL, NULL,
|
||||||
|
'session.lembrete.whatsapp',
|
||||||
|
'session', 'whatsapp', 'lembrete_sessao',
|
||||||
|
E'Olá, {{nome_paciente}}! \U0001F44B\n\nLembrete da sua sessão com {{nome_terapeuta}}.\n\n\U0001F4C5 Data: {{data_sessao}}\n\U0001F550 Hora: {{hora_sessao}}\n\U0001F4CD Modalidade: {{modalidade}}\n\nQualquer dúvida, entre em contato. Até lá! \U0001F49A',
|
||||||
|
'["nome_paciente","nome_terapeuta","data_sessao","hora_sessao","modalidade"]',
|
||||||
|
true, true
|
||||||
|
),
|
||||||
|
-- Lembrete 2h
|
||||||
|
(
|
||||||
|
NULL, NULL,
|
||||||
|
'session.lembrete_2h.whatsapp',
|
||||||
|
'session', 'whatsapp', 'lembrete_sessao',
|
||||||
|
E'Olá, {{nome_paciente}}! Sua sessão com {{nome_terapeuta}} começa em 2 horas.\n\n\U0001F550 Hora: {{hora_sessao}}\n\U0001F4CD Modalidade: {{modalidade}}\n\nAté já! \U0001F60A',
|
||||||
|
'["nome_paciente","nome_terapeuta","data_sessao","hora_sessao","modalidade"]',
|
||||||
|
true, true
|
||||||
|
),
|
||||||
|
-- Confirmação
|
||||||
|
(
|
||||||
|
NULL, NULL,
|
||||||
|
'session.confirmacao.whatsapp',
|
||||||
|
'session', 'whatsapp', 'confirmacao_sessao',
|
||||||
|
E'Olá, {{nome_paciente}}! \u2705\n\nSua sessão foi confirmada!\n\n\U0001F4C5 Data: {{data_sessao}}\n\U0001F550 Hora: {{hora_sessao}}\n\U0001F4CD Modalidade: {{modalidade}}\n\nAté lá! \U0001F49A',
|
||||||
|
'["nome_paciente","nome_terapeuta","data_sessao","hora_sessao","modalidade","link_confirmacao"]',
|
||||||
|
true, true
|
||||||
|
),
|
||||||
|
-- Cancelamento
|
||||||
|
(
|
||||||
|
NULL, NULL,
|
||||||
|
'session.cancelamento.whatsapp',
|
||||||
|
'session', 'whatsapp', 'cancelamento_sessao',
|
||||||
|
E'Olá, {{nome_paciente}}. Infelizmente a sessão do dia {{data_sessao}} às {{hora_sessao}} foi cancelada.\n\nEntre em contato para reagendar. \U0001F64F',
|
||||||
|
'["nome_paciente","nome_terapeuta","data_sessao","hora_sessao","modalidade"]',
|
||||||
|
true, true
|
||||||
|
),
|
||||||
|
-- Reagendamento
|
||||||
|
(
|
||||||
|
NULL, NULL,
|
||||||
|
'session.reagendamento.whatsapp',
|
||||||
|
'session', 'whatsapp', 'reagendamento',
|
||||||
|
E'Olá, {{nome_paciente}}! Sua sessão foi reagendada.\n\n\U0001F4C5 Nova data: {{data_sessao}}\n\U0001F550 Novo horário: {{hora_sessao}}\n\U0001F4CD Modalidade: {{modalidade}}\n\nAté lá! \U0001F60A',
|
||||||
|
'["nome_paciente","nome_terapeuta","data_sessao","hora_sessao","modalidade"]',
|
||||||
|
true, true
|
||||||
|
),
|
||||||
|
-- Cobrança pendente
|
||||||
|
(
|
||||||
|
NULL, NULL,
|
||||||
|
'cobranca.pendente.whatsapp',
|
||||||
|
'billing', 'whatsapp', 'cobranca_pendente',
|
||||||
|
E'Olá, {{nome_paciente}}. Identificamos uma cobrança pendente no valor de {{valor_sessao}} referente à sua sessão do dia {{data_sessao}}.\n\nPor favor, entre em contato para regularizar. \U0001F64F',
|
||||||
|
'["nome_paciente","nome_terapeuta","data_sessao","hora_sessao","modalidade","valor_sessao"]',
|
||||||
|
true, true
|
||||||
|
),
|
||||||
|
-- Boas-vindas
|
||||||
|
(
|
||||||
|
NULL, NULL,
|
||||||
|
'sistema.boas_vindas.whatsapp',
|
||||||
|
'system', 'whatsapp', 'boas_vindas_paciente',
|
||||||
|
E'Olá, {{nome_paciente}}! \U0001F389\n\nSeja muito bem-vindo(a)! Estamos felizes em ter você aqui.\n\nEm caso de dúvidas, estamos à disposição. Até a nossa primeira sessão! \U0001F49A',
|
||||||
|
'["nome_paciente","nome_terapeuta","data_sessao","hora_sessao","modalidade"]',
|
||||||
|
true, true
|
||||||
|
)
|
||||||
|
ON CONFLICT (tenant_id, owner_id, key, deleted_at) DO NOTHING;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. Notification Templates — SMS (default)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO public.notification_templates
|
||||||
|
(tenant_id, owner_id, key, domain, channel, event_type, body_text, variables, is_default, is_active)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
NULL, NULL,
|
||||||
|
'session.reminder.sms',
|
||||||
|
'session', 'sms', 'lembrete_sessao',
|
||||||
|
'Olá {{patient_name}}, lembrete da sua sessão em {{session_date}} às {{session_time}} com {{therapist_name}}. Modalidade: {{session_modality}}.',
|
||||||
|
'["patient_name","session_date","session_time","therapist_name","session_modality"]',
|
||||||
|
true, true
|
||||||
|
),
|
||||||
|
(
|
||||||
|
NULL, NULL,
|
||||||
|
'session.confirmation.sms',
|
||||||
|
'session', 'sms', 'confirmacao_sessao',
|
||||||
|
'Sessão confirmada! {{session_date}} às {{session_time}} ({{session_modality}}) com {{therapist_name}}.',
|
||||||
|
'["patient_name","session_date","session_time","session_modality","therapist_name"]',
|
||||||
|
true, true
|
||||||
|
),
|
||||||
|
(
|
||||||
|
NULL, NULL,
|
||||||
|
'session.cancellation.sms',
|
||||||
|
'session', 'sms', 'cancelamento_sessao',
|
||||||
|
'Sua sessão de {{session_date}} às {{session_time}} foi cancelada. Entre em contato para reagendar. — {{therapist_name}}',
|
||||||
|
'["patient_name","session_date","session_time","therapist_name"]',
|
||||||
|
true, true
|
||||||
|
),
|
||||||
|
(
|
||||||
|
NULL, NULL,
|
||||||
|
'session.rescheduled.sms',
|
||||||
|
'session', 'sms', 'reagendamento',
|
||||||
|
'Sua sessão foi reagendada para {{session_date}} às {{session_time}} ({{session_modality}}). — {{therapist_name}}',
|
||||||
|
'["patient_name","session_date","session_time","session_modality","therapist_name"]',
|
||||||
|
true, true
|
||||||
|
),
|
||||||
|
(
|
||||||
|
NULL, NULL,
|
||||||
|
'intake.received.sms',
|
||||||
|
'intake', 'sms', 'intake_recebido',
|
||||||
|
'Olá {{patient_name}}, recebemos seu cadastro. Em breve entraremos em contato. — {{clinic_name}}',
|
||||||
|
'["patient_name","clinic_name"]',
|
||||||
|
true, true
|
||||||
|
),
|
||||||
|
(
|
||||||
|
NULL, NULL,
|
||||||
|
'intake.approved.sms',
|
||||||
|
'intake', 'sms', 'intake_aprovado',
|
||||||
|
'Olá {{patient_name}}, seu cadastro foi aprovado! Acesse o portal para agendar sua sessão. — {{therapist_name}}',
|
||||||
|
'["patient_name","therapist_name"]',
|
||||||
|
true, true
|
||||||
|
)
|
||||||
|
ON CONFLICT (tenant_id, owner_id, key, deleted_at) DO NOTHING;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. Login Carousel Slides
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Limpa slides existentes e reinsere
|
||||||
|
DELETE FROM public.login_carousel_slides;
|
||||||
|
|
||||||
|
INSERT INTO public.login_carousel_slides (title, body, icon, ordem, ativo)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
'<strong>Gestão clínica simplificada</strong>',
|
||||||
|
'Agendamentos, prontuários e sessões em um único painel. Foco no que importa: <em>seus pacientes</em>.',
|
||||||
|
'pi-calendar-clock',
|
||||||
|
0,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'<strong>Múltiplos profissionais, uma só plataforma</strong>',
|
||||||
|
'Adicione terapeutas, gerencie vínculos por clínica e mantenha equipes organizadas com controle de acesso por papel.',
|
||||||
|
'pi-users',
|
||||||
|
1,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'<strong>Seguro, privado e sempre disponível</strong>',
|
||||||
|
'Dados protegidos com autenticação robusta, controle de acesso por perfil e conformidade com as boas práticas de privacidade.',
|
||||||
|
'pi-shield',
|
||||||
|
2,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 5. Confirma
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
v_emails integer;
|
||||||
|
v_notif integer;
|
||||||
|
v_slides integer;
|
||||||
|
BEGIN
|
||||||
|
SELECT count(*) INTO v_emails FROM public.email_templates_global;
|
||||||
|
SELECT count(*) INTO v_notif FROM public.notification_templates WHERE is_default = true;
|
||||||
|
SELECT count(*) INTO v_slides FROM public.login_carousel_slides WHERE ativo = true;
|
||||||
|
RAISE NOTICE 'seed_014_global_data: % email templates, % notification templates, % carousel slides.', v_emails, v_notif, v_slides;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
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
|
||||||
|
-- ==========================================================================
|
||||||
929
database-novo/seeds/seed_020_test_data.sql
Normal file
929
database-novo/seeds/seed_020_test_data.sql
Normal file
@@ -0,0 +1,929 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- SEED DE TESTE — AgenciaPsi
|
||||||
|
-- ============================================================
|
||||||
|
-- Cria 10 terapeutas com pacientes, agendamentos, sessões,
|
||||||
|
-- cancelamentos, remarcações, faltas, grupos, financeiro, etc.
|
||||||
|
--
|
||||||
|
-- IMPORTANTE:
|
||||||
|
-- 1) Os usuários (auth.users) devem ser criados via Supabase Auth
|
||||||
|
-- ou via auth.users INSERT direto (requer service_role).
|
||||||
|
-- Este script assume que os 10 users já existem no auth.users.
|
||||||
|
--
|
||||||
|
-- 2) Execute este script com a service_role key no SQL Editor
|
||||||
|
-- do Supabase (ou via psql com permissão).
|
||||||
|
--
|
||||||
|
-- 3) Todos os UUIDs são fixos e determinísticos para facilitar
|
||||||
|
-- depuração. Prefixo:
|
||||||
|
-- Terapeutas: aaaaaaaa-0001..0010
|
||||||
|
-- Tenants: bbbbbbbb-0001..0010
|
||||||
|
-- Pacientes: cccccccc-0001..0050
|
||||||
|
-- Eventos: dddddddd-0001..0200
|
||||||
|
-- Financeiro: eeeeeeee-0001..0200
|
||||||
|
-- Grupos: ffffffff-0001..0020
|
||||||
|
-- Recorrência: 11111111-0001..0010
|
||||||
|
--
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 0. LIMPAR DADOS DE TESTE (idempotente)
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
_tenant_ids uuid[] := ARRAY[
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000002'::uuid,
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000003'::uuid,
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000005'::uuid,
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000006'::uuid,
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000007'::uuid,
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000008'::uuid,
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000009'::uuid,
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000010'::uuid
|
||||||
|
];
|
||||||
|
BEGIN
|
||||||
|
-- Financeiro
|
||||||
|
DELETE FROM public.financial_records WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
-- Eventos
|
||||||
|
DELETE FROM public.agenda_eventos WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
-- Recorrências
|
||||||
|
DELETE FROM public.recurrence_rules WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
-- Grupos
|
||||||
|
DELETE FROM public.patient_group_patient WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
DELETE FROM public.patient_groups WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
-- Pacientes
|
||||||
|
DELETE FROM public.patients WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
-- Notificações
|
||||||
|
DELETE FROM public.notification_channels WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
DELETE FROM public.notification_templates WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
DELETE FROM public.notification_logs WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
DELETE FROM public.notification_schedules WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
-- Regras semanais
|
||||||
|
DELETE FROM public.agenda_regras_semanais WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
-- Serviços
|
||||||
|
DELETE FROM public.services WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
-- Tenant members
|
||||||
|
DELETE FROM public.tenant_members WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
-- Company profiles
|
||||||
|
DELETE FROM public.company_profiles WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
-- Tenants
|
||||||
|
DELETE FROM public.tenants WHERE id = ANY(_tenant_ids);
|
||||||
|
-- Profiles
|
||||||
|
DELETE FROM public.profiles WHERE id IN (
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000002'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000003'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000005'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000006'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000007'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000008'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000009'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000010'::uuid
|
||||||
|
);
|
||||||
|
-- Auth users (requer service_role)
|
||||||
|
DELETE FROM auth.users WHERE id IN (
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000002'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000003'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000005'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000006'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000007'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000008'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000009'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000010'::uuid
|
||||||
|
);
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 1. USUÁRIOS (auth.users) — 10 terapeutas
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- Desabilitar triggers que chamam funções inexistentes
|
||||||
|
ALTER TABLE auth.users DISABLE TRIGGER ALL;
|
||||||
|
|
||||||
|
-- Senha: Test@1234 (hash bcrypt)
|
||||||
|
INSERT INTO auth.users (
|
||||||
|
id, instance_id, email, encrypted_password,
|
||||||
|
email_confirmed_at, aud, role,
|
||||||
|
raw_app_meta_data, raw_user_meta_data,
|
||||||
|
created_at, updated_at
|
||||||
|
) VALUES
|
||||||
|
('aaaaaaaa-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000000', 'terapeuta01@teste.agenciapsi.com', '$2a$10$PwGnTf1HMjHGZSM0YDi6ku7U7ZZZcXOiI4XOoFSqLJ3szGBTniCeG', now(), 'authenticated', 'authenticated', '{"provider":"email","providers":["email"]}', '{"full_name":"Dra. Ana Beatriz"}', now(), now()),
|
||||||
|
('aaaaaaaa-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000000', 'terapeuta02@teste.agenciapsi.com', '$2a$10$PwGnTf1HMjHGZSM0YDi6ku7U7ZZZcXOiI4XOoFSqLJ3szGBTniCeG', now(), 'authenticated', 'authenticated', '{"provider":"email","providers":["email"]}', '{"full_name":"Dr. Carlos Eduardo"}', now(), now()),
|
||||||
|
('aaaaaaaa-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000000', 'terapeuta03@teste.agenciapsi.com', '$2a$10$PwGnTf1HMjHGZSM0YDi6ku7U7ZZZcXOiI4XOoFSqLJ3szGBTniCeG', now(), 'authenticated', 'authenticated', '{"provider":"email","providers":["email"]}', '{"full_name":"Dra. Fernanda Lima"}', now(), now()),
|
||||||
|
('aaaaaaaa-0000-0000-0000-000000000004', '00000000-0000-0000-0000-000000000000', 'terapeuta04@teste.agenciapsi.com', '$2a$10$PwGnTf1HMjHGZSM0YDi6ku7U7ZZZcXOiI4XOoFSqLJ3szGBTniCeG', now(), 'authenticated', 'authenticated', '{"provider":"email","providers":["email"]}', '{"full_name":"Dr. Gabriel Santos"}', now(), now()),
|
||||||
|
('aaaaaaaa-0000-0000-0000-000000000005', '00000000-0000-0000-0000-000000000000', 'terapeuta05@teste.agenciapsi.com', '$2a$10$PwGnTf1HMjHGZSM0YDi6ku7U7ZZZcXOiI4XOoFSqLJ3szGBTniCeG', now(), 'authenticated', 'authenticated', '{"provider":"email","providers":["email"]}', '{"full_name":"Dra. Juliana Alves"}', now(), now()),
|
||||||
|
('aaaaaaaa-0000-0000-0000-000000000006', '00000000-0000-0000-0000-000000000000', 'terapeuta06@teste.agenciapsi.com', '$2a$10$PwGnTf1HMjHGZSM0YDi6ku7U7ZZZcXOiI4XOoFSqLJ3szGBTniCeG', now(), 'authenticated', 'authenticated', '{"provider":"email","providers":["email"]}', '{"full_name":"Dr. Lucas Oliveira"}', now(), now()),
|
||||||
|
('aaaaaaaa-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000000', 'terapeuta07@teste.agenciapsi.com', '$2a$10$PwGnTf1HMjHGZSM0YDi6ku7U7ZZZcXOiI4XOoFSqLJ3szGBTniCeG', now(), 'authenticated', 'authenticated', '{"provider":"email","providers":["email"]}', '{"full_name":"Dra. Mariana Costa"}', now(), now()),
|
||||||
|
('aaaaaaaa-0000-0000-0000-000000000008', '00000000-0000-0000-0000-000000000000', 'terapeuta08@teste.agenciapsi.com', '$2a$10$PwGnTf1HMjHGZSM0YDi6ku7U7ZZZcXOiI4XOoFSqLJ3szGBTniCeG', now(), 'authenticated', 'authenticated', '{"provider":"email","providers":["email"]}', '{"full_name":"Dr. Pedro Henrique"}', now(), now()),
|
||||||
|
('aaaaaaaa-0000-0000-0000-000000000009', '00000000-0000-0000-0000-000000000000', 'terapeuta09@teste.agenciapsi.com', '$2a$10$PwGnTf1HMjHGZSM0YDi6ku7U7ZZZcXOiI4XOoFSqLJ3szGBTniCeG', now(), 'authenticated', 'authenticated', '{"provider":"email","providers":["email"]}', '{"full_name":"Dra. Renata Souza"}', now(), now()),
|
||||||
|
('aaaaaaaa-0000-0000-0000-000000000010', '00000000-0000-0000-0000-000000000000', 'terapeuta10@teste.agenciapsi.com', '$2a$10$PwGnTf1HMjHGZSM0YDi6ku7U7ZZZcXOiI4XOoFSqLJ3szGBTniCeG', now(), 'authenticated', 'authenticated', '{"provider":"email","providers":["email"]}', '{"full_name":"Dr. Thiago Nunes"}', now(), now())
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- Identities (necessárias para login via email)
|
||||||
|
INSERT INTO auth.identities (id, user_id, identity_data, provider, provider_id, last_sign_in_at, created_at, updated_at)
|
||||||
|
SELECT id, id, jsonb_build_object('sub', id::text, 'email', email), 'email', id::text, now(), now(), now()
|
||||||
|
FROM auth.users WHERE id IN (
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000001','aaaaaaaa-0000-0000-0000-000000000002',
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000003','aaaaaaaa-0000-0000-0000-000000000004',
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000005','aaaaaaaa-0000-0000-0000-000000000006',
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000007','aaaaaaaa-0000-0000-0000-000000000008',
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000009','aaaaaaaa-0000-0000-0000-000000000010'
|
||||||
|
)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- Reabilitar triggers
|
||||||
|
ALTER TABLE auth.users ENABLE TRIGGER ALL;
|
||||||
|
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 2. PROFILES
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
INSERT INTO public.profiles (id, full_name, role, phone, bio, account_type) VALUES
|
||||||
|
('aaaaaaaa-0000-0000-0000-000000000001', 'Dra. Ana Beatriz', 'tenant_member', '(11) 91234-0001', 'Psicóloga clínica — TCC e EMDR', 'therapist'),
|
||||||
|
('aaaaaaaa-0000-0000-0000-000000000002', 'Dr. Carlos Eduardo', 'tenant_member', '(11) 91234-0002', 'Psicólogo — Psicanálise', 'therapist'),
|
||||||
|
('aaaaaaaa-0000-0000-0000-000000000003', 'Dra. Fernanda Lima', 'tenant_member', '(16) 91234-0003', 'Psicóloga infantil — Ludoterapia', 'therapist'),
|
||||||
|
('aaaaaaaa-0000-0000-0000-000000000004', 'Dr. Gabriel Santos', 'tenant_member', '(16) 91234-0004', 'Psicólogo — Gestalt-terapia', 'therapist'),
|
||||||
|
('aaaaaaaa-0000-0000-0000-000000000005', 'Dra. Juliana Alves', 'tenant_member', '(21) 91234-0005', 'Psicóloga — Neuropsicologia', 'therapist'),
|
||||||
|
('aaaaaaaa-0000-0000-0000-000000000006', 'Dr. Lucas Oliveira', 'tenant_member', '(21) 91234-0006', 'Psicólogo — Terapia Sistêmica', 'therapist'),
|
||||||
|
('aaaaaaaa-0000-0000-0000-000000000007', 'Dra. Mariana Costa', 'tenant_member', '(31) 91234-0007', 'Psicóloga — Humanismo', 'therapist'),
|
||||||
|
('aaaaaaaa-0000-0000-0000-000000000008', 'Dr. Pedro Henrique', 'tenant_member', '(31) 91234-0008', 'Psicólogo — Análise do Comportamento', 'therapist'),
|
||||||
|
('aaaaaaaa-0000-0000-0000-000000000009', 'Dra. Renata Souza', 'tenant_member', '(41) 91234-0009', 'Psicóloga — Psicossomática', 'therapist'),
|
||||||
|
('aaaaaaaa-0000-0000-0000-000000000010', 'Dr. Thiago Nunes', 'tenant_member', '(41) 91234-0010', 'Psicólogo — Terapia Cognitiva', 'therapist')
|
||||||
|
ON CONFLICT (id) DO UPDATE SET full_name = EXCLUDED.full_name, role = EXCLUDED.role;
|
||||||
|
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 3. TENANTS + MEMBERS
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
INSERT INTO public.tenants (id, name, kind) VALUES
|
||||||
|
('bbbbbbbb-0000-0000-0000-000000000001', 'Consultório Dra. Ana Beatriz', 'therapist'),
|
||||||
|
('bbbbbbbb-0000-0000-0000-000000000002', 'Consultório Dr. Carlos Eduardo', 'therapist'),
|
||||||
|
('bbbbbbbb-0000-0000-0000-000000000003', 'Consultório Dra. Fernanda Lima', 'therapist'),
|
||||||
|
('bbbbbbbb-0000-0000-0000-000000000004', 'Consultório Dr. Gabriel Santos', 'therapist'),
|
||||||
|
('bbbbbbbb-0000-0000-0000-000000000005', 'Consultório Dra. Juliana Alves', 'therapist'),
|
||||||
|
('bbbbbbbb-0000-0000-0000-000000000006', 'Consultório Dr. Lucas Oliveira', 'therapist'),
|
||||||
|
('bbbbbbbb-0000-0000-0000-000000000007', 'Consultório Dra. Mariana Costa', 'therapist'),
|
||||||
|
('bbbbbbbb-0000-0000-0000-000000000008', 'Consultório Dr. Pedro Henrique', 'therapist'),
|
||||||
|
('bbbbbbbb-0000-0000-0000-000000000009', 'Consultório Dra. Renata Souza', 'therapist'),
|
||||||
|
('bbbbbbbb-0000-0000-0000-000000000010', 'Consultório Dr. Thiago Nunes', 'therapist')
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO public.tenant_members (tenant_id, user_id, role, status)
|
||||||
|
SELECT
|
||||||
|
('bbbbbbbb-0000-0000-0000-00000000000' || i)::uuid,
|
||||||
|
('aaaaaaaa-0000-0000-0000-00000000000' || i)::uuid,
|
||||||
|
'tenant_admin', 'active'
|
||||||
|
FROM generate_series(1, 9) AS s(i)
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000010'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000010'::uuid,
|
||||||
|
'tenant_admin', 'active'
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 4. SERVIÇOS (1 serviço padrão por terapeuta)
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
INSERT INTO public.services (id, owner_id, tenant_id, name, price, duration_min, active)
|
||||||
|
SELECT
|
||||||
|
('99999999-0000-0000-0000-00000000000' || i)::uuid,
|
||||||
|
('aaaaaaaa-0000-0000-0000-00000000000' || i)::uuid,
|
||||||
|
('bbbbbbbb-0000-0000-0000-00000000000' || i)::uuid,
|
||||||
|
'Sessão Individual', (150 + i * 10)::numeric, 50, true
|
||||||
|
FROM generate_series(1, 9) AS s(i)
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
'99999999-0000-0000-0000-000000000010'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000010'::uuid,
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000010'::uuid,
|
||||||
|
'Sessão Individual', 250::numeric, 50, true
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 5. PACIENTES — 5 por terapeuta = 50 pacientes
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
_nomes text[] := ARRAY[
|
||||||
|
'Maria Silva','João Pereira','Camila Rocha','Rafael Dias','Larissa Melo',
|
||||||
|
'Bruno Costa','Amanda Ferreira','Felipe Souza','Isabela Nunes','Diego Almeida',
|
||||||
|
'Patrícia Lima','Rodrigo Santos','Letícia Araujo','Gustavo Ribeiro','Bianca Carvalho',
|
||||||
|
'Matheus Gomes','Natália Barbosa','André Vieira','Carolina Martins','Lucas Teixeira',
|
||||||
|
'Aline Pinto','Henrique Moura','Vanessa Cardoso','Marcos Fernandes','Tatiana Lopes',
|
||||||
|
'Thiago Nascimento','Juliana Duarte','Eduardo Correia','Fernanda Monteiro','Paulo Campos',
|
||||||
|
'Simone Reis','Ricardo Azevedo','Debora Mendes','Alexandre Freitas','Sandra Oliveira',
|
||||||
|
'Daniel Cunha','Priscila Moreira','Roberto Andrade','Cláudia Batista','Leandro Rezende',
|
||||||
|
'Adriana Sousa','Vinicius Castro','Luciana Pereira','Fabio Miranda','Elaine Barros',
|
||||||
|
'Renato Faria','Cristina Machado','Marcelo Prado','Gabriela Assis','Sergio Nogueira'
|
||||||
|
];
|
||||||
|
_phones text[] := ARRAY[
|
||||||
|
'(11) 98765-1001','(11) 98765-1002','(11) 98765-1003','(11) 98765-1004','(11) 98765-1005',
|
||||||
|
'(11) 98765-1006','(11) 98765-1007','(11) 98765-1008','(11) 98765-1009','(11) 98765-1010',
|
||||||
|
'(16) 98765-1011','(16) 98765-1012','(16) 98765-1013','(16) 98765-1014','(16) 98765-1015',
|
||||||
|
'(16) 98765-1016','(16) 98765-1017','(16) 98765-1018','(16) 98765-1019','(16) 98765-1020',
|
||||||
|
'(21) 98765-1021','(21) 98765-1022','(21) 98765-1023','(21) 98765-1024','(21) 98765-1025',
|
||||||
|
'(21) 98765-1026','(21) 98765-1027','(21) 98765-1028','(21) 98765-1029','(21) 98765-1030',
|
||||||
|
'(31) 98765-1031','(31) 98765-1032','(31) 98765-1033','(31) 98765-1034','(31) 98765-1035',
|
||||||
|
'(31) 98765-1036','(31) 98765-1037','(31) 98765-1038','(31) 98765-1039','(31) 98765-1040',
|
||||||
|
'(41) 98765-1041','(41) 98765-1042','(41) 98765-1043','(41) 98765-1044','(41) 98765-1045',
|
||||||
|
'(41) 98765-1046','(41) 98765-1047','(41) 98765-1048','(41) 98765-1049','(41) 98765-1050'
|
||||||
|
];
|
||||||
|
_statuses text[] := ARRAY['Ativo','Ativo','Ativo','Ativo','Inativo'];
|
||||||
|
_t int;
|
||||||
|
_p int;
|
||||||
|
_idx int;
|
||||||
|
_pid uuid;
|
||||||
|
_oid uuid;
|
||||||
|
_tid uuid;
|
||||||
|
_mid uuid;
|
||||||
|
BEGIN
|
||||||
|
FOR _t IN 1..10 LOOP
|
||||||
|
IF _t < 10 THEN
|
||||||
|
_oid := ('aaaaaaaa-0000-0000-0000-00000000000' || _t)::uuid;
|
||||||
|
_tid := ('bbbbbbbb-0000-0000-0000-00000000000' || _t)::uuid;
|
||||||
|
ELSE
|
||||||
|
_oid := 'aaaaaaaa-0000-0000-0000-000000000010'::uuid;
|
||||||
|
_tid := 'bbbbbbbb-0000-0000-0000-000000000010'::uuid;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Buscar o id do tenant_member para este user/tenant
|
||||||
|
SELECT id INTO _mid FROM public.tenant_members
|
||||||
|
WHERE tenant_id = _tid AND user_id = _oid LIMIT 1;
|
||||||
|
|
||||||
|
FOR _p IN 1..5 LOOP
|
||||||
|
_idx := (_t - 1) * 5 + _p;
|
||||||
|
_pid := ('cccccccc-0000-0000-0000-' || lpad(_idx::text, 12, '0'))::uuid;
|
||||||
|
|
||||||
|
INSERT INTO public.patients (
|
||||||
|
id, nome_completo, email_principal, telefone,
|
||||||
|
status, owner_id, tenant_id, responsible_member_id,
|
||||||
|
data_nascimento, genero, cidade, estado
|
||||||
|
) VALUES (
|
||||||
|
_pid,
|
||||||
|
_nomes[_idx],
|
||||||
|
'paciente' || _idx || '@teste.agenciapsi.com',
|
||||||
|
_phones[_idx],
|
||||||
|
_statuses[_p],
|
||||||
|
_oid, _tid, _mid,
|
||||||
|
'1985-01-01'::date + (_idx * 120 || ' days')::interval,
|
||||||
|
CASE WHEN _idx % 2 = 0 THEN 'Feminino' ELSE 'Masculino' END,
|
||||||
|
CASE
|
||||||
|
WHEN _t <= 2 THEN 'são Paulo'
|
||||||
|
WHEN _t <= 4 THEN 'são Carlos'
|
||||||
|
WHEN _t <= 6 THEN 'Rio de Janeiro'
|
||||||
|
WHEN _t <= 8 THEN 'Belo Horizonte'
|
||||||
|
ELSE 'Curitiba'
|
||||||
|
END,
|
||||||
|
CASE
|
||||||
|
WHEN _t <= 2 THEN 'SP'
|
||||||
|
WHEN _t <= 4 THEN 'SP'
|
||||||
|
WHEN _t <= 6 THEN 'RJ'
|
||||||
|
WHEN _t <= 8 THEN 'MG'
|
||||||
|
ELSE 'PR'
|
||||||
|
END
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
END LOOP;
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 6. GRUPOS DE PACIENTES — 2 por terapeuta
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
_t int;
|
||||||
|
_oid uuid;
|
||||||
|
_tid uuid;
|
||||||
|
_g1 uuid;
|
||||||
|
_g2 uuid;
|
||||||
|
BEGIN
|
||||||
|
FOR _t IN 1..10 LOOP
|
||||||
|
IF _t < 10 THEN
|
||||||
|
_oid := ('aaaaaaaa-0000-0000-0000-00000000000' || _t)::uuid;
|
||||||
|
_tid := ('bbbbbbbb-0000-0000-0000-00000000000' || _t)::uuid;
|
||||||
|
ELSE
|
||||||
|
_oid := 'aaaaaaaa-0000-0000-0000-000000000010'::uuid;
|
||||||
|
_tid := 'bbbbbbbb-0000-0000-0000-000000000010'::uuid;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
_g1 := ('ffffffff-0000-0000-0000-' || lpad(((_t-1)*2+1)::text, 12, '0'))::uuid;
|
||||||
|
_g2 := ('ffffffff-0000-0000-0000-' || lpad(((_t-1)*2+2)::text, 12, '0'))::uuid;
|
||||||
|
|
||||||
|
INSERT INTO public.patient_groups (id, owner_id, tenant_id, nome, cor, is_active)
|
||||||
|
VALUES
|
||||||
|
(_g1, _oid, _tid, 'Adultos', '#6366f1', true),
|
||||||
|
(_g2, _oid, _tid, 'Crianças', '#f59e0b', true)
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- Adicionar 3 pacientes ao grupo Adultos, 2 ao grupo Crianças
|
||||||
|
INSERT INTO public.patient_group_patient (patient_id, patient_group_id, tenant_id)
|
||||||
|
SELECT
|
||||||
|
('cccccccc-0000-0000-0000-' || lpad(((_t-1)*5 + p)::text, 12, '0'))::uuid,
|
||||||
|
CASE WHEN p <= 3 THEN _g1 ELSE _g2 END,
|
||||||
|
_tid
|
||||||
|
FROM generate_series(1, 5) AS s(p)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 7. AGENDA — Eventos do último mês + próxima semana
|
||||||
|
-- Tipos: realizado, faltou, cancelado, agendado, remarcar
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
_t int;
|
||||||
|
_p int;
|
||||||
|
_d int;
|
||||||
|
_idx int;
|
||||||
|
_oid uuid;
|
||||||
|
_tid uuid;
|
||||||
|
_pid uuid;
|
||||||
|
_eid uuid;
|
||||||
|
_status text;
|
||||||
|
_statuses text[] := ARRAY['realizado','realizado','realizado','faltou','cancelado','realizado','agendado','remarcar','realizado','realizado',
|
||||||
|
'realizado','realizado','faltou','realizado','cancelado','agendado','agendado','realizado','realizado','realizado'];
|
||||||
|
_dia date;
|
||||||
|
_hora time;
|
||||||
|
_horas time[] := ARRAY['08:00','09:00','10:00','11:00','14:00'];
|
||||||
|
_evt_counter int := 0;
|
||||||
|
_modalidade text;
|
||||||
|
BEGIN
|
||||||
|
FOR _t IN 1..10 LOOP
|
||||||
|
IF _t < 10 THEN
|
||||||
|
_oid := ('aaaaaaaa-0000-0000-0000-00000000000' || _t)::uuid;
|
||||||
|
_tid := ('bbbbbbbb-0000-0000-0000-00000000000' || _t)::uuid;
|
||||||
|
ELSE
|
||||||
|
_oid := 'aaaaaaaa-0000-0000-0000-000000000010'::uuid;
|
||||||
|
_tid := 'bbbbbbbb-0000-0000-0000-000000000010'::uuid;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 4 semanas de sessões (seg-sex), 1 sessão por paciente por semana
|
||||||
|
FOR _d IN 0..27 LOOP
|
||||||
|
_dia := (current_date - interval '28 days' + (_d || ' days')::interval)::date;
|
||||||
|
-- Pular finais de semana
|
||||||
|
IF extract(dow FROM _dia) IN (0, 6) THEN CONTINUE; END IF;
|
||||||
|
|
||||||
|
FOR _p IN 1..5 LOOP
|
||||||
|
-- Cada paciente tem sessão 1x/semana (dias alternados)
|
||||||
|
IF (_d % 5) != (_p - 1) THEN CONTINUE; END IF;
|
||||||
|
|
||||||
|
_evt_counter := _evt_counter + 1;
|
||||||
|
_idx := (_t - 1) * 5 + _p;
|
||||||
|
_pid := ('cccccccc-0000-0000-0000-' || lpad(_idx::text, 12, '0'))::uuid;
|
||||||
|
_eid := ('dddddddd-0000-0000-0000-' || lpad(_evt_counter::text, 12, '0'))::uuid;
|
||||||
|
_hora := _horas[_p];
|
||||||
|
_status := _statuses[(_evt_counter - 1) % 20 + 1];
|
||||||
|
_modalidade := CASE WHEN _p <= 3 THEN 'presencial' ELSE 'online' END;
|
||||||
|
|
||||||
|
-- Eventos futuros devem ser 'agendado'
|
||||||
|
IF _dia > current_date THEN _status := 'agendado'; END IF;
|
||||||
|
|
||||||
|
INSERT INTO public.agenda_eventos (
|
||||||
|
id, owner_id, tenant_id, tipo, status,
|
||||||
|
patient_id, terapeuta_id,
|
||||||
|
inicio_em, fim_em,
|
||||||
|
titulo, modalidade
|
||||||
|
) VALUES (
|
||||||
|
_eid, _oid, _tid, 'sessao'::tipo_evento_agenda, _status::status_evento_agenda,
|
||||||
|
_pid, _oid,
|
||||||
|
(_dia || ' ' || _hora)::timestamptz,
|
||||||
|
(_dia || ' ' || _hora)::timestamptz + interval '50 minutes',
|
||||||
|
'Sessão — ' || (SELECT nome_completo FROM public.patients WHERE id = _pid),
|
||||||
|
_modalidade
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
END LOOP;
|
||||||
|
END LOOP;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
RAISE NOTICE 'Total de eventos criados: %', _evt_counter;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 8. FINANCEIRO — 1 lançamento por evento realizado
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
INSERT INTO public.financial_records (
|
||||||
|
id, owner_id, tenant_id, type, amount, final_amount, description,
|
||||||
|
category, payment_method, status, paid_at, due_date,
|
||||||
|
agenda_evento_id, patient_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
gen_random_uuid(),
|
||||||
|
ae.owner_id,
|
||||||
|
ae.tenant_id,
|
||||||
|
'receita',
|
||||||
|
v.amt,
|
||||||
|
v.amt,
|
||||||
|
CASE
|
||||||
|
WHEN ae.status = 'realizado' THEN 'Sessão realizada'
|
||||||
|
WHEN ae.status = 'faltou' THEN 'Falta — cobrança parcial'
|
||||||
|
ELSE 'Sessão cancelada'
|
||||||
|
END,
|
||||||
|
'Sessão',
|
||||||
|
CASE WHEN random() > 0.5 THEN 'pix' ELSE 'cartao_credito' END,
|
||||||
|
CASE
|
||||||
|
WHEN ae.status = 'realizado' THEN 'paid'
|
||||||
|
WHEN ae.status = 'faltou' THEN 'pending'
|
||||||
|
ELSE 'cancelled'
|
||||||
|
END,
|
||||||
|
CASE WHEN ae.status = 'realizado' THEN ae.inicio_em ELSE NULL END,
|
||||||
|
ae.inicio_em::date,
|
||||||
|
ae.id,
|
||||||
|
ae.patient_id
|
||||||
|
FROM public.agenda_eventos ae
|
||||||
|
CROSS JOIN LATERAL (
|
||||||
|
SELECT CASE
|
||||||
|
WHEN ae.status = 'realizado' THEN (150 + (random() * 100)::int)::numeric
|
||||||
|
WHEN ae.status = 'faltou' THEN (75 + (random() * 50)::int)::numeric
|
||||||
|
ELSE 0
|
||||||
|
END AS amt
|
||||||
|
) v
|
||||||
|
WHERE ae.tenant_id IN (
|
||||||
|
SELECT id FROM public.tenants WHERE id::text LIKE 'bbbbbbbb%'
|
||||||
|
)
|
||||||
|
AND ae.status IN ('realizado', 'faltou', 'cancelado');
|
||||||
|
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 9. REGRAS SEMANAIS — horários de atendimento
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
_t int;
|
||||||
|
_d int;
|
||||||
|
_oid uuid;
|
||||||
|
_tid uuid;
|
||||||
|
BEGIN
|
||||||
|
FOR _t IN 1..10 LOOP
|
||||||
|
IF _t < 10 THEN
|
||||||
|
_oid := ('aaaaaaaa-0000-0000-0000-00000000000' || _t)::uuid;
|
||||||
|
_tid := ('bbbbbbbb-0000-0000-0000-00000000000' || _t)::uuid;
|
||||||
|
ELSE
|
||||||
|
_oid := 'aaaaaaaa-0000-0000-0000-000000000010'::uuid;
|
||||||
|
_tid := 'bbbbbbbb-0000-0000-0000-000000000010'::uuid;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Segunda a sexta, 8h-12h e 14h-18h
|
||||||
|
FOR _d IN 1..5 LOOP
|
||||||
|
INSERT INTO public.agenda_regras_semanais (owner_id, tenant_id, dia_semana, hora_inicio, hora_fim, modalidade, ativo)
|
||||||
|
VALUES
|
||||||
|
(_oid, _tid, _d, '08:00', '12:00', 'ambos', true),
|
||||||
|
(_oid, _tid, _d, '14:00', '18:00', 'ambos', true)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
END LOOP;
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 10. RECORRÊNCIAS — 1 por terapeuta (paciente 1)
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
_t int;
|
||||||
|
_oid uuid;
|
||||||
|
_tid uuid;
|
||||||
|
_pid uuid;
|
||||||
|
_rid uuid;
|
||||||
|
BEGIN
|
||||||
|
FOR _t IN 1..10 LOOP
|
||||||
|
IF _t < 10 THEN
|
||||||
|
_oid := ('aaaaaaaa-0000-0000-0000-00000000000' || _t)::uuid;
|
||||||
|
_tid := ('bbbbbbbb-0000-0000-0000-00000000000' || _t)::uuid;
|
||||||
|
ELSE
|
||||||
|
_oid := 'aaaaaaaa-0000-0000-0000-000000000010'::uuid;
|
||||||
|
_tid := 'bbbbbbbb-0000-0000-0000-000000000010'::uuid;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
_pid := ('cccccccc-0000-0000-0000-' || lpad(((_t-1)*5 + 1)::text, 12, '0'))::uuid;
|
||||||
|
_rid := ('11111111-0000-0000-0000-' || lpad(_t::text, 12, '0'))::uuid;
|
||||||
|
|
||||||
|
INSERT INTO public.recurrence_rules (
|
||||||
|
id, tenant_id, owner_id, patient_id,
|
||||||
|
type, weekdays, start_time, end_time,
|
||||||
|
start_date, open_ended, modalidade, status
|
||||||
|
) VALUES (
|
||||||
|
_rid, _tid, _oid, _pid,
|
||||||
|
'weekly', ARRAY[(_t % 5) + 1]::smallint[], '09:00', '09:50',
|
||||||
|
current_date - interval '30 days', true, 'presencial', 'ativo'
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 11. NOTIFICATION SCHEDULES — 1 set por terapeuta
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
_t int;
|
||||||
|
_oid uuid;
|
||||||
|
_tid uuid;
|
||||||
|
BEGIN
|
||||||
|
FOR _t IN 1..10 LOOP
|
||||||
|
IF _t < 10 THEN
|
||||||
|
_oid := ('aaaaaaaa-0000-0000-0000-00000000000' || _t)::uuid;
|
||||||
|
_tid := ('bbbbbbbb-0000-0000-0000-00000000000' || _t)::uuid;
|
||||||
|
ELSE
|
||||||
|
_oid := 'aaaaaaaa-0000-0000-0000-000000000010'::uuid;
|
||||||
|
_tid := 'bbbbbbbb-0000-0000-0000-000000000010'::uuid;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
INSERT INTO public.notification_schedules (
|
||||||
|
tenant_id, owner_id, schedule_key, event_type,
|
||||||
|
trigger_type, offset_minutes,
|
||||||
|
whatsapp_enabled, email_enabled, is_active
|
||||||
|
) VALUES
|
||||||
|
(_tid, _oid, 'lembrete_24h', 'lembrete_sessao', 'before_event', 1440, true, true, true),
|
||||||
|
(_tid, _oid, 'lembrete_2h', 'lembrete_sessao', 'before_event', 120, true, false, true),
|
||||||
|
(_tid, _oid, 'confirmacao', 'confirmacao_sessao', 'immediate', 0, true, true, true),
|
||||||
|
(_tid, _oid, 'cancelamento', 'cancelamento_sessao', 'immediate', 0, true, true, true)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════
|
||||||
|
-- 12. DADOS PARA terapeuta@agenciapsi.com.br
|
||||||
|
-- ════════════════════════════════════════════════════════════
|
||||||
|
-- 20 pacientes, 10+ grupos, 10+ eventos de cada status,
|
||||||
|
-- 10+ lançamentos, 10+ regras semanais, 10+ recorrências,
|
||||||
|
-- 10+ notificações
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- 12a. LIMPEZA dos dados de teste deste usuário
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
_oid uuid;
|
||||||
|
_tid uuid;
|
||||||
|
BEGIN
|
||||||
|
SELECT id INTO _oid FROM auth.users WHERE email = 'terapeuta@agenciapsi.com.br';
|
||||||
|
IF _oid IS NULL THEN
|
||||||
|
RAISE NOTICE 'Usuário terapeuta@agenciapsi.com.br não encontrado — pulando limpeza';
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT t.id INTO _tid
|
||||||
|
FROM public.tenants t
|
||||||
|
JOIN public.tenant_members tm ON tm.tenant_id = t.id
|
||||||
|
WHERE tm.user_id = _oid
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF _tid IS NULL THEN
|
||||||
|
RAISE NOTICE 'Tenant não encontrado para terapeuta@agenciapsi.com.br — pulando limpeza';
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Limpar apenas dados de seed (pacientes com email @seed.teste)
|
||||||
|
DELETE FROM public.financial_records WHERE tenant_id = _tid AND description LIKE '%[SEED]%';
|
||||||
|
DELETE FROM public.agenda_eventos WHERE tenant_id = _tid AND titulo LIKE '%[SEED]%';
|
||||||
|
DELETE FROM public.recurrence_rules WHERE tenant_id = _tid AND observacoes = '[SEED]';
|
||||||
|
DELETE FROM public.patient_group_patient WHERE tenant_id = _tid AND patient_id IN (
|
||||||
|
SELECT id FROM public.patients WHERE tenant_id = _tid AND email_principal LIKE '%@seed.teste.agenciapsi.com'
|
||||||
|
);
|
||||||
|
DELETE FROM public.patient_groups WHERE tenant_id = _tid AND nome LIKE '%[SEED]%';
|
||||||
|
DELETE FROM public.patients WHERE tenant_id = _tid AND email_principal LIKE '%@seed.teste.agenciapsi.com';
|
||||||
|
DELETE FROM public.agenda_regras_semanais WHERE tenant_id = _tid AND owner_id = _oid;
|
||||||
|
DELETE FROM public.notification_schedules WHERE tenant_id = _tid AND owner_id = _oid;
|
||||||
|
|
||||||
|
RAISE NOTICE 'Limpeza concluída para terapeuta@agenciapsi.com.br (tenant: %)', _tid;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
|
||||||
|
-- 12b. INSERIR DADOS
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
_oid uuid;
|
||||||
|
_tid uuid;
|
||||||
|
_mid uuid;
|
||||||
|
_pid uuid;
|
||||||
|
_eid uuid;
|
||||||
|
_gid uuid;
|
||||||
|
_i int;
|
||||||
|
_d int;
|
||||||
|
_dia date;
|
||||||
|
_hora time;
|
||||||
|
_status text;
|
||||||
|
_evt_counter int := 0;
|
||||||
|
|
||||||
|
_nomes text[] := ARRAY[
|
||||||
|
'Amanda Rodrigues','Bruno Ferreira','Camila Teixeira','Diego Nascimento','Elisa Cardoso',
|
||||||
|
'Felipe Moura','Giovana Ribeiro','Hugo Martins','Isabela Correia','João Vitor Almeida',
|
||||||
|
'Karen Oliveira','Leonardo Duarte','Marina Santos','Nicolas Costa','Olívia Mendes',
|
||||||
|
'Paulo Ricardo','Quésia Lima','Rafael Araújo','Sofia Carvalho','Tomás Barbosa'
|
||||||
|
];
|
||||||
|
_phones text[] := ARRAY[
|
||||||
|
'(16) 99001-0001','(16) 99001-0002','(16) 99001-0003','(16) 99001-0004','(16) 99001-0005',
|
||||||
|
'(16) 99001-0006','(16) 99001-0007','(16) 99001-0008','(16) 99001-0009','(16) 99001-0010',
|
||||||
|
'(16) 99001-0011','(16) 99001-0012','(16) 99001-0013','(16) 99001-0014','(16) 99001-0015',
|
||||||
|
'(16) 99001-0016','(16) 99001-0017','(16) 99001-0018','(16) 99001-0019','(16) 99001-0020'
|
||||||
|
];
|
||||||
|
_cidades text[] := ARRAY[
|
||||||
|
'são Carlos','são Carlos','Araraquara','Araraquara','Ribeirão Preto',
|
||||||
|
'Ribeirão Preto','são Carlos','são Carlos','Araraquara','Araraquara',
|
||||||
|
'são Carlos','são Carlos','Ribeirão Preto','Ribeirão Preto','são Carlos',
|
||||||
|
'são Carlos','Araraquara','Araraquara','Ribeirão Preto','são Carlos'
|
||||||
|
];
|
||||||
|
_generos text[] := ARRAY[
|
||||||
|
'Feminino','Masculino','Feminino','Masculino','Feminino',
|
||||||
|
'Masculino','Feminino','Masculino','Feminino','Masculino',
|
||||||
|
'Feminino','Masculino','Feminino','Masculino','Feminino',
|
||||||
|
'Masculino','Feminino','Masculino','Feminino','Masculino'
|
||||||
|
];
|
||||||
|
|
||||||
|
_grupo_nomes text[] := ARRAY[
|
||||||
|
'Adultos [SEED]','Adolescentes [SEED]','Crianças [SEED]','Casais [SEED]','Idosos [SEED]',
|
||||||
|
'TCC [SEED]','Psicanálise [SEED]','Gestalt [SEED]','EMDR [SEED]','Grupo Familiar [SEED]'
|
||||||
|
];
|
||||||
|
_grupo_cores text[] := ARRAY[
|
||||||
|
'#6366f1','#f59e0b','#10b981','#ef4444','#8b5cf6',
|
||||||
|
'#3b82f6','#ec4899','#14b8a6','#f97316','#64748b'
|
||||||
|
];
|
||||||
|
|
||||||
|
-- Status cíclicos para eventos passados
|
||||||
|
_statuses text[] := ARRAY[
|
||||||
|
'realizado','realizado','realizado','faltou','cancelado',
|
||||||
|
'realizado','realizado','remarcar','realizado','faltou',
|
||||||
|
'realizado','realizado','realizado','cancelado','realizado',
|
||||||
|
'realizado','faltou','realizado','realizado','realizado'
|
||||||
|
];
|
||||||
|
|
||||||
|
_horas time[] := ARRAY['08:00','09:00','10:00','11:00','14:00','15:00','16:00','17:00'];
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
-- Buscar user
|
||||||
|
SELECT id INTO _oid FROM auth.users WHERE email = 'terapeuta@agenciapsi.com.br';
|
||||||
|
IF _oid IS NULL THEN
|
||||||
|
RAISE NOTICE 'Usuário terapeuta@agenciapsi.com.br não encontrado — abortando seed';
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Buscar tenant
|
||||||
|
SELECT t.id INTO _tid
|
||||||
|
FROM public.tenants t
|
||||||
|
JOIN public.tenant_members tm ON tm.tenant_id = t.id
|
||||||
|
WHERE tm.user_id = _oid
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF _tid IS NULL THEN
|
||||||
|
RAISE NOTICE 'Tenant não encontrado — abortando seed';
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Buscar member_id
|
||||||
|
SELECT id INTO _mid FROM public.tenant_members
|
||||||
|
WHERE tenant_id = _tid AND user_id = _oid LIMIT 1;
|
||||||
|
|
||||||
|
RAISE NOTICE 'Seed para: % / tenant: % / member: %', _oid, _tid, _mid;
|
||||||
|
|
||||||
|
-- ── 20 PACIENTES ──────────────────────────────────────────
|
||||||
|
FOR _i IN 1..20 LOOP
|
||||||
|
_pid := gen_random_uuid();
|
||||||
|
|
||||||
|
INSERT INTO public.patients (
|
||||||
|
id, nome_completo, email_principal, telefone,
|
||||||
|
status, owner_id, tenant_id, responsible_member_id,
|
||||||
|
data_nascimento, genero, cidade, estado
|
||||||
|
) VALUES (
|
||||||
|
_pid,
|
||||||
|
_nomes[_i],
|
||||||
|
'seed.paciente' || _i || '@seed.teste.agenciapsi.com',
|
||||||
|
_phones[_i],
|
||||||
|
CASE WHEN _i <= 16 THEN 'Ativo' WHEN _i <= 18 THEN 'Inativo' ELSE 'Alta' END,
|
||||||
|
_oid, _tid, _mid,
|
||||||
|
('1980-01-01'::date + (_i * 400 || ' days')::interval)::date,
|
||||||
|
_generos[_i],
|
||||||
|
_cidades[_i],
|
||||||
|
'SP'
|
||||||
|
);
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
-- ── 10 GRUPOS ─────────────────────────────────────────────
|
||||||
|
FOR _i IN 1..10 LOOP
|
||||||
|
_gid := gen_random_uuid();
|
||||||
|
|
||||||
|
INSERT INTO public.patient_groups (id, owner_id, tenant_id, nome, cor, is_active)
|
||||||
|
VALUES (_gid, _oid, _tid, _grupo_nomes[_i], _grupo_cores[_i], true);
|
||||||
|
|
||||||
|
-- Adicionar 2 pacientes por grupo
|
||||||
|
INSERT INTO public.patient_group_patient (patient_id, patient_group_id, tenant_id)
|
||||||
|
SELECT p.id, _gid, _tid
|
||||||
|
FROM public.patients p
|
||||||
|
WHERE p.tenant_id = _tid
|
||||||
|
AND p.email_principal LIKE '%@seed.teste.agenciapsi.com'
|
||||||
|
ORDER BY p.nome_completo
|
||||||
|
OFFSET ((_i - 1) * 2)
|
||||||
|
LIMIT 2;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
-- ── EVENTOS (últimos 42 dias + próxima semana) ────────────
|
||||||
|
-- Garante pelo menos: 10 realizado, 10 faltou, 10 cancelado,
|
||||||
|
-- 10 agendado, 10 remarcar
|
||||||
|
_evt_counter := 0;
|
||||||
|
|
||||||
|
FOR _d IN 0..48 LOOP
|
||||||
|
_dia := (current_date - interval '42 days' + (_d || ' days')::interval)::date;
|
||||||
|
IF extract(dow FROM _dia) IN (0, 6) THEN CONTINUE; END IF;
|
||||||
|
|
||||||
|
FOR _i IN 1..4 LOOP
|
||||||
|
_evt_counter := _evt_counter + 1;
|
||||||
|
_hora := _horas[((_evt_counter - 1) % 8) + 1];
|
||||||
|
|
||||||
|
-- Pegar um paciente cíclico
|
||||||
|
SELECT id INTO _pid FROM public.patients
|
||||||
|
WHERE tenant_id = _tid AND email_principal LIKE '%@seed.teste.agenciapsi.com'
|
||||||
|
ORDER BY nome_completo
|
||||||
|
OFFSET ((_evt_counter - 1) % 20)
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF _pid IS NULL THEN CONTINUE; END IF;
|
||||||
|
|
||||||
|
-- Status: passado = cíclico, futuro = agendado
|
||||||
|
IF _dia > current_date THEN
|
||||||
|
_status := 'agendado';
|
||||||
|
ELSE
|
||||||
|
_status := _statuses[((_evt_counter - 1) % 20) + 1];
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
_eid := gen_random_uuid();
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO public.agenda_eventos (
|
||||||
|
id, owner_id, tenant_id, tipo, status,
|
||||||
|
patient_id, terapeuta_id,
|
||||||
|
inicio_em, fim_em,
|
||||||
|
titulo, modalidade
|
||||||
|
) VALUES (
|
||||||
|
_eid, _oid, _tid,
|
||||||
|
'sessao'::tipo_evento_agenda,
|
||||||
|
_status::status_evento_agenda,
|
||||||
|
_pid, _oid,
|
||||||
|
(_dia || ' ' || _hora)::timestamptz,
|
||||||
|
(_dia || ' ' || _hora)::timestamptz + interval '50 minutes',
|
||||||
|
'[SEED] Sessão — ' || _nomes[((_evt_counter - 1) % 20) + 1],
|
||||||
|
CASE WHEN _i <= 2 THEN 'presencial' ELSE 'online' END
|
||||||
|
);
|
||||||
|
EXCEPTION WHEN exclusion_violation THEN
|
||||||
|
-- Horário já ocupado, pular
|
||||||
|
NULL;
|
||||||
|
END;
|
||||||
|
END LOOP;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
RAISE NOTICE 'Eventos tentados: % (alguns podem ter sido pulados por sobreposição)', _evt_counter;
|
||||||
|
|
||||||
|
-- ── FINANCEIRO ────────────────────────────────────────────
|
||||||
|
INSERT INTO public.financial_records (
|
||||||
|
id, owner_id, tenant_id, type, amount, final_amount, description,
|
||||||
|
category, payment_method, status, paid_at, due_date,
|
||||||
|
agenda_evento_id, patient_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
gen_random_uuid(),
|
||||||
|
ae.owner_id,
|
||||||
|
ae.tenant_id,
|
||||||
|
'receita',
|
||||||
|
v.amt,
|
||||||
|
v.amt,
|
||||||
|
'[SEED] ' || CASE
|
||||||
|
WHEN ae.status = 'realizado'::status_evento_agenda THEN 'Sessão realizada'
|
||||||
|
WHEN ae.status = 'faltou'::status_evento_agenda THEN 'Falta — cobrança parcial'
|
||||||
|
ELSE 'Sessão cancelada'
|
||||||
|
END,
|
||||||
|
'Sessão',
|
||||||
|
CASE WHEN random() > 0.5 THEN 'pix' ELSE 'cartao_credito' END,
|
||||||
|
CASE
|
||||||
|
WHEN ae.status = 'realizado'::status_evento_agenda THEN 'paid'
|
||||||
|
WHEN ae.status = 'faltou'::status_evento_agenda THEN 'pending'
|
||||||
|
ELSE 'cancelled'
|
||||||
|
END,
|
||||||
|
CASE WHEN ae.status = 'realizado'::status_evento_agenda THEN ae.inicio_em ELSE NULL END,
|
||||||
|
ae.inicio_em::date,
|
||||||
|
ae.id,
|
||||||
|
ae.patient_id
|
||||||
|
FROM public.agenda_eventos ae
|
||||||
|
CROSS JOIN LATERAL (
|
||||||
|
SELECT CASE
|
||||||
|
WHEN ae.status = 'realizado'::status_evento_agenda THEN (150 + (random() * 100)::int)::numeric
|
||||||
|
WHEN ae.status = 'faltou'::status_evento_agenda THEN (75 + (random() * 50)::int)::numeric
|
||||||
|
ELSE 0
|
||||||
|
END AS amt
|
||||||
|
) v
|
||||||
|
WHERE ae.tenant_id = _tid
|
||||||
|
AND ae.titulo LIKE '[SEED]%'
|
||||||
|
AND ae.status IN ('realizado'::status_evento_agenda, 'faltou'::status_evento_agenda, 'cancelado'::status_evento_agenda);
|
||||||
|
|
||||||
|
-- ── REGRAS SEMANAIS (10 — manhã e tarde, seg-sex) ────────
|
||||||
|
INSERT INTO public.agenda_regras_semanais (owner_id, tenant_id, dia_semana, hora_inicio, hora_fim, modalidade, ativo)
|
||||||
|
VALUES
|
||||||
|
(_oid, _tid, 1, '08:00', '12:00', 'ambos', true),
|
||||||
|
(_oid, _tid, 1, '14:00', '18:00', 'ambos', true),
|
||||||
|
(_oid, _tid, 2, '08:00', '12:00', 'ambos', true),
|
||||||
|
(_oid, _tid, 2, '14:00', '18:00', 'ambos', true),
|
||||||
|
(_oid, _tid, 3, '08:00', '12:00', 'ambos', true),
|
||||||
|
(_oid, _tid, 3, '14:00', '18:00', 'ambos', true),
|
||||||
|
(_oid, _tid, 4, '08:00', '12:00', 'ambos', true),
|
||||||
|
(_oid, _tid, 4, '14:00', '18:00', 'ambos', true),
|
||||||
|
(_oid, _tid, 5, '08:00', '12:00', 'ambos', true),
|
||||||
|
(_oid, _tid, 5, '14:00', '18:00', 'ambos', true)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- ── RECORRÊNCIAS (10 pacientes com sessão semanal) ────────
|
||||||
|
FOR _i IN 1..10 LOOP
|
||||||
|
SELECT id INTO _pid FROM public.patients
|
||||||
|
WHERE tenant_id = _tid AND email_principal LIKE '%@seed.teste.agenciapsi.com'
|
||||||
|
ORDER BY nome_completo
|
||||||
|
OFFSET (_i - 1) LIMIT 1;
|
||||||
|
|
||||||
|
IF _pid IS NULL THEN CONTINUE; END IF;
|
||||||
|
|
||||||
|
INSERT INTO public.recurrence_rules (
|
||||||
|
id, tenant_id, owner_id, patient_id, type, weekdays,
|
||||||
|
start_time, end_time, start_date, open_ended, modalidade, status, observacoes
|
||||||
|
) VALUES (
|
||||||
|
gen_random_uuid(), _tid, _oid, _pid, 'weekly',
|
||||||
|
ARRAY[_i % 5]::int[],
|
||||||
|
('08:00'::time + ((_i - 1) * 60 || ' minutes')::interval),
|
||||||
|
('08:50'::time + ((_i - 1) * 60 || ' minutes')::interval),
|
||||||
|
current_date - interval '30 days',
|
||||||
|
true,
|
||||||
|
CASE WHEN _i <= 5 THEN 'presencial' ELSE 'online' END,
|
||||||
|
'ativo',
|
||||||
|
'[SEED]'
|
||||||
|
);
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
-- ── NOTIFICATION SCHEDULES (10) ───────────────────────────
|
||||||
|
INSERT INTO public.notification_schedules (
|
||||||
|
tenant_id, owner_id, schedule_key, event_type,
|
||||||
|
trigger_type, offset_minutes,
|
||||||
|
whatsapp_enabled, email_enabled, is_active
|
||||||
|
) VALUES
|
||||||
|
(_tid, _oid, 'lembrete_24h', 'lembrete_sessao', 'before_event', 1440, true, true, true),
|
||||||
|
(_tid, _oid, 'lembrete_2h', 'lembrete_sessao', 'before_event', 120, true, false, true),
|
||||||
|
(_tid, _oid, 'lembrete_30min', 'lembrete_sessao', 'before_event', 30, true, false, true),
|
||||||
|
(_tid, _oid, 'confirmacao', 'confirmacao_sessao', 'immediate', 0, true, true, true),
|
||||||
|
(_tid, _oid, 'cancelamento', 'cancelamento_sessao', 'immediate', 0, true, true, true),
|
||||||
|
(_tid, _oid, 'lembrete_48h', 'lembrete_sessao', 'before_event', 2880, false, true, true),
|
||||||
|
(_tid, _oid, 'lembrete_1h', 'lembrete_sessao', 'before_event', 60, true, false, true),
|
||||||
|
(_tid, _oid, 'confirmacao_email', 'confirmacao_sessao', 'immediate', 0, false, true, true),
|
||||||
|
(_tid, _oid, 'cancel_email', 'cancelamento_sessao', 'immediate', 0, false, true, true),
|
||||||
|
(_tid, _oid, 'lembrete_1semana', 'lembrete_sessao', 'before_event', 10080,false, true, false)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
RAISE NOTICE '══════════════════════════════════════════';
|
||||||
|
RAISE NOTICE ' SEED terapeuta@agenciapsi.com.br COMPLETO';
|
||||||
|
RAISE NOTICE ' Pacientes: 20';
|
||||||
|
RAISE NOTICE ' Grupos: 10';
|
||||||
|
RAISE NOTICE ' Eventos: %', _evt_counter;
|
||||||
|
RAISE NOTICE ' Regras semanais: 10';
|
||||||
|
RAISE NOTICE ' Recorrências: 10';
|
||||||
|
RAISE NOTICE ' Notificações: 10';
|
||||||
|
RAISE NOTICE '══════════════════════════════════════════';
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 13. RESUMO
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
_users int;
|
||||||
|
_tenants int;
|
||||||
|
_patients int;
|
||||||
|
_events int;
|
||||||
|
_records int;
|
||||||
|
_groups int;
|
||||||
|
_recurrences int;
|
||||||
|
BEGIN
|
||||||
|
SELECT count(*) INTO _users FROM auth.users WHERE id::text LIKE 'aaaaaaaa%';
|
||||||
|
SELECT count(*) INTO _tenants FROM public.tenants WHERE id::text LIKE 'bbbbbbbb%';
|
||||||
|
SELECT count(*) INTO _patients FROM public.patients WHERE tenant_id::text LIKE 'bbbbbbbb%';
|
||||||
|
SELECT count(*) INTO _events FROM public.agenda_eventos WHERE tenant_id::text LIKE 'bbbbbbbb%';
|
||||||
|
SELECT count(*) INTO _records FROM public.financial_records WHERE tenant_id::text LIKE 'bbbbbbbb%';
|
||||||
|
SELECT count(*) INTO _groups FROM public.patient_groups WHERE tenant_id::text LIKE 'bbbbbbbb%';
|
||||||
|
SELECT count(*) INTO _recurrences FROM public.recurrence_rules WHERE tenant_id::text LIKE 'bbbbbbbb%';
|
||||||
|
|
||||||
|
RAISE NOTICE '══════════════════════════════════════════';
|
||||||
|
RAISE NOTICE ' SEED COMPLETO — RESUMO';
|
||||||
|
RAISE NOTICE '══════════════════════════════════════════';
|
||||||
|
RAISE NOTICE ' Terapeutas: %', _users;
|
||||||
|
RAISE NOTICE ' Tenants: %', _tenants;
|
||||||
|
RAISE NOTICE ' Pacientes: %', _patients;
|
||||||
|
RAISE NOTICE ' Eventos agenda: %', _events;
|
||||||
|
RAISE NOTICE ' Lançamentos fin.: %', _records;
|
||||||
|
RAISE NOTICE ' Grupos: %', _groups;
|
||||||
|
RAISE NOTICE ' Recorrências: %', _recurrences;
|
||||||
|
RAISE NOTICE '══════════════════════════════════════════';
|
||||||
|
RAISE NOTICE ' Login: terapeuta01@teste.agenciapsi.com';
|
||||||
|
RAISE NOTICE ' Senha: Test@1234';
|
||||||
|
RAISE NOTICE '══════════════════════════════════════════';
|
||||||
|
END $$;
|
||||||
231
database-novo/seeds/seed_020_test_data_cleanup.sql
Normal file
231
database-novo/seeds/seed_020_test_data_cleanup.sql
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- CLEANUP — Remove TODOS os dados de seed/teste
|
||||||
|
-- AgenciaPsi — 2026-03-22
|
||||||
|
-- ============================================================
|
||||||
|
-- Remove dados criados por seed_test_data.sql:
|
||||||
|
-- - 10 terapeutas fictícios (aaaaaaaa-*)
|
||||||
|
-- - 10 tenants fictícios (bbbbbbbb-*)
|
||||||
|
-- - 50 pacientes fictícios (cccccccc-*)
|
||||||
|
-- - Eventos, financeiro, grupos, recorrências
|
||||||
|
-- - Dados seed do terapeuta@agenciapsi.com.br ([SEED])
|
||||||
|
-- - Notification queue/logs dos tenants de teste
|
||||||
|
-- - Addon credits/transactions dos tenants de teste
|
||||||
|
--
|
||||||
|
-- SEGURO: Não apaga dados reais (apenas UUIDs/emails conhecidos)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
_tenant_ids uuid[] := ARRAY[
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000002'::uuid,
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000003'::uuid,
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000005'::uuid,
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000006'::uuid,
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000007'::uuid,
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000008'::uuid,
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000009'::uuid,
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000010'::uuid
|
||||||
|
];
|
||||||
|
_user_ids uuid[] := ARRAY[
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000002'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000003'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000005'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000006'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000007'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000008'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000009'::uuid,
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000010'::uuid
|
||||||
|
];
|
||||||
|
_real_oid uuid;
|
||||||
|
_real_tid uuid;
|
||||||
|
_deleted int;
|
||||||
|
_total int := 0;
|
||||||
|
BEGIN
|
||||||
|
RAISE NOTICE '══════════════════════════════════════════';
|
||||||
|
RAISE NOTICE ' CLEANUP — Removendo dados de teste...';
|
||||||
|
RAISE NOTICE '══════════════════════════════════════════';
|
||||||
|
|
||||||
|
-- ── Notification queue e logs (tenants fictícios) ──────────
|
||||||
|
DELETE FROM public.notification_logs WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' notification_logs: % removidos', _deleted;
|
||||||
|
|
||||||
|
DELETE FROM public.notification_queue WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' notification_queue: % removidos', _deleted;
|
||||||
|
|
||||||
|
DELETE FROM public.notification_preferences WHERE owner_id = ANY(_user_ids);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' notification_preferences: % removidos', _deleted;
|
||||||
|
|
||||||
|
DELETE FROM public.notification_schedules WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' notification_schedules: % removidos', _deleted;
|
||||||
|
|
||||||
|
DELETE FROM public.notification_channels WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' notification_channels: % removidos', _deleted;
|
||||||
|
|
||||||
|
DELETE FROM public.notification_templates WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' notification_templates: % removidos', _deleted;
|
||||||
|
|
||||||
|
-- ── Addon credits/transactions (tenants fictícios) ─────────
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'addon_transactions') THEN
|
||||||
|
DELETE FROM public.addon_transactions WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' addon_transactions: % removidos', _deleted;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'addon_credits') THEN
|
||||||
|
DELETE FROM public.addon_credits WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' addon_credits: % removidos', _deleted;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ── Financeiro ─────────────────────────────────────────────
|
||||||
|
DELETE FROM public.financial_records WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' financial_records: % removidos', _deleted;
|
||||||
|
|
||||||
|
-- ── Eventos agenda ─────────────────────────────────────────
|
||||||
|
DELETE FROM public.agenda_eventos WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' agenda_eventos: % removidos', _deleted;
|
||||||
|
|
||||||
|
-- ── Recorrências ───────────────────────────────────────────
|
||||||
|
DELETE FROM public.recurrence_rules WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' recurrence_rules: % removidos', _deleted;
|
||||||
|
|
||||||
|
-- ── Grupos ─────────────────────────────────────────────────
|
||||||
|
DELETE FROM public.patient_group_patient WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' patient_group_patient: % removidos', _deleted;
|
||||||
|
|
||||||
|
DELETE FROM public.patient_groups WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' patient_groups: % removidos', _deleted;
|
||||||
|
|
||||||
|
-- ── Pacientes ──────────────────────────────────────────────
|
||||||
|
DELETE FROM public.patients WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' patients: % removidos', _deleted;
|
||||||
|
|
||||||
|
-- ── Regras semanais ────────────────────────────────────────
|
||||||
|
DELETE FROM public.agenda_regras_semanais WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' agenda_regras_semanais: % removidos', _deleted;
|
||||||
|
|
||||||
|
-- ── Serviços ───────────────────────────────────────────────
|
||||||
|
DELETE FROM public.services WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' services: % removidos', _deleted;
|
||||||
|
|
||||||
|
-- ── Tenant members ─────────────────────────────────────────
|
||||||
|
DELETE FROM public.tenant_members WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' tenant_members: % removidos', _deleted;
|
||||||
|
|
||||||
|
-- ── Company profiles ───────────────────────────────────────
|
||||||
|
DELETE FROM public.company_profiles WHERE tenant_id = ANY(_tenant_ids);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' company_profiles: % removidos', _deleted;
|
||||||
|
|
||||||
|
-- ── Tenants ────────────────────────────────────────────────
|
||||||
|
DELETE FROM public.tenants WHERE id = ANY(_tenant_ids);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' tenants: % removidos', _deleted;
|
||||||
|
|
||||||
|
-- ── Profiles ───────────────────────────────────────────────
|
||||||
|
DELETE FROM public.profiles WHERE id = ANY(_user_ids);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' profiles: % removidos', _deleted;
|
||||||
|
|
||||||
|
-- ── Auth users ─────────────────────────────────────────────
|
||||||
|
ALTER TABLE auth.users DISABLE TRIGGER ALL;
|
||||||
|
DELETE FROM auth.identities WHERE user_id = ANY(_user_ids);
|
||||||
|
DELETE FROM auth.users WHERE id = ANY(_user_ids);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' auth.users: % removidos', _deleted;
|
||||||
|
ALTER TABLE auth.users ENABLE TRIGGER ALL;
|
||||||
|
|
||||||
|
-- ══════════════════════════════════════════════════════════
|
||||||
|
-- DADOS [SEED] DO terapeuta@agenciapsi.com.br
|
||||||
|
-- ══════════════════════════════════════════════════════════
|
||||||
|
SELECT id INTO _real_oid FROM auth.users WHERE email = 'terapeuta@agenciapsi.com.br';
|
||||||
|
|
||||||
|
IF _real_oid IS NOT NULL THEN
|
||||||
|
SELECT t.id INTO _real_tid
|
||||||
|
FROM public.tenants t
|
||||||
|
JOIN public.tenant_members tm ON tm.tenant_id = t.id
|
||||||
|
WHERE tm.user_id = _real_oid
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF _real_tid IS NOT NULL THEN
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE ' Limpando [SEED] de terapeuta@agenciapsi.com.br...';
|
||||||
|
|
||||||
|
-- Notification queue/logs deste tenant (apenas os de teste)
|
||||||
|
DELETE FROM public.notification_logs WHERE tenant_id = _real_tid
|
||||||
|
AND queue_id IN (SELECT id FROM public.notification_queue WHERE tenant_id = _real_tid);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' notification_logs (real): % removidos', _deleted;
|
||||||
|
|
||||||
|
DELETE FROM public.notification_queue WHERE tenant_id = _real_tid;
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' notification_queue (real):% removidos', _deleted;
|
||||||
|
|
||||||
|
-- Financeiro [SEED]
|
||||||
|
DELETE FROM public.financial_records WHERE tenant_id = _real_tid AND description LIKE '[SEED]%';
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' financial_records [SEED]: % removidos', _deleted;
|
||||||
|
|
||||||
|
-- Eventos [SEED]
|
||||||
|
DELETE FROM public.agenda_eventos WHERE tenant_id = _real_tid AND titulo LIKE '[SEED]%';
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' agenda_eventos [SEED]: % removidos', _deleted;
|
||||||
|
|
||||||
|
-- Recorrências [SEED]
|
||||||
|
DELETE FROM public.recurrence_rules WHERE tenant_id = _real_tid AND observacoes = '[SEED]';
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' recurrence_rules [SEED]: % removidos', _deleted;
|
||||||
|
|
||||||
|
-- Grupos [SEED]
|
||||||
|
DELETE FROM public.patient_group_patient WHERE tenant_id = _real_tid AND patient_id IN (
|
||||||
|
SELECT id FROM public.patients WHERE tenant_id = _real_tid AND email_principal LIKE '%@seed.teste.agenciapsi.com'
|
||||||
|
);
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' group_patient [SEED]: % removidos', _deleted;
|
||||||
|
|
||||||
|
DELETE FROM public.patient_groups WHERE tenant_id = _real_tid AND nome LIKE '%[SEED]%';
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' patient_groups [SEED]: % removidos', _deleted;
|
||||||
|
|
||||||
|
-- Pacientes seed
|
||||||
|
DELETE FROM public.patients WHERE tenant_id = _real_tid AND email_principal LIKE '%@seed.teste.agenciapsi.com';
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' patients [SEED]: % removidos', _deleted;
|
||||||
|
|
||||||
|
-- Regras semanais e schedules (reseta tudo deste owner)
|
||||||
|
DELETE FROM public.agenda_regras_semanais WHERE tenant_id = _real_tid AND owner_id = _real_oid;
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' regras_semanais (real): % removidos', _deleted;
|
||||||
|
|
||||||
|
DELETE FROM public.notification_schedules WHERE tenant_id = _real_tid AND owner_id = _real_oid;
|
||||||
|
GET DIAGNOSTICS _deleted = ROW_COUNT; _total := _total + _deleted;
|
||||||
|
RAISE NOTICE ' notif_schedules (real): % removidos', _deleted;
|
||||||
|
END IF;
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE ' terapeuta@agenciapsi.com.br não encontrado — pulando';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE '══════════════════════════════════════════';
|
||||||
|
RAISE NOTICE ' CLEANUP COMPLETO — % registros removidos', _total;
|
||||||
|
RAISE NOTICE '══════════════════════════════════════════';
|
||||||
|
END $$;
|
||||||
566
docs/USER_ARCHETYPES.html
Normal file
566
docs/USER_ARCHETYPES.html
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>AgenciaPsi — Arquétipos de Usuário</title>
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||||
|
background: #0f1117;
|
||||||
|
color: #e2e8f0;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 2rem 1rem 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Header ── */
|
||||||
|
.page-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
.page-header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
background: linear-gradient(135deg, #818cf8, #34d399);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
.page-header p {
|
||||||
|
margin-top: .5rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: .95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Grid ── */
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Card ── */
|
||||||
|
.card {
|
||||||
|
background: #1e2330;
|
||||||
|
border: 1px solid #2d3548;
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
transition: border-color .2s, transform .2s;
|
||||||
|
}
|
||||||
|
.card:hover {
|
||||||
|
border-color: #4f6ef7;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Card header ── */
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .75rem;
|
||||||
|
}
|
||||||
|
.card-icon {
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
border-radius: .75rem;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.card-title { font-size: 1.05rem; font-weight: 700; line-height: 1.2; }
|
||||||
|
.card-subtitle { font-size: .75rem; color: #64748b; margin-top: 2px; font-family: monospace; }
|
||||||
|
|
||||||
|
/* ── Tree ── */
|
||||||
|
.tree {
|
||||||
|
background: #0f1117;
|
||||||
|
border-radius: .75rem;
|
||||||
|
padding: 1rem 1.1rem;
|
||||||
|
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
||||||
|
font-size: .78rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
.tree-root { color: #a5b4fc; font-weight: 600; }
|
||||||
|
.tree-branch { color: #475569; }
|
||||||
|
.tree-leaf { color: #cbd5e1; }
|
||||||
|
.tree-comment { color: #475569; font-style: italic; }
|
||||||
|
.tree-key { color: #f472b6; }
|
||||||
|
.tree-val { color: #34d399; }
|
||||||
|
.tree-warn { color: #fb923c; }
|
||||||
|
.tree-new { color: #facc15; }
|
||||||
|
|
||||||
|
/* ── Badges ── */
|
||||||
|
.badges { display: flex; flex-wrap: wrap; gap: .4rem; }
|
||||||
|
.badge {
|
||||||
|
font-size: .68rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: .2rem .6rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
letter-spacing: .02em;
|
||||||
|
}
|
||||||
|
.badge-purple { background: #312e81; border-color: #4f46e5; color: #a5b4fc; }
|
||||||
|
.badge-green { background: #064e3b; border-color: #059669; color: #6ee7b7; }
|
||||||
|
.badge-blue { background: #1e3a5f; border-color: #2563eb; color: #93c5fd; }
|
||||||
|
.badge-orange { background: #431407; border-color: #ea580c; color: #fdba74; }
|
||||||
|
.badge-yellow { background: #422006; border-color: #ca8a04; color: #fde047; }
|
||||||
|
.badge-pink { background: #500724; border-color: #db2777; color: #f9a8d4; }
|
||||||
|
.badge-gray { background: #1e293b; border-color: #475569; color: #94a3b8; }
|
||||||
|
.badge-red { background: #450a0a; border-color: #dc2626; color: #fca5a5; }
|
||||||
|
|
||||||
|
/* ── Notes ── */
|
||||||
|
.note {
|
||||||
|
font-size: .75rem;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.5;
|
||||||
|
border-left: 2px solid #2d3548;
|
||||||
|
padding-left: .75rem;
|
||||||
|
}
|
||||||
|
.note strong { color: #94a3b8; }
|
||||||
|
|
||||||
|
/* ── Phase tag ── */
|
||||||
|
.phase {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .3rem;
|
||||||
|
font-size: .7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: .15rem .55rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.phase-1 { background: #064e3b; color: #6ee7b7; border: 1px solid #059669; }
|
||||||
|
.phase-2 { background: #422006; color: #fde047; border: 1px solid #ca8a04; }
|
||||||
|
|
||||||
|
/* ── Section label ── */
|
||||||
|
.section-label {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
font-size: .7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #475569;
|
||||||
|
padding: .25rem 0;
|
||||||
|
border-bottom: 1px solid #2d3548;
|
||||||
|
margin-bottom: -.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Legend ── */
|
||||||
|
.legend {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 2.5rem auto 0;
|
||||||
|
background: #1e2330;
|
||||||
|
border: 1px solid #2d3548;
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
}
|
||||||
|
.legend h3 { font-size: .8rem; font-weight: 700; color: #64748b; letter-spacing: .08em; text-transform: uppercase; margin-bottom: .75rem; }
|
||||||
|
.legend-grid { display: flex; flex-wrap: wrap; gap: 1rem 2rem; }
|
||||||
|
.legend-item { display: flex; align-items: center; gap: .5rem; font-size: .78rem; color: #94a3b8; }
|
||||||
|
.legend-dot { width: .65rem; height: .65rem; border-radius: 50%; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>AgenciaPsi — Arquétipos de Usuário</h1>
|
||||||
|
<p>Como cada tipo de usuário está estruturado no banco de dados e no sistema de permissões.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════ PLATAFORMA ══ -->
|
||||||
|
<div class="section-label">🏛️ Plataforma</div>
|
||||||
|
|
||||||
|
<!-- SaaS Admin -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon" style="background:#312e81">🛡️</div>
|
||||||
|
<div>
|
||||||
|
<div class="card-title">SaaS Admin</div>
|
||||||
|
<div class="card-subtitle">profiles.role = 'saas_admin'</div>
|
||||||
|
</div>
|
||||||
|
<span class="phase phase-1">Fase 1</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tree">
|
||||||
|
<div class="tree-root">Usuário (saas@agenciapsi.com.br)</div>
|
||||||
|
<div> <span class="tree-branch">└──</span> <span class="tree-key">profiles.role</span> <span class="tree-val">'saas_admin'</span></div>
|
||||||
|
<div> <span class="tree-comment">// sem memberships de tenant</span></div>
|
||||||
|
<div> <span class="tree-comment">// acessa /saas/*</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="badges">
|
||||||
|
<span class="badge badge-purple">role: saas_admin</span>
|
||||||
|
<span class="badge badge-gray">sem tenant</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="note">Acesso total à plataforma. Gerencia planos, features, assinaturas e usuários. Nunca entra em contexto de tenant.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Editor -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon" style="background:#1e3a5f">✍️</div>
|
||||||
|
<div>
|
||||||
|
<div class="card-title">Editor de Conteúdo</div>
|
||||||
|
<div class="card-subtitle">profiles.platform_roles[] = 'editor'</div>
|
||||||
|
</div>
|
||||||
|
<span class="phase phase-1">Fase 1</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tree">
|
||||||
|
<div class="tree-root">Usuário (editor@agenciapsi.com.br)</div>
|
||||||
|
<div> <span class="tree-branch">└──</span> <span class="tree-key">profiles.platform_roles</span> <span class="tree-val">['editor']</span></div>
|
||||||
|
<div> <span class="tree-comment">// sem memberships de tenant</span></div>
|
||||||
|
<div> <span class="tree-comment">// acessa /editor/*</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="badges">
|
||||||
|
<span class="badge badge-blue">platform_roles: editor</span>
|
||||||
|
<span class="badge badge-gray">sem tenant</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="note">Papel de plataforma (não de tenant). Gerencia conteúdo público, landing pages, textos. Verificado via <code>platform_roles[]</code>.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════ CLÍNICA ══ -->
|
||||||
|
<div class="section-label">🏥 Clínica</div>
|
||||||
|
|
||||||
|
<!-- Clinic Admin -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon" style="background:#064e3b">🏥</div>
|
||||||
|
<div>
|
||||||
|
<div class="card-title">Admin da Clínica</div>
|
||||||
|
<div class="card-subtitle">tenant.kind = 'clinic'</div>
|
||||||
|
</div>
|
||||||
|
<span class="phase phase-1">Fase 1</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tree">
|
||||||
|
<div class="tree-root">Usuário (admin@clinicax.com.br)</div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">profiles.role</span> <span class="tree-val">'tenant_member'</span></div>
|
||||||
|
<div> <span class="tree-branch">└──</span> Membership: <span class="tree-val">Clínica X</span></div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">tenant.kind</span> <span class="tree-val">'clinic'</span></div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">role</span> <span class="tree-val">'clinic_admin'</span></div>
|
||||||
|
<div> <span class="tree-branch">└──</span> <span class="tree-key">plano</span> <span class="tree-val">clinic_free | clinic_pro</span></div>
|
||||||
|
<div> <span class="tree-comment">// acessa /admin/*</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="badges">
|
||||||
|
<span class="badge badge-green">role: clinic_admin</span>
|
||||||
|
<span class="badge badge-blue">tenant: clinic</span>
|
||||||
|
<span class="badge badge-gray">clinic_free</span>
|
||||||
|
<span class="badge badge-purple">clinic_pro</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="note">Dono ou gestor de uma clínica. Gerencia profissionais, pacientes, agenda e módulos da clínica.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Terapeuta da Clínica -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon" style="background:#1e3a5f">🧑⚕️</div>
|
||||||
|
<div>
|
||||||
|
<div class="card-title">Terapeuta da Clínica</div>
|
||||||
|
<div class="card-subtitle">tenant.kind = 'clinic' / role = 'therapist'</div>
|
||||||
|
</div>
|
||||||
|
<span class="phase phase-1">Fase 1</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tree">
|
||||||
|
<div class="tree-root">Usuário (terapeuta@clinicax.com.br)</div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">profiles.role</span> <span class="tree-val">'tenant_member'</span></div>
|
||||||
|
<div> <span class="tree-branch">└──</span> Membership: <span class="tree-val">Clínica X</span></div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">tenant.kind</span> <span class="tree-val">'clinic'</span></div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">role</span> <span class="tree-val">'therapist'</span></div>
|
||||||
|
<div> <span class="tree-branch">└──</span> <span class="tree-key">entitlements</span> <span class="tree-val">via plano da clínica</span></div>
|
||||||
|
<div> <span class="tree-comment">// acessa /therapist/*</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="badges">
|
||||||
|
<span class="badge badge-green">role: therapist</span>
|
||||||
|
<span class="badge badge-blue">tenant: clinic</span>
|
||||||
|
<span class="badge badge-gray">entitlements da clínica</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="note">Terapeuta vinculado a uma clínica. Seus entitlements vêm do plano do tenant (clínica), não de assinatura pessoal.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════ TERAPEUTA INDEPENDENTE ══ -->
|
||||||
|
<div class="section-label">🧑💼 Terapeuta Independente</div>
|
||||||
|
|
||||||
|
<!-- Terapeuta Solo -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon" style="background:#064e3b">🧑💼</div>
|
||||||
|
<div>
|
||||||
|
<div class="card-title">Terapeuta Solo</div>
|
||||||
|
<div class="card-subtitle">tenant.kind = 'saas'</div>
|
||||||
|
</div>
|
||||||
|
<span class="phase phase-1">Fase 1</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tree">
|
||||||
|
<div class="tree-root">Usuário (terapeuta@gmail.com)</div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">profiles.role</span> <span class="tree-val">'tenant_member'</span></div>
|
||||||
|
<div> <span class="tree-branch">└──</span> Membership: <span class="tree-val">Tenant Pessoal</span></div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">tenant.kind</span> <span class="tree-val">'saas'</span></div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">role</span> <span class="tree-val">'therapist'</span></div>
|
||||||
|
<div> <span class="tree-branch">└──</span> <span class="tree-key">plano</span> <span class="tree-val">therapist_free | therapist_pro</span></div>
|
||||||
|
<div> <span class="tree-comment">// entitlements via v_user_entitlements</span></div>
|
||||||
|
<div> <span class="tree-comment">// acessa /therapist/*</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="badges">
|
||||||
|
<span class="badge badge-green">role: therapist</span>
|
||||||
|
<span class="badge badge-gray">tenant: saas (pessoal)</span>
|
||||||
|
<span class="badge badge-gray">therapist_free</span>
|
||||||
|
<span class="badge badge-purple">therapist_pro</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="note">Terapeuta autônomo sem clínica. Assina diretamente. Entitlements vêm de <code>v_user_entitlements</code> (assinatura pessoal).</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Terapeuta Solo + Clínica -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon" style="background:#1e3a5f">🔀</div>
|
||||||
|
<div>
|
||||||
|
<div class="card-title">Terapeuta Solo + Clínica</div>
|
||||||
|
<div class="card-subtitle">2 memberships / contexto switcher</div>
|
||||||
|
</div>
|
||||||
|
<span class="phase phase-1">Fase 1</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tree">
|
||||||
|
<div class="tree-root">Usuário (terapeuta@gmail.com)</div>
|
||||||
|
<div> <span class="tree-branch">├──</span> Membership A: <span class="tree-val">Tenant Pessoal</span></div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">tenant.kind</span> <span class="tree-val">'saas'</span></div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">role</span> <span class="tree-val">'therapist'</span></div>
|
||||||
|
<div> <span class="tree-branch">└──</span> <span class="tree-key">plano</span> <span class="tree-val">therapist_pro</span></div>
|
||||||
|
<div> <span class="tree-branch">└──</span> Membership B: <span class="tree-val">Clínica X</span></div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">tenant.kind</span> <span class="tree-val">'clinic'</span></div>
|
||||||
|
<div> <span class="tree-branch">└──</span> <span class="tree-key">role</span> <span class="tree-val">'therapist'</span></div>
|
||||||
|
<div> <span class="tree-comment">// switcher de contexto no topbar</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="badges">
|
||||||
|
<span class="badge badge-green">role: therapist</span>
|
||||||
|
<span class="badge badge-blue">2 tenants</span>
|
||||||
|
<span class="badge badge-purple">therapist_pro</span>
|
||||||
|
<span class="badge badge-gray">switcher de contexto</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="note">Atua em dois contextos. No tenant pessoal usa PRO. Na clínica usa os entitlements da clínica. Precisa de switcher de tenant no topbar.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════ SUPERVISOR ══ -->
|
||||||
|
<div class="section-label">🎓 Supervisor (Fase 1 — novo)</div>
|
||||||
|
|
||||||
|
<!-- Supervisor Solo -->
|
||||||
|
<div class="card" style="border-color: #ca8a04">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon" style="background:#422006">🎓</div>
|
||||||
|
<div>
|
||||||
|
<div class="card-title">Supervisor Solo</div>
|
||||||
|
<div class="card-subtitle">tenant.kind = 'supervisor'</div>
|
||||||
|
</div>
|
||||||
|
<span class="phase phase-1">Fase 1</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tree">
|
||||||
|
<div class="tree-root">Usuário (supervisor@gmail.com)</div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">profiles.role</span> <span class="tree-val">'tenant_member'</span></div>
|
||||||
|
<div> <span class="tree-branch">└──</span> Membership: <span class="tree-new">Tenant Supervisão (novo)</span></div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">tenant.kind</span> <span class="tree-new">'supervisor'</span></div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">role</span> <span class="tree-new">'supervisor'</span></div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">plano</span> <span class="tree-new">supervisor_free | supervisor_pro</span></div>
|
||||||
|
<div> <span class="tree-branch">└──</span> <span class="tree-key">max_supervisees</span> <span class="tree-new">3 | 20</span></div>
|
||||||
|
<div> <span class="tree-comment">// acessa /supervisor/*</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="badges">
|
||||||
|
<span class="badge badge-yellow">role: supervisor</span>
|
||||||
|
<span class="badge badge-yellow">tenant: supervisor</span>
|
||||||
|
<span class="badge badge-yellow">supervisor_free</span>
|
||||||
|
<span class="badge badge-orange">supervisor_pro</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="note"><strong>Novo.</strong> Supervisor independente. Tem sua própria sala de supervisão. O plano define o limite de terapeutas supervisionados (<code>plans.max_supervisees</code>).</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Terapeuta + Supervisor -->
|
||||||
|
<div class="card" style="border-color: #ca8a04">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon" style="background:#422006">🔀🎓</div>
|
||||||
|
<div>
|
||||||
|
<div class="card-title">Terapeuta + Supervisor</div>
|
||||||
|
<div class="card-subtitle">2 tenants / 2 papéis</div>
|
||||||
|
</div>
|
||||||
|
<span class="phase phase-1">Fase 1</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tree">
|
||||||
|
<div class="tree-root">Usuário (terapeuta@gmail.com)</div>
|
||||||
|
<div> <span class="tree-branch">├──</span> Membership A: <span class="tree-val">Tenant Pessoal</span></div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">tenant.kind</span> <span class="tree-val">'saas'</span></div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">role</span> <span class="tree-val">'therapist'</span></div>
|
||||||
|
<div> <span class="tree-branch">└──</span> <span class="tree-key">plano</span> <span class="tree-val">therapist_pro</span></div>
|
||||||
|
<div> <span class="tree-branch">└──</span> Membership B: <span class="tree-new">Tenant Supervisão (novo)</span></div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">tenant.kind</span> <span class="tree-new">'supervisor'</span></div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">role</span> <span class="tree-new">'supervisor'</span></div>
|
||||||
|
<div> <span class="tree-branch">└──</span> <span class="tree-key">plano</span> <span class="tree-new">supervisor_pro</span></div>
|
||||||
|
<div> <span class="tree-comment">// switcher: "Meu consultório" / "Minha supervisão"</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="badges">
|
||||||
|
<span class="badge badge-green">therapist</span>
|
||||||
|
<span class="badge badge-yellow">supervisor</span>
|
||||||
|
<span class="badge badge-blue">2 tenants</span>
|
||||||
|
<span class="badge badge-purple">therapist_pro</span>
|
||||||
|
<span class="badge badge-orange">supervisor_pro</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="note"><strong>O caso mais comum.</strong> Atua como terapeuta no tenant pessoal e como supervisor no tenant de supervisão. Switcher de contexto no topbar.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Terapeuta (clínica) + Supervisor -->
|
||||||
|
<div class="card" style="border-color: #ca8a04">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon" style="background:#422006">🏥🎓</div>
|
||||||
|
<div>
|
||||||
|
<div class="card-title">Terapeuta (Clínica) + Supervisor</div>
|
||||||
|
<div class="card-subtitle">3 tenants possíveis</div>
|
||||||
|
</div>
|
||||||
|
<span class="phase phase-1">Fase 1</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tree">
|
||||||
|
<div class="tree-root">Usuário (terapeuta@clinicax.com.br)</div>
|
||||||
|
<div> <span class="tree-branch">├──</span> Membership A: <span class="tree-val">Clínica X</span></div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">tenant.kind</span> <span class="tree-val">'clinic'</span></div>
|
||||||
|
<div> <span class="tree-branch">└──</span> <span class="tree-key">role</span> <span class="tree-val">'therapist'</span></div>
|
||||||
|
<div> <span class="tree-branch">└──</span> Membership B: <span class="tree-new">Tenant Supervisão (novo)</span></div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">tenant.kind</span> <span class="tree-new">'supervisor'</span></div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">role</span> <span class="tree-new">'supervisor'</span></div>
|
||||||
|
<div> <span class="tree-branch">└──</span> <span class="tree-key">plano</span> <span class="tree-new">supervisor_free | supervisor_pro</span></div>
|
||||||
|
<div> <span class="tree-comment">// supervisão é INDEPENDENTE da clínica</span></div>
|
||||||
|
<div> <span class="tree-comment">// colegas da clínica podem ser supervisionados</span></div>
|
||||||
|
<div> <span class="tree-comment">// via convite no tenant de supervisão</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="badges">
|
||||||
|
<span class="badge badge-green">therapist (clínica)</span>
|
||||||
|
<span class="badge badge-yellow">supervisor (independente)</span>
|
||||||
|
<span class="badge badge-orange">supervisor_pro</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="note">Trabalha na clínica como terapeuta <strong>e</strong> supervisiona outros terapeutas (inclusive colegas da clínica) de forma independente. A clínica não interfere na supervisão.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════ FASE 2 ══ -->
|
||||||
|
<div class="section-label">🚀 Fase 2 — Marketplace de Supervisão</div>
|
||||||
|
|
||||||
|
<!-- Clínica com Supervisor Associado -->
|
||||||
|
<div class="card" style="border-color: #475569; opacity: .8">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon" style="background:#1e293b">🏥🤝🎓</div>
|
||||||
|
<div>
|
||||||
|
<div class="card-title">Clínica com Supervisor Contratado</div>
|
||||||
|
<div class="card-subtitle">repasse financeiro AgenciaPsi</div>
|
||||||
|
</div>
|
||||||
|
<span class="phase phase-2">Fase 2</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tree">
|
||||||
|
<div class="tree-root">Clínica X</div>
|
||||||
|
<div> <span class="tree-branch">├──</span> Ativa módulo <span class="tree-key">supervisao</span> (feature)</div>
|
||||||
|
<div> <span class="tree-branch">├──</span> Associa supervisor externo via convite</div>
|
||||||
|
<div> <span class="tree-branch">├──</span> Sessões registradas na plataforma</div>
|
||||||
|
<div> <span class="tree-branch">└──</span> Pagamento via AgenciaPsi</div>
|
||||||
|
<div> </div>
|
||||||
|
<div class="tree-root">Fluxo financeiro</div>
|
||||||
|
<div> <span class="tree-branch">└──</span> Clínica paga <span class="tree-warn">R$ 200/sessão</span></div>
|
||||||
|
<div> <span class="tree-branch">├──</span> Supervisor recebe <span class="tree-val">R$ 180</span></div>
|
||||||
|
<div> <span class="tree-branch">└──</span> AgenciaPsi retém <span class="tree-warn">R$ 20 (10%)</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="badges">
|
||||||
|
<span class="badge badge-pink">split de pagamento</span>
|
||||||
|
<span class="badge badge-gray">Stripe Connect / Iugu</span>
|
||||||
|
<span class="badge badge-red">Fase 2</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="note"><strong>Futuro.</strong> A clínica contrata supervisão via plataforma. AgenciaPsi faz o split automático. Requer gateway com marketplace split (Stripe Connect, Iugu, Pagar.me).</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════ PACIENTE ══ -->
|
||||||
|
<div class="section-label">👤 Paciente / Portal</div>
|
||||||
|
|
||||||
|
<!-- Paciente -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon" style="background:#500724">👤</div>
|
||||||
|
<div>
|
||||||
|
<div class="card-title">Paciente</div>
|
||||||
|
<div class="card-subtitle">profiles.role = 'portal_user'</div>
|
||||||
|
</div>
|
||||||
|
<span class="phase phase-1">Fase 1</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tree">
|
||||||
|
<div class="tree-root">Usuário (paciente@gmail.com)</div>
|
||||||
|
<div> <span class="tree-branch">├──</span> <span class="tree-key">profiles.role</span> <span class="tree-val">'portal_user'</span></div>
|
||||||
|
<div> <span class="tree-branch">└──</span> <span class="tree-comment">// sem memberships de tenant</span></div>
|
||||||
|
<div> <span class="tree-comment">// acessa /portal/*</span></div>
|
||||||
|
<div> <span class="tree-comment">// identidade global, não tenant</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="badges">
|
||||||
|
<span class="badge badge-pink">role: portal_user</span>
|
||||||
|
<span class="badge badge-gray">sem tenant</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="note">Acessa apenas o portal do paciente. Vê suas sessões, agenda e documentos. Nunca entra na área de tenant-app.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- /grid -->
|
||||||
|
|
||||||
|
<!-- ── LEGEND ── -->
|
||||||
|
<div class="legend">
|
||||||
|
<h3>Legenda</h3>
|
||||||
|
<div class="legend-grid">
|
||||||
|
<div class="legend-item"><div class="legend-dot" style="background:#facc15"></div> Novo — Fase 1 (supervisor)</div>
|
||||||
|
<div class="legend-item"><div class="legend-dot" style="background:#6ee7b7"></div> Existente — Fase 1</div>
|
||||||
|
<div class="legend-item"><div class="legend-dot" style="background:#fb923c"></div> Planejado — Fase 2</div>
|
||||||
|
<div class="legend-item"><div class="legend-dot" style="background:#a5b4fc"></div> Plataforma (sem tenant)</div>
|
||||||
|
<div class="legend-item"><div class="legend-dot" style="background:#94a3b8"></div> profiles.role → identidade global</div>
|
||||||
|
<div class="legend-item"><div class="legend-dot" style="background:#f9a8d4"></div> memberships.role → contexto de tenant</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── RESUMO TÉCNICO ── -->
|
||||||
|
<div class="legend" style="margin-top: 1rem">
|
||||||
|
<h3>Resumo Técnico — Como o Guard decide o menu</h3>
|
||||||
|
<div style="font-family: monospace; font-size: .8rem; line-height: 2; color: #94a3b8;">
|
||||||
|
<div><span style="color:#a5b4fc">profiles.role</span> = identidade global (saas_admin | tenant_member | portal_user)</div>
|
||||||
|
<div><span style="color:#6ee7b7">memberships.role</span> = papel dentro do tenant (clinic_admin | therapist | supervisor | editor)</div>
|
||||||
|
<div><span style="color:#f9a8d4">tenant.kind</span> = tipo do tenant (clinic | saas | supervisor) → define qual menu e contexto</div>
|
||||||
|
<div><span style="color:#fde047">plans.target</span> = para quem é o plano (clinic | therapist | supervisor)</div>
|
||||||
|
<div><span style="color:#fdba74">plans.max_supervisees</span> = limite de supervisionados (novo — Fase 1)</div>
|
||||||
|
<div style="margin-top:.5rem; color: #475569">
|
||||||
|
Entitlements: v_tenant_entitlements (plano do tenant) UNION v_user_entitlements (assinatura pessoal)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,766 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Documentos & Arquivos — Status de Implementacao</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #0d1117;
|
||||||
|
--bg-card: #161b22;
|
||||||
|
--bg-card-hover: #1c2129;
|
||||||
|
--border: #30363d;
|
||||||
|
--border-light: #21262d;
|
||||||
|
--text: #e6edf3;
|
||||||
|
--text-secondary: #8b949e;
|
||||||
|
--text-muted: #6e7681;
|
||||||
|
--accent: #58a6ff;
|
||||||
|
--accent-dim: #1f6feb33;
|
||||||
|
--green: #3fb950;
|
||||||
|
--green-dim: #23863633;
|
||||||
|
--orange: #d29922;
|
||||||
|
--orange-dim: #9e6a0333;
|
||||||
|
--red: #f85149;
|
||||||
|
--red-dim: #da363333;
|
||||||
|
--purple: #bc8cff;
|
||||||
|
--purple-dim: #8957e533;
|
||||||
|
--cyan: #39d2c0;
|
||||||
|
--cyan-dim: #1b7c6e33;
|
||||||
|
--pink: #f778ba;
|
||||||
|
--pink-dim: #db61a233;
|
||||||
|
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||||
|
--font-mono: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||||||
|
--radius: 8px;
|
||||||
|
--radius-lg: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font);
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 32px 24px 64px;
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Header ─────────────────────────── */
|
||||||
|
.page-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 36px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.page-header h1 {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.page-header .subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.page-header .meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 14px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.meta-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--bg-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Summary stats ─────────────────── */
|
||||||
|
.summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 36px;
|
||||||
|
}
|
||||||
|
.summary-card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 14px 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.summary-card .number { font-size: 24px; font-weight: 700; line-height: 1.2; }
|
||||||
|
.summary-card .label { font-size: 10px; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.06em; margin-top: 2px; }
|
||||||
|
.c-green .number { color: var(--green); }
|
||||||
|
.c-orange .number { color: var(--orange); }
|
||||||
|
.c-red .number { color: var(--red); }
|
||||||
|
.c-accent .number { color: var(--accent); }
|
||||||
|
.c-purple .number { color: var(--purple); }
|
||||||
|
.c-cyan .number { color: var(--cyan); }
|
||||||
|
|
||||||
|
/* ── Section ────────────────────────── */
|
||||||
|
.section {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
.section-icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.section-icon.blue { background: var(--accent-dim); color: var(--accent); }
|
||||||
|
.section-icon.green { background: var(--green-dim); color: var(--green); }
|
||||||
|
.section-icon.orange { background: var(--orange-dim); color: var(--orange); }
|
||||||
|
.section-icon.purple { background: var(--purple-dim); color: var(--purple); }
|
||||||
|
.section-icon.cyan { background: var(--cyan-dim); color: var(--cyan); }
|
||||||
|
.section-icon.pink { background: var(--pink-dim); color: var(--pink); }
|
||||||
|
.section-icon.red { background: var(--red-dim); color: var(--red); }
|
||||||
|
|
||||||
|
.section-title { font-size: 16px; font-weight: 600; }
|
||||||
|
|
||||||
|
/* ── Layout: cards + sidebar ────────── */
|
||||||
|
.content-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 260px;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
.content-row { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Cards ──────────────────────────── */
|
||||||
|
.cards {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 16px 18px;
|
||||||
|
transition: border-color .15s;
|
||||||
|
}
|
||||||
|
.card:hover { border-color: #484f58; }
|
||||||
|
.card-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.card-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.card-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
.card-fields {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.field {
|
||||||
|
font-size: 10px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
padding: 2px 7px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
.card-file {
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: var(--accent);
|
||||||
|
margin-top: 6px;
|
||||||
|
opacity: .8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Badges ─────────────────────────── */
|
||||||
|
.badge {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 9px;
|
||||||
|
border-radius: 20px;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
.badge-done { background: var(--green-dim); color: var(--green); }
|
||||||
|
.badge-partial { background: var(--orange-dim); color: var(--orange); }
|
||||||
|
.badge-pending { background: var(--red-dim); color: var(--red); }
|
||||||
|
.badge-db { background: var(--accent-dim); color: var(--accent); }
|
||||||
|
|
||||||
|
/* ── Sidebar ────────────────────────── */
|
||||||
|
.sidebar {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 16px;
|
||||||
|
position: sticky;
|
||||||
|
top: 20px;
|
||||||
|
}
|
||||||
|
.sidebar-title {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
.sidebar-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 5px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.sidebar-item .dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.dot-green { background: var(--green); }
|
||||||
|
.dot-orange { background: var(--orange); }
|
||||||
|
.dot-red { background: var(--red); }
|
||||||
|
.sidebar-item .label { flex: 1; }
|
||||||
|
.sidebar-item .status {
|
||||||
|
font-size: 10px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.sidebar-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--border-light);
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Note box ──────────────────────── */
|
||||||
|
.note {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-left: 3px solid var(--accent);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.note strong { color: var(--text); }
|
||||||
|
.note.warn { border-left-color: var(--orange); }
|
||||||
|
|
||||||
|
/* ── Legend ─────────────────────────── */
|
||||||
|
.legend {
|
||||||
|
display: flex;
|
||||||
|
gap: 18px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.legend-item .dot { width: 8px; height: 8px; border-radius: 50%; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════ HEADER ══════════════════════════════════ -->
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>Documentos & Arquivos</h1>
|
||||||
|
<div class="subtitle">Status de implementacao confrontado com o banco de dados</div>
|
||||||
|
<div class="meta">
|
||||||
|
<span class="meta-tag">AgenciaPsi v5</span>
|
||||||
|
<span class="meta-tag">Vue 3 + Supabase</span>
|
||||||
|
<span class="meta-tag">Atualizado: 2026-03-30</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════ STATS ═══════════════════════════════════ -->
|
||||||
|
<div class="summary-grid">
|
||||||
|
<div class="summary-card c-green">
|
||||||
|
<div class="number">6/6</div>
|
||||||
|
<div class="label">Tabelas</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card c-green">
|
||||||
|
<div class="number">2/2</div>
|
||||||
|
<div class="label">Buckets</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card c-green">
|
||||||
|
<div class="number">7/7</div>
|
||||||
|
<div class="label">Services</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card c-green">
|
||||||
|
<div class="number">3/3</div>
|
||||||
|
<div class="label">Composables</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card c-green">
|
||||||
|
<div class="number">10/10</div>
|
||||||
|
<div class="label">Componentes</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card c-green">
|
||||||
|
<div class="number">14</div>
|
||||||
|
<div class="label">Templates seed</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════ LEGENDA ═════════════════════════════════ -->
|
||||||
|
<div class="legend">
|
||||||
|
<div class="legend-item"><span class="dot dot-green"></span> Implementado</div>
|
||||||
|
<div class="legend-item"><span class="dot dot-orange"></span> Parcial / Migration pendente</div>
|
||||||
|
<div class="legend-item"><span class="dot dot-red"></span> Nao implementado</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════ 1. UPLOAD & ORGANIZACAO ═════════════════ -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon blue">1</div>
|
||||||
|
<div class="section-title">Upload & Organizacao de Arquivos</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-row">
|
||||||
|
<div class="cards">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-green"></span> Upload de arquivo ao paciente</div>
|
||||||
|
<span class="badge badge-done">pronto</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">PDF, imagem, DOCX. Vinculado ao patient_id. Supabase Storage com path estruturado. Drag & drop + seletor. Validacao de tamanho (50MB) e tipo MIME.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field">patient_id</span><span class="field">bucket_path</span><span class="field">storage_bucket</span>
|
||||||
|
<span class="field">nome_original</span><span class="field">mime_type</span><span class="field">tamanho_bytes</span>
|
||||||
|
<span class="field">uploaded_by</span><span class="field">uploaded_at</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-file">Documents.service.js → uploadDocument()</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-green"></span> Tipo, categoria & tags</div>
|
||||||
|
<span class="badge badge-done">pronto</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">11 tipos (laudo, receita, exame, atestado, declaracao, recibo, etc.). Categoria livre. Tags[] com autocomplete. Filtros na listagem.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field">tipo_documento</span><span class="field">categoria</span><span class="field">descricao</span><span class="field">tags[]</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-file">DB: CHECK constraint + GIN index em tags</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-green"></span> Vinculo com sessao</div>
|
||||||
|
<span class="badge badge-done">pronto</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Arquivo linkado a agenda_eventos (sessao) ou session_note. Colunas nullable — nem todo arquivo tem sessao.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field">agenda_evento_id</span><span class="field">session_note_id</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-file">DB: FK para agenda_eventos (ON DELETE SET NULL)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-green"></span> Visibilidade & controle de acesso</div>
|
||||||
|
<span class="badge badge-done">pronto</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Privado, compartilhado com supervisor, ou visivel no portal do paciente. Granular por arquivo. Expiracao de compartilhamento.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field">visibilidade</span><span class="field">compartilhado_portal</span><span class="field">compartilhado_supervisor</span>
|
||||||
|
<span class="field">compartilhado_em</span><span class="field">expira_compartilhamento</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-file">DB: CHECK (privado | compartilhado_supervisor | compartilhado_portal)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-green"></span> Soft delete com retencao LGPD</div>
|
||||||
|
<span class="badge badge-done">pronto</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Arquivo "excluido" some da UI mas fica retido por 5 anos (CFP). Colunas de controle + index parcial para listagem ativa.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field">deleted_at</span><span class="field">deleted_by</span><span class="field">retencao_ate</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-file">DB: idx_documents_active (WHERE deleted_at IS NULL)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-green"></span> Preview & download</div>
|
||||||
|
<span class="badge badge-done">pronto</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Preview inline de PDF e imagens via dialog. Download com URL assinada (60s). Suporte a storage_bucket dinamico (documents ou generated-docs).</div>
|
||||||
|
<div class="card-file">DocumentPreviewDialog.vue + getDownloadUrl(path, expires, bucket)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="sidebar-title">Banco de Dados</div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">documents</span><span class="status">27 cols</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">RLS owner_id</span><span class="status">ativo</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">Indexes</span><span class="status">9</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">Trigger timeline</span><span class="status">insert</span></div>
|
||||||
|
<div class="sidebar-divider"></div>
|
||||||
|
<div class="sidebar-title">Storage</div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">documents</span><span class="status">50MB</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">generated-docs</span><span class="status">20MB</span></div>
|
||||||
|
<div class="sidebar-divider"></div>
|
||||||
|
<div class="sidebar-title">Frontend</div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">DocumentsListPage</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">DocumentCard</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">DocumentUploadDialog</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">DocumentPreviewDialog</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">DocumentTagsInput</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">useDocuments.js</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════ 2. GERACAO DE DOCUMENTOS ════════════════ -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon green">2</div>
|
||||||
|
<div class="section-title">Geracao de Documentos (PDF)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-row">
|
||||||
|
<div class="cards">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-green"></span> Templates de documentos</div>
|
||||||
|
<span class="badge badge-done">pronto</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">16 tipos de template. 14 templates globais no seed. Corpo HTML com {{variaveis}}. Cabecalho/rodape personalizaveis. Templates por tenant + globais do sistema.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field">nome_template</span><span class="field">tipo</span><span class="field">corpo_html</span>
|
||||||
|
<span class="field">cabecalho_html</span><span class="field">rodape_html</span><span class="field">variaveis[]</span>
|
||||||
|
<span class="field">is_global</span><span class="field">logo_url</span><span class="field">ativo</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-file">document_templates (DB) + seed_015_document_templates.sql</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-green"></span> Geracao de PDF (client-side)</div>
|
||||||
|
<span class="badge badge-done">pronto</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">jsPDF + html2canvas-pro (substituiu pdfmake por incompatibilidade com Vite). Renderiza HTML preenchido em canvas, converte para PDF A4 com paginacao. JPEG 85%, scale 1.5. ~200-400KB por documento.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field">buildFullHtml()</span><span class="field">htmlToPdfBlob()</span><span class="field">fillTemplate()</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-file">pdf.service.js + DocumentGenerate.service.js</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-green"></span> Documento gerado (instancia + listagem)</div>
|
||||||
|
<span class="badge badge-done">pronto</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Cada PDF gerado: salva snapshot em document_generated (dados preenchidos para auditoria) E automaticamente registra na tabela documents (para aparecer na listagem do paciente). Bucket: generated-docs. Nomes sanitizados (sem acentos).</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field">template_id</span><span class="field">dados_preenchidos</span><span class="field">pdf_path</span>
|
||||||
|
<span class="field">gerado_em</span><span class="field">gerado_por</span><span class="field">→ documents</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-file">saveGeneratedDocument() → document_generated + documents</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-green"></span> Fluxo de geracao (UI)</div>
|
||||||
|
<span class="badge badge-done">pronto</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Dialog 3 etapas: selecionar template → editar variaveis (auto-preenchidas com dados paciente/sessao/terapeuta/clinica) → preview via iframe sandbox → "Salvar documento" (online) ou "So baixar" (local).</div>
|
||||||
|
<div class="card-file">DocumentGenerateDialog.vue + useDocumentGenerate.js</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-orange"></span> Dados da clinica no template</div>
|
||||||
|
<span class="badge badge-partial">parcial</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">loadClinicData() usa select('*') na tabela tenants. Atualmente so retorna name. Campos phone, contact_email, logradouro, numero, bairro, cidade, estado dependem da migration 003_tenants_address_fields.sql ser aplicada.</div>
|
||||||
|
<div class="card-file">Migration pendente: 003_tenants_address_fields.sql</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-green"></span> Editor de templates</div>
|
||||||
|
<span class="badge badge-done">pronto</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Editor rich text para corpo HTML. Insercao de variaveis via dropdown. Preview ao vivo. Config de cabecalho/rodape/logo. Gestao de templates globais e por tenant.</div>
|
||||||
|
<div class="card-file">DocumentTemplateEditor.vue + DocumentTemplatesPage.vue</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="sidebar-title">Banco de Dados</div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">document_templates</span><span class="status">15 cols</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">document_generated</span><span class="status">10 cols</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">RLS</span><span class="status">ativo</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">14 seeds globais</span></div>
|
||||||
|
<div class="sidebar-divider"></div>
|
||||||
|
<div class="sidebar-title">Motor PDF</div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">jsPDF</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">html2canvas-pro</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-red"></span><span class="label">pdfmake</span><span class="status">removido</span></div>
|
||||||
|
<div class="sidebar-divider"></div>
|
||||||
|
<div class="sidebar-title">Pendencias</div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-orange"></span><span class="label">tenants address</span><span class="status">migration</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-orange"></span><span class="label">terapeuta_crp</span><span class="status">campo</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════ 3. ASSINATURA ELETRONICA ════════════════ -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon purple">3</div>
|
||||||
|
<div class="section-title">Assinatura Eletronica</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-row">
|
||||||
|
<div class="cards">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-green"></span> TCLE & consentimento</div>
|
||||||
|
<span class="badge badge-done">db pronto</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Tabela document_signatures com rastreamento completo: IP, timestamp, hash SHA-256, user_agent. Suporte a 3 tipos de signatario (paciente, responsavel_legal, terapeuta). 5 status possiveis.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field">documento_id</span><span class="field">signatario_tipo</span><span class="field">signatario_id</span>
|
||||||
|
<span class="field">ordem</span><span class="field">status</span><span class="field">ip</span>
|
||||||
|
<span class="field">hash_documento</span><span class="field">assinado_em</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-file">document_signatures (DB) + DocumentSignatures.service.js</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-orange"></span> UI de assinatura</div>
|
||||||
|
<span class="badge badge-partial">parcial</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Componente DocumentSignatureDialog.vue existe. Service DocumentSignatures.service.js existe. Fluxo completo de envio por link e assinatura pelo paciente ainda precisa ser validado end-to-end.</div>
|
||||||
|
<div class="card-file">DocumentSignatureDialog.vue</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="sidebar-title">Banco de Dados</div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">document_signatures</span><span class="status">14 cols</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">RLS tenant</span><span class="status">ativo</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">Trigger timeline</span><span class="status">assinado</span></div>
|
||||||
|
<div class="sidebar-divider"></div>
|
||||||
|
<div class="sidebar-title">Frontend</div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">SignatureDialog</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">Signatures.service</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-orange"></span><span class="label">Fluxo e2e</span><span class="status">validar</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════ 4. COMPARTILHAMENTO ═════════════════════ -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon cyan">4</div>
|
||||||
|
<div class="section-title">Compartilhamento & Portal do Paciente</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-row">
|
||||||
|
<div class="cards">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-green"></span> Links temporarios de acesso</div>
|
||||||
|
<span class="badge badge-done">pronto</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Token hex 32 bytes, prazo de expiracao, limite de usos. RLS publica por token valido. Link seguro sem necessidade de login.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field">token</span><span class="field">expira_em</span><span class="field">usos_max</span>
|
||||||
|
<span class="field">usos</span><span class="field">ativo</span><span class="field">criado_por</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-file">document_share_links (DB) + DocumentShareLinks.service.js</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-green"></span> Documentos compartilhados com paciente</div>
|
||||||
|
<span class="badge badge-done">db pronto</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Terapeuta decide quais arquivos ficam visiveis pro paciente. Campos compartilhado_portal e expira_compartilhamento na tabela documents.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field">compartilhado_portal</span><span class="field">compartilhado_em</span><span class="field">expira_compartilhamento</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-green"></span> Upload pelo paciente</div>
|
||||||
|
<span class="badge badge-done">db pronto</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Paciente envia exames/laudos pelo portal. Fila de "pendentes de revisao" para o terapeuta aprovar.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field">enviado_pelo_paciente</span><span class="field">status_revisao</span>
|
||||||
|
<span class="field">revisado_por</span><span class="field">revisado_em</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="sidebar-title">Banco de Dados</div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">document_share_links</span><span class="status">10 cols</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">RLS token publico</span><span class="status">ativo</span></div>
|
||||||
|
<div class="sidebar-divider"></div>
|
||||||
|
<div class="sidebar-title">Frontend</div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">ShareDialog</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">ShareLinks.service</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">SharedDocumentPage</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════ 5. AUDITORIA ═══════════════════════════ -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon red">5</div>
|
||||||
|
<div class="section-title">Auditoria & Conformidade</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-row">
|
||||||
|
<div class="cards">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-green"></span> Log de acesso a arquivos</div>
|
||||||
|
<span class="badge badge-done">pronto</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Tabela imutavel (somente INSERT + SELECT, sem UPDATE/DELETE). Cada visualizacao ou download registrado. Conformidade CFP e LGPD. Integrado no composable useDocuments (logAccess automatico).</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field">documento_id</span><span class="field">acao</span><span class="field">user_id</span>
|
||||||
|
<span class="field">ip</span><span class="field">user_agent</span><span class="field">acessado_em</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-file">document_access_logs (DB) + DocumentAuditLog.service.js</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-green"></span> Timeline do paciente</div>
|
||||||
|
<span class="badge badge-done">pronto</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Triggers automaticos registram na patient_timeline quando: documento uploadado (INSERT em documents) e documento assinado (UPDATE em document_signatures).</div>
|
||||||
|
<div class="card-file">DB Triggers: trg_documents_timeline_insert + trg_ds_timeline</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="sidebar-title">Banco de Dados</div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">document_access_logs</span><span class="status">8 cols</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">Imutavel</span><span class="status">no UPDATE</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">RLS tenant</span><span class="status">ativo</span></div>
|
||||||
|
<div class="sidebar-divider"></div>
|
||||||
|
<div class="sidebar-title">Acoes rastreadas</div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">visualizou</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">baixou</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">imprimiu</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">compartilhou</span></div>
|
||||||
|
<div class="sidebar-item"><span class="dot dot-green"></span><span class="label">assinou</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════ PENDENCIAS ══════════════════════════════ -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon orange">!</div>
|
||||||
|
<div class="section-title">Pendencias & Migrations Nao Aplicadas</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cards">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-orange"></span> Migration: tenants address fields</div>
|
||||||
|
<span class="badge badge-partial">pendente</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">003_tenants_address_fields.sql — adiciona cep, logradouro, numero, complemento, bairro, cidade, estado a tabela tenants. Tambem faltam phone e contact_email. Necessario para preencher variaveis clinica_endereco, clinica_telefone nos templates.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-orange"></span> Campo CRP do terapeuta</div>
|
||||||
|
<span class="badge badge-partial">pendente</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Variavel terapeuta_crp nos templates retorna vazio. O campo CRP nao existe na tabela profiles nem em tenant_members. Precisa de migration para adicionar coluna crp em profiles.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-orange"></span> Fluxo de assinatura end-to-end</div>
|
||||||
|
<span class="badge badge-partial">validar</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Tabela, service e componente existem. Falta validar: envio de link por email/whatsapp, pagina publica de assinatura, registro de IP/hash, notificacao ao terapeuta quando assinado.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-title"><span class="dot dot-orange"></span> Portal do paciente — visualizacao de docs</div>
|
||||||
|
<span class="badge badge-partial">validar</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Campos compartilhado_portal e visibilidade existem no banco. SharedDocumentPage.vue existe. Falta validar se o portal do paciente (CadastroPacienteExterno) exibe corretamente os documentos compartilhados.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="note warn" style="margin-top: 14px;">
|
||||||
|
<strong>Decisao tecnica — Motor PDF:</strong> pdfmake foi substituido por jsPDF + html2canvas-pro. O pdfmake (UMD) trava silenciosamente com Vite (ESM) — createPdf().getBlob()/getBuffer() nunca retornam. html2canvas-pro e um fork open source (MIT) com suporte a cores oklch usadas pelo PrimeVue/Tailwind.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,870 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Plano de Implementacao — Modulo Documentos & Arquivos</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #0d1117;
|
||||||
|
--bg-card: #161b22;
|
||||||
|
--bg-card-hover: #1c2129;
|
||||||
|
--bg-table-head: #1c2129;
|
||||||
|
--bg-table-row: #161b22;
|
||||||
|
--bg-table-row-alt: #0d1117;
|
||||||
|
--border: #30363d;
|
||||||
|
--border-light: #21262d;
|
||||||
|
--text: #e6edf3;
|
||||||
|
--text-secondary: #8b949e;
|
||||||
|
--text-muted: #6e7681;
|
||||||
|
--accent: #58a6ff;
|
||||||
|
--accent-dim: #1f6feb33;
|
||||||
|
--green: #3fb950;
|
||||||
|
--green-dim: #23863633;
|
||||||
|
--orange: #d29922;
|
||||||
|
--orange-dim: #9e6a0333;
|
||||||
|
--purple: #bc8cff;
|
||||||
|
--purple-dim: #8957e533;
|
||||||
|
--red: #f85149;
|
||||||
|
--red-dim: #da363333;
|
||||||
|
--cyan: #39d2c0;
|
||||||
|
--cyan-dim: #1b7c6e33;
|
||||||
|
--pink: #f778ba;
|
||||||
|
--pink-dim: #db61a233;
|
||||||
|
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||||
|
--font-mono: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||||||
|
--radius: 8px;
|
||||||
|
--radius-lg: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font);
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 32px 24px 64px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Header ─────────────────────────── */
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.page-header h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
.page-header .subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.page-header .meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.meta-tag {
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--bg-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Summary cards ──────────────────── */
|
||||||
|
.summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.summary-card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 14px 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.summary-card .number {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.summary-card .label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.summary-card.c-blue .number { color: var(--accent); }
|
||||||
|
.summary-card.c-green .number { color: var(--green); }
|
||||||
|
.summary-card.c-orange .number { color: var(--orange); }
|
||||||
|
.summary-card.c-purple .number { color: var(--purple); }
|
||||||
|
.summary-card.c-cyan .number { color: var(--cyan); }
|
||||||
|
.summary-card.c-pink .number { color: var(--pink); }
|
||||||
|
|
||||||
|
/* ── Sections ───────────────────────── */
|
||||||
|
.section {
|
||||||
|
margin-bottom: 36px;
|
||||||
|
}
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
.section-icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.section-icon.blue { background: var(--accent-dim); color: var(--accent); }
|
||||||
|
.section-icon.green { background: var(--green-dim); color: var(--green); }
|
||||||
|
.section-icon.orange { background: var(--orange-dim); color: var(--orange); }
|
||||||
|
.section-icon.purple { background: var(--purple-dim); color: var(--purple); }
|
||||||
|
.section-icon.cyan { background: var(--cyan-dim); color: var(--cyan); }
|
||||||
|
.section-icon.pink { background: var(--pink-dim); color: var(--pink); }
|
||||||
|
.section-icon.red { background: var(--red-dim); color: var(--red); }
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.section-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: -6px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding-left: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Tables ─────────────────────────── */
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
thead th {
|
||||||
|
background: var(--bg-table-head);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
padding: 10px 14px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
tbody td {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
tbody tr:nth-child(odd) { background: var(--bg-table-row); }
|
||||||
|
tbody tr:nth-child(even) { background: var(--bg-table-row-alt); }
|
||||||
|
tbody tr:hover { background: var(--bg-card-hover); }
|
||||||
|
tbody tr:last-child td { border-bottom: none; }
|
||||||
|
|
||||||
|
.col-file {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--accent);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.col-table {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--green);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.col-route {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--orange);
|
||||||
|
}
|
||||||
|
.col-key {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--purple);
|
||||||
|
}
|
||||||
|
.col-bucket {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Field chips ────────────────────── */
|
||||||
|
.fields {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.field {
|
||||||
|
font-size: 10px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
padding: 2px 7px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Notes ──────────────────────────── */
|
||||||
|
.note {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-left: 3px solid var(--accent);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Responsive ─────────────────────── */
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
body { padding: 16px 12px 40px; }
|
||||||
|
.summary-grid { grid-template-columns: repeat(3, 1fr); }
|
||||||
|
table { font-size: 12px; }
|
||||||
|
thead th, tbody td { padding: 8px 10px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════ HEADER ════════════════════════════════════════ -->
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>Plano de Implementacao — Documentos & Arquivos</h1>
|
||||||
|
<div class="subtitle">Modulo completo: upload, templates, geracao PDF, assinatura eletronica, portal do paciente, auditoria</div>
|
||||||
|
<div class="meta">
|
||||||
|
<span class="meta-tag">AgenciaPsi v5</span>
|
||||||
|
<span class="meta-tag">Vue 3 + Supabase</span>
|
||||||
|
<span class="meta-tag">2026-03-30</span>
|
||||||
|
<span class="meta-tag">Status: em andamento</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════ RESUMO ═══════════════════════════════════════ -->
|
||||||
|
<div class="summary-grid">
|
||||||
|
<div class="summary-card c-blue">
|
||||||
|
<div class="number">6</div>
|
||||||
|
<div class="label">Tabelas</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card c-cyan">
|
||||||
|
<div class="number">2</div>
|
||||||
|
<div class="label">Buckets</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card c-green">
|
||||||
|
<div class="number">7</div>
|
||||||
|
<div class="label">Services</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card c-orange">
|
||||||
|
<div class="number">3</div>
|
||||||
|
<div class="label">Composables</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card c-purple">
|
||||||
|
<div class="number">~10</div>
|
||||||
|
<div class="label">Componentes</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card c-pink">
|
||||||
|
<div class="number">5</div>
|
||||||
|
<div class="label">Feature flags</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════ 1. BANCO ═════════════════════════════════════ -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon blue">1</div>
|
||||||
|
<div class="section-title">Banco de Dados — Migrations</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-desc">Tabelas, RLS policies, indexes, triggers</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Migration</th>
|
||||||
|
<th>Tabela / Objeto</th>
|
||||||
|
<th>O que faz</th>
|
||||||
|
<th>Campos principais</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file" rowspan="4">005_create_documents_tables.sql</td>
|
||||||
|
<td class="col-table">documents</td>
|
||||||
|
<td>Arquivo vinculado a paciente. Path no Supabase Storage, tipo/categoria, visibilidade, tags, soft delete com retencao LGPD. Tabela central do modulo. O campo storage_bucket indica qual bucket do Storage contem o arquivo (documents ou generated-docs), permitindo que PDFs gerados aparecam na mesma listagem.</td>
|
||||||
|
<td>
|
||||||
|
<div class="fields">
|
||||||
|
<span class="field">id</span>
|
||||||
|
<span class="field">patient_id</span>
|
||||||
|
<span class="field">tenant_id</span>
|
||||||
|
<span class="field">owner_id</span>
|
||||||
|
<span class="field">bucket_path</span>
|
||||||
|
<span class="field">storage_bucket</span>
|
||||||
|
<span class="field">nome_original</span>
|
||||||
|
<span class="field">mime_type</span>
|
||||||
|
<span class="field">tamanho_bytes</span>
|
||||||
|
<span class="field">tipo_documento</span>
|
||||||
|
<span class="field">categoria</span>
|
||||||
|
<span class="field">descricao</span>
|
||||||
|
<span class="field">tags[]</span>
|
||||||
|
<span class="field">visibilidade</span>
|
||||||
|
<span class="field">compartilhado_portal</span>
|
||||||
|
<span class="field">compartilhado_supervisor</span>
|
||||||
|
<span class="field">agenda_evento_id</span>
|
||||||
|
<span class="field">session_note_id</span>
|
||||||
|
<span class="field">enviado_pelo_paciente</span>
|
||||||
|
<span class="field">status_revisao</span>
|
||||||
|
<span class="field">revisado_por</span>
|
||||||
|
<span class="field">revisado_em</span>
|
||||||
|
<span class="field">uploaded_by</span>
|
||||||
|
<span class="field">uploaded_at</span>
|
||||||
|
<span class="field">deleted_at</span>
|
||||||
|
<span class="field">deleted_by</span>
|
||||||
|
<span class="field">retencao_ate</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-table">document_access_logs</td>
|
||||||
|
<td>Log imutavel de quem visualizou ou baixou cada arquivo. Conformidade CFP e LGPD. Sem UPDATE/DELETE — somente INSERT e SELECT.</td>
|
||||||
|
<td>
|
||||||
|
<div class="fields">
|
||||||
|
<span class="field">id</span>
|
||||||
|
<span class="field">documento_id</span>
|
||||||
|
<span class="field">acao</span>
|
||||||
|
<span class="field">user_id</span>
|
||||||
|
<span class="field">ip</span>
|
||||||
|
<span class="field">user_agent</span>
|
||||||
|
<span class="field">acessado_em</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-table">document_signatures</td>
|
||||||
|
<td>Assinaturas eletronicas. Cada signatario (paciente, responsavel, terapeuta) tem seu registro com IP, timestamp e hash do documento.</td>
|
||||||
|
<td>
|
||||||
|
<div class="fields">
|
||||||
|
<span class="field">id</span>
|
||||||
|
<span class="field">documento_id</span>
|
||||||
|
<span class="field">signatario_tipo</span>
|
||||||
|
<span class="field">signatario_id</span>
|
||||||
|
<span class="field">ordem</span>
|
||||||
|
<span class="field">status</span>
|
||||||
|
<span class="field">ip</span>
|
||||||
|
<span class="field">user_agent</span>
|
||||||
|
<span class="field">assinado_em</span>
|
||||||
|
<span class="field">hash_documento</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-table">document_share_links</td>
|
||||||
|
<td>Links temporarios assinados para compartilhar documento com profissional externo sem conta no sistema. Prazo e limite de usos.</td>
|
||||||
|
<td>
|
||||||
|
<div class="fields">
|
||||||
|
<span class="field">id</span>
|
||||||
|
<span class="field">documento_id</span>
|
||||||
|
<span class="field">token</span>
|
||||||
|
<span class="field">expira_em</span>
|
||||||
|
<span class="field">usos_max</span>
|
||||||
|
<span class="field">usos</span>
|
||||||
|
<span class="field">criado_por</span>
|
||||||
|
<span class="field">criado_em</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file" rowspan="2">006_create_document_templates.sql</td>
|
||||||
|
<td class="col-table">document_templates</td>
|
||||||
|
<td>Templates de documentos (declaracao de comparecimento, atestado, recibo etc.). Corpo HTML com variaveis. Templates globais do sistema + personalizados por tenant com logo/cabecalho.</td>
|
||||||
|
<td>
|
||||||
|
<div class="fields">
|
||||||
|
<span class="field">id</span>
|
||||||
|
<span class="field">tenant_id</span>
|
||||||
|
<span class="field">nome_template</span>
|
||||||
|
<span class="field">tipo</span>
|
||||||
|
<span class="field">corpo_html</span>
|
||||||
|
<span class="field">variaveis[]</span>
|
||||||
|
<span class="field">is_global</span>
|
||||||
|
<span class="field">owner_id</span>
|
||||||
|
<span class="field">logo_url</span>
|
||||||
|
<span class="field">cabecalho_html</span>
|
||||||
|
<span class="field">rodape_html</span>
|
||||||
|
<span class="field">ativo</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-table">document_generated</td>
|
||||||
|
<td>Cada PDF gerado a partir de um template. Guarda os dados usados no preenchimento e o path do PDF resultante no Storage.</td>
|
||||||
|
<td>
|
||||||
|
<div class="fields">
|
||||||
|
<span class="field">id</span>
|
||||||
|
<span class="field">template_id</span>
|
||||||
|
<span class="field">patient_id</span>
|
||||||
|
<span class="field">tenant_id</span>
|
||||||
|
<span class="field">dados_preenchidos</span>
|
||||||
|
<span class="field">pdf_path</span>
|
||||||
|
<span class="field">gerado_em</span>
|
||||||
|
<span class="field">gerado_por</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════ 2. STORAGE ═══════════════════════════════════ -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon cyan">2</div>
|
||||||
|
<div class="section-title">Supabase Storage — Buckets</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Bucket</th>
|
||||||
|
<th>Uso</th>
|
||||||
|
<th>Path pattern</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="col-bucket">documents</td>
|
||||||
|
<td>Arquivos enviados por terapeuta ou paciente (PDF, imagem, DOCX, etc.)</td>
|
||||||
|
<td class="col-file">{tenant_id}/{patient_id}/{timestamp}-{filename}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-bucket">generated-docs</td>
|
||||||
|
<td>PDFs gerados pelo sistema a partir de templates. Referenciado tanto por document_generated (snapshot) quanto por documents (listagem do paciente) via campo storage_bucket.</td>
|
||||||
|
<td class="col-file">{tenant_id}/{patient_id}/{template_nome_sanitizado}_{timestamp}.pdf</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════ 3. SERVICES ══════════════════════════════════ -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon green">3</div>
|
||||||
|
<div class="section-title">Services — Camada de dados</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-desc">src/services/ — seguem o padrao Medicos.service.js (getOwnerId + getActiveTenantId + CRUD)</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Arquivo</th>
|
||||||
|
<th>O que faz</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">Documents.service.js</td>
|
||||||
|
<td>CRUD completo de documentos: upload ao Storage + insert no banco, listagem por paciente com filtros (tipo, categoria, tags), soft delete com retencao, restauracao, download com URL assinada</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">DocumentTemplates.service.js</td>
|
||||||
|
<td>CRUD de templates: criar/editar templates (globais e por tenant), listar variaveis disponiveis, duplicar template, ativar/desativar</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">DocumentGenerate.service.js</td>
|
||||||
|
<td>Gerar PDF a partir de template: preencher variaveis com dados do paciente/sessao, renderizar HTML para PDF via pdf.service.js (jsPDF + html2canvas-pro), salvar no bucket generated-docs, registrar em document_generated E automaticamente na tabela documents (para aparecer na listagem do paciente). Nomes de arquivo sanitizados (sem acentos) para compatibilidade com Supabase Storage.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">pdf.service.js</td>
|
||||||
|
<td>Servico de geracao de PDF client-side usando jsPDF + html2canvas-pro. Substitui pdfmake que apresenta incompatibilidade com Vite (UMD vs ESM — getBlob/getBuffer travam silenciosamente). Recebe HTML completo, renderiza em canvas oculto (scale 1.5, JPEG 85%), gera PDF A4 com paginacao automatica. Retorna Blob para upload/download.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">DocumentSignatures.service.js</td>
|
||||||
|
<td>Criar solicitacao de assinatura, registrar assinatura (IP, hash, timestamp, user_agent), consultar status de cada signatario, verificar integridade via hash</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">DocumentShareLinks.service.js</td>
|
||||||
|
<td>Gerar link temporario com token, validar token no acesso, registrar uso, expirar link</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">DocumentAuditLog.service.js</td>
|
||||||
|
<td>Registrar log de acesso (visualizacao/download) e consultar historico de acessos por documento</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════ 4. COMPOSABLES ═══════════════════════════════ -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon orange">4</div>
|
||||||
|
<div class="section-title">Composables — Logica reativa</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-desc">src/features/documents/composables/</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Arquivo</th>
|
||||||
|
<th>O que faz</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">useDocuments.js</td>
|
||||||
|
<td>State reativo: lista de documentos do paciente, loading, filtros ativos (tipo, categoria, tags), operacoes CRUD, refresh automatico apos upload/delete</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">useDocumentTemplates.js</td>
|
||||||
|
<td>State reativo: lista de templates disponiveis (globais + tenant), preview com dados ficticios, variaveis extraidas do corpo HTML</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">useDocumentGenerate.js</td>
|
||||||
|
<td>Logica de geracao: carregar dados do paciente/sessao, mapear variaveis, chamar servico de geracao, retornar URL do PDF</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════ 5. PAGINAS & COMPONENTES ═════════════════════ -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon purple">5</div>
|
||||||
|
<div class="section-title">Paginas & Componentes Vue</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-desc">src/features/documents/</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Arquivo</th>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>O que faz</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">DocumentsListPage.vue</td>
|
||||||
|
<td>Pagina</td>
|
||||||
|
<td>Pagina principal — lista todos os documentos do paciente com DataTable, filtros (tipo, categoria, tags), botoes de upload, preview, download. Hero header sticky com stats rapidos.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">DocumentUploadDialog.vue</td>
|
||||||
|
<td>Dialog</td>
|
||||||
|
<td>Upload de arquivo — drag & drop ou seletor, campos: tipo do documento, categoria, descricao, tags, vinculo com sessao (opcional), visibilidade. Validacao de tamanho e tipo de arquivo.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">DocumentPreviewDialog.vue</td>
|
||||||
|
<td>Dialog</td>
|
||||||
|
<td>Preview inline — renderiza PDF/imagem no dialog. Botoes: download, compartilhar, solicitar assinatura, excluir. Exibe metadados (tipo, tags, quem enviou, data).</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">DocumentTemplatesPage.vue</td>
|
||||||
|
<td>Pagina</td>
|
||||||
|
<td>Gestao de templates — lista templates disponiveis (globais + do tenant), criar novo, editar, duplicar, ativar/desativar. Cards com preview do template.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">DocumentTemplateEditor.vue</td>
|
||||||
|
<td>Componente</td>
|
||||||
|
<td>Editor de template — edicao do corpo HTML (editor rich text), insercao de variaveis via dropdown, preview ao vivo com dados ficticios, config de cabecalho/rodape/logo.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">DocumentGenerateDialog.vue</td>
|
||||||
|
<td>Dialog</td>
|
||||||
|
<td>Gerar documento — selecionar template, campos preenchidos automaticamente com dados do paciente/sessao, edicao manual se necessario, preview final via iframe sandbox, botao "Salvar documento" (salva online, sem download automatico). Botao "So baixar" gera PDF local sem salvar no banco.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">DocumentSignatureDialog.vue</td>
|
||||||
|
<td>Dialog</td>
|
||||||
|
<td>Solicitar assinatura — adicionar signatarios (paciente, responsavel, terapeuta), definir ordem, enviar link por email/whatsapp, acompanhar status de cada signatario.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">DocumentShareDialog.vue</td>
|
||||||
|
<td>Dialog</td>
|
||||||
|
<td>Compartilhar — gerar link temporario com prazo (24h, 48h, 7d) e limite de usos, copiar link, enviar por email. Exibe links ja criados com status.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">components/DocumentCard.vue</td>
|
||||||
|
<td>Componente</td>
|
||||||
|
<td>Card reutilizavel de documento — thumbnail (icone por tipo ou preview de imagem), nome, tipo, data, tags, menu de acoes (3 dots).</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">components/DocumentTagsInput.vue</td>
|
||||||
|
<td>Componente</td>
|
||||||
|
<td>Input de tags livres — chips editaveis com autocomplete baseado em tags ja usadas pelo terapeuta. Criacao de novas tags inline.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════ 6. INTEGRACAO PRONTUARIO ═════════════════════ -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon pink">6</div>
|
||||||
|
<div class="section-title">Integracao com Prontuario (arquivo existente)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Arquivo existente</th>
|
||||||
|
<th>Alteracao</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">src/features/patients/prontuario/PatientProntuario.vue</td>
|
||||||
|
<td>Adicionar aba/secao "Documentos" que renderiza DocumentsListPage filtrada pelo patient_id atual. Botao rapido de upload direto do prontuario.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════ 7. ROTAS ═════════════════════════════════════ -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon orange">7</div>
|
||||||
|
<div class="section-title">Rotas</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-desc">Adicionadas em routes.therapist.js e routes.clinic.js</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Rota</th>
|
||||||
|
<th>Pagina</th>
|
||||||
|
<th>Descricao</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="col-route">/therapist/documents</td>
|
||||||
|
<td class="col-file">DocumentsListPage.vue</td>
|
||||||
|
<td>Lista geral de documentos (todos os pacientes do terapeuta)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-route">/therapist/documents/templates</td>
|
||||||
|
<td class="col-file">DocumentTemplatesPage.vue</td>
|
||||||
|
<td>Gestao de templates do terapeuta</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-route">/therapist/patients/:id/documents</td>
|
||||||
|
<td class="col-file">DocumentsListPage.vue</td>
|
||||||
|
<td>Documentos de um paciente especifico (via props)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-route">/clinic/documents/templates</td>
|
||||||
|
<td class="col-file">DocumentTemplatesPage.vue</td>
|
||||||
|
<td>Templates da clinica (admin configura templates compartilhados)</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════ 8. MENUS ═════════════════════════════════════ -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon green">8</div>
|
||||||
|
<div class="section-title">Menus de Navegacao</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Arquivo</th>
|
||||||
|
<th>Item adicionado</th>
|
||||||
|
<th>Onde no menu</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">therapist.menu.js</td>
|
||||||
|
<td>"Documentos" — icon: pi-file, to: /therapist/documents</td>
|
||||||
|
<td>Grupo "Pacientes", abaixo de "Tags"</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">therapist.menu.js</td>
|
||||||
|
<td>"Templates" — icon: pi-file-edit, to: /therapist/documents/templates</td>
|
||||||
|
<td>Sub-item de Documentos</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">clinic.menu.js</td>
|
||||||
|
<td>"Templates de Documentos" — icon: pi-file-edit, to: /clinic/documents/templates</td>
|
||||||
|
<td>Grupo "Configuracoes"</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════ 9. SAAS FEATURES ═════════════════════════════ -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon red">9</div>
|
||||||
|
<div class="section-title">SaaS — Feature Flags</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-desc">Inseridas em saas_features e vinculadas aos planos via plan_features</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Feature key</th>
|
||||||
|
<th>Descricao</th>
|
||||||
|
<th>Planos</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="col-key">documents.upload</td>
|
||||||
|
<td>Upload de arquivos a pacientes — funcionalidade base</td>
|
||||||
|
<td>Free + Pro</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-key">documents.templates</td>
|
||||||
|
<td>Templates de documentos (declaracao, atestado, recibo etc.)</td>
|
||||||
|
<td>Pro</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-key">documents.signatures</td>
|
||||||
|
<td>Assinatura eletronica (TCLE, consentimentos)</td>
|
||||||
|
<td>Pro</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-key">documents.share_links</td>
|
||||||
|
<td>Links temporarios para compartilhamento externo</td>
|
||||||
|
<td>Pro</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-key">documents.patient_portal</td>
|
||||||
|
<td>Paciente visualiza e envia documentos pelo portal</td>
|
||||||
|
<td>Pro</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════ 10. SEED DATA ════════════════════════════════ -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon cyan">10</div>
|
||||||
|
<div class="section-title">Seed Data — Templates Padrao</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Arquivo</th>
|
||||||
|
<th>O que insere</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="col-file">seed_015_document_templates.sql</td>
|
||||||
|
<td>
|
||||||
|
4 templates globais (is_global = true) com corpo HTML e variaveis mapeadas:
|
||||||
|
<div class="fields" style="margin-top: 8px;">
|
||||||
|
<span class="field">Declaracao de Comparecimento</span>
|
||||||
|
<span class="field">Atestado Psicologico</span>
|
||||||
|
<span class="field">Relatorio de Acompanhamento</span>
|
||||||
|
<span class="field">Recibo de Pagamento</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="note">
|
||||||
|
<strong>Variaveis dos templates:</strong> {{paciente_nome}}, {{paciente_cpf}}, {{data_sessao}}, {{hora_inicio}}, {{hora_fim}}, {{terapeuta_nome}}, {{terapeuta_crp}}, {{clinica_nome}}, {{clinica_endereco}}, {{valor}}, {{data_atual}}, entre outras. Cada template define quais variaveis utiliza no campo variaveis[].
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="note" style="border-left-color: var(--orange); margin-top: 8px;">
|
||||||
|
<strong>Decisao tecnica — Motor PDF:</strong> pdfmake foi substituido por jsPDF + html2canvas-pro. O pdfmake (UMD) trava silenciosamente com Vite (ESM) — createPdf().getBlob()/getBuffer() nunca retornam, mesmo com optimizeDeps configurado. A solucao final usa html2canvas-pro (fork com suporte a cores oklch do PrimeVue/Tailwind) para renderizar o HTML preenchido em canvas, e jsPDF para converter em PDF A4 com paginacao. Resultado: ~200-400KB por documento (JPEG 85%, scale 1.5).
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ════════════════════════════════════════ ORDEM DE EXECUCAO ════════════════════════════ -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-icon blue">!</div>
|
||||||
|
<div class="section-title">Ordem de Execucao Sugerida</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Fase</th>
|
||||||
|
<th>O que</th>
|
||||||
|
<th>Depende de</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><strong>1</strong></td>
|
||||||
|
<td>Migrations (tabelas, RLS, triggers, indexes)</td>
|
||||||
|
<td>—</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>2</strong></td>
|
||||||
|
<td>Buckets no Supabase Storage</td>
|
||||||
|
<td>Fase 1</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>3</strong></td>
|
||||||
|
<td>Services (camada de dados)</td>
|
||||||
|
<td>Fase 1 + 2</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>4</strong></td>
|
||||||
|
<td>Composables (logica reativa)</td>
|
||||||
|
<td>Fase 3</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>5</strong></td>
|
||||||
|
<td>Componentes e Paginas Vue</td>
|
||||||
|
<td>Fase 4</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>6</strong></td>
|
||||||
|
<td>Rotas, menus, feature flags</td>
|
||||||
|
<td>Fase 5</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>7</strong></td>
|
||||||
|
<td>Integracao com Prontuario</td>
|
||||||
|
<td>Fase 5</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>8</strong></td>
|
||||||
|
<td>Seed data (templates padrao)</td>
|
||||||
|
<td>Fase 1</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
372
docs/architecture/Pacientes/cadastro_pacientes_levantamento.html
Normal file
372
docs/architecture/Pacientes/cadastro_pacientes_levantamento.html
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
.page { padding: 0 0 32px; }
|
||||||
|
.section { margin-bottom: 28px; }
|
||||||
|
.section-title { font-size: 11px; font-weight: 500; letter-spacing: .08em; text-transform: uppercase; color: var(--color-text-tertiary); margin: 0 0 10px; padding-bottom: 6px; border-bottom: 0.5px solid var(--color-border-tertiary); }
|
||||||
|
.cards { display: grid; gap: 10px; }
|
||||||
|
.cards-2 { grid-template-columns: repeat(2, minmax(0,1fr)); }
|
||||||
|
.cards-3 { grid-template-columns: repeat(3, minmax(0,1fr)); }
|
||||||
|
.card { background: var(--color-background-primary); border: 0.5px solid var(--color-border-tertiary); border-radius: var(--border-radius-lg); padding: 14px 16px; }
|
||||||
|
.card-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; margin-bottom: 4px; }
|
||||||
|
.card-title { font-size: 13px; font-weight: 500; color: var(--color-text-primary); margin: 0; display: flex; align-items: center; gap: 7px; }
|
||||||
|
.card-desc { font-size: 12px; color: var(--color-text-secondary); line-height: 1.55; margin: 0; }
|
||||||
|
.card-fields { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 5px; }
|
||||||
|
.field { font-size: 11px; padding: 3px 8px; border-radius: 20px; border: 0.5px solid var(--color-border-secondary); color: var(--color-text-secondary); background: var(--color-background-secondary); font-family: var(--font-mono); }
|
||||||
|
.field-has { background: #EAF3DE; border-color: #C0DD97; color: #27500A; }
|
||||||
|
.field-miss { background: #FCEBEB; border-color: #F7C1C1; color: #791F1F; }
|
||||||
|
.badge { font-size: 11px; font-weight: 500; padding: 2px 8px; border-radius: 20px; white-space: nowrap; flex-shrink: 0; align-self: flex-start; margin-top: 1px; }
|
||||||
|
.badge-has { background: #EAF3DE; color: #27500A; }
|
||||||
|
.badge-part { background: #FAEEDA; color: #633806; }
|
||||||
|
.badge-miss { background: #FCEBEB; color: #791F1F; }
|
||||||
|
.badge-diff { background: #E6F1FB; color: #0C447C; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.field-has { background: #173404; border-color: #27500A; color: #C0DD97; }
|
||||||
|
.field-miss { background: #501313; border-color: #791F1F; color: #F7C1C1; }
|
||||||
|
.badge-has { background: #173404; color: #C0DD97; }
|
||||||
|
.badge-part { background: #412402; color: #FAC775; }
|
||||||
|
.badge-miss { background: #501313; color: #F7C1C1; }
|
||||||
|
.badge-diff { background: #042C53; color: #B5D4F4; }
|
||||||
|
}
|
||||||
|
.icon-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; display: inline-block; margin-top: 3px; }
|
||||||
|
.dot-has { background: #639922; }
|
||||||
|
.dot-part { background: #EF9F27; }
|
||||||
|
.dot-miss { background: #E24B4A; }
|
||||||
|
.dot-diff { background: #378ADD; }
|
||||||
|
.legend { display: flex; gap: 16px; margin-bottom: 20px; flex-wrap: wrap; }
|
||||||
|
.legend-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--color-text-secondary); }
|
||||||
|
.sub { font-size: 11px; color: var(--color-text-tertiary); margin: 2px 0 6px; font-family: var(--font-mono); }
|
||||||
|
.note { font-size: 12px; color: var(--color-text-secondary); background: var(--color-background-secondary); border-left: 2px solid var(--color-border-secondary); padding: 8px 12px; margin-top: 10px; line-height: 1.5; border-radius: 0; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<div class="legend">
|
||||||
|
<div class="legend-item"><span class="icon-dot dot-has"></span> você já tem</div>
|
||||||
|
<div class="legend-item"><span class="icon-dot dot-part"></span> tem parcialmente</div>
|
||||||
|
<div class="legend-item"><span class="icon-dot dot-miss"></span> faltando</div>
|
||||||
|
<div class="legend-item"><span class="icon-dot dot-diff"></span> diferencial de mercado</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">1 · Identificação & dados pessoais</div>
|
||||||
|
<div class="cards cards-2">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-has"></span> Dados básicos de identificação</div><div class="sub">núcleo do cadastro</div></div>
|
||||||
|
<span class="badge badge-has">completo</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Nome, email, telefone, data de nascimento, CPF, RG, gênero, naturalidade, estado civil, escolaridade e profissão.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-has">nome_completo</span><span class="field field-has">email_principal</span><span class="field field-has">telefone</span><span class="field field-has">data_nascimento</span><span class="field field-has">cpf</span><span class="field field-has">rg</span><span class="field field-has">genero</span><span class="field field-has">estado_civil</span><span class="field field-has">escolaridade</span><span class="field field-has">profissao</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-part"></span> Gênero & pronomes</div><div class="sub">campo genero existe, pronomes não</div></div>
|
||||||
|
<span class="badge badge-part">parcial</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Você tem o campo <span style="font-family:var(--font-mono);font-size:11px">genero</span> como texto livre. Faltam pronomes preferidos (ele/ela/eles) — padrão nos sistemas modernos de saúde mental, especialmente para público LGBTQIA+.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-has">genero</span>
|
||||||
|
<span class="field field-miss">pronomes</span>
|
||||||
|
<span class="field field-miss">nome_social</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-has"></span> Endereço completo</div><div class="sub">CEP, cidade, estado, complemento</div></div>
|
||||||
|
<span class="badge badge-has">completo</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">CEP, endereço, número, bairro, complemento, cidade, estado e país. Estrutura adequada.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-has">cep</span><span class="field field-has">endereco</span><span class="field field-has">numero</span><span class="field field-has">bairro</span><span class="field field-has">cidade</span><span class="field field-has">estado</span><span class="field field-has">pais</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-miss"></span> Dados socioeconômicos</div><div class="sub">renda e contexto social</div></div>
|
||||||
|
<span class="badge badge-miss">faltando</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Faixa de renda, religião/espiritualidade, etnia. Campos opcionais mas relevantes clinicamente e para política de precificação solidária. SimplePractice e Psicologia Viva coletam isso.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-miss">faixa_renda</span>
|
||||||
|
<span class="field field-miss">etnia</span>
|
||||||
|
<span class="field field-miss">religiao</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">2 · Contatos & rede de suporte</div>
|
||||||
|
<div class="cards cards-2">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-part"></span> Contato de emergência</div><div class="sub">só um contato, sem estrutura</div></div>
|
||||||
|
<span class="badge badge-part">parcial</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Você tem <span style="font-family:var(--font-mono);font-size:11px">nome_parente</span>, <span style="font-family:var(--font-mono);font-size:11px">grau_parentesco</span> e <span style="font-family:var(--font-mono);font-size:11px">telefone_parente</span> como campos soltos na tabela. Falta suporte a múltiplos contatos e campo de email do contato.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-has">nome_parente</span><span class="field field-has">grau_parentesco</span><span class="field field-has">telefone_parente</span>
|
||||||
|
<span class="field field-miss">email_contato</span><span class="field field-miss">multiplos_contatos</span><span class="field field-miss">contato_primario</span>
|
||||||
|
</div>
|
||||||
|
<div class="note">Ideal: tabela separada <span style="font-family:var(--font-mono)">patient_contacts</span> com N contatos por paciente.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-has"></span> Responsável legal</div><div class="sub">para menores de idade</div></div>
|
||||||
|
<span class="badge badge-has">completo</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Nome, CPF, telefone do responsável e flag de cobrança no responsável. Cobre bem o caso de pacientes menores.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-has">nome_responsavel</span><span class="field field-has">telefone_responsavel</span><span class="field field-has">cpf_responsavel</span><span class="field field-has">cobranca_no_responsavel</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-miss"></span> Outros profissionais de saúde</div><div class="sub">psiquiatra, médico, nutricionista</div></div>
|
||||||
|
<span class="badge badge-miss">faltando</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Nome e contato do psiquiatra, médico ou outros profissionais que acompanham o paciente. Essencial para coordenação de cuidados. Presente no SimplePractice e TheraNest.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-miss">nome_profissional</span><span class="field field-miss">especialidade</span><span class="field field-miss">telefone_profissional</span><span class="field field-miss">email_profissional</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-diff"></span> Preferências de comunicação</div><div class="sub">como o paciente quer ser contatado</div></div>
|
||||||
|
<span class="badge badge-diff">diferencial</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Canal preferido (WhatsApp, email, SMS), horário preferido para contato, idioma preferido. Alimenta diretamente os lembretes automáticos com as preferências do paciente.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-miss">canal_preferido</span><span class="field field-miss">horario_contato</span><span class="field field-miss">idioma</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">3 · Origem & encaminhamento</div>
|
||||||
|
<div class="cards cards-2">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-part"></span> Como chegou ao terapeuta</div><div class="sub">campos existem mas são texto livre</div></div>
|
||||||
|
<span class="badge badge-part">parcial</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Você tem <span style="font-family:var(--font-mono);font-size:11px">onde_nos_conheceu</span> e <span style="font-family:var(--font-mono);font-size:11px">encaminhado_por</span> como texto livre. Ideal ser enum + texto opcional para permitir filtros e relatórios de origem.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-has">onde_nos_conheceu</span><span class="field field-has">encaminhado_por</span>
|
||||||
|
<span class="field field-miss">origem_enum</span><span class="field field-miss">agendador_publico_ref</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-miss"></span> Motivo de inatividade ou alta</div><div class="sub">por que o paciente saiu</div></div>
|
||||||
|
<span class="badge badge-miss">faltando</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Quando paciente vai para "Alta", "Inativo" ou "Encaminhado" — qual o motivo? Alta terapêutica, abandono, encaminhamento, mudança de cidade. Essencial para relatórios e qualidade clínica.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-miss">motivo_saida</span><span class="field field-miss">data_saida</span><span class="field field-miss">encaminhado_para</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">4 · Status & ciclo de vida do paciente</div>
|
||||||
|
<div class="cards cards-2">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-has"></span> Status do paciente</div><div class="sub">Ativo, Inativo, Alta, Encaminhado, Arquivado</div></div>
|
||||||
|
<span class="badge badge-has">completo</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Enum bem definido com os 5 status mais relevantes. Constraint no banco garante integridade.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-has">Ativo</span><span class="field field-has">Inativo</span><span class="field field-has">Alta</span><span class="field field-has">Encaminhado</span><span class="field field-has">Arquivado</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-miss"></span> Histórico de mudanças de status</div><div class="sub">trilha de auditoria do ciclo de vida</div></div>
|
||||||
|
<span class="badge badge-miss">faltando</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Quando o status mudou, quem mudou e por quê. Permite ver o histórico completo: "Ativo → Inativo (01/03) → Ativo (15/04)". Exigência de auditoria clínica.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-miss">status_anterior</span><span class="field field-miss">status_novo</span><span class="field field-miss">motivo</span><span class="field field-miss">alterado_por</span><span class="field field-miss">alterado_em</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-has"></span> Escopo do paciente (clínica vs. terapeuta)</div><div class="sub">patient_scope bem modelado</div></div>
|
||||||
|
<span class="badge badge-has">completo</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Distinção entre paciente da clínica (qualquer terapeuta pode atender) e paciente particular do terapeuta. Com constraint de consistência.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-has">patient_scope</span><span class="field field-has">therapist_member_id</span><span class="field field-has">responsible_member_id</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-diff"></span> Alerta & flag de risco</div><div class="sub">sinalização visível no topo do cadastro</div></div>
|
||||||
|
<span class="badge badge-diff">diferencial</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Flag booleano de risco elevado com nota associada. Exibe alerta vermelho no topo do cadastro e do prontuário. Terapeuta sinaliza pacientes que precisam de atenção especial (ideação, crise recente).</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-miss">risco_elevado</span><span class="field field-miss">nota_risco</span><span class="field field-miss">sinalizado_em</span><span class="field field-miss">sinalizado_por</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">5 · Organização & segmentação</div>
|
||||||
|
<div class="cards cards-2">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-has"></span> Tags de paciente</div><div class="sub">patient_tags + patient_patient_tag</div></div>
|
||||||
|
<span class="badge badge-has">completo</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Tags com nome e cor, por tenant, com many-to-many. Bem estruturado.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-has">patient_tags</span><span class="field field-has">patient_patient_tag</span><span class="field field-has">cor</span><span class="field field-has">is_padrao</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-has"></span> Grupos de pacientes</div><div class="sub">patient_groups com many-to-many</div></div>
|
||||||
|
<span class="badge badge-has">completo</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Grupos com nome, cor, descrição, status ativo e flag de sistema. Relação many-to-many com <span style="font-family:var(--font-mono);font-size:11px">patient_group_patient</span>.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-has">patient_groups</span><span class="field field-has">patient_group_patient</span><span class="field field-has">is_system</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-has"></span> Cor de identificação</div><div class="sub">identification_color na agenda</div></div>
|
||||||
|
<span class="badge badge-has">completo</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Cor atribuída ao paciente para visualização rápida na agenda. Diferencial visual que poucos sistemas brasileiros têm.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-has">identification_color</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-diff"></span> Score de engajamento</div><div class="sub">calculado automaticamente</div></div>
|
||||||
|
<span class="badge badge-diff">diferencial</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Score calculado por view/função baseado em: frequência de sessões, taxa de comparecimento, dias desde última sessão, pagamentos em dia. Exibido como indicador no card do paciente. Ajuda a identificar quem precisa de atenção.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-miss">engajamento_score</span><span class="field field-miss">taxa_comparecimento</span><span class="field field-miss">dias_sem_sessao</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">6 · Financeiro vinculado ao paciente</div>
|
||||||
|
<div class="cards cards-2">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-has"></span> Descontos individuais</div><div class="sub">patient_discounts bem modelado</div></div>
|
||||||
|
<span class="badge badge-has">completo</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Desconto percentual ou fixo por paciente, com período de validade e motivo. Bem estruturado com active_from e active_to.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-has">discount_pct</span><span class="field field-has">discount_flat</span><span class="field field-has">active_from</span><span class="field field-has">active_to</span><span class="field field-has">reason</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-miss"></span> Limite de sessões por período</div><div class="sub">controle de plano ou convênio</div></div>
|
||||||
|
<span class="badge badge-miss">faltando</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Pacientes de convênio frequentemente têm limite de sessões autorizadas por mês. Campo para registrar o limite e controlar o consumo — alerta quando está próximo do teto.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-miss">limite_sessoes_mes</span><span class="field field-miss">sessoes_usadas</span><span class="field field-miss">periodo_referencia</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-miss"></span> Método de pagamento preferido</div><div class="sub">como esse paciente costuma pagar</div></div>
|
||||||
|
<span class="badge badge-miss">faltando</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">PIX, cartão, dinheiro, convênio. Aparece como sugestão padrão ao registrar cobrança. Evita perguntar toda vez como o paciente paga.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-miss">metodo_pagamento_preferido</span><span class="field field-miss">dados_pagamento_obs</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-diff"></span> LTV & métricas financeiras do paciente</div><div class="sub">calculado por view</div></div>
|
||||||
|
<span class="badge badge-diff">diferencial</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Total pago desde o início, ticket médio por sessão, total de sessões realizadas. Calculado por view em cima de financial_records — sem armazenar, sem inconsistência.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-miss">v_patient_ltv</span><span class="field field-miss">total_pago</span><span class="field field-miss">ticket_medio</span><span class="field field-miss">total_sessoes</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">7 · Observações & notas internas</div>
|
||||||
|
<div class="cards cards-2">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-part"></span> Observações gerais</div><div class="sub">dois campos de texto soltos</div></div>
|
||||||
|
<span class="badge badge-part">parcial</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Você tem <span style="font-family:var(--font-mono);font-size:11px">observacoes</span> e <span style="font-family:var(--font-mono);font-size:11px">notas_internas</span> como campos de texto livre. Funciona, mas sem distinção clara de propósito ou histórico de edições.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-has">observacoes</span><span class="field field-has">notas_internas</span>
|
||||||
|
<span class="field field-miss">historico_edicoes</span><span class="field field-miss">editado_por</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div><div class="card-title"><span class="icon-dot dot-diff"></span> Linha do tempo do paciente</div><div class="sub">feed cronológico de tudo que aconteceu</div></div>
|
||||||
|
<span class="badge badge-diff">diferencial</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-desc">Feed automático com eventos relevantes: "Primeira sessão", "Mudança de status", "Documento assinado", "Escala respondida", "Pagamento em atraso". Visível no topo do cadastro como timeline. SimplePractice tem isso.</div>
|
||||||
|
<div class="card-fields">
|
||||||
|
<span class="field field-miss">patient_timeline</span><span class="field field-miss">evento_tipo</span><span class="field field-miss">descricao</span><span class="field field-miss">ocorrido_em</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user