Agenda, Agendador, Configurações
This commit is contained in:
17
.claude/settings.local.json
Normal file
17
.claude/settings.local.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"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)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
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.*
|
||||||
414
DBS/2026-03-11/Nova-Dev-Doc/supervisor_fase1.sql
Normal file
414
DBS/2026-03-11/Nova-Dev-Doc/supervisor_fase1.sql
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- SUPERVISOR — Fase 1
|
||||||
|
-- Aplicar no Supabase SQL Editor (em ordem)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 1. tenants.kind → adiciona 'supervisor'
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
ALTER TABLE public.tenants
|
||||||
|
DROP CONSTRAINT IF EXISTS tenants_kind_check;
|
||||||
|
|
||||||
|
ALTER TABLE public.tenants
|
||||||
|
ADD CONSTRAINT tenants_kind_check
|
||||||
|
CHECK (kind = ANY (ARRAY[
|
||||||
|
'therapist',
|
||||||
|
'clinic_coworking',
|
||||||
|
'clinic_reception',
|
||||||
|
'clinic_full',
|
||||||
|
'clinic',
|
||||||
|
'saas',
|
||||||
|
'supervisor' -- ← novo
|
||||||
|
]));
|
||||||
|
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 2. plans.target → adiciona 'supervisor'
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
ALTER TABLE public.plans
|
||||||
|
DROP CONSTRAINT IF EXISTS plans_target_check;
|
||||||
|
|
||||||
|
ALTER TABLE public.plans
|
||||||
|
ADD CONSTRAINT plans_target_check
|
||||||
|
CHECK (target = ANY (ARRAY[
|
||||||
|
'patient',
|
||||||
|
'therapist',
|
||||||
|
'clinic',
|
||||||
|
'supervisor' -- ← novo
|
||||||
|
]));
|
||||||
|
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 3. plans.max_supervisees — limite de supervisionados
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
ALTER TABLE public.plans
|
||||||
|
ADD COLUMN IF NOT EXISTS max_supervisees integer DEFAULT NULL;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN public.plans.max_supervisees IS
|
||||||
|
'Limite de terapeutas que podem ser supervisionados. Apenas para planos target=supervisor. NULL = sem limite.';
|
||||||
|
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 4. Planos supervisor_free e supervisor_pro
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
INSERT INTO public.plans (key, name, description, target, is_active, max_supervisees)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
'supervisor_free',
|
||||||
|
'Supervisor Free',
|
||||||
|
'Plano gratuito de supervisão. Até 3 terapeutas supervisionados.',
|
||||||
|
'supervisor',
|
||||||
|
true,
|
||||||
|
3
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'supervisor_pro',
|
||||||
|
'Supervisor PRO',
|
||||||
|
'Plano profissional de supervisão. Até 20 terapeutas supervisionados.',
|
||||||
|
'supervisor',
|
||||||
|
true,
|
||||||
|
20
|
||||||
|
)
|
||||||
|
ON CONFLICT (key) DO UPDATE
|
||||||
|
SET
|
||||||
|
name = EXCLUDED.name,
|
||||||
|
description = EXCLUDED.description,
|
||||||
|
target = EXCLUDED.target,
|
||||||
|
is_active = EXCLUDED.is_active,
|
||||||
|
max_supervisees = EXCLUDED.max_supervisees;
|
||||||
|
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 5. Features de supervisor
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
INSERT INTO public.features (key, name, descricao)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
'supervisor.access',
|
||||||
|
'Acesso à Supervisão',
|
||||||
|
'Acesso básico ao espaço de supervisão (sala, lista de supervisionados).'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'supervisor.invite',
|
||||||
|
'Convidar Supervisionados',
|
||||||
|
'Permite convidar terapeutas para participar da sala de supervisão.'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'supervisor.sessions',
|
||||||
|
'Sessões de Supervisão',
|
||||||
|
'Agendamento e registro de sessões de supervisão.'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'supervisor.reports',
|
||||||
|
'Relatórios de Supervisão',
|
||||||
|
'Relatórios avançados de progresso e evolução dos supervisionados.'
|
||||||
|
)
|
||||||
|
ON CONFLICT (key) DO UPDATE
|
||||||
|
SET
|
||||||
|
name = EXCLUDED.name,
|
||||||
|
descricao = EXCLUDED.descricao;
|
||||||
|
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 6. plan_features — vincula features aos planos supervisor
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
v_free_id uuid;
|
||||||
|
v_pro_id uuid;
|
||||||
|
v_f_access uuid;
|
||||||
|
v_f_invite uuid;
|
||||||
|
v_f_sessions uuid;
|
||||||
|
v_f_reports uuid;
|
||||||
|
BEGIN
|
||||||
|
SELECT id INTO v_free_id FROM public.plans WHERE key = 'supervisor_free';
|
||||||
|
SELECT id INTO v_pro_id FROM public.plans WHERE key = 'supervisor_pro';
|
||||||
|
|
||||||
|
SELECT id INTO v_f_access FROM public.features WHERE key = 'supervisor.access';
|
||||||
|
SELECT id INTO v_f_invite FROM public.features WHERE key = 'supervisor.invite';
|
||||||
|
SELECT id INTO v_f_sessions FROM public.features WHERE key = 'supervisor.sessions';
|
||||||
|
SELECT id INTO v_f_reports FROM public.features WHERE key = 'supervisor.reports';
|
||||||
|
|
||||||
|
-- supervisor_free: access + invite (limitado por max_supervisees=3)
|
||||||
|
INSERT INTO public.plan_features (plan_id, feature_id)
|
||||||
|
VALUES
|
||||||
|
(v_free_id, v_f_access),
|
||||||
|
(v_free_id, v_f_invite)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- supervisor_pro: tudo
|
||||||
|
INSERT INTO public.plan_features (plan_id, feature_id)
|
||||||
|
VALUES
|
||||||
|
(v_pro_id, v_f_access),
|
||||||
|
(v_pro_id, v_f_invite),
|
||||||
|
(v_pro_id, v_f_sessions),
|
||||||
|
(v_pro_id, v_f_reports)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 7. activate_subscription_from_intent — suporte a supervisor
|
||||||
|
-- Supervisor = pessoal (user_id), sem tenant_id (igual therapist)
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
CREATE OR REPLACE FUNCTION public.activate_subscription_from_intent(p_intent_id uuid)
|
||||||
|
RETURNS public.subscriptions
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
declare
|
||||||
|
v_intent record;
|
||||||
|
v_sub public.subscriptions;
|
||||||
|
v_days int;
|
||||||
|
v_user_id uuid;
|
||||||
|
v_plan_id uuid;
|
||||||
|
v_target text;
|
||||||
|
begin
|
||||||
|
-- lê pela VIEW unificada
|
||||||
|
select * into v_intent
|
||||||
|
from public.subscription_intents
|
||||||
|
where id = p_intent_id;
|
||||||
|
|
||||||
|
if not found then
|
||||||
|
raise exception 'Intent não encontrado: %', p_intent_id;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if v_intent.status <> 'paid' then
|
||||||
|
raise exception 'Intent precisa estar paid para ativar assinatura';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- resolve target e plan_id via plans.key
|
||||||
|
select p.id, p.target
|
||||||
|
into v_plan_id, v_target
|
||||||
|
from public.plans p
|
||||||
|
where p.key = v_intent.plan_key
|
||||||
|
limit 1;
|
||||||
|
|
||||||
|
if v_plan_id is null then
|
||||||
|
raise exception 'Plano não encontrado em plans.key = %', v_intent.plan_key;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
v_target := lower(coalesce(v_target, ''));
|
||||||
|
|
||||||
|
-- ✅ supervisor adicionado
|
||||||
|
if v_target not in ('clinic', 'therapist', 'supervisor') then
|
||||||
|
raise exception 'Target inválido em plans.target: %', v_target;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- regra por target
|
||||||
|
if v_target = 'clinic' then
|
||||||
|
if v_intent.tenant_id is null then
|
||||||
|
raise exception 'Intent sem tenant_id';
|
||||||
|
end if;
|
||||||
|
else
|
||||||
|
-- therapist ou supervisor: vinculado ao user
|
||||||
|
v_user_id := v_intent.user_id;
|
||||||
|
if v_user_id is null then
|
||||||
|
v_user_id := v_intent.created_by_user_id;
|
||||||
|
end if;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if v_target in ('therapist', 'supervisor') and v_user_id is null then
|
||||||
|
raise exception 'Não foi possível determinar user_id para assinatura %.', v_target;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- cancela assinatura ativa anterior
|
||||||
|
if v_target = 'clinic' then
|
||||||
|
update public.subscriptions
|
||||||
|
set status = 'cancelled',
|
||||||
|
cancelled_at = now()
|
||||||
|
where tenant_id = v_intent.tenant_id
|
||||||
|
and plan_id = v_plan_id
|
||||||
|
and status = 'active';
|
||||||
|
else
|
||||||
|
-- therapist ou supervisor
|
||||||
|
update public.subscriptions
|
||||||
|
set status = 'cancelled',
|
||||||
|
cancelled_at = now()
|
||||||
|
where user_id = v_user_id
|
||||||
|
and plan_id = v_plan_id
|
||||||
|
and status = 'active'
|
||||||
|
and tenant_id is null;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- duração do plano (30 dias para mensal)
|
||||||
|
v_days := case
|
||||||
|
when lower(coalesce(v_intent.interval, 'month')) = 'year' then 365
|
||||||
|
else 30
|
||||||
|
end;
|
||||||
|
|
||||||
|
-- cria nova assinatura
|
||||||
|
insert into public.subscriptions (
|
||||||
|
user_id,
|
||||||
|
plan_id,
|
||||||
|
status,
|
||||||
|
started_at,
|
||||||
|
expires_at,
|
||||||
|
cancelled_at,
|
||||||
|
activated_at,
|
||||||
|
tenant_id,
|
||||||
|
plan_key,
|
||||||
|
interval,
|
||||||
|
source,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
values (
|
||||||
|
case when v_target = 'clinic' then null else v_user_id end,
|
||||||
|
v_plan_id,
|
||||||
|
'active',
|
||||||
|
now(),
|
||||||
|
now() + make_interval(days => v_days),
|
||||||
|
null,
|
||||||
|
now(),
|
||||||
|
case when v_target = 'clinic' then v_intent.tenant_id else null end,
|
||||||
|
v_intent.plan_key,
|
||||||
|
v_intent.interval,
|
||||||
|
'manual',
|
||||||
|
now(),
|
||||||
|
now()
|
||||||
|
)
|
||||||
|
returning * into v_sub;
|
||||||
|
|
||||||
|
-- grava vínculo intent → subscription
|
||||||
|
if v_target = 'clinic' then
|
||||||
|
update public.subscription_intents_tenant
|
||||||
|
set subscription_id = v_sub.id
|
||||||
|
where id = p_intent_id;
|
||||||
|
else
|
||||||
|
update public.subscription_intents_personal
|
||||||
|
set subscription_id = v_sub.id
|
||||||
|
where id = p_intent_id;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
return v_sub;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 8. subscriptions_validate_scope — suporte a supervisor
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
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 IN ('therapist', 'supervisor') THEN
|
||||||
|
-- supervisor é pessoal como therapist
|
||||||
|
IF NEW.tenant_id IS NOT NULL THEN
|
||||||
|
RAISE EXCEPTION 'Assinatura % não deve ter tenant_id.', v_target;
|
||||||
|
END IF;
|
||||||
|
IF NEW.user_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'Assinatura % exige user_id.', v_target;
|
||||||
|
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;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- 9. subscription_intents_view_insert — suporte a supervisor
|
||||||
|
-- supervisor é roteado como therapist (tabela personal)
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
CREATE OR REPLACE FUNCTION public.subscription_intents_view_insert()
|
||||||
|
RETURNS trigger
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
declare
|
||||||
|
v_target text;
|
||||||
|
v_plan_id uuid;
|
||||||
|
begin
|
||||||
|
select p.id, p.target into v_plan_id, v_target
|
||||||
|
from public.plans p
|
||||||
|
where p.key = new.plan_key;
|
||||||
|
|
||||||
|
if v_plan_id is null then
|
||||||
|
raise exception 'Plano inválido: plan_key=%', new.plan_key;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if lower(v_target) = 'clinic' then
|
||||||
|
if new.tenant_id is null then
|
||||||
|
raise exception 'Intenção clinic exige tenant_id.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
insert into public.subscription_intents_tenant (
|
||||||
|
id, tenant_id, created_by_user_id, email,
|
||||||
|
plan_id, plan_key, interval, amount_cents, currency,
|
||||||
|
status, source, notes, created_at, paid_at
|
||||||
|
) values (
|
||||||
|
coalesce(new.id, gen_random_uuid()),
|
||||||
|
new.tenant_id, new.created_by_user_id, new.email,
|
||||||
|
v_plan_id, new.plan_key, coalesce(new.interval,'month'),
|
||||||
|
new.amount_cents, coalesce(new.currency,'BRL'),
|
||||||
|
coalesce(new.status,'pending'), coalesce(new.source,'manual'),
|
||||||
|
new.notes, coalesce(new.created_at, now()), new.paid_at
|
||||||
|
);
|
||||||
|
|
||||||
|
new.plan_target := 'clinic';
|
||||||
|
return new;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- therapist ou supervisor → tabela personal
|
||||||
|
if lower(v_target) in ('therapist', 'supervisor') then
|
||||||
|
insert into public.subscription_intents_personal (
|
||||||
|
id, user_id, created_by_user_id, email,
|
||||||
|
plan_id, plan_key, interval, amount_cents, currency,
|
||||||
|
status, source, notes, created_at, paid_at
|
||||||
|
) values (
|
||||||
|
coalesce(new.id, gen_random_uuid()),
|
||||||
|
new.user_id, new.created_by_user_id, new.email,
|
||||||
|
v_plan_id, new.plan_key, coalesce(new.interval,'month'),
|
||||||
|
new.amount_cents, coalesce(new.currency,'BRL'),
|
||||||
|
coalesce(new.status,'pending'), coalesce(new.source,'manual'),
|
||||||
|
new.notes, coalesce(new.created_at, now()), new.paid_at
|
||||||
|
);
|
||||||
|
|
||||||
|
new.plan_target := lower(v_target); -- 'therapist' ou 'supervisor'
|
||||||
|
return new;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
raise exception 'Target de plano não suportado: %', v_target;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- FIM — verificação rápida
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
SELECT key, name, target, max_supervisees
|
||||||
|
FROM public.plans
|
||||||
|
WHERE target = 'supervisor'
|
||||||
|
ORDER BY key;
|
||||||
220
DBS/2026-03-11/Novo-DB/fix_missing_subscriptions.sql
Normal file
220
DBS/2026-03-11/Novo-DB/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;
|
||||||
50
DBS/2026-03-11/Novo-DB/fix_subscriptions_validate_scope.sql
Normal file
50
DBS/2026-03-11/Novo-DB/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;
|
||||||
296
DBS/2026-03-11/Novo-DB/migration_001.sql
Normal file
296
DBS/2026-03-11/Novo-DB/migration_001.sql
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- SEED 001 — Usuários fictícios para teste
|
||||||
|
-- =============================================================================
|
||||||
|
-- Execute APÓS migration_001.sql
|
||||||
|
--
|
||||||
|
-- Usuários criados:
|
||||||
|
-- paciente@agenciapsi.com.br senha: Teste@123 → patient
|
||||||
|
-- terapeuta@agenciapsi.com.br senha: Teste@123 → therapist
|
||||||
|
-- clinica1@agenciapsi.com.br senha: Teste@123 → clinic_coworking
|
||||||
|
-- clinica2@agenciapsi.com.br senha: Teste@123 → clinic_reception
|
||||||
|
-- clinica3@agenciapsi.com.br senha: Teste@123 → clinic_full
|
||||||
|
-- saas@agenciapsi.com.br senha: Teste@123 → saas_admin
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Limpeza de seeds anteriores
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
ALTER TABLE public.patient_groups DISABLE TRIGGER ALL;
|
||||||
|
|
||||||
|
DELETE FROM public.tenant_members
|
||||||
|
WHERE user_id IN (
|
||||||
|
SELECT id 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'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
DELETE FROM public.tenants WHERE id IN (
|
||||||
|
'bbbbbbbb-0002-0002-0002-000000000002',
|
||||||
|
'bbbbbbbb-0003-0003-0003-000000000003',
|
||||||
|
'bbbbbbbb-0004-0004-0004-000000000004',
|
||||||
|
'bbbbbbbb-0005-0005-0005-000000000005'
|
||||||
|
);
|
||||||
|
|
||||||
|
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'
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE public.patient_groups ENABLE TRIGGER ALL;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. Usuários no auth.users
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO auth.users (
|
||||||
|
id, email, encrypted_password, email_confirmed_at,
|
||||||
|
created_at, updated_at, raw_user_meta_data, role, aud
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
'aaaaaaaa-0001-0001-0001-000000000001',
|
||||||
|
'paciente@agenciapsi.com.br',
|
||||||
|
crypt('Teste@123', gen_salt('bf')),
|
||||||
|
now(), now(), now(),
|
||||||
|
'{"name": "Ana Paciente"}'::jsonb,
|
||||||
|
'authenticated', 'authenticated'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'aaaaaaaa-0002-0002-0002-000000000002',
|
||||||
|
'terapeuta@agenciapsi.com.br',
|
||||||
|
crypt('Teste@123', gen_salt('bf')),
|
||||||
|
now(), now(), now(),
|
||||||
|
'{"name": "Bruno Terapeuta"}'::jsonb,
|
||||||
|
'authenticated', 'authenticated'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'aaaaaaaa-0003-0003-0003-000000000003',
|
||||||
|
'clinica1@agenciapsi.com.br',
|
||||||
|
crypt('Teste@123', gen_salt('bf')),
|
||||||
|
now(), now(), now(),
|
||||||
|
'{"name": "Clinica Espaco Psi"}'::jsonb,
|
||||||
|
'authenticated', 'authenticated'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'aaaaaaaa-0004-0004-0004-000000000004',
|
||||||
|
'clinica2@agenciapsi.com.br',
|
||||||
|
crypt('Teste@123', gen_salt('bf')),
|
||||||
|
now(), now(), now(),
|
||||||
|
'{"name": "Clinica Mente Sa"}'::jsonb,
|
||||||
|
'authenticated', 'authenticated'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'aaaaaaaa-0005-0005-0005-000000000005',
|
||||||
|
'clinica3@agenciapsi.com.br',
|
||||||
|
crypt('Teste@123', gen_salt('bf')),
|
||||||
|
now(), now(), now(),
|
||||||
|
'{"name": "Clinica Bem Estar"}'::jsonb,
|
||||||
|
'authenticated', 'authenticated'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'aaaaaaaa-0006-0006-0006-000000000006',
|
||||||
|
'saas@agenciapsi.com.br',
|
||||||
|
crypt('Teste@123', gen_salt('bf')),
|
||||||
|
now(), now(), now(),
|
||||||
|
'{"name": "Admin Plataforma"}'::jsonb,
|
||||||
|
'authenticated', 'authenticated'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 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', 'portal_user', 'therapist', 'Bruno Terapeuta'),
|
||||||
|
('aaaaaaaa-0003-0003-0003-000000000003', 'portal_user', 'clinic', 'Clinica Espaco Psi'),
|
||||||
|
('aaaaaaaa-0004-0004-0004-000000000004', 'portal_user', 'clinic', 'Clinica Mente Sa'),
|
||||||
|
('aaaaaaaa-0005-0005-0005-000000000005', 'portal_user', 'clinic', 'Clinica 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
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
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 Clinica 1 — Coworking
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO public.tenants (id, name, kind, created_at)
|
||||||
|
VALUES ('bbbbbbbb-0003-0003-0003-000000000003', 'Clinica Espaco 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 Clinica 2 — Recepcao
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO public.tenants (id, name, kind, created_at)
|
||||||
|
VALUES ('bbbbbbbb-0004-0004-0004-000000000004', 'Clinica Mente Sa', '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 Clinica 3 — Full
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO public.tenants (id, name, kind, created_at)
|
||||||
|
VALUES ('bbbbbbbb-0005-0005-0005-000000000005', 'Clinica 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
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- 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 '30 days', '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 '30 days', 'seed', now(), now()
|
||||||
|
FROM public.plans p WHERE p.key = 'therapist_free';
|
||||||
|
|
||||||
|
-- Clinica 1 → 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';
|
||||||
|
|
||||||
|
-- Clinica 2 → 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';
|
||||||
|
|
||||||
|
-- Clinica 3 → 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';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 9. Vincula terapeuta à Clinica 3 (exemplo de associacao)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Confirmacao
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
RAISE NOTICE '✅ Seed aplicado com sucesso.';
|
||||||
|
RAISE NOTICE ' paciente@agenciapsi.com.br → patient';
|
||||||
|
RAISE NOTICE ' terapeuta@agenciapsi.com.br → therapist';
|
||||||
|
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: Teste@123';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
13
DBS/2026-03-11/Novo-DB/migration_002_layout_variant.sql
Normal file
13
DBS/2026-03-11/Novo-DB/migration_002_layout_variant.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- Migration 002 — Adiciona layout_variant em user_settings
|
||||||
|
-- =============================================================================
|
||||||
|
-- Execute no SQL Editor do Supabase (ou via Docker psql).
|
||||||
|
-- Tolerante: usa IF NOT EXISTS / DEFAULT para não quebrar dados existentes.
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
ALTER TABLE public.user_settings
|
||||||
|
ADD COLUMN IF NOT EXISTS layout_variant TEXT NOT NULL DEFAULT 'classic';
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
RAISE NOTICE '✅ Coluna layout_variant adicionada a user_settings.';
|
||||||
|
-- =============================================================================
|
||||||
334
DBS/2026-03-11/Novo-DB/seed_001.sql
Normal file
334
DBS/2026-03-11/Novo-DB/seed_001.sql
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- 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;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Helper: cria usuário no auth.users + profile
|
||||||
|
-- (Supabase não expõe auth.users diretamente, mas em SQL Editor
|
||||||
|
-- com acesso de service_role podemos inserir diretamente)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO auth.users (
|
||||||
|
instance_id,
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
encrypted_password,
|
||||||
|
email_confirmed_at,
|
||||||
|
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(), 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(), 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(), 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(), 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(), 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(), 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 (o trigger handle_new_user não dispara em inserts
|
||||||
|
-- diretos no auth.users via SQL, então criamos manualmente)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
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
DBS/2026-03-11/Novo-DB/seed_002.sql
Normal file
199
DBS/2026-03-11/Novo-DB/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
DBS/2026-03-11/Novo-DB/seed_003.sql
Normal file
283
DBS/2026-03-11/Novo-DB/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;
|
||||||
36
DBS/2026-03-11/migrations/agendador_check_email.sql
Normal file
36
DBS/2026-03-11/migrations/agendador_check_email.sql
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- agendador_check_email
|
||||||
|
-- Verifica se um e-mail já possui solicitação anterior para este agendador
|
||||||
|
-- SECURITY DEFINER → anon pode chamar sem burlar RLS diretamente
|
||||||
|
-- Execute no Supabase SQL Editor
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.agendador_check_email(
|
||||||
|
p_slug text,
|
||||||
|
p_email text
|
||||||
|
)
|
||||||
|
RETURNS boolean
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_owner_id uuid;
|
||||||
|
BEGIN
|
||||||
|
SELECT c.owner_id INTO v_owner_id
|
||||||
|
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 false; END IF;
|
||||||
|
|
||||||
|
RETURN EXISTS (
|
||||||
|
SELECT 1 FROM public.agendador_solicitacoes s
|
||||||
|
WHERE s.owner_id = v_owner_id
|
||||||
|
AND lower(s.paciente_email) = lower(trim(p_email))
|
||||||
|
LIMIT 1
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION public.agendador_check_email(text, text) TO anon, authenticated;
|
||||||
62
DBS/2026-03-11/migrations/agendador_features.sql
Normal file
62
DBS/2026-03-11/migrations/agendador_features.sql
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Feature keys do Agendador Online
|
||||||
|
-- Execute no Supabase SQL Editor
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- ── 1. Inserir as features ──────────────────────────────────────────────────
|
||||||
|
INSERT INTO public.features (key, name, descricao)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
'agendador.online',
|
||||||
|
'Agendador Online',
|
||||||
|
'Permite que pacientes solicitem agendamentos via link público. Inclui aprovação manual ou automática, controle de horários e notificações.'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'agendador.link_personalizado',
|
||||||
|
'Link Personalizado do Agendador',
|
||||||
|
'Permite que o profissional escolha um slug de URL próprio para o agendador (ex: /agendar/dra-ana-silva) em vez de um link gerado automaticamente.'
|
||||||
|
)
|
||||||
|
ON CONFLICT (key) DO UPDATE
|
||||||
|
SET name = EXCLUDED.name,
|
||||||
|
descricao = EXCLUDED.descricao;
|
||||||
|
|
||||||
|
-- ── 2. Vincular aos planos ──────────────────────────────────────────────────
|
||||||
|
-- ATENÇÃO: ajuste os filtros de plan key/name conforme seus planos reais.
|
||||||
|
-- Exemplo: agendador.online disponível para planos PRO e acima.
|
||||||
|
-- agendador.link_personalizado apenas para planos Elite/Superior.
|
||||||
|
|
||||||
|
-- agendador.online → todos os planos com target 'therapist' ou 'clinic'
|
||||||
|
-- (Adapte o WHERE conforme necessário)
|
||||||
|
INSERT INTO public.plan_features (plan_id, feature_id, enabled)
|
||||||
|
SELECT
|
||||||
|
p.id,
|
||||||
|
f.id,
|
||||||
|
true
|
||||||
|
FROM public.plans p
|
||||||
|
CROSS JOIN public.features f
|
||||||
|
WHERE f.key = 'agendador.online'
|
||||||
|
AND p.is_active = true
|
||||||
|
-- Comente a linha abaixo para liberar para TODOS os planos:
|
||||||
|
-- AND p.key IN ('pro', 'elite', 'clinic_pro', 'clinic_elite')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- agendador.link_personalizado → apenas planos superiores
|
||||||
|
-- Deixe comentado e adicione manualmente quando definir os planos:
|
||||||
|
-- INSERT INTO public.plan_features (plan_id, feature_id, enabled)
|
||||||
|
-- SELECT p.id, f.id, true
|
||||||
|
-- FROM public.plans p
|
||||||
|
-- CROSS JOIN public.features f
|
||||||
|
-- WHERE f.key = 'agendador.link_personalizado'
|
||||||
|
-- AND p.key IN ('elite', 'clinic_elite', 'pro_plus')
|
||||||
|
-- ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- ── 3. Verificação ─────────────────────────────────────────────────────────
|
||||||
|
SELECT
|
||||||
|
f.key,
|
||||||
|
f.name,
|
||||||
|
COUNT(pf.plan_id) AS planos_vinculados
|
||||||
|
FROM public.features f
|
||||||
|
LEFT JOIN public.plan_features pf ON pf.feature_id = f.id AND pf.enabled = true
|
||||||
|
WHERE f.key IN ('agendador.online', 'agendador.link_personalizado')
|
||||||
|
GROUP BY f.key, f.name
|
||||||
|
ORDER BY f.key;
|
||||||
221
DBS/2026-03-11/migrations/agendador_fix_slots.sql
Normal file
221
DBS/2026-03-11/migrations/agendador_fix_slots.sql
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- FIX: agendador_slots_disponiveis + agendador_dias_disponiveis
|
||||||
|
-- Usa agenda_online_slots como fonte de slots
|
||||||
|
-- Cruzamento com: agenda_eventos, recurrence_rules/exceptions, agendador_solicitacoes
|
||||||
|
-- Execute no Supabase SQL Editor
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.agendador_slots_disponiveis(
|
||||||
|
p_slug text,
|
||||||
|
p_data date
|
||||||
|
)
|
||||||
|
RETURNS TABLE (hora time, disponivel boolean)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = 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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
-- ── 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) ───────────────────────────────
|
||||||
|
-- Loop explícito para evitar erros de tipo no cálculo do ciclo semanal
|
||||||
|
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
|
||||||
|
-- Calcula a primeira ocorrência do dia-da-semana a partir do start_date
|
||||||
|
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;
|
||||||
|
|
||||||
|
-- Ocorrência válida: diff >= 0 e divisível pelo ciclo semanal
|
||||||
|
IF v_day_diff >= 0 AND v_day_diff % (7 * v_rule.interval) = 0 THEN
|
||||||
|
|
||||||
|
-- Verifica se há exceção para esta data
|
||||||
|
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;
|
||||||
|
|
||||||
|
-- Sem exceção, ou exceção que não cancela → bloqueia o slot
|
||||||
|
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; -- já basta uma regra que conflite
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ── Recorrências remarcadas para este dia (reschedule → new_date = p_data) ─
|
||||||
|
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;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION public.agendador_slots_disponiveis(text, date) TO anon, authenticated;
|
||||||
|
|
||||||
|
|
||||||
|
-- ── agendador_dias_disponiveis ───────────────────────────────────────────────
|
||||||
|
CREATE OR REPLACE FUNCTION public.agendador_dias_disponiveis(
|
||||||
|
p_slug text,
|
||||||
|
p_ano int,
|
||||||
|
p_mes int
|
||||||
|
)
|
||||||
|
RETURNS TABLE (data date, tem_slots boolean)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = 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;
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION public.agendador_dias_disponiveis(text, int, int) TO anon, authenticated;
|
||||||
170
DBS/2026-03-11/migrations/agendador_online.sql
Normal file
170
DBS/2026-03-11/migrations/agendador_online.sql
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Agendador Online — tabelas de configuração e solicitações
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- ── 1. agendador_configuracoes ──────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS "public"."agendador_configuracoes" (
|
||||||
|
"owner_id" "uuid" NOT NULL,
|
||||||
|
"tenant_id" "uuid",
|
||||||
|
|
||||||
|
-- PRO / Ativação
|
||||||
|
"ativo" boolean DEFAULT false NOT NULL,
|
||||||
|
"link_slug" "text",
|
||||||
|
|
||||||
|
-- Identidade Visual
|
||||||
|
"imagem_fundo_url" "text",
|
||||||
|
"imagem_header_url" "text",
|
||||||
|
"logomarca_url" "text",
|
||||||
|
"cor_primaria" "text" DEFAULT '#4b6bff',
|
||||||
|
|
||||||
|
-- Perfil Público
|
||||||
|
"nome_exibicao" "text",
|
||||||
|
"endereco" "text",
|
||||||
|
"botao_como_chegar_ativo" boolean DEFAULT true NOT NULL,
|
||||||
|
"maps_url" "text",
|
||||||
|
|
||||||
|
-- Fluxo de Agendamento
|
||||||
|
"modo_aprovacao" "text" DEFAULT 'aprovacao' NOT NULL,
|
||||||
|
"modalidade" "text" DEFAULT 'presencial' 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
|
||||||
|
"pagamento_obrigatorio" boolean DEFAULT false NOT NULL,
|
||||||
|
"pix_chave" "text",
|
||||||
|
"pix_countdown_minutos" integer DEFAULT 20 NOT NULL,
|
||||||
|
|
||||||
|
-- Triagem & Conformidade
|
||||||
|
"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,
|
||||||
|
|
||||||
|
-- Textos
|
||||||
|
"mensagem_boas_vindas" "text",
|
||||||
|
"texto_como_se_preparar" "text",
|
||||||
|
"texto_termos_lgpd" "text",
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "agendador_configuracoes_pkey" PRIMARY KEY ("owner_id"),
|
||||||
|
CONSTRAINT "agendador_configuracoes_owner_fk"
|
||||||
|
FOREIGN KEY ("owner_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "agendador_configuracoes_tenant_fk"
|
||||||
|
FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "agendador_configuracoes_modo_check"
|
||||||
|
CHECK ("modo_aprovacao" = ANY (ARRAY['automatico','aprovacao'])),
|
||||||
|
CONSTRAINT "agendador_configuracoes_modalidade_check"
|
||||||
|
CHECK ("modalidade" = ANY (ARRAY['presencial','online','ambos'])),
|
||||||
|
CONSTRAINT "agendador_configuracoes_duracao_check"
|
||||||
|
CHECK ("duracao_sessao_min" >= 10 AND "duracao_sessao_min" <= 240),
|
||||||
|
CONSTRAINT "agendador_configuracoes_antecedencia_check"
|
||||||
|
CHECK ("antecedencia_minima_horas" >= 0 AND "antecedencia_minima_horas" <= 720),
|
||||||
|
CONSTRAINT "agendador_configuracoes_reserva_check"
|
||||||
|
CHECK ("reserva_horas" >= 1 AND "reserva_horas" <= 48),
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE "public"."agendador_configuracoes" ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "agendador_cfg_select" ON "public"."agendador_configuracoes";
|
||||||
|
CREATE POLICY "agendador_cfg_select" ON "public"."agendador_configuracoes"
|
||||||
|
FOR SELECT USING (auth.uid() = owner_id);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "agendador_cfg_write" ON "public"."agendador_configuracoes";
|
||||||
|
CREATE POLICY "agendador_cfg_write" ON "public"."agendador_configuracoes"
|
||||||
|
USING (auth.uid() = owner_id)
|
||||||
|
WITH CHECK (auth.uid() = owner_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "agendador_cfg_tenant_idx"
|
||||||
|
ON "public"."agendador_configuracoes" ("tenant_id");
|
||||||
|
|
||||||
|
-- ── 2. agendador_solicitacoes ───────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS "public"."agendador_solicitacoes" (
|
||||||
|
"id" "uuid" DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"owner_id" "uuid" NOT NULL,
|
||||||
|
"tenant_id" "uuid",
|
||||||
|
|
||||||
|
-- Dados do paciente
|
||||||
|
"paciente_nome" "text" NOT NULL,
|
||||||
|
"paciente_sobrenome" "text",
|
||||||
|
"paciente_email" "text" NOT NULL,
|
||||||
|
"paciente_celular" "text",
|
||||||
|
"paciente_cpf" "text",
|
||||||
|
|
||||||
|
-- Agendamento solicitado
|
||||||
|
"tipo" "text" NOT NULL,
|
||||||
|
"modalidade" "text" NOT NULL,
|
||||||
|
"data_solicitada" date NOT NULL,
|
||||||
|
"hora_solicitada" time NOT NULL,
|
||||||
|
|
||||||
|
-- Reserva temporária
|
||||||
|
"reservado_ate" timestamp with time zone,
|
||||||
|
|
||||||
|
-- Triagem
|
||||||
|
"motivo" "text",
|
||||||
|
"como_conheceu" "text",
|
||||||
|
|
||||||
|
-- Pagamento
|
||||||
|
"pix_status" "text" DEFAULT 'pendente',
|
||||||
|
"pix_pago_em" timestamp with time zone,
|
||||||
|
|
||||||
|
-- Status geral
|
||||||
|
"status" "text" DEFAULT 'pendente' NOT NULL,
|
||||||
|
"recusado_motivo" "text",
|
||||||
|
|
||||||
|
-- Autorização
|
||||||
|
"autorizado_em" timestamp with time zone,
|
||||||
|
"autorizado_por" "uuid",
|
||||||
|
|
||||||
|
-- Vínculos internos
|
||||||
|
"user_id" "uuid",
|
||||||
|
"patient_id" "uuid",
|
||||||
|
"evento_id" "uuid",
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "agendador_solicitacoes_pkey" PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "agendador_sol_owner_fk"
|
||||||
|
FOREIGN KEY ("owner_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "agendador_sol_tenant_fk"
|
||||||
|
FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "agendador_sol_status_check"
|
||||||
|
CHECK ("status" = ANY (ARRAY['pendente','autorizado','recusado','expirado'])),
|
||||||
|
CONSTRAINT "agendador_sol_tipo_check"
|
||||||
|
CHECK ("tipo" = ANY (ARRAY['primeira','retorno','reagendar'])),
|
||||||
|
CONSTRAINT "agendador_sol_modalidade_check"
|
||||||
|
CHECK ("modalidade" = ANY (ARRAY['presencial','online'])),
|
||||||
|
CONSTRAINT "agendador_sol_pix_check"
|
||||||
|
CHECK ("pix_status" IS NULL OR "pix_status" = ANY (ARRAY['pendente','pago','expirado']))
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE "public"."agendador_solicitacoes" ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "agendador_sol_owner_select" ON "public"."agendador_solicitacoes";
|
||||||
|
CREATE POLICY "agendador_sol_owner_select" ON "public"."agendador_solicitacoes"
|
||||||
|
FOR SELECT USING (auth.uid() = owner_id);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "agendador_sol_owner_write" ON "public"."agendador_solicitacoes";
|
||||||
|
CREATE POLICY "agendador_sol_owner_write" ON "public"."agendador_solicitacoes"
|
||||||
|
USING (auth.uid() = owner_id)
|
||||||
|
WITH CHECK (auth.uid() = owner_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "agendador_sol_owner_idx"
|
||||||
|
ON "public"."agendador_solicitacoes" ("owner_id", "status");
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "agendador_sol_tenant_idx"
|
||||||
|
ON "public"."agendador_solicitacoes" ("tenant_id");
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "agendador_sol_data_idx"
|
||||||
|
ON "public"."agendador_solicitacoes" ("data_solicitada", "hora_solicitada");
|
||||||
219
DBS/2026-03-11/migrations/agendador_publico.sql
Normal file
219
DBS/2026-03-11/migrations/agendador_publico.sql
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Agendador Online — acesso público (anon) + função de slots disponíveis
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- ── 1. Geração automática de slug ──────────────────────────────────────────
|
||||||
|
-- Cria slug único de 8 chars quando o profissional ativa sem link_personalizado
|
||||||
|
CREATE OR REPLACE 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;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS agendador_slug_trigger ON public.agendador_configuracoes;
|
||||||
|
CREATE TRIGGER agendador_slug_trigger
|
||||||
|
BEFORE INSERT OR UPDATE ON public.agendador_configuracoes
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION public.agendador_gerar_slug();
|
||||||
|
|
||||||
|
-- ── 2. Políticas públicas (anon) ────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Leitura pública da config pelo slug (só ativo)
|
||||||
|
DROP POLICY IF EXISTS "agendador_cfg_public_read" ON public.agendador_configuracoes;
|
||||||
|
CREATE POLICY "agendador_cfg_public_read" ON public.agendador_configuracoes
|
||||||
|
FOR SELECT TO anon
|
||||||
|
USING (ativo = true AND link_slug IS NOT NULL);
|
||||||
|
|
||||||
|
-- Inserção pública de solicitações (qualquer pessoa pode solicitar)
|
||||||
|
DROP POLICY IF EXISTS "agendador_sol_public_insert" ON public.agendador_solicitacoes;
|
||||||
|
CREATE POLICY "agendador_sol_public_insert" ON public.agendador_solicitacoes
|
||||||
|
FOR INSERT TO anon
|
||||||
|
WITH CHECK (true);
|
||||||
|
|
||||||
|
-- Leitura da própria solicitação (pelo paciente logado)
|
||||||
|
DROP POLICY IF EXISTS "agendador_sol_patient_read" ON public.agendador_solicitacoes;
|
||||||
|
CREATE POLICY "agendador_sol_patient_read" ON public.agendador_solicitacoes
|
||||||
|
FOR SELECT TO authenticated
|
||||||
|
USING (auth.uid() = user_id OR auth.uid() = owner_id);
|
||||||
|
|
||||||
|
-- ── 3. Função: retorna slots disponíveis para uma data ──────────────────────
|
||||||
|
-- Roda como SECURITY DEFINER (acessa agenda_regras e agenda_eventos sem RLS)
|
||||||
|
CREATE OR REPLACE FUNCTION public.agendador_slots_disponiveis(
|
||||||
|
p_slug text,
|
||||||
|
p_data date
|
||||||
|
)
|
||||||
|
RETURNS TABLE (hora time, disponivel boolean)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_owner_id uuid;
|
||||||
|
v_duracao int;
|
||||||
|
v_reserva int;
|
||||||
|
v_antecedencia int;
|
||||||
|
v_dia_semana int; -- 0=dom..6=sab (JS) → convertemos
|
||||||
|
v_db_dow int; -- 0=dom..6=sab no Postgres (extract dow)
|
||||||
|
v_inicio time;
|
||||||
|
v_fim time;
|
||||||
|
v_slot time;
|
||||||
|
v_slot_fim time;
|
||||||
|
v_agora timestamptz;
|
||||||
|
BEGIN
|
||||||
|
-- carrega config do agendador
|
||||||
|
SELECT
|
||||||
|
c.owner_id,
|
||||||
|
c.duracao_sessao_min,
|
||||||
|
c.reserva_horas,
|
||||||
|
c.antecedencia_minima_horas
|
||||||
|
INTO v_owner_id, v_duracao, v_reserva, 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; -- 0=dom..6=sab
|
||||||
|
|
||||||
|
-- regra semanal para o dia da semana
|
||||||
|
SELECT hora_inicio, hora_fim
|
||||||
|
INTO v_inicio, v_fim
|
||||||
|
FROM public.agenda_regras_semanais
|
||||||
|
WHERE owner_id = v_owner_id
|
||||||
|
AND dia_semana = v_db_dow
|
||||||
|
AND ativo = true
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF v_inicio IS NULL THEN
|
||||||
|
RETURN; -- profissional não atende nesse dia
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- itera slots de v_duracao em v_duracao dentro da jornada
|
||||||
|
v_slot := v_inicio;
|
||||||
|
WHILE v_slot + (v_duracao || ' minutes')::interval <= v_fim LOOP
|
||||||
|
v_slot_fim := v_slot + (v_duracao || ' minutes')::interval;
|
||||||
|
|
||||||
|
-- bloco temporário para verificar conflitos
|
||||||
|
DECLARE
|
||||||
|
v_ocupado boolean := false;
|
||||||
|
v_slot_ts timestamptz;
|
||||||
|
BEGIN
|
||||||
|
-- antecedência mínima (compara em horário de Brasília)
|
||||||
|
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;
|
||||||
|
|
||||||
|
-- conflito com eventos existentes na agenda
|
||||||
|
IF NOT v_ocupado THEN
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM public.agenda_eventos
|
||||||
|
WHERE owner_id = v_owner_id
|
||||||
|
AND status::text NOT IN ('cancelado', 'faltou')
|
||||||
|
AND inicio_em AT TIME ZONE 'America/Sao_Paulo' >= p_data::timestamp
|
||||||
|
AND inicio_em AT TIME ZONE 'America/Sao_Paulo' < p_data::timestamp + interval '1 day'
|
||||||
|
AND (inicio_em AT TIME ZONE 'America/Sao_Paulo')::time < v_slot_fim
|
||||||
|
AND (fim_em AT TIME ZONE 'America/Sao_Paulo')::time > v_slot
|
||||||
|
) INTO v_ocupado;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- conflito com solicitações pendentes (reservadas)
|
||||||
|
IF NOT v_ocupado THEN
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM public.agendador_solicitacoes
|
||||||
|
WHERE owner_id = v_owner_id
|
||||||
|
AND status = 'pendente'
|
||||||
|
AND data_solicitada = p_data
|
||||||
|
AND hora_solicitada = v_slot
|
||||||
|
AND (reservado_ate IS NULL OR reservado_ate > v_agora)
|
||||||
|
) INTO v_ocupado;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
hora := v_slot;
|
||||||
|
disponivel := NOT v_ocupado;
|
||||||
|
RETURN NEXT;
|
||||||
|
END;
|
||||||
|
|
||||||
|
v_slot := v_slot + (v_duracao || ' minutes')::interval;
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION public.agendador_slots_disponiveis(text, date) TO anon, authenticated;
|
||||||
|
|
||||||
|
-- ── 4. Função: retorna dias com disponibilidade no mês ─────────────────────
|
||||||
|
CREATE OR REPLACE FUNCTION public.agendador_dias_disponiveis(
|
||||||
|
p_slug text,
|
||||||
|
p_ano int,
|
||||||
|
p_mes int -- 1-12
|
||||||
|
)
|
||||||
|
RETURNS TABLE (data date, tem_slots boolean)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_owner_id uuid;
|
||||||
|
v_antecedencia int;
|
||||||
|
v_data date;
|
||||||
|
v_data_inicio date;
|
||||||
|
v_data_fim date;
|
||||||
|
v_agora timestamptz;
|
||||||
|
v_db_dow int;
|
||||||
|
v_tem_regra 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
|
||||||
|
-- não oferece dias no passado ou dentro da antecedência mínima
|
||||||
|
IF v_data::timestamptz + '23:59:59'::interval > v_agora + (v_antecedencia || ' hours')::interval THEN
|
||||||
|
v_db_dow := extract(dow from v_data::timestamp)::int;
|
||||||
|
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM public.agenda_regras_semanais
|
||||||
|
WHERE owner_id = v_owner_id AND dia_semana = v_db_dow AND ativo = true
|
||||||
|
) INTO v_tem_regra;
|
||||||
|
|
||||||
|
IF v_tem_regra THEN
|
||||||
|
data := v_data;
|
||||||
|
tem_slots := true;
|
||||||
|
RETURN NEXT;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
v_data := v_data + 1;
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION public.agendador_dias_disponiveis(text, int, int) TO anon, authenticated;
|
||||||
19
DBS/2026-03-11/migrations/agendador_status_convertido.sql
Normal file
19
DBS/2026-03-11/migrations/agendador_status_convertido.sql
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- FIX: adiciona status 'convertido' na constraint de agendador_solicitacoes
|
||||||
|
-- e adiciona coluna motivo_recusa (alias amigável de recusado_motivo)
|
||||||
|
-- Execute no Supabase SQL Editor
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- 1. Remove o CHECK existente e recria com os novos valores
|
||||||
|
ALTER TABLE public.agendador_solicitacoes
|
||||||
|
DROP CONSTRAINT IF EXISTS "agendador_sol_status_check";
|
||||||
|
|
||||||
|
ALTER TABLE public.agendador_solicitacoes
|
||||||
|
ADD CONSTRAINT "agendador_sol_status_check"
|
||||||
|
CHECK (status = ANY (ARRAY[
|
||||||
|
'pendente',
|
||||||
|
'autorizado',
|
||||||
|
'recusado',
|
||||||
|
'expirado',
|
||||||
|
'convertido'
|
||||||
|
]));
|
||||||
56
DBS/2026-03-11/migrations/agendador_storage.sql
Normal file
56
DBS/2026-03-11/migrations/agendador_storage.sql
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Storage bucket para imagens do Agendador Online
|
||||||
|
-- Execute no Supabase SQL Editor
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- ── 1. Criar o bucket ──────────────────────────────────────────────────────
|
||||||
|
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
||||||
|
VALUES (
|
||||||
|
'agendador',
|
||||||
|
'agendador',
|
||||||
|
true, -- público (URLs diretas sem assinar)
|
||||||
|
5242880, -- 5 MB
|
||||||
|
ARRAY['image/jpeg','image/png','image/webp','image/gif']
|
||||||
|
)
|
||||||
|
ON CONFLICT (id) DO UPDATE
|
||||||
|
SET public = true,
|
||||||
|
file_size_limit = 5242880,
|
||||||
|
allowed_mime_types = ARRAY['image/jpeg','image/png','image/webp','image/gif'];
|
||||||
|
|
||||||
|
-- ── 2. Políticas ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Leitura pública (anon e authenticated)
|
||||||
|
DROP POLICY IF EXISTS "agendador_storage_public_read" ON storage.objects;
|
||||||
|
CREATE POLICY "agendador_storage_public_read"
|
||||||
|
ON storage.objects FOR SELECT
|
||||||
|
USING (bucket_id = 'agendador');
|
||||||
|
|
||||||
|
-- Upload: apenas o dono da pasta (owner_id é o primeiro segmento do path)
|
||||||
|
DROP POLICY IF EXISTS "agendador_storage_owner_insert" ON storage.objects;
|
||||||
|
CREATE POLICY "agendador_storage_owner_insert"
|
||||||
|
ON storage.objects FOR INSERT
|
||||||
|
TO authenticated
|
||||||
|
WITH CHECK (
|
||||||
|
bucket_id = 'agendador'
|
||||||
|
AND (storage.foldername(name))[1] = auth.uid()::text
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Update/upsert pelo dono
|
||||||
|
DROP POLICY IF EXISTS "agendador_storage_owner_update" ON storage.objects;
|
||||||
|
CREATE POLICY "agendador_storage_owner_update"
|
||||||
|
ON storage.objects FOR UPDATE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
bucket_id = 'agendador'
|
||||||
|
AND (storage.foldername(name))[1] = auth.uid()::text
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Delete pelo dono
|
||||||
|
DROP POLICY IF EXISTS "agendador_storage_owner_delete" ON storage.objects;
|
||||||
|
CREATE POLICY "agendador_storage_owner_delete"
|
||||||
|
ON storage.objects FOR DELETE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
bucket_id = 'agendador'
|
||||||
|
AND (storage.foldername(name))[1] = auth.uid()::text
|
||||||
|
);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- Migration: remove session_start_offset_min from agenda_configuracoes
|
||||||
|
-- This field is replaced by hora_inicio in agenda_regras_semanais (work schedule per day)
|
||||||
|
-- The first session slot is now derived directly from hora_inicio of the work rule.
|
||||||
|
|
||||||
|
ALTER TABLE public.agenda_configuracoes
|
||||||
|
DROP COLUMN IF EXISTS session_start_offset_min;
|
||||||
BIN
DBS/2026-03-11/root/backup.sql
Normal file
BIN
DBS/2026-03-11/root/backup.sql
Normal file
Binary file not shown.
3195
DBS/2026-03-11/root/data_dump.sql
Normal file
3195
DBS/2026-03-11/root/data_dump.sql
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
BIN
DBS/2026-03-11/root/schema.sql
Normal file
BIN
DBS/2026-03-11/root/schema.sql
Normal file
Binary file not shown.
110
DBS/2026-03-11/src-sql-arquivos/01_profiles.sql
Normal file
110
DBS/2026-03-11/src-sql-arquivos/01_profiles.sql
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
-- =========================================================
|
||||||
|
-- Agência PSI — Profiles (v2) + Trigger + RLS
|
||||||
|
-- - 1 profile por auth.users.id
|
||||||
|
-- - role base (admin|therapist|patient)
|
||||||
|
-- - pronto para evoluir p/ multi-tenant depois
|
||||||
|
-- =========================================================
|
||||||
|
|
||||||
|
-- 0) Função padrão updated_at (se já existir, mantém)
|
||||||
|
create or replace function public.set_updated_at()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
new.updated_at = now();
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- 1) Tabela profiles
|
||||||
|
create table if not exists public.profiles (
|
||||||
|
id uuid primary key, -- = auth.users.id
|
||||||
|
email text,
|
||||||
|
full_name text,
|
||||||
|
avatar_url text,
|
||||||
|
|
||||||
|
role text not null default 'patient',
|
||||||
|
status text not null default 'active',
|
||||||
|
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now(),
|
||||||
|
|
||||||
|
constraint profiles_role_check check (role in ('admin','therapist','patient')),
|
||||||
|
constraint profiles_status_check check (status in ('active','inactive','invited'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- FK opcional (em Supabase costuma ser ok)
|
||||||
|
do $$
|
||||||
|
begin
|
||||||
|
if not exists (
|
||||||
|
select 1
|
||||||
|
from pg_constraint
|
||||||
|
where conname = 'profiles_id_fkey'
|
||||||
|
) then
|
||||||
|
alter table public.profiles
|
||||||
|
add constraint profiles_id_fkey
|
||||||
|
foreign key (id) references auth.users(id)
|
||||||
|
on delete cascade;
|
||||||
|
end if;
|
||||||
|
end $$;
|
||||||
|
|
||||||
|
-- Índices úteis
|
||||||
|
create index if not exists profiles_role_idx on public.profiles(role);
|
||||||
|
create index if not exists profiles_status_idx on public.profiles(status);
|
||||||
|
|
||||||
|
-- 2) Trigger updated_at
|
||||||
|
drop trigger if exists t_profiles_set_updated_at on public.profiles;
|
||||||
|
create trigger t_profiles_set_updated_at
|
||||||
|
before update on public.profiles
|
||||||
|
for each row execute function public.set_updated_at();
|
||||||
|
|
||||||
|
-- 3) Trigger pós-signup: cria profile automático
|
||||||
|
-- Observação: roda como SECURITY DEFINER
|
||||||
|
create or replace function public.handle_new_user()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
security definer
|
||||||
|
set search_path = public
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
insert into public.profiles (id, email, role, status)
|
||||||
|
values (new.id, new.email, 'patient', 'active')
|
||||||
|
on conflict (id) do update
|
||||||
|
set email = excluded.email;
|
||||||
|
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
drop trigger if exists on_auth_user_created on auth.users;
|
||||||
|
create trigger on_auth_user_created
|
||||||
|
after insert on auth.users
|
||||||
|
for each row execute function public.handle_new_user();
|
||||||
|
|
||||||
|
-- 4) RLS
|
||||||
|
alter table public.profiles enable row level security;
|
||||||
|
|
||||||
|
-- Leitura do próprio profile
|
||||||
|
drop policy if exists "profiles_select_own" on public.profiles;
|
||||||
|
create policy "profiles_select_own"
|
||||||
|
on public.profiles
|
||||||
|
for select
|
||||||
|
to authenticated
|
||||||
|
using (id = auth.uid());
|
||||||
|
|
||||||
|
-- Update do próprio profile (campos não-sensíveis)
|
||||||
|
drop policy if exists "profiles_update_own" on public.profiles;
|
||||||
|
create policy "profiles_update_own"
|
||||||
|
on public.profiles
|
||||||
|
for update
|
||||||
|
to authenticated
|
||||||
|
using (id = auth.uid())
|
||||||
|
with check (id = auth.uid());
|
||||||
|
|
||||||
|
-- Insert só do próprio (na prática quem insere é trigger, mas deixa coerente)
|
||||||
|
drop policy if exists "profiles_insert_own" on public.profiles;
|
||||||
|
create policy "profiles_insert_own"
|
||||||
|
on public.profiles
|
||||||
|
for insert
|
||||||
|
to authenticated
|
||||||
|
with check (id = auth.uid());
|
||||||
212
DBS/2026-03-11/src-sql-arquivos/supabase_cadastro_externo.sql
Normal file
212
DBS/2026-03-11/src-sql-arquivos/supabase_cadastro_externo.sql
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
-- =========================================================
|
||||||
|
-- Agência PSI Quasar — Cadastro Externo de Paciente (Supabase/Postgres)
|
||||||
|
-- Objetivo:
|
||||||
|
-- - Ter um link público com TOKEN que o terapeuta envia ao paciente
|
||||||
|
-- - Paciente preenche um formulário público
|
||||||
|
-- - Salva em "intake requests" (pré-cadastro)
|
||||||
|
-- - Terapeuta revisa e converte em paciente dentro do sistema
|
||||||
|
--
|
||||||
|
-- Tabelas:
|
||||||
|
-- - patient_invites
|
||||||
|
-- - patient_intake_requests
|
||||||
|
--
|
||||||
|
-- Funções:
|
||||||
|
-- - create_patient_intake_request (RPC pública - anon)
|
||||||
|
--
|
||||||
|
-- Segurança:
|
||||||
|
-- - RLS habilitada
|
||||||
|
-- - Público (anon) não lê nada, só executa RPC
|
||||||
|
-- - Terapeuta (authenticated) lê/atualiza somente seus registros
|
||||||
|
-- =========================================================
|
||||||
|
|
||||||
|
-- 0) Tabelas
|
||||||
|
create table if not exists public.patient_invites (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
owner_id uuid not null,
|
||||||
|
token text not null unique,
|
||||||
|
active boolean not null default true,
|
||||||
|
expires_at timestamptz null,
|
||||||
|
max_uses int null,
|
||||||
|
uses int not null default 0,
|
||||||
|
created_at timestamptz not null default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
create index if not exists patient_invites_owner_id_idx on public.patient_invites(owner_id);
|
||||||
|
create index if not exists patient_invites_token_idx on public.patient_invites(token);
|
||||||
|
|
||||||
|
create table if not exists public.patient_intake_requests (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
owner_id uuid not null,
|
||||||
|
token text not null,
|
||||||
|
name text not null,
|
||||||
|
email text null,
|
||||||
|
phone text null,
|
||||||
|
notes text null,
|
||||||
|
consent boolean not null default false,
|
||||||
|
status text not null default 'new', -- new | converted | rejected
|
||||||
|
created_at timestamptz not null default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
create index if not exists patient_intake_owner_id_idx on public.patient_intake_requests(owner_id);
|
||||||
|
create index if not exists patient_intake_token_idx on public.patient_intake_requests(token);
|
||||||
|
create index if not exists patient_intake_status_idx on public.patient_intake_requests(status);
|
||||||
|
|
||||||
|
-- 1) RLS
|
||||||
|
alter table public.patient_invites enable row level security;
|
||||||
|
alter table public.patient_intake_requests enable row level security;
|
||||||
|
|
||||||
|
-- 2) Fechar acesso direto para anon (público)
|
||||||
|
revoke all on table public.patient_invites from anon;
|
||||||
|
revoke all on table public.patient_intake_requests from anon;
|
||||||
|
|
||||||
|
-- 3) Policies: terapeuta (authenticated) - somente próprios registros
|
||||||
|
|
||||||
|
-- patient_invites
|
||||||
|
drop policy if exists invites_select_own on public.patient_invites;
|
||||||
|
create policy invites_select_own
|
||||||
|
on public.patient_invites for select
|
||||||
|
to authenticated
|
||||||
|
using (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists invites_insert_own on public.patient_invites;
|
||||||
|
create policy invites_insert_own
|
||||||
|
on public.patient_invites for insert
|
||||||
|
to authenticated
|
||||||
|
with check (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists invites_update_own on public.patient_invites;
|
||||||
|
create policy invites_update_own
|
||||||
|
on public.patient_invites for update
|
||||||
|
to authenticated
|
||||||
|
using (owner_id = auth.uid())
|
||||||
|
with check (owner_id = auth.uid());
|
||||||
|
|
||||||
|
-- patient_intake_requests
|
||||||
|
drop policy if exists intake_select_own on public.patient_intake_requests;
|
||||||
|
create policy intake_select_own
|
||||||
|
on public.patient_intake_requests for select
|
||||||
|
to authenticated
|
||||||
|
using (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists intake_update_own on public.patient_intake_requests;
|
||||||
|
create policy intake_update_own
|
||||||
|
on public.patient_intake_requests for update
|
||||||
|
to authenticated
|
||||||
|
using (owner_id = auth.uid())
|
||||||
|
with check (owner_id = auth.uid());
|
||||||
|
|
||||||
|
-- 4) RPC pública para criar intake (página pública)
|
||||||
|
-- Importantíssimo: security definer + search_path fixo
|
||||||
|
create or replace function public.create_patient_intake_request(
|
||||||
|
p_token text,
|
||||||
|
p_name text,
|
||||||
|
p_email text default null,
|
||||||
|
p_phone text default null,
|
||||||
|
p_notes text default null,
|
||||||
|
p_consent boolean default false
|
||||||
|
)
|
||||||
|
returns uuid
|
||||||
|
language plpgsql
|
||||||
|
security definer
|
||||||
|
set search_path = public
|
||||||
|
as $$
|
||||||
|
declare
|
||||||
|
v_owner uuid;
|
||||||
|
v_active boolean;
|
||||||
|
v_expires timestamptz;
|
||||||
|
v_max_uses int;
|
||||||
|
v_uses int;
|
||||||
|
v_id uuid;
|
||||||
|
begin
|
||||||
|
select owner_id, active, expires_at, max_uses, uses
|
||||||
|
into v_owner, v_active, v_expires, v_max_uses, v_uses
|
||||||
|
from public.patient_invites
|
||||||
|
where token = p_token
|
||||||
|
limit 1;
|
||||||
|
|
||||||
|
if v_owner is null then
|
||||||
|
raise exception 'Token inválido';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if v_active is not true then
|
||||||
|
raise exception 'Link desativado';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if v_expires is not null and now() > v_expires then
|
||||||
|
raise exception 'Link expirado';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if v_max_uses is not null and v_uses >= v_max_uses then
|
||||||
|
raise exception 'Limite de uso atingido';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if p_name is null or length(trim(p_name)) = 0 then
|
||||||
|
raise exception 'Nome é obrigatório';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
insert into public.patient_intake_requests
|
||||||
|
(owner_id, token, name, email, phone, notes, consent, status)
|
||||||
|
values
|
||||||
|
(v_owner, p_token, trim(p_name),
|
||||||
|
nullif(lower(trim(p_email)), ''),
|
||||||
|
nullif(trim(p_phone), ''),
|
||||||
|
nullif(trim(p_notes), ''),
|
||||||
|
coalesce(p_consent, false),
|
||||||
|
'new')
|
||||||
|
returning id into v_id;
|
||||||
|
|
||||||
|
update public.patient_invites
|
||||||
|
set uses = uses + 1
|
||||||
|
where token = p_token;
|
||||||
|
|
||||||
|
return v_id;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
grant execute on function public.create_patient_intake_request(text, text, text, text, text, boolean) to anon;
|
||||||
|
grant execute on function public.create_patient_intake_request(text, text, text, text, text, boolean) to authenticated;
|
||||||
|
|
||||||
|
-- 5) (Opcional) helper para rotacionar token no painel (somente authenticated)
|
||||||
|
-- Você pode usar no front via supabase.rpc('rotate_patient_invite_token')
|
||||||
|
create or replace function public.rotate_patient_invite_token(
|
||||||
|
p_new_token text
|
||||||
|
)
|
||||||
|
returns uuid
|
||||||
|
language plpgsql
|
||||||
|
security definer
|
||||||
|
set search_path = public
|
||||||
|
as $$
|
||||||
|
declare
|
||||||
|
v_uid uuid;
|
||||||
|
v_id uuid;
|
||||||
|
begin
|
||||||
|
-- pega o usuário logado
|
||||||
|
v_uid := auth.uid();
|
||||||
|
if v_uid is null then
|
||||||
|
raise exception 'Usuário não autenticado';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- desativa tokens antigos ativos do usuário
|
||||||
|
update public.patient_invites
|
||||||
|
set active = false
|
||||||
|
where owner_id = v_uid
|
||||||
|
and active = true;
|
||||||
|
|
||||||
|
-- cria novo token
|
||||||
|
insert into public.patient_invites (owner_id, token, active)
|
||||||
|
values (v_uid, p_new_token, true)
|
||||||
|
returning id into v_id;
|
||||||
|
|
||||||
|
return v_id;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
grant execute on function public.rotate_patient_invite_token(text) to authenticated;
|
||||||
|
|
||||||
|
grant select, insert, update, delete on table public.patient_invites to authenticated;
|
||||||
|
grant select, insert, update, delete on table public.patient_intake_requests to authenticated;
|
||||||
|
|
||||||
|
-- anon não precisa acessar tabelas diretamente
|
||||||
|
revoke all on table public.patient_invites from anon;
|
||||||
|
revoke all on table public.patient_intake_requests from anon;
|
||||||
|
|
||||||
266
DBS/2026-03-11/src-sql-arquivos/supabase_cadastro_pacientes.sql
Normal file
266
DBS/2026-03-11/src-sql-arquivos/supabase_cadastro_pacientes.sql
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
-- =========================================================
|
||||||
|
-- PATCH — Completar cadastro para bater com PatientsCadastroPage.vue
|
||||||
|
-- (rode DEPOIS do seu supabase_cadastro_pacientes.sql)
|
||||||
|
-- =========================================================
|
||||||
|
|
||||||
|
create extension if not exists pgcrypto;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------
|
||||||
|
-- 1) Completar colunas que o front usa e hoje faltam em patients
|
||||||
|
-- ---------------------------------------------------------
|
||||||
|
do $$
|
||||||
|
begin
|
||||||
|
if not exists (
|
||||||
|
select 1 from information_schema.columns
|
||||||
|
where table_schema='public' and table_name='patients' and column_name='email_alt'
|
||||||
|
) then
|
||||||
|
alter table public.patients add column email_alt text;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if not exists (
|
||||||
|
select 1 from information_schema.columns
|
||||||
|
where table_schema='public' and table_name='patients' and column_name='phones'
|
||||||
|
) then
|
||||||
|
-- array de textos (Postgres). No JS você manda ["...","..."] normalmente.
|
||||||
|
alter table public.patients add column phones text[];
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if not exists (
|
||||||
|
select 1 from information_schema.columns
|
||||||
|
where table_schema='public' and table_name='patients' and column_name='gender'
|
||||||
|
) then
|
||||||
|
alter table public.patients add column gender text;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if not exists (
|
||||||
|
select 1 from information_schema.columns
|
||||||
|
where table_schema='public' and table_name='patients' and column_name='marital_status'
|
||||||
|
) then
|
||||||
|
alter table public.patients add column marital_status text;
|
||||||
|
end if;
|
||||||
|
end $$;
|
||||||
|
|
||||||
|
-- (opcional) índices úteis pra busca/filtro por nome/email
|
||||||
|
create index if not exists idx_patients_owner_name on public.patients(owner_id, name);
|
||||||
|
create index if not exists idx_patients_owner_email on public.patients(owner_id, email);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------
|
||||||
|
-- 2) patient_groups
|
||||||
|
-- ---------------------------------------------------------
|
||||||
|
create table if not exists public.patient_groups (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
owner_id uuid not null references auth.users(id) on delete cascade,
|
||||||
|
name text not null,
|
||||||
|
color text,
|
||||||
|
is_system boolean not null default false,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- nome único por owner
|
||||||
|
do $$
|
||||||
|
begin
|
||||||
|
if not exists (
|
||||||
|
select 1 from pg_constraint
|
||||||
|
where conname = 'patient_groups_owner_name_uniq'
|
||||||
|
and conrelid = 'public.patient_groups'::regclass
|
||||||
|
) then
|
||||||
|
alter table public.patient_groups
|
||||||
|
add constraint patient_groups_owner_name_uniq unique(owner_id, name);
|
||||||
|
end if;
|
||||||
|
end $$;
|
||||||
|
|
||||||
|
drop trigger if exists trg_patient_groups_set_updated_at on public.patient_groups;
|
||||||
|
create trigger trg_patient_groups_set_updated_at
|
||||||
|
before update on public.patient_groups
|
||||||
|
for each row execute function public.set_updated_at();
|
||||||
|
|
||||||
|
create index if not exists idx_patient_groups_owner on public.patient_groups(owner_id);
|
||||||
|
|
||||||
|
alter table public.patient_groups enable row level security;
|
||||||
|
|
||||||
|
drop policy if exists "patient_groups_select_own" on public.patient_groups;
|
||||||
|
create policy "patient_groups_select_own"
|
||||||
|
on public.patient_groups for select
|
||||||
|
to authenticated
|
||||||
|
using (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists "patient_groups_insert_own" on public.patient_groups;
|
||||||
|
create policy "patient_groups_insert_own"
|
||||||
|
on public.patient_groups for insert
|
||||||
|
to authenticated
|
||||||
|
with check (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists "patient_groups_update_own" on public.patient_groups;
|
||||||
|
create policy "patient_groups_update_own"
|
||||||
|
on public.patient_groups for update
|
||||||
|
to authenticated
|
||||||
|
using (owner_id = auth.uid())
|
||||||
|
with check (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists "patient_groups_delete_own" on public.patient_groups;
|
||||||
|
create policy "patient_groups_delete_own"
|
||||||
|
on public.patient_groups for delete
|
||||||
|
to authenticated
|
||||||
|
using (owner_id = auth.uid());
|
||||||
|
|
||||||
|
grant select, insert, update, delete on public.patient_groups to authenticated;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------
|
||||||
|
-- 3) patient_tags
|
||||||
|
-- ---------------------------------------------------------
|
||||||
|
create table if not exists public.patient_tags (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
owner_id uuid not null references auth.users(id) on delete cascade,
|
||||||
|
name text not null,
|
||||||
|
color text,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
do $$
|
||||||
|
begin
|
||||||
|
if not exists (
|
||||||
|
select 1 from pg_constraint
|
||||||
|
where conname = 'patient_tags_owner_name_uniq'
|
||||||
|
and conrelid = 'public.patient_tags'::regclass
|
||||||
|
) then
|
||||||
|
alter table public.patient_tags
|
||||||
|
add constraint patient_tags_owner_name_uniq unique(owner_id, name);
|
||||||
|
end if;
|
||||||
|
end $$;
|
||||||
|
|
||||||
|
drop trigger if exists trg_patient_tags_set_updated_at on public.patient_tags;
|
||||||
|
create trigger trg_patient_tags_set_updated_at
|
||||||
|
before update on public.patient_tags
|
||||||
|
for each row execute function public.set_updated_at();
|
||||||
|
|
||||||
|
create index if not exists idx_patient_tags_owner on public.patient_tags(owner_id);
|
||||||
|
|
||||||
|
alter table public.patient_tags enable row level security;
|
||||||
|
|
||||||
|
drop policy if exists "patient_tags_select_own" on public.patient_tags;
|
||||||
|
create policy "patient_tags_select_own"
|
||||||
|
on public.patient_tags for select
|
||||||
|
to authenticated
|
||||||
|
using (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists "patient_tags_insert_own" on public.patient_tags;
|
||||||
|
create policy "patient_tags_insert_own"
|
||||||
|
on public.patient_tags for insert
|
||||||
|
to authenticated
|
||||||
|
with check (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists "patient_tags_update_own" on public.patient_tags;
|
||||||
|
create policy "patient_tags_update_own"
|
||||||
|
on public.patient_tags for update
|
||||||
|
to authenticated
|
||||||
|
using (owner_id = auth.uid())
|
||||||
|
with check (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists "patient_tags_delete_own" on public.patient_tags;
|
||||||
|
create policy "patient_tags_delete_own"
|
||||||
|
on public.patient_tags for delete
|
||||||
|
to authenticated
|
||||||
|
using (owner_id = auth.uid());
|
||||||
|
|
||||||
|
grant select, insert, update, delete on public.patient_tags to authenticated;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------
|
||||||
|
-- 4) pivôs (patient_group_patient / patient_patient_tag)
|
||||||
|
-- ---------------------------------------------------------
|
||||||
|
create table if not exists public.patient_group_patient (
|
||||||
|
patient_id uuid not null references public.patients(id) on delete cascade,
|
||||||
|
patient_group_id uuid not null references public.patient_groups(id) on delete cascade,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
primary key (patient_id, patient_group_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create index if not exists idx_pgp_patient on public.patient_group_patient(patient_id);
|
||||||
|
create index if not exists idx_pgp_group on public.patient_group_patient(patient_group_id);
|
||||||
|
|
||||||
|
alter table public.patient_group_patient enable row level security;
|
||||||
|
|
||||||
|
-- a pivot “herda” tenant via join; policy usando exists pra validar owner do patient
|
||||||
|
drop policy if exists "pgp_select_own" on public.patient_group_patient;
|
||||||
|
create policy "pgp_select_own"
|
||||||
|
on public.patient_group_patient for select
|
||||||
|
to authenticated
|
||||||
|
using (
|
||||||
|
exists (
|
||||||
|
select 1 from public.patients p
|
||||||
|
where p.id = patient_group_patient.patient_id
|
||||||
|
and p.owner_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
drop policy if exists "pgp_write_own" on public.patient_group_patient;
|
||||||
|
create policy "pgp_write_own"
|
||||||
|
on public.patient_group_patient for all
|
||||||
|
to authenticated
|
||||||
|
using (
|
||||||
|
exists (
|
||||||
|
select 1 from public.patients p
|
||||||
|
where p.id = patient_group_patient.patient_id
|
||||||
|
and p.owner_id = auth.uid()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
with check (
|
||||||
|
exists (
|
||||||
|
select 1 from public.patients p
|
||||||
|
where p.id = patient_group_patient.patient_id
|
||||||
|
and p.owner_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
grant select, insert, update, delete on public.patient_group_patient to authenticated;
|
||||||
|
|
||||||
|
-- tags pivot (ATENÇÃO: coluna é tag_id, como teu Vue usa!)
|
||||||
|
create table if not exists public.patient_patient_tag (
|
||||||
|
patient_id uuid not null references public.patients(id) on delete cascade,
|
||||||
|
tag_id uuid not null references public.patient_tags(id) on delete cascade,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
primary key (patient_id, tag_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create index if not exists idx_ppt_patient on public.patient_patient_tag(patient_id);
|
||||||
|
create index if not exists idx_ppt_tag on public.patient_patient_tag(tag_id);
|
||||||
|
|
||||||
|
alter table public.patient_patient_tag enable row level security;
|
||||||
|
|
||||||
|
drop policy if exists "ppt_select_own" on public.patient_patient_tag;
|
||||||
|
create policy "ppt_select_own"
|
||||||
|
on public.patient_patient_tag for select
|
||||||
|
to authenticated
|
||||||
|
using (
|
||||||
|
exists (
|
||||||
|
select 1 from public.patients p
|
||||||
|
where p.id = patient_patient_tag.patient_id
|
||||||
|
and p.owner_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
drop policy if exists "ppt_write_own" on public.patient_patient_tag;
|
||||||
|
create policy "ppt_write_own"
|
||||||
|
on public.patient_patient_tag for all
|
||||||
|
to authenticated
|
||||||
|
using (
|
||||||
|
exists (
|
||||||
|
select 1 from public.patients p
|
||||||
|
where p.id = patient_patient_tag.patient_id
|
||||||
|
and p.owner_id = auth.uid()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
with check (
|
||||||
|
exists (
|
||||||
|
select 1 from public.patients p
|
||||||
|
where p.id = patient_patient_tag.patient_id
|
||||||
|
and p.owner_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
grant select, insert, update, delete on public.patient_patient_tag to authenticated;
|
||||||
|
|
||||||
|
-- =========================================================
|
||||||
|
-- FIM PATCH
|
||||||
|
-- =========================================================
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
-- =========================================================
|
||||||
|
-- INTakes / Cadastros Recebidos - Supabase Local
|
||||||
|
-- =========================================================
|
||||||
|
|
||||||
|
-- 0) Extensões úteis (geralmente já existem no Supabase, mas é seguro)
|
||||||
|
create extension if not exists pgcrypto;
|
||||||
|
|
||||||
|
-- 1) Função padrão para updated_at
|
||||||
|
create or replace function public.set_updated_at()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
new.updated_at = now();
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- 2) Tabela patient_intake_requests (espelhando nuvem)
|
||||||
|
create table if not exists public.patient_intake_requests (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
owner_id uuid not null,
|
||||||
|
token text,
|
||||||
|
name text,
|
||||||
|
email text,
|
||||||
|
phone text,
|
||||||
|
notes text,
|
||||||
|
consent boolean not null default false,
|
||||||
|
status text not null default 'new',
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now(),
|
||||||
|
payload jsonb
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 3) Índices (performance em listagem e filtros)
|
||||||
|
create index if not exists idx_intakes_owner_created
|
||||||
|
on public.patient_intake_requests (owner_id, created_at desc);
|
||||||
|
|
||||||
|
create index if not exists idx_intakes_owner_status_created
|
||||||
|
on public.patient_intake_requests (owner_id, status, created_at desc);
|
||||||
|
|
||||||
|
create index if not exists idx_intakes_status_created
|
||||||
|
on public.patient_intake_requests (status, created_at desc);
|
||||||
|
|
||||||
|
-- 4) Trigger updated_at
|
||||||
|
drop trigger if exists trg_patient_intake_requests_updated_at on public.patient_intake_requests;
|
||||||
|
|
||||||
|
create trigger trg_patient_intake_requests_updated_at
|
||||||
|
before update on public.patient_intake_requests
|
||||||
|
for each row execute function public.set_updated_at();
|
||||||
|
|
||||||
|
-- 5) RLS
|
||||||
|
alter table public.patient_intake_requests enable row level security;
|
||||||
|
|
||||||
|
-- 6) Policies (iguais às que você mostrou na nuvem)
|
||||||
|
drop policy if exists intake_select_own on public.patient_intake_requests;
|
||||||
|
create policy intake_select_own
|
||||||
|
on public.patient_intake_requests
|
||||||
|
for select
|
||||||
|
to authenticated
|
||||||
|
using (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists intake_update_own on public.patient_intake_requests;
|
||||||
|
create policy intake_update_own
|
||||||
|
on public.patient_intake_requests
|
||||||
|
for update
|
||||||
|
to authenticated
|
||||||
|
using (owner_id = auth.uid())
|
||||||
|
with check (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists "delete own intake requests" on public.patient_intake_requests;
|
||||||
|
create policy "delete own intake requests"
|
||||||
|
on public.patient_intake_requests
|
||||||
|
for delete
|
||||||
|
to authenticated
|
||||||
|
using (owner_id = auth.uid());
|
||||||
|
|
||||||
|
-- =========================================================
|
||||||
|
-- OPCIONAL (RECOMENDADO): registrar conversão
|
||||||
|
-- =========================================================
|
||||||
|
-- Se você pretende marcar intake como convertido e guardar o patient_id:
|
||||||
|
alter table public.patient_intake_requests
|
||||||
|
add column if not exists converted_patient_id uuid;
|
||||||
|
|
||||||
|
create index if not exists idx_intakes_converted_patient_id
|
||||||
|
on public.patient_intake_requests (converted_patient_id);
|
||||||
|
|
||||||
|
-- Opcional: impedir delete de intakes convertidos (melhor para auditoria)
|
||||||
|
-- (Se quiser manter delete liberado como na nuvem, comente este bloco.)
|
||||||
|
drop policy if exists "delete own intake requests" on public.patient_intake_requests;
|
||||||
|
create policy "delete_own_intakes_not_converted"
|
||||||
|
on public.patient_intake_requests
|
||||||
|
for delete
|
||||||
|
to authenticated
|
||||||
|
using (owner_id = auth.uid() and status <> 'converted');
|
||||||
|
|
||||||
|
-- =========================================================
|
||||||
|
-- OPCIONAL: check de status (evita status inválido)
|
||||||
|
-- =========================================================
|
||||||
|
alter table public.patient_intake_requests
|
||||||
|
drop constraint if exists chk_intakes_status;
|
||||||
|
|
||||||
|
alter table public.patient_intake_requests
|
||||||
|
add constraint chk_intakes_status
|
||||||
|
check (status in ('new', 'converted', 'rejected'));
|
||||||
174
DBS/2026-03-11/src-sql-arquivos/supabase_patient_groups.sql
Normal file
174
DBS/2026-03-11/src-sql-arquivos/supabase_patient_groups.sql
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
/*
|
||||||
|
patient_groups_setup.sql
|
||||||
|
Setup completo para:
|
||||||
|
- public.patient_groups
|
||||||
|
- public.patient_group_patient (tabela ponte)
|
||||||
|
- view public.v_patient_groups_with_counts
|
||||||
|
- índice único por owner + nome (case-insensitive)
|
||||||
|
- 3 grupos padrão do sistema (Crianças, Adolescentes, Idosos) NÃO editáveis / NÃO removíveis
|
||||||
|
- triggers de proteção
|
||||||
|
|
||||||
|
Observação (importante):
|
||||||
|
- Os grupos padrão são criados com owner_id = '00000000-0000-0000-0000-000000000000' (SYSTEM_OWNER),
|
||||||
|
para ficarem "globais" e não dependerem de auth.uid() em migrations.
|
||||||
|
- Se você quiser que os grupos padrão pertençam a um owner específico (tenant),
|
||||||
|
basta trocar o SYSTEM_OWNER abaixo por esse UUID.
|
||||||
|
*/
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
-- ===========================
|
||||||
|
-- 0) Constante de "dono do sistema"
|
||||||
|
-- ===========================
|
||||||
|
-- Troque aqui se você quiser que os grupos padrão pertençam a um owner específico.
|
||||||
|
-- Ex.: '816b24fe-a0c3-4409-b79b-c6c0a6935d03'
|
||||||
|
do $$
|
||||||
|
begin
|
||||||
|
-- só para documentar; não cria nada
|
||||||
|
end $$;
|
||||||
|
|
||||||
|
-- ===========================
|
||||||
|
-- 1) Tabela principal: patient_groups
|
||||||
|
-- ===========================
|
||||||
|
create table if not exists public.patient_groups (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
name text not null,
|
||||||
|
description text,
|
||||||
|
color text,
|
||||||
|
is_active boolean not null default true,
|
||||||
|
is_system boolean not null default false,
|
||||||
|
owner_id uuid not null,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- (Opcional, mas recomendado) Garante que name não seja só espaços
|
||||||
|
-- e evita nomes vazios.
|
||||||
|
alter table public.patient_groups
|
||||||
|
drop constraint if exists patient_groups_name_not_blank_check;
|
||||||
|
|
||||||
|
alter table public.patient_groups
|
||||||
|
add constraint patient_groups_name_not_blank_check
|
||||||
|
check (length(btrim(name)) > 0);
|
||||||
|
|
||||||
|
-- ===========================
|
||||||
|
-- 2) Tabela ponte: patient_group_patient
|
||||||
|
-- ===========================
|
||||||
|
-- Se você já tiver essa tabela com FKs, ajuste aqui conforme seu schema.
|
||||||
|
create table if not exists public.patient_group_patient (
|
||||||
|
patient_group_id uuid not null references public.patient_groups(id) on delete cascade,
|
||||||
|
patient_id uuid not null references public.patients(id) on delete cascade,
|
||||||
|
created_at timestamptz not null default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Evita duplicar vínculo paciente<->grupo
|
||||||
|
create unique index if not exists patient_group_patient_unique
|
||||||
|
on public.patient_group_patient (patient_group_id, patient_id);
|
||||||
|
|
||||||
|
-- ===========================
|
||||||
|
-- 3) View com contagem
|
||||||
|
-- ===========================
|
||||||
|
create or replace view public.v_patient_groups_with_counts as
|
||||||
|
select
|
||||||
|
g.*,
|
||||||
|
coalesce(count(distinct pgp.patient_id), 0)::int as patients_count
|
||||||
|
from public.patient_groups g
|
||||||
|
left join public.patient_group_patient pgp
|
||||||
|
on pgp.patient_group_id = g.id
|
||||||
|
group by g.id;
|
||||||
|
|
||||||
|
-- ===========================
|
||||||
|
-- 4) Índice único: não permitir mesmo nome por owner (case-insensitive)
|
||||||
|
-- ===========================
|
||||||
|
-- Atenção: se já existirem duplicados, este índice pode falhar ao criar.
|
||||||
|
create unique index if not exists patient_groups_owner_name_unique
|
||||||
|
on public.patient_groups (owner_id, (lower(name)));
|
||||||
|
|
||||||
|
-- ===========================
|
||||||
|
-- 5) Triggers de proteção: system não edita / não remove
|
||||||
|
-- ===========================
|
||||||
|
create or replace function public.prevent_system_group_changes()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
if old.is_system = true then
|
||||||
|
raise exception 'Grupos padrão do sistema não podem ser alterados ou excluídos.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if tg_op = 'DELETE' then
|
||||||
|
return old;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
drop trigger if exists trg_prevent_system_group_changes on public.patient_groups;
|
||||||
|
|
||||||
|
create trigger trg_prevent_system_group_changes
|
||||||
|
before update or delete on public.patient_groups
|
||||||
|
for each row
|
||||||
|
execute function public.prevent_system_group_changes();
|
||||||
|
|
||||||
|
-- Impede "promover" um grupo comum para system
|
||||||
|
create or replace function public.prevent_promoting_to_system()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
if new.is_system = true and old.is_system is distinct from true then
|
||||||
|
raise exception 'Não é permitido transformar um grupo comum em grupo do sistema.';
|
||||||
|
end if;
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
drop trigger if exists trg_prevent_promoting_to_system on public.patient_groups;
|
||||||
|
|
||||||
|
create trigger trg_prevent_promoting_to_system
|
||||||
|
before update on public.patient_groups
|
||||||
|
for each row
|
||||||
|
execute function public.prevent_promoting_to_system();
|
||||||
|
|
||||||
|
-- ===========================
|
||||||
|
-- 6) Inserir 3 grupos padrão (imutáveis)
|
||||||
|
-- ===========================
|
||||||
|
-- Dono "global" do sistema (mude se quiser):
|
||||||
|
-- 00000000-0000-0000-0000-000000000000
|
||||||
|
with sys_owner as (
|
||||||
|
select '00000000-0000-0000-0000-000000000000'::uuid as owner_id
|
||||||
|
)
|
||||||
|
insert into public.patient_groups (name, description, color, is_active, is_system, owner_id)
|
||||||
|
select v.name, v.description, v.color, v.is_active, v.is_system, s.owner_id
|
||||||
|
from sys_owner s
|
||||||
|
join (values
|
||||||
|
('Crianças', 'Grupo padrão do sistema', null, true, true),
|
||||||
|
('Adolescentes', 'Grupo padrão do sistema', null, true, true),
|
||||||
|
('Idosos', 'Grupo padrão do sistema', null, true, true)
|
||||||
|
) as v(name, description, color, is_active, is_system)
|
||||||
|
on true
|
||||||
|
where not exists (
|
||||||
|
select 1
|
||||||
|
from public.patient_groups g
|
||||||
|
where g.owner_id = s.owner_id
|
||||||
|
and lower(g.name) = lower(v.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
commit;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Testes rápidos:
|
||||||
|
1) Ver tudo:
|
||||||
|
select * from public.v_patient_groups_with_counts order by is_system desc, name;
|
||||||
|
|
||||||
|
2) Tentar editar um system (deve falhar):
|
||||||
|
update public.patient_groups set name='X' where name='Crianças';
|
||||||
|
|
||||||
|
3) Tentar deletar um system (deve falhar):
|
||||||
|
delete from public.patient_groups where name='Crianças';
|
||||||
|
|
||||||
|
4) Tentar duplicar nome no mesmo owner (deve falhar por índice único):
|
||||||
|
insert into public.patient_groups (name, is_active, is_system, owner_id)
|
||||||
|
values ('teste22', true, false, '816b24fe-a0c3-4409-b79b-c6c0a6935d03');
|
||||||
|
*/
|
||||||
147
DBS/2026-03-11/src-sql-arquivos/supabase_patient_index_page.sql
Normal file
147
DBS/2026-03-11/src-sql-arquivos/supabase_patient_index_page.sql
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
-- =========================================================
|
||||||
|
-- pacientesIndexPage.sql
|
||||||
|
-- Views + índices para a tela PatientsListPage
|
||||||
|
-- =========================================================
|
||||||
|
|
||||||
|
-- 0) Extensões úteis
|
||||||
|
create extension if not exists pg_trgm;
|
||||||
|
|
||||||
|
-- 1) updated_at automático (se você quiser manter updated_at sempre correto)
|
||||||
|
create or replace function public.set_updated_at()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
new.updated_at = now();
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
drop trigger if exists trg_patients_set_updated_at on public.patients;
|
||||||
|
create trigger trg_patients_set_updated_at
|
||||||
|
before update on public.patients
|
||||||
|
for each row execute function public.set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
-- =========================================================
|
||||||
|
-- 2) Views de contagem (usadas em KPIs e telas auxiliares)
|
||||||
|
-- =========================================================
|
||||||
|
|
||||||
|
-- 2.1) Grupos com contagem de pacientes
|
||||||
|
create or replace view public.v_patient_groups_with_counts as
|
||||||
|
select
|
||||||
|
g.id,
|
||||||
|
g.name,
|
||||||
|
g.color,
|
||||||
|
coalesce(count(pgp.patient_id), 0)::int as patients_count
|
||||||
|
from public.patient_groups g
|
||||||
|
left join public.patient_group_patient pgp
|
||||||
|
on pgp.patient_group_id = g.id
|
||||||
|
group by g.id, g.name, g.color;
|
||||||
|
|
||||||
|
-- 2.2) Tags com contagem de pacientes
|
||||||
|
create or replace view public.v_tag_patient_counts as
|
||||||
|
select
|
||||||
|
t.id,
|
||||||
|
t.name,
|
||||||
|
t.color,
|
||||||
|
coalesce(count(ppt.patient_id), 0)::int as patients_count
|
||||||
|
from public.patient_tags t
|
||||||
|
left join public.patient_patient_tag ppt
|
||||||
|
on ppt.tag_id = t.id
|
||||||
|
group by t.id, t.name, t.color;
|
||||||
|
|
||||||
|
|
||||||
|
-- =========================================================
|
||||||
|
-- 3) View principal da Index (pacientes + grupos/tags agregados)
|
||||||
|
-- =========================================================
|
||||||
|
|
||||||
|
create or replace view public.v_patients_index as
|
||||||
|
select
|
||||||
|
p.*,
|
||||||
|
|
||||||
|
-- array JSON com os grupos do paciente
|
||||||
|
coalesce(gx.groups, '[]'::jsonb) as groups,
|
||||||
|
|
||||||
|
-- array JSON com as tags do paciente
|
||||||
|
coalesce(tx.tags, '[]'::jsonb) as tags,
|
||||||
|
|
||||||
|
-- contagens para UI/KPIs
|
||||||
|
coalesce(gx.groups_count, 0)::int as groups_count,
|
||||||
|
coalesce(tx.tags_count, 0)::int as tags_count
|
||||||
|
|
||||||
|
from public.patients p
|
||||||
|
|
||||||
|
left join lateral (
|
||||||
|
select
|
||||||
|
jsonb_agg(
|
||||||
|
distinct jsonb_build_object(
|
||||||
|
'id', g.id,
|
||||||
|
'name', g.name,
|
||||||
|
'color', g.color
|
||||||
|
)
|
||||||
|
) filter (where g.id is not null) as groups,
|
||||||
|
count(distinct g.id) as groups_count
|
||||||
|
from public.patient_group_patient pgp
|
||||||
|
join public.patient_groups g
|
||||||
|
on g.id = pgp.patient_group_id
|
||||||
|
where pgp.patient_id = p.id
|
||||||
|
) gx on true
|
||||||
|
|
||||||
|
left join lateral (
|
||||||
|
select
|
||||||
|
jsonb_agg(
|
||||||
|
distinct jsonb_build_object(
|
||||||
|
'id', t.id,
|
||||||
|
'name', t.name,
|
||||||
|
'color', t.color
|
||||||
|
)
|
||||||
|
) filter (where t.id is not null) as tags,
|
||||||
|
count(distinct t.id) as tags_count
|
||||||
|
from public.patient_patient_tag ppt
|
||||||
|
join public.patient_tags t
|
||||||
|
on t.id = ppt.tag_id
|
||||||
|
where ppt.patient_id = p.id
|
||||||
|
) tx on true;
|
||||||
|
|
||||||
|
|
||||||
|
-- =========================================================
|
||||||
|
-- 4) Índices recomendados (performance real na listagem/filtros)
|
||||||
|
-- =========================================================
|
||||||
|
|
||||||
|
-- Patients
|
||||||
|
create index if not exists idx_patients_owner_id
|
||||||
|
on public.patients (owner_id);
|
||||||
|
|
||||||
|
create index if not exists idx_patients_created_at
|
||||||
|
on public.patients (created_at desc);
|
||||||
|
|
||||||
|
create index if not exists idx_patients_status
|
||||||
|
on public.patients (status);
|
||||||
|
|
||||||
|
create index if not exists idx_patients_last_attended_at
|
||||||
|
on public.patients (last_attended_at desc);
|
||||||
|
|
||||||
|
-- Busca rápida (name/email/phone)
|
||||||
|
create index if not exists idx_patients_name_trgm
|
||||||
|
on public.patients using gin (name gin_trgm_ops);
|
||||||
|
|
||||||
|
create index if not exists idx_patients_email_trgm
|
||||||
|
on public.patients using gin (email gin_trgm_ops);
|
||||||
|
|
||||||
|
create index if not exists idx_patients_phone_trgm
|
||||||
|
on public.patients using gin (phone gin_trgm_ops);
|
||||||
|
|
||||||
|
-- Pivot: grupos
|
||||||
|
create index if not exists idx_pgp_patient_id
|
||||||
|
on public.patient_group_patient (patient_id);
|
||||||
|
|
||||||
|
create index if not exists idx_pgp_group_id
|
||||||
|
on public.patient_group_patient (patient_group_id);
|
||||||
|
|
||||||
|
-- Pivot: tags
|
||||||
|
create index if not exists idx_ppt_patient_id
|
||||||
|
on public.patient_patient_tag (patient_id);
|
||||||
|
|
||||||
|
create index if not exists idx_ppt_tag_id
|
||||||
|
on public.patient_patient_tag (tag_id);
|
||||||
134
DBS/2026-03-11/src-sql-arquivos/supabase_tags.sql
Normal file
134
DBS/2026-03-11/src-sql-arquivos/supabase_tags.sql
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
create extension if not exists pgcrypto;
|
||||||
|
|
||||||
|
-- ===============================
|
||||||
|
-- TABELA: patient_tags
|
||||||
|
-- ===============================
|
||||||
|
create table if not exists public.patient_tags (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
owner_id uuid not null,
|
||||||
|
name text not null,
|
||||||
|
color text,
|
||||||
|
is_native boolean not null default false,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz
|
||||||
|
);
|
||||||
|
|
||||||
|
create unique index if not exists patient_tags_owner_name_uq
|
||||||
|
on public.patient_tags (owner_id, lower(name));
|
||||||
|
|
||||||
|
-- ===============================
|
||||||
|
-- TABELA: patient_patient_tag (pivot)
|
||||||
|
-- ===============================
|
||||||
|
create table if not exists public.patient_patient_tag (
|
||||||
|
owner_id uuid not null,
|
||||||
|
patient_id uuid not null,
|
||||||
|
tag_id uuid not null,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
primary key (patient_id, tag_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create index if not exists ppt_owner_idx on public.patient_patient_tag(owner_id);
|
||||||
|
create index if not exists ppt_tag_idx on public.patient_patient_tag(tag_id);
|
||||||
|
create index if not exists ppt_patient_idx on public.patient_patient_tag(patient_id);
|
||||||
|
|
||||||
|
-- ===============================
|
||||||
|
-- FOREIGN KEYS (com checagem)
|
||||||
|
-- ===============================
|
||||||
|
do $$
|
||||||
|
begin
|
||||||
|
if not exists (
|
||||||
|
select 1 from pg_constraint
|
||||||
|
where conname = 'ppt_tag_fk'
|
||||||
|
and conrelid = 'public.patient_patient_tag'::regclass
|
||||||
|
) then
|
||||||
|
alter table public.patient_patient_tag
|
||||||
|
add constraint ppt_tag_fk
|
||||||
|
foreign key (tag_id)
|
||||||
|
references public.patient_tags(id)
|
||||||
|
on delete cascade;
|
||||||
|
end if;
|
||||||
|
end $$;
|
||||||
|
|
||||||
|
do $$
|
||||||
|
begin
|
||||||
|
if not exists (
|
||||||
|
select 1 from pg_constraint
|
||||||
|
where conname = 'ppt_patient_fk'
|
||||||
|
and conrelid = 'public.patient_patient_tag'::regclass
|
||||||
|
) then
|
||||||
|
alter table public.patient_patient_tag
|
||||||
|
add constraint ppt_patient_fk
|
||||||
|
foreign key (patient_id)
|
||||||
|
references public.patients(id)
|
||||||
|
on delete cascade;
|
||||||
|
end if;
|
||||||
|
end $$;
|
||||||
|
|
||||||
|
-- ===============================
|
||||||
|
-- VIEW: contagem por tag
|
||||||
|
-- ===============================
|
||||||
|
create or replace view public.v_tag_patient_counts as
|
||||||
|
select
|
||||||
|
t.id,
|
||||||
|
t.owner_id,
|
||||||
|
t.name,
|
||||||
|
t.color,
|
||||||
|
t.is_native,
|
||||||
|
t.created_at,
|
||||||
|
t.updated_at,
|
||||||
|
coalesce(count(ppt.patient_id), 0)::int as patient_count
|
||||||
|
from public.patient_tags t
|
||||||
|
left join public.patient_patient_tag ppt
|
||||||
|
on ppt.tag_id = t.id
|
||||||
|
and ppt.owner_id = t.owner_id
|
||||||
|
group by
|
||||||
|
t.id, t.owner_id, t.name, t.color, t.is_native, t.created_at, t.updated_at;
|
||||||
|
|
||||||
|
-- ===============================
|
||||||
|
-- RLS
|
||||||
|
-- ===============================
|
||||||
|
alter table public.patient_tags enable row level security;
|
||||||
|
alter table public.patient_patient_tag enable row level security;
|
||||||
|
|
||||||
|
drop policy if exists tags_select_own on public.patient_tags;
|
||||||
|
create policy tags_select_own
|
||||||
|
on public.patient_tags
|
||||||
|
for select
|
||||||
|
using (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists tags_insert_own on public.patient_tags;
|
||||||
|
create policy tags_insert_own
|
||||||
|
on public.patient_tags
|
||||||
|
for insert
|
||||||
|
with check (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists tags_update_own on public.patient_tags;
|
||||||
|
create policy tags_update_own
|
||||||
|
on public.patient_tags
|
||||||
|
for update
|
||||||
|
using (owner_id = auth.uid())
|
||||||
|
with check (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists tags_delete_own on public.patient_tags;
|
||||||
|
create policy tags_delete_own
|
||||||
|
on public.patient_tags
|
||||||
|
for delete
|
||||||
|
using (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists ppt_select_own on public.patient_patient_tag;
|
||||||
|
create policy ppt_select_own
|
||||||
|
on public.patient_patient_tag
|
||||||
|
for select
|
||||||
|
using (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists ppt_insert_own on public.patient_patient_tag;
|
||||||
|
create policy ppt_insert_own
|
||||||
|
on public.patient_patient_tag
|
||||||
|
for insert
|
||||||
|
with check (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists ppt_delete_own on public.patient_patient_tag;
|
||||||
|
create policy ppt_delete_own
|
||||||
|
on public.patient_patient_tag
|
||||||
|
for delete
|
||||||
|
using (owner_id = auth.uid());
|
||||||
2
DBS/2026-03-11/supabase-snippets/Untitled query 116.sql
Normal file
2
DBS/2026-03-11/supabase-snippets/Untitled query 116.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE public.agenda_configuracoes DROP COLUMN IF EXISTS
|
||||||
|
session_start_offset_min;
|
||||||
1
DBS/2026-03-11/supabase-snippets/Untitled query 130.sql
Normal file
1
DBS/2026-03-11/supabase-snippets/Untitled query 130.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
select pg_get_functiondef('public.NOME_DA_FUNCAO(args_aqui)'::regprocedure);
|
||||||
45
DBS/2026-03-11/supabase-snippets/Untitled query 132.sql
Normal file
45
DBS/2026-03-11/supabase-snippets/Untitled query 132.sql
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
-- 1) Tabela profiles
|
||||||
|
create table if not exists public.profiles (
|
||||||
|
id uuid primary key references auth.users(id) on delete cascade,
|
||||||
|
role text not null default 'patient' check (role in ('admin','therapist','patient')),
|
||||||
|
full_name text,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2) updated_at automático
|
||||||
|
create or replace function public.set_updated_at()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
new.updated_at = now();
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
drop trigger if exists trg_profiles_updated_at on public.profiles;
|
||||||
|
create trigger trg_profiles_updated_at
|
||||||
|
before update on public.profiles
|
||||||
|
for each row execute function public.set_updated_at();
|
||||||
|
|
||||||
|
-- 3) Trigger: cria profile automaticamente quando usuário nasce no auth
|
||||||
|
create or replace function public.handle_new_user()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
security definer
|
||||||
|
set search_path = public
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
insert into public.profiles (id, role)
|
||||||
|
values (new.id, 'patient')
|
||||||
|
on conflict (id) do nothing;
|
||||||
|
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
drop trigger if exists on_auth_user_created on auth.users;
|
||||||
|
create trigger on_auth_user_created
|
||||||
|
after insert on auth.users
|
||||||
|
for each row execute function public.handle_new_user();
|
||||||
20
DBS/2026-03-11/supabase-snippets/Untitled query 157.sql
Normal file
20
DBS/2026-03-11/supabase-snippets/Untitled query 157.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
do $$
|
||||||
|
begin
|
||||||
|
if exists (
|
||||||
|
select 1
|
||||||
|
from information_schema.columns
|
||||||
|
where table_schema = 'public'
|
||||||
|
and table_name = 'patient_patient_tag'
|
||||||
|
and column_name = 'patient_tag_id'
|
||||||
|
)
|
||||||
|
and not exists (
|
||||||
|
select 1
|
||||||
|
from information_schema.columns
|
||||||
|
where table_schema = 'public'
|
||||||
|
and table_name = 'patient_patient_tag'
|
||||||
|
and column_name = 'tag_id'
|
||||||
|
) then
|
||||||
|
alter table public.patient_patient_tag
|
||||||
|
rename column patient_tag_id to tag_id;
|
||||||
|
end if;
|
||||||
|
end $$;
|
||||||
5
DBS/2026-03-11/supabase-snippets/Untitled query 159.sql
Normal file
5
DBS/2026-03-11/supabase-snippets/Untitled query 159.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
insert into public.profiles (id, role)
|
||||||
|
select u.id, 'patient'
|
||||||
|
from auth.users u
|
||||||
|
left join public.profiles p on p.id = u.id
|
||||||
|
where p.id is null;
|
||||||
12
DBS/2026-03-11/supabase-snippets/Untitled query 174.sql
Normal file
12
DBS/2026-03-11/supabase-snippets/Untitled query 174.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
select
|
||||||
|
id, name, email, phone,
|
||||||
|
birth_date, cpf, rg, gender,
|
||||||
|
marital_status, profession,
|
||||||
|
place_of_birth, education_level,
|
||||||
|
cep, address_street, address_number,
|
||||||
|
address_neighborhood, address_city, address_state,
|
||||||
|
phone_alt,
|
||||||
|
notes, consent, created_at
|
||||||
|
from public.patient_intake_requests
|
||||||
|
order by created_at desc
|
||||||
|
limit 1;
|
||||||
23
DBS/2026-03-11/supabase-snippets/Untitled query 209.sql
Normal file
23
DBS/2026-03-11/supabase-snippets/Untitled query 209.sql
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
create or replace 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;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
drop trigger if exists trg_agenda_cfg_sync on public.agenda_configuracoes;
|
||||||
|
|
||||||
|
create trigger trg_agenda_cfg_sync
|
||||||
|
before insert or update on public.agenda_configuracoes
|
||||||
|
for each row
|
||||||
|
execute function public.agenda_cfg_sync();
|
||||||
2
DBS/2026-03-11/supabase-snippets/Untitled query 216.sql
Normal file
2
DBS/2026-03-11/supabase-snippets/Untitled query 216.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
drop index if exists public.uq_subscriptions_tenant;
|
||||||
|
drop index if exists public.uq_subscriptions_personal_user;
|
||||||
6
DBS/2026-03-11/supabase-snippets/Untitled query 219.sql
Normal file
6
DBS/2026-03-11/supabase-snippets/Untitled query 219.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
-- Limpa TUDO da agenda (eventos, regras, exceções)
|
||||||
|
-- Execute no Supabase Studio — não tem volta!
|
||||||
|
|
||||||
|
DELETE FROM recurrence_exceptions;
|
||||||
|
DELETE FROM recurrence_rules;
|
||||||
|
DELETE FROM agenda_eventos;
|
||||||
12
DBS/2026-03-11/supabase-snippets/Untitled query 221.sql
Normal file
12
DBS/2026-03-11/supabase-snippets/Untitled query 221.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
select
|
||||||
|
t.tgname as trigger_name,
|
||||||
|
pg_get_triggerdef(t.oid) as trigger_def,
|
||||||
|
p.proname as function_name,
|
||||||
|
n.nspname as function_schema
|
||||||
|
from pg_trigger t
|
||||||
|
join pg_proc p on p.oid = t.tgfoid
|
||||||
|
join pg_namespace n on n.oid = p.pronamespace
|
||||||
|
join pg_class c on c.oid = t.tgrelid
|
||||||
|
where not t.tgisinternal
|
||||||
|
and c.relname = 'patient_intake_requests'
|
||||||
|
order by t.tgname;
|
||||||
46
DBS/2026-03-11/supabase-snippets/Untitled query 235.sql
Normal file
46
DBS/2026-03-11/supabase-snippets/Untitled query 235.sql
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- LIMPEZA DE DADOS DE TESTE — filtra por tenant/owner
|
||||||
|
-- Execute no Supabase Studio com cuidado -- ============================================================
|
||||||
|
DO $$ DECLARE
|
||||||
|
v_tenant_id uuid := 'bbbbbbbb-0002-0002-0002-000000000002';
|
||||||
|
v_owner_id uuid := 'aaaaaaaa-0002-0002-0002-000000000002';
|
||||||
|
n_exc int;
|
||||||
|
n_ev int;
|
||||||
|
n_rule int;
|
||||||
|
n_sol int;
|
||||||
|
BEGIN
|
||||||
|
|
||||||
|
-- 1. Exceções (filha de recurrence_rules — apagar primeiro)
|
||||||
|
DELETE FROM public.recurrence_exceptions
|
||||||
|
WHERE recurrence_id IN (
|
||||||
|
SELECT id FROM public.recurrence_rules
|
||||||
|
WHERE (v_tenant_id IS NULL OR tenant_id = v_tenant_id)
|
||||||
|
AND (v_owner_id IS NULL OR owner_id = v_owner_id)
|
||||||
|
);
|
||||||
|
GET DIAGNOSTICS n_exc = ROW_COUNT;
|
||||||
|
|
||||||
|
-- 2. Regras de recorrência
|
||||||
|
DELETE FROM public.recurrence_rules
|
||||||
|
WHERE (v_tenant_id IS NULL OR tenant_id = v_tenant_id)
|
||||||
|
AND (v_owner_id IS NULL OR owner_id = v_owner_id);
|
||||||
|
GET DIAGNOSTICS n_rule = ROW_COUNT;
|
||||||
|
|
||||||
|
-- 3. Eventos da agenda
|
||||||
|
DELETE FROM public.agenda_eventos
|
||||||
|
WHERE (v_tenant_id IS NULL OR tenant_id = v_tenant_id)
|
||||||
|
AND (v_owner_id IS NULL OR owner_id = v_owner_id);
|
||||||
|
GET DIAGNOSTICS n_ev = ROW_COUNT;
|
||||||
|
|
||||||
|
-- 4. Solicitações públicas (agendador online)
|
||||||
|
DELETE FROM public.agendador_solicitacoes
|
||||||
|
WHERE (v_tenant_id IS NULL OR tenant_id = v_tenant_id)
|
||||||
|
AND (v_owner_id IS NULL OR owner_id = v_owner_id);
|
||||||
|
GET DIAGNOSTICS n_sol = ROW_COUNT;
|
||||||
|
|
||||||
|
RAISE NOTICE '✅ Limpeza concluída:';
|
||||||
|
RAISE NOTICE ' recurrence_exceptions : %', n_exc;
|
||||||
|
RAISE NOTICE ' recurrence_rules : %', n_rule;
|
||||||
|
RAISE NOTICE ' agenda_eventos : %', n_ev;
|
||||||
|
RAISE NOTICE ' agendador_solicitacoes : %', n_sol;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
10
DBS/2026-03-11/supabase-snippets/Untitled query 271.sql
Normal file
10
DBS/2026-03-11/supabase-snippets/Untitled query 271.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
select
|
||||||
|
id as tenant_member_id,
|
||||||
|
tenant_id,
|
||||||
|
user_id,
|
||||||
|
role,
|
||||||
|
status,
|
||||||
|
created_at
|
||||||
|
from public.tenant_members
|
||||||
|
where user_id = '824f125c-55bb-40f5-a8c4-7a33618b91c7'
|
||||||
|
order by created_at desc;
|
||||||
4
DBS/2026-03-11/supabase-snippets/Untitled query 277.sql
Normal file
4
DBS/2026-03-11/supabase-snippets/Untitled query 277.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
select *
|
||||||
|
from agenda_eventos
|
||||||
|
order by created_at desc nulls last
|
||||||
|
limit 10;
|
||||||
6
DBS/2026-03-11/supabase-snippets/Untitled query 319.sql
Normal file
6
DBS/2026-03-11/supabase-snippets/Untitled query 319.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
select
|
||||||
|
routine_name,
|
||||||
|
routine_type
|
||||||
|
from information_schema.routines
|
||||||
|
where routine_schema = 'public'
|
||||||
|
and routine_name ilike '%intake%';
|
||||||
4
DBS/2026-03-11/supabase-snippets/Untitled query 323.sql
Normal file
4
DBS/2026-03-11/supabase-snippets/Untitled query 323.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
select id as owner_id, email, created_at
|
||||||
|
from auth.users
|
||||||
|
where email = 'admin@agendapsi.com.br'
|
||||||
|
limit 1;
|
||||||
8
DBS/2026-03-11/supabase-snippets/Untitled query 324.sql
Normal file
8
DBS/2026-03-11/supabase-snippets/Untitled query 324.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
select
|
||||||
|
id,
|
||||||
|
owner_id,
|
||||||
|
status,
|
||||||
|
created_at,
|
||||||
|
converted_patient_id
|
||||||
|
from public.patient_intake_requests
|
||||||
|
where id = '54daa09a-b2cb-4a0b-91aa-e4cea1915efe';
|
||||||
6
DBS/2026-03-11/supabase-snippets/Untitled query 330.sql
Normal file
6
DBS/2026-03-11/supabase-snippets/Untitled query 330.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
select f.key as feature_key
|
||||||
|
from public.plan_features pf
|
||||||
|
join public.features f on f.id = pf.feature_id
|
||||||
|
where pf.plan_id = 'fdc2813d-dfaa-4e2c-b71d-ef7b84dfd9e9'
|
||||||
|
and pf.enabled = true
|
||||||
|
order by f.key;
|
||||||
3
DBS/2026-03-11/supabase-snippets/Untitled query 361.sql
Normal file
3
DBS/2026-03-11/supabase-snippets/Untitled query 361.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
select id, key, name
|
||||||
|
from public.plans
|
||||||
|
order by key;
|
||||||
1
DBS/2026-03-11/supabase-snippets/Untitled query 376.sql
Normal file
1
DBS/2026-03-11/supabase-snippets/Untitled query 376.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
SELECT * FROM public.agendador_solicitacoes LIMIT 5;
|
||||||
5
DBS/2026-03-11/supabase-snippets/Untitled query 431.sql
Normal file
5
DBS/2026-03-11/supabase-snippets/Untitled query 431.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
select table_schema, table_name, column_name
|
||||||
|
from information_schema.columns
|
||||||
|
where column_name in ('owner_id', 'tenant_id')
|
||||||
|
and table_schema = 'public'
|
||||||
|
order by table_name;
|
||||||
12
DBS/2026-03-11/supabase-snippets/Untitled query 437.sql
Normal file
12
DBS/2026-03-11/supabase-snippets/Untitled query 437.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
alter table public.patient_groups enable row level security;
|
||||||
|
|
||||||
|
drop policy if exists patient_groups_select on public.patient_groups;
|
||||||
|
|
||||||
|
create policy patient_groups_select
|
||||||
|
on public.patient_groups
|
||||||
|
for select
|
||||||
|
to authenticated
|
||||||
|
using (
|
||||||
|
owner_id = auth.uid()
|
||||||
|
or owner_id is null
|
||||||
|
);
|
||||||
9
DBS/2026-03-11/supabase-snippets/Untitled query 439.sql
Normal file
9
DBS/2026-03-11/supabase-snippets/Untitled query 439.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
|
||||||
|
select *
|
||||||
|
from public.owner_feature_entitlements
|
||||||
|
where owner_id = '816b24fe-a0c3-4409-b79b-c6c0a6935d03'::uuid
|
||||||
|
order by feature_key;
|
||||||
|
|
||||||
|
select public.has_feature('816b24fe-a0c3-4409-b79b-c6c0a6935d03'::uuid, 'online_scheduling.manage') as can_manage;
|
||||||
|
|
||||||
5
DBS/2026-03-11/supabase-snippets/Untitled query 449.sql
Normal file
5
DBS/2026-03-11/supabase-snippets/Untitled query 449.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
create index if not exists idx_patient_group_patient_group_id
|
||||||
|
on public.patient_group_patient (patient_group_id);
|
||||||
|
|
||||||
|
create index if not exists idx_patient_groups_owner_system_nome
|
||||||
|
on public.patient_groups (owner_id, is_system, nome);
|
||||||
9
DBS/2026-03-11/supabase-snippets/Untitled query 457.sql
Normal file
9
DBS/2026-03-11/supabase-snippets/Untitled query 457.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
-- 1) Marcar como SaaS master
|
||||||
|
insert into public.saas_admins (user_id)
|
||||||
|
values ('40a4b683-a0c9-4890-a201-20faf41fca06')
|
||||||
|
on conflict (user_id) do nothing;
|
||||||
|
|
||||||
|
-- 2) Garantir profile (seu session.js busca role em profiles)
|
||||||
|
insert into public.profiles (id, role)
|
||||||
|
values ('40a4b683-a0c9-4890-a201-20faf41fca06', 'saas_admin')
|
||||||
|
on conflict (id) do update set role = excluded.role;
|
||||||
5
DBS/2026-03-11/supabase-snippets/Untitled query 468.sql
Normal file
5
DBS/2026-03-11/supabase-snippets/Untitled query 468.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
select *
|
||||||
|
from public.owner_feature_entitlements
|
||||||
|
where owner_id = '816b24fe-a0c3-4409-b79b-c6c0a6935d03'::uuid
|
||||||
|
order by feature_key;
|
||||||
|
|
||||||
5
DBS/2026-03-11/supabase-snippets/Untitled query 476.sql
Normal file
5
DBS/2026-03-11/supabase-snippets/Untitled query 476.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
select column_name, data_type
|
||||||
|
from information_schema.columns
|
||||||
|
where table_schema = 'public'
|
||||||
|
and table_name = 'patient_patient_tag'
|
||||||
|
order by ordinal_position;
|
||||||
18
DBS/2026-03-11/supabase-snippets/Untitled query 508.sql
Normal file
18
DBS/2026-03-11/supabase-snippets/Untitled query 508.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
create or replace function public.prevent_promoting_to_system()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
if new.is_system = true and old.is_system is distinct from true then
|
||||||
|
raise exception 'Não é permitido transformar um grupo comum em grupo do sistema.';
|
||||||
|
end if;
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
drop trigger if exists trg_prevent_promoting_to_system on public.patient_groups;
|
||||||
|
|
||||||
|
create trigger trg_prevent_promoting_to_system
|
||||||
|
before update on public.patient_groups
|
||||||
|
for each row
|
||||||
|
execute function public.prevent_promoting_to_system();
|
||||||
4
DBS/2026-03-11/supabase-snippets/Untitled query 521.sql
Normal file
4
DBS/2026-03-11/supabase-snippets/Untitled query 521.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
select id, tenant_id, user_id, role, status, created_at
|
||||||
|
from tenant_members
|
||||||
|
where user_id = '1715ec83-9a30-4dce-b73a-2deb66dcfb13'
|
||||||
|
order by created_at desc;
|
||||||
2
DBS/2026-03-11/supabase-snippets/Untitled query 566.sql
Normal file
2
DBS/2026-03-11/supabase-snippets/Untitled query 566.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
const { data, error } = await supabase.rpc('my_tenants')
|
||||||
|
console.log({ data, error })
|
||||||
6
DBS/2026-03-11/supabase-snippets/Untitled query 633.sql
Normal file
6
DBS/2026-03-11/supabase-snippets/Untitled query 633.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
select id, tenant_id, user_id, role, status, created_at
|
||||||
|
from tenant_members
|
||||||
|
where tenant_id = '816b24fe-a0c3-4409-b79b-c6c0a6935d03'::uuid
|
||||||
|
order by
|
||||||
|
(case when role = 'clinic_admin' then 0 else 1 end),
|
||||||
|
created_at;
|
||||||
15
DBS/2026-03-11/supabase-snippets/Untitled query 641.sql
Normal file
15
DBS/2026-03-11/supabase-snippets/Untitled query 641.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
-- Para o tenant A:
|
||||||
|
select id as responsible_member_id
|
||||||
|
from public.tenant_members
|
||||||
|
where tenant_id = '816b24fe-a0c3-4409-b79b-c6c0a6935d03'
|
||||||
|
and user_id = '824f125c-55bb-40f5-a8c4-7a33618b91c7'
|
||||||
|
and status = 'active'
|
||||||
|
limit 1;
|
||||||
|
|
||||||
|
-- Para o tenant B:
|
||||||
|
select id as responsible_member_id
|
||||||
|
from public.tenant_members
|
||||||
|
where tenant_id = 'e8b10543-fb36-4e75-9d37-6fece9745637'
|
||||||
|
and user_id = '824f125c-55bb-40f5-a8c4-7a33618b91c7'
|
||||||
|
and status = 'active'
|
||||||
|
limit 1;
|
||||||
4
DBS/2026-03-11/supabase-snippets/Untitled query 649.sql
Normal file
4
DBS/2026-03-11/supabase-snippets/Untitled query 649.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
select id, name, kind, created_at
|
||||||
|
from public.tenants
|
||||||
|
order by created_at desc
|
||||||
|
limit 10;
|
||||||
5
DBS/2026-03-11/supabase-snippets/Untitled query 677.sql
Normal file
5
DBS/2026-03-11/supabase-snippets/Untitled query 677.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
select id as member_id
|
||||||
|
from tenant_members
|
||||||
|
where tenant_id = '816b24fe-a0c3-4409-b79b-c6c0a6935d03'
|
||||||
|
and user_id = '824f125c-55bb-40f5-a8c4-7a33618b91c7'
|
||||||
|
limit 1;
|
||||||
2
DBS/2026-03-11/supabase-snippets/Untitled query 744.sql
Normal file
2
DBS/2026-03-11/supabase-snippets/Untitled query 744.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
insert into public.users (id, created_at)
|
||||||
|
values ('e8b10543-fb36-4e75-9d37-6fece9745637', now());
|
||||||
4
DBS/2026-03-11/supabase-snippets/Untitled query 781.sql
Normal file
4
DBS/2026-03-11/supabase-snippets/Untitled query 781.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
select *
|
||||||
|
from public.tenant_members
|
||||||
|
where tenant_id = 'UUID_AQUI'
|
||||||
|
order by created_at desc;
|
||||||
17
DBS/2026-03-11/supabase-snippets/Untitled query 790.sql
Normal file
17
DBS/2026-03-11/supabase-snippets/Untitled query 790.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
create table if not exists public.subscriptions (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
user_id uuid not null references auth.users(id) on delete cascade,
|
||||||
|
plan_key text not null,
|
||||||
|
interval text not null check (interval in ('month','year')),
|
||||||
|
status text not null check (status in ('active','canceled','past_due','trial')) default 'active',
|
||||||
|
started_at timestamptz not null default now(),
|
||||||
|
current_period_start timestamptz not null default now(),
|
||||||
|
current_period_end timestamptz null,
|
||||||
|
canceled_at timestamptz null,
|
||||||
|
source text not null default 'manual',
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
create index if not exists subscriptions_user_id_idx on public.subscriptions(user_id);
|
||||||
|
create index if not exists subscriptions_status_idx on public.subscriptions(status);
|
||||||
13
DBS/2026-03-11/supabase-snippets/Untitled query 830.sql
Normal file
13
DBS/2026-03-11/supabase-snippets/Untitled query 830.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
-- Ver todas as regras no banco
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
owner_id,
|
||||||
|
status,
|
||||||
|
type,
|
||||||
|
weekdays,
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
start_time,
|
||||||
|
created_at
|
||||||
|
FROM recurrence_rules
|
||||||
|
ORDER BY created_at DESC;
|
||||||
3
DBS/2026-03-11/supabase-snippets/Untitled query 843.sql
Normal file
3
DBS/2026-03-11/supabase-snippets/Untitled query 843.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
select id, name, created_at
|
||||||
|
from public.tenants
|
||||||
|
order by created_at desc;
|
||||||
8
DBS/2026-03-11/supabase-snippets/Untitled query 856.sql
Normal file
8
DBS/2026-03-11/supabase-snippets/Untitled query 856.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
alter table public.profiles enable row level security;
|
||||||
|
|
||||||
|
drop policy if exists "profiles_select_own" on public.profiles;
|
||||||
|
create policy "profiles_select_own"
|
||||||
|
on public.profiles
|
||||||
|
for select
|
||||||
|
to authenticated
|
||||||
|
using (id = auth.uid());
|
||||||
7
DBS/2026-03-11/supabase-snippets/Untitled query 869.sql
Normal file
7
DBS/2026-03-11/supabase-snippets/Untitled query 869.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-- 2) Tenho membership ativa no tenant atual?
|
||||||
|
select *
|
||||||
|
from tenant_members
|
||||||
|
where tenant_id = 'e8b10543-fb36-4e75-9d37-6fece9745637'
|
||||||
|
and user_id = auth.uid()
|
||||||
|
order by created_at desc;
|
||||||
|
|
||||||
8
DBS/2026-03-11/supabase-snippets/Untitled query 880.sql
Normal file
8
DBS/2026-03-11/supabase-snippets/Untitled query 880.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
select
|
||||||
|
n.nspname as schema,
|
||||||
|
p.proname as function_name,
|
||||||
|
pg_get_functiondef(p.oid) as definition
|
||||||
|
from pg_proc p
|
||||||
|
join pg_namespace n on n.oid = p.pronamespace
|
||||||
|
where n.nspname = 'public'
|
||||||
|
and pg_get_functiondef(p.oid) ilike '%entitlements_invalidation%';
|
||||||
266
DBS/2026-03-11/supabase-snippets/Untitled query 886.sql
Normal file
266
DBS/2026-03-11/supabase-snippets/Untitled query 886.sql
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
-- =========================================================
|
||||||
|
-- PATCH — Completar cadastro para bater com PatientsCadastroPage.vue
|
||||||
|
-- (rode DEPOIS do seu supabase_cadastro_pacientes.sql)
|
||||||
|
-- =========================================================
|
||||||
|
|
||||||
|
create extension if not exists pgcrypto;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------
|
||||||
|
-- 1) Completar colunas que o front usa e hoje faltam em patients
|
||||||
|
-- ---------------------------------------------------------
|
||||||
|
do $$
|
||||||
|
begin
|
||||||
|
if not exists (
|
||||||
|
select 1 from information_schema.columns
|
||||||
|
where table_schema='public' and table_name='patients' and column_name='email_alt'
|
||||||
|
) then
|
||||||
|
alter table public.patients add column email_alt text;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if not exists (
|
||||||
|
select 1 from information_schema.columns
|
||||||
|
where table_schema='public' and table_name='patients' and column_name='phones'
|
||||||
|
) then
|
||||||
|
-- array de textos (Postgres). No JS você manda ["...","..."] normalmente.
|
||||||
|
alter table public.patients add column phones text[];
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if not exists (
|
||||||
|
select 1 from information_schema.columns
|
||||||
|
where table_schema='public' and table_name='patients' and column_name='gender'
|
||||||
|
) then
|
||||||
|
alter table public.patients add column gender text;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if not exists (
|
||||||
|
select 1 from information_schema.columns
|
||||||
|
where table_schema='public' and table_name='patients' and column_name='marital_status'
|
||||||
|
) then
|
||||||
|
alter table public.patients add column marital_status text;
|
||||||
|
end if;
|
||||||
|
end $$;
|
||||||
|
|
||||||
|
-- (opcional) índices úteis pra busca/filtro por nome/email
|
||||||
|
create index if not exists idx_patients_owner_name on public.patients(owner_id, name);
|
||||||
|
create index if not exists idx_patients_owner_email on public.patients(owner_id, email);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------
|
||||||
|
-- 2) patient_groups
|
||||||
|
-- ---------------------------------------------------------
|
||||||
|
create table if not exists public.patient_groups (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
owner_id uuid not null references auth.users(id) on delete cascade,
|
||||||
|
name text not null,
|
||||||
|
color text,
|
||||||
|
is_system boolean not null default false,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- nome único por owner
|
||||||
|
do $$
|
||||||
|
begin
|
||||||
|
if not exists (
|
||||||
|
select 1 from pg_constraint
|
||||||
|
where conname = 'patient_groups_owner_name_uniq'
|
||||||
|
and conrelid = 'public.patient_groups'::regclass
|
||||||
|
) then
|
||||||
|
alter table public.patient_groups
|
||||||
|
add constraint patient_groups_owner_name_uniq unique(owner_id, name);
|
||||||
|
end if;
|
||||||
|
end $$;
|
||||||
|
|
||||||
|
drop trigger if exists trg_patient_groups_set_updated_at on public.patient_groups;
|
||||||
|
create trigger trg_patient_groups_set_updated_at
|
||||||
|
before update on public.patient_groups
|
||||||
|
for each row execute function public.set_updated_at();
|
||||||
|
|
||||||
|
create index if not exists idx_patient_groups_owner on public.patient_groups(owner_id);
|
||||||
|
|
||||||
|
alter table public.patient_groups enable row level security;
|
||||||
|
|
||||||
|
drop policy if exists "patient_groups_select_own" on public.patient_groups;
|
||||||
|
create policy "patient_groups_select_own"
|
||||||
|
on public.patient_groups for select
|
||||||
|
to authenticated
|
||||||
|
using (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists "patient_groups_insert_own" on public.patient_groups;
|
||||||
|
create policy "patient_groups_insert_own"
|
||||||
|
on public.patient_groups for insert
|
||||||
|
to authenticated
|
||||||
|
with check (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists "patient_groups_update_own" on public.patient_groups;
|
||||||
|
create policy "patient_groups_update_own"
|
||||||
|
on public.patient_groups for update
|
||||||
|
to authenticated
|
||||||
|
using (owner_id = auth.uid())
|
||||||
|
with check (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists "patient_groups_delete_own" on public.patient_groups;
|
||||||
|
create policy "patient_groups_delete_own"
|
||||||
|
on public.patient_groups for delete
|
||||||
|
to authenticated
|
||||||
|
using (owner_id = auth.uid());
|
||||||
|
|
||||||
|
grant select, insert, update, delete on public.patient_groups to authenticated;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------
|
||||||
|
-- 3) patient_tags
|
||||||
|
-- ---------------------------------------------------------
|
||||||
|
create table if not exists public.patient_tags (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
owner_id uuid not null references auth.users(id) on delete cascade,
|
||||||
|
name text not null,
|
||||||
|
color text,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
do $$
|
||||||
|
begin
|
||||||
|
if not exists (
|
||||||
|
select 1 from pg_constraint
|
||||||
|
where conname = 'patient_tags_owner_name_uniq'
|
||||||
|
and conrelid = 'public.patient_tags'::regclass
|
||||||
|
) then
|
||||||
|
alter table public.patient_tags
|
||||||
|
add constraint patient_tags_owner_name_uniq unique(owner_id, name);
|
||||||
|
end if;
|
||||||
|
end $$;
|
||||||
|
|
||||||
|
drop trigger if exists trg_patient_tags_set_updated_at on public.patient_tags;
|
||||||
|
create trigger trg_patient_tags_set_updated_at
|
||||||
|
before update on public.patient_tags
|
||||||
|
for each row execute function public.set_updated_at();
|
||||||
|
|
||||||
|
create index if not exists idx_patient_tags_owner on public.patient_tags(owner_id);
|
||||||
|
|
||||||
|
alter table public.patient_tags enable row level security;
|
||||||
|
|
||||||
|
drop policy if exists "patient_tags_select_own" on public.patient_tags;
|
||||||
|
create policy "patient_tags_select_own"
|
||||||
|
on public.patient_tags for select
|
||||||
|
to authenticated
|
||||||
|
using (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists "patient_tags_insert_own" on public.patient_tags;
|
||||||
|
create policy "patient_tags_insert_own"
|
||||||
|
on public.patient_tags for insert
|
||||||
|
to authenticated
|
||||||
|
with check (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists "patient_tags_update_own" on public.patient_tags;
|
||||||
|
create policy "patient_tags_update_own"
|
||||||
|
on public.patient_tags for update
|
||||||
|
to authenticated
|
||||||
|
using (owner_id = auth.uid())
|
||||||
|
with check (owner_id = auth.uid());
|
||||||
|
|
||||||
|
drop policy if exists "patient_tags_delete_own" on public.patient_tags;
|
||||||
|
create policy "patient_tags_delete_own"
|
||||||
|
on public.patient_tags for delete
|
||||||
|
to authenticated
|
||||||
|
using (owner_id = auth.uid());
|
||||||
|
|
||||||
|
grant select, insert, update, delete on public.patient_tags to authenticated;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------
|
||||||
|
-- 4) pivôs (patient_group_patient / patient_patient_tag)
|
||||||
|
-- ---------------------------------------------------------
|
||||||
|
create table if not exists public.patient_group_patient (
|
||||||
|
patient_id uuid not null references public.patients(id) on delete cascade,
|
||||||
|
patient_group_id uuid not null references public.patient_groups(id) on delete cascade,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
primary key (patient_id, patient_group_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create index if not exists idx_pgp_patient on public.patient_group_patient(patient_id);
|
||||||
|
create index if not exists idx_pgp_group on public.patient_group_patient(patient_group_id);
|
||||||
|
|
||||||
|
alter table public.patient_group_patient enable row level security;
|
||||||
|
|
||||||
|
-- a pivot “herda” tenant via join; policy usando exists pra validar owner do patient
|
||||||
|
drop policy if exists "pgp_select_own" on public.patient_group_patient;
|
||||||
|
create policy "pgp_select_own"
|
||||||
|
on public.patient_group_patient for select
|
||||||
|
to authenticated
|
||||||
|
using (
|
||||||
|
exists (
|
||||||
|
select 1 from public.patients p
|
||||||
|
where p.id = patient_group_patient.patient_id
|
||||||
|
and p.owner_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
drop policy if exists "pgp_write_own" on public.patient_group_patient;
|
||||||
|
create policy "pgp_write_own"
|
||||||
|
on public.patient_group_patient for all
|
||||||
|
to authenticated
|
||||||
|
using (
|
||||||
|
exists (
|
||||||
|
select 1 from public.patients p
|
||||||
|
where p.id = patient_group_patient.patient_id
|
||||||
|
and p.owner_id = auth.uid()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
with check (
|
||||||
|
exists (
|
||||||
|
select 1 from public.patients p
|
||||||
|
where p.id = patient_group_patient.patient_id
|
||||||
|
and p.owner_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
grant select, insert, update, delete on public.patient_group_patient to authenticated;
|
||||||
|
|
||||||
|
-- tags pivot (ATENÇÃO: coluna é tag_id, como teu Vue usa!)
|
||||||
|
create table if not exists public.patient_patient_tag (
|
||||||
|
patient_id uuid not null references public.patients(id) on delete cascade,
|
||||||
|
tag_id uuid not null references public.patient_tags(id) on delete cascade,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
primary key (patient_id, tag_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create index if not exists idx_ppt_patient on public.patient_patient_tag(patient_id);
|
||||||
|
create index if not exists idx_ppt_tag on public.patient_patient_tag(tag_id);
|
||||||
|
|
||||||
|
alter table public.patient_patient_tag enable row level security;
|
||||||
|
|
||||||
|
drop policy if exists "ppt_select_own" on public.patient_patient_tag;
|
||||||
|
create policy "ppt_select_own"
|
||||||
|
on public.patient_patient_tag for select
|
||||||
|
to authenticated
|
||||||
|
using (
|
||||||
|
exists (
|
||||||
|
select 1 from public.patients p
|
||||||
|
where p.id = patient_patient_tag.patient_id
|
||||||
|
and p.owner_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
drop policy if exists "ppt_write_own" on public.patient_patient_tag;
|
||||||
|
create policy "ppt_write_own"
|
||||||
|
on public.patient_patient_tag for all
|
||||||
|
to authenticated
|
||||||
|
using (
|
||||||
|
exists (
|
||||||
|
select 1 from public.patients p
|
||||||
|
where p.id = patient_patient_tag.patient_id
|
||||||
|
and p.owner_id = auth.uid()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
with check (
|
||||||
|
exists (
|
||||||
|
select 1 from public.patients p
|
||||||
|
where p.id = patient_patient_tag.patient_id
|
||||||
|
and p.owner_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
grant select, insert, update, delete on public.patient_patient_tag to authenticated;
|
||||||
|
|
||||||
|
-- =========================================================
|
||||||
|
-- FIM PATCH
|
||||||
|
-- =========================================================
|
||||||
42
DBS/2026-03-11/supabase-snippets/Untitled query 899.sql
Normal file
42
DBS/2026-03-11/supabase-snippets/Untitled query 899.sql
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
-- BUCKET avatars: RLS por pasta do usuário "<uid>/..."
|
||||||
|
-- Requer que seu path seja: `${auth.uid()}/...` (no seu código já é)
|
||||||
|
|
||||||
|
drop policy if exists "avatars_select_own" on storage.objects;
|
||||||
|
create policy "avatars_select_own"
|
||||||
|
on storage.objects for select
|
||||||
|
to authenticated
|
||||||
|
using (
|
||||||
|
bucket_id = 'avatars'
|
||||||
|
and name like auth.uid()::text || '/%'
|
||||||
|
);
|
||||||
|
|
||||||
|
drop policy if exists "avatars_insert_own" on storage.objects;
|
||||||
|
create policy "avatars_insert_own"
|
||||||
|
on storage.objects for insert
|
||||||
|
to authenticated
|
||||||
|
with check (
|
||||||
|
bucket_id = 'avatars'
|
||||||
|
and name like auth.uid()::text || '/%'
|
||||||
|
);
|
||||||
|
|
||||||
|
drop policy if exists "avatars_update_own" on storage.objects;
|
||||||
|
create policy "avatars_update_own"
|
||||||
|
on storage.objects for update
|
||||||
|
to authenticated
|
||||||
|
using (
|
||||||
|
bucket_id = 'avatars'
|
||||||
|
and name like auth.uid()::text || '/%'
|
||||||
|
)
|
||||||
|
with check (
|
||||||
|
bucket_id = 'avatars'
|
||||||
|
and name like auth.uid()::text || '/%'
|
||||||
|
);
|
||||||
|
|
||||||
|
drop policy if exists "avatars_delete_own" on storage.objects;
|
||||||
|
create policy "avatars_delete_own"
|
||||||
|
on storage.objects for delete
|
||||||
|
to authenticated
|
||||||
|
using (
|
||||||
|
bucket_id = 'avatars'
|
||||||
|
and name like auth.uid()::text || '/%'
|
||||||
|
);
|
||||||
3
DBS/2026-03-11/supabase-snippets/Untitled query 934.sql
Normal file
3
DBS/2026-03-11/supabase-snippets/Untitled query 934.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
select *
|
||||||
|
from tenant_members
|
||||||
|
where user_id = 'SEU_USER_ID';
|
||||||
14
DBS/2026-03-11/supabase-snippets/Untitled query 938.sql
Normal file
14
DBS/2026-03-11/supabase-snippets/Untitled query 938.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
select
|
||||||
|
tm.id as responsible_member_id,
|
||||||
|
tm.tenant_id,
|
||||||
|
tm.user_id,
|
||||||
|
tm.role,
|
||||||
|
tm.status,
|
||||||
|
tm.created_at
|
||||||
|
from public.tenant_members tm
|
||||||
|
where tm.tenant_id = (
|
||||||
|
select owner_id from public.patient_intake_requests
|
||||||
|
where id = '54daa09a-b2cb-4a0b-91aa-e4cea1915efe'
|
||||||
|
)
|
||||||
|
and tm.user_id = '824f125c-55bb-40f5-a8c4-7a33618b91c7'
|
||||||
|
limit 10;
|
||||||
17
DBS/2026-03-11/supabase-snippets/Untitled query 975.sql
Normal file
17
DBS/2026-03-11/supabase-snippets/Untitled query 975.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
-- 1) Qual é meu uid?
|
||||||
|
select auth.uid() as my_uid;
|
||||||
|
|
||||||
|
-- 2) Tenho membership ativa no tenant atual?
|
||||||
|
select *
|
||||||
|
from tenant_members
|
||||||
|
where tenant_id = 'e8b10543-fb36-4e75-9d37-6fece9745637'
|
||||||
|
and user_id = auth.uid()
|
||||||
|
order by created_at desc;
|
||||||
|
|
||||||
|
-- 3) Se você usa status:
|
||||||
|
select *
|
||||||
|
from tenant_members
|
||||||
|
where tenant_id = 'e8b10543-fb36-4e75-9d37-6fece9745637'
|
||||||
|
and user_id = auth.uid()
|
||||||
|
and status = 'active'
|
||||||
|
order by created_at desc;
|
||||||
123
DBS/2026-03-11/supabase-snippets/saas_docs.sql
Normal file
123
DBS/2026-03-11/supabase-snippets/saas_docs.sql
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- saas_docs — Documentação dinâmica do sistema
|
||||||
|
-- Exibida nas páginas do frontend via botão "Ajuda"
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- 1. TABELA
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS public.saas_docs (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
titulo text NOT NULL,
|
||||||
|
conteudo text NOT NULL DEFAULT '',
|
||||||
|
medias jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||||
|
-- formato: [{ "tipo": "imagem"|"video", "url": "..." }, ...]
|
||||||
|
|
||||||
|
tipo_acesso text NOT NULL DEFAULT 'usuario'
|
||||||
|
CHECK (tipo_acesso IN ('usuario', 'admin')),
|
||||||
|
-- 'usuario' → todos os autenticados
|
||||||
|
-- 'admin' → clinic_admin, tenant_admin, saas_admin
|
||||||
|
|
||||||
|
pagina_path text NOT NULL,
|
||||||
|
-- path da rota do frontend, ex: '/therapist/agenda'
|
||||||
|
|
||||||
|
pagina_label text,
|
||||||
|
-- label amigável (informativo, não usado no match)
|
||||||
|
|
||||||
|
docs_relacionados uuid[] NOT NULL DEFAULT '{}',
|
||||||
|
-- IDs de outros saas_docs exibidos como "Veja também"
|
||||||
|
|
||||||
|
ativo boolean NOT NULL DEFAULT true,
|
||||||
|
ordem int NOT NULL DEFAULT 0,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- 2. ÍNDICE
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- Query principal do frontend: filtra por path + ativo
|
||||||
|
CREATE INDEX IF NOT EXISTS saas_docs_path_ativo_idx
|
||||||
|
ON public.saas_docs (pagina_path, ativo);
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- 3. RLS
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
ALTER TABLE public.saas_docs ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- SaaS admin: acesso total (SELECT, INSERT, UPDATE, DELETE)
|
||||||
|
-- Verificado via tabela saas_admins
|
||||||
|
CREATE POLICY "saas_admin_full_access" ON public.saas_docs
|
||||||
|
FOR ALL
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.saas_admins
|
||||||
|
WHERE saas_admins.user_id = auth.uid()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
WITH CHECK (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.saas_admins
|
||||||
|
WHERE saas_admins.user_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Admins de clínica: leem todos os docs ativos (usuario + admin)
|
||||||
|
CREATE POLICY "clinic_admin_read_all_docs" ON public.saas_docs
|
||||||
|
FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
ativo = true
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM public.profiles
|
||||||
|
WHERE profiles.id = auth.uid()
|
||||||
|
AND profiles.role IN ('clinic_admin', 'tenant_admin')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Demais usuários autenticados: leem apenas docs do tipo 'usuario'
|
||||||
|
CREATE POLICY "users_read_usuario_docs" ON public.saas_docs
|
||||||
|
FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
ativo = true
|
||||||
|
AND tipo_acesso = 'usuario'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- 4. STORAGE — bucket saas-docs (imagens dos documentos)
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
INSERT INTO storage.buckets (id, name, public)
|
||||||
|
VALUES ('saas-docs', 'saas-docs', true)
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- SaaS admin: pode fazer upload
|
||||||
|
CREATE POLICY "saas_admin_storage_upload" ON storage.objects
|
||||||
|
FOR INSERT
|
||||||
|
TO authenticated
|
||||||
|
WITH CHECK (
|
||||||
|
bucket_id = 'saas-docs'
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM public.saas_admins
|
||||||
|
WHERE saas_admins.user_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- SaaS admin: pode deletar
|
||||||
|
CREATE POLICY "saas_admin_storage_delete" ON storage.objects
|
||||||
|
FOR DELETE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
bucket_id = 'saas-docs'
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM public.saas_admins
|
||||||
|
WHERE saas_admins.user_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Leitura pública (bucket é público, mas policy explícita para clareza)
|
||||||
|
CREATE POLICY "saas_docs_public_read" ON storage.objects
|
||||||
|
FOR SELECT
|
||||||
|
TO public
|
||||||
|
USING (bucket_id = 'saas-docs');
|
||||||
87
Debugs/agenda-terapeuta-debug.txt
Normal file
87
Debugs/agenda-terapeuta-debug.txt
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
Debug Agenda
|
||||||
|
|
||||||
|
Funcionando :
|
||||||
|
|
||||||
|
- Cadastrar compromisso do tipo Sessão, ok
|
||||||
|
- Editar compromisso do tipo Sessão, ok
|
||||||
|
- Trocar Valor do compromisso do tipo sessão, ok
|
||||||
|
- Ajustar horário arrastando do compromisso do tipo sessão, ok
|
||||||
|
|
||||||
|
- Cada compromisso exibe sua cor própria ao ser adicionado na agenda, ok
|
||||||
|
- Ao Atualizar as cores na Sessão de Compromissos, elas são ajustadas na agenda também, ok
|
||||||
|
|
||||||
|
- Status [Agendado, Realizado, Faltou, Cancelado e Remarcar] Todos estão funcionando, ok
|
||||||
|
- O criar compromisso do tipo sessão está trazendo o valor padrão das configuraçãoes, ok
|
||||||
|
- O editar está salvando o novo valor informado, ok
|
||||||
|
|
||||||
|
- Modalidade [Presencial, Online] está sendo salva ao editar, ok
|
||||||
|
|
||||||
|
- Ajuste de Mensagem : "Conflito com X às HH:MM" não é do samePatientConflict : Encontrei o bug. Na timeConflict (linha 1785), a exclusão do evento atual usa form.value.id — mas ocorrências virtuais têm id: null, então nunca são excluídas. A ocorrência fica conflitando consigo mesma.
|
||||||
|
Causa: ocorrências virtuais têm id: null, então if (form.value.id && ...) nunca excluía a própria ocorrência da checagem — ela conflitava consigo mesma. Agora, para ocorrências virtuais, a exclusão é feita pelo par recurrence_id + original_date.
|
||||||
|
|
||||||
|
- Adição de Recorrencia, ok
|
||||||
|
- Remoção de reorrencia, ok
|
||||||
|
- Remoção de recorrencias futuras, ok
|
||||||
|
- Remoção de todas as recorrencias, mantendo o compromisso raiz (inicial), ok
|
||||||
|
- Adicionado Confirmação de remoção e frase de efeito, ok
|
||||||
|
- "Todos os agendamentos futuros da série serão removidos permanentemente. Esta sessão será mantida como avulsa. Esta ação é irreversível."
|
||||||
|
- "A série foi encerrada. Esta sessão foi mantida como avulsa."
|
||||||
|
|
||||||
|
Bugs conhecidos corrigidos :
|
||||||
|
|
||||||
|
Feito. Resumo dos 4 fixes:
|
||||||
|
|
||||||
|
┌──────┬─────────────────────────────────────────┬───────────────────────────────────────────────────────────────┐
|
||||||
|
│ # │ Problema │ Solução │ ├──────┼─────────────────────────────────────────┼───────────────────────────────────────────────────────────────┤
|
||||||
|
│ Bug │ Recorrência em evento existente → só │ Removida condição !id — agora cria a regra e vincula o evento │ │ 1 │ fazia update │ existente │
|
||||||
|
├──────┼─────────────────────────────────────────┼───────────────────────────────────────────────────────────────┤
|
||||||
|
│ Bug │ Botão de lixeira some quando form.id é │ Substituído por botão "Encerrar série" para hasSerie, lixeira │
|
||||||
|
│ 2a │ null (virtual) │ simples para eventos avulsos │
|
||||||
|
├──────┼─────────────────────────────────────────┼───────────────────────────────────────────────────────────────┤
|
||||||
|
│ Bug │ X das pills some para sessões │ Removida restrição v-if="!isPast" — X sempre visível │
|
||||||
|
│ 2b │ passadas/canceladas │ │
|
||||||
|
├──────┼─────────────────────────────────────────┼───────────────────────────────────────────────────────────────┤
|
||||||
|
│ Bug │ Sem confirmação ao encerrar série pelo │ onEncerrarSerie() com confirm.require() e aviso de │
|
||||||
|
│ 2c │ botão novo │ irreversibilidade │
|
||||||
|
└──────┴─────────────────────────────────────────┴───────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
- Dots (mini-day-dot) adicionados em todos os dias que possuem compromissos, ok
|
||||||
|
|
||||||
|
Corrigido também na AgendaClinicaPage:
|
||||||
|
|
||||||
|
┌────────────────────────┬────────────────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Fix │ O que foi feito │ ├────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ Mini-day-dot │ loadMiniMonthEvents agora itera sobre todos os ownerIds da clínica chamando │ │ │ loadAndExpand para incluir ocorrências virtuais de recorrência │
|
||||||
|
├────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ 'todos' delete │ Mantém sessão atual como avulsa antes de cancelar a regra (igual ao terapeuta, usando │
|
||||||
|
│ │ createClinic/updateClinic) │
|
||||||
|
├────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ Recorrente em evento │ Removida condição !id, cria a regra e vincula evento via updateClinic │
|
||||||
|
│ existente │ │
|
||||||
|
├────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ ConfirmDialog │ Adicionado <ConfirmDialog /> na página para as confirmações funcionarem corretamente │
|
||||||
|
└────────────────────────┴────────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Quando adiciona um compromisso na agenda e não aparece :
|
||||||
|
|
||||||
|
1. Join FK errada (!determined_commitment_id → !agenda_eventos_determined_commitment_fk) em ambos
|
||||||
|
agendaClinicRepository.js e useAgendaEvents.js — esse era o bug principal. O Supabase usava o nome do constraint FK (agenda_eventos_determined_commitment_fk), não o nome da coluna. A query estava falhando silenciosamente, mantendo
|
||||||
|
rows.value no estado anterior (vazio na primeira carga). 2. modalidade ausente do pickDbFields — adicionado para não perder a modalidade selecionada pelo usuário.
|
||||||
|
3. Toast de erro após _reloadRange — agora, se o reload falhar, um warning aparece em vez de mostrar sucesso sem
|
||||||
|
eventos.
|
||||||
|
|
||||||
|
Para colorir o background dos compromissos :
|
||||||
|
|
||||||
|
Pronto. Adicionei eventContent no buildFcOptions do AgendaClinicMosaic — mesmo código do AgendaTerapeutaPage — com
|
||||||
|
avatar (foto ou iniciais), horário, título e observações. O CSS foi adicionado em bloco não-scoped para que o HTML
|
||||||
|
injetado pelo FullCalendar seja estilizado corretamente.
|
||||||
|
|
||||||
|
As cores dos compromissos já chegavam via backgroundColor no mapper (tanto para o terapeuta quanto para a clínica), o
|
||||||
|
que estava faltando era o eventContent customizado que dá o mesmo visual.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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 */)
|
||||||
|
})
|
||||||
|
```
|
||||||
15
comandos.txt
Normal file
15
comandos.txt
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
para gerar o sql
|
||||||
|
supabase db dump --local -f full_dump.sql
|
||||||
|
|
||||||
|
para exportar todo o banco:
|
||||||
|
docker exec -i supabase_db_agenciapsi-primesakai pg_dump -U postgres postgres > backup.sql
|
||||||
|
|
||||||
|
para exportar o schema.sql:
|
||||||
|
docker exec -i supabase_db_agenciapsi-primesakai pg_dump -U postgres --schema-only postgres > schema.sql
|
||||||
|
|
||||||
|
para restaurar o banco:
|
||||||
|
docker exec -i supabase_db_agenciapsi-primesakai psql -U postgres postgres < backup.sql
|
||||||
|
|
||||||
|
para exportar sem ownership e ACLs (deixa o dump portável para outro ambiente):
|
||||||
|
docker exec -i supabase_db_agenciapsi-primesakai pg_dump -U postgres --no-owner --no-acl postgres > full_dump.sql
|
||||||
|
|
||||||
BIN
estrutura_src.txt
Normal file
BIN
estrutura_src.txt
Normal file
Binary file not shown.
29
logs/simulation-cleanup.sql
Normal file
29
logs/simulation-cleanup.sql
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- CLEANUP — remove dados da simulação AgenciaPsi
|
||||||
|
-- Data: 11/03/2026, 13:48:50
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
SET session_replication_role = replica;
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
DELETE FROM recurrence_exceptions WHERE id IN ('036ec898-d994-4892-9f6d-d0367d941e7f', '153bbd3a-d6f8-4199-bd1b-f744ad400e50', '8e4af6a8-beba-4cae-86cf-be8bbc02f8a9', 'a568ceac-0be6-4ec6-b4c8-1d666ff27fd5', '37a01ea8-65d1-4810-b8d6-869100251f47', '50c4b11e-a4ca-4f1f-ac09-05da87db3cd9', '3721079d-4ee3-46e5-a873-a33fce0e8080', '0e99db44-019d-40b5-a67e-cd55b4a096c2');
|
||||||
|
|
||||||
|
DELETE FROM agenda_eventos WHERE id IN ('19a93b33-68be-4916-9069-48587ba5b11f', '30d76055-1a5d-409a-b716-5e27aa109b0f', 'aa3d5832-a462-4380-8d4c-390aff127952', '32fc2417-ab45-4091-be6d-8b66b9231fc7', '42bd5e8b-4bc3-4fa2-abad-5cc8c520d44d', '7f180aac-b117-4237-a725-18333b3e6791', '3122bb93-3cea-4cfc-a3a3-ce66be7ccb0d', 'bb8536db-35c5-47f4-88d6-c8bab4bfad8e', 'e10632b8-2a40-4fb6-a318-7e95db39b216', 'c8d815ea-9445-4c33-8777-9a4d0af7977e', '0041d195-5203-4e83-9b18-c60b5ececa40', '25672cbd-939d-4f64-bf3a-807cb206b0aa', '94eeb1cb-2bc3-49c9-8394-1cda9ff51638', 'a008de79-810b-4b6f-ad1f-313053955c09', 'db00a8cc-44a8-4c23-a577-0eb75e750f18', '12efaf45-57a9-4c91-8020-d3bf6fbca1be', '0a5fd25c-d8c6-4f07-b88a-49db13881e09', '64d0e09a-dd16-46cb-90dd-2973bae26327', '49b90ec6-3136-446d-a6f0-c0a3ae06f4f8', 'f981e635-d536-409c-b6c6-ff6d9276394a', 'c609091f-0414-4bb6-99a5-0db40cf18148', '7cdc08f3-8ae0-42f9-a324-4f751782ff2a', '90c2a326-d173-4ad2-b716-86db878d035e', '1bd0b8ff-e058-4fdc-905e-87f958a9b9cd', '049aac05-cfc2-4107-8e07-2178174a4c91', 'f045c147-758e-4cc8-b4fd-633488e263e2', 'b097f0ac-67fd-4c6e-8cab-6417772a24ff', 'c0256c42-c125-4d17-ac8d-e210b50ca56f', 'd43bd403-ac9a-4e3d-a00d-91b2fdaa8b84', '8515afad-90a0-4a7f-936a-9b6977759bf6', 'a8f0bdb6-9502-49e5-8c2b-a00822113847', '041a0295-4986-43ba-9ef1-49eb962a64e1', '074d85af-d24e-4de6-8c90-0073f1411a0b', 'a07e5625-6c44-4c05-9607-e60cea9ab226');
|
||||||
|
|
||||||
|
DELETE FROM recurrence_rules WHERE id IN ('f94e562a-dee3-4b6c-9a63-62f54173f7e5', '84614add-030b-4110-8542-fbb2af844648', '9e493b9e-06a5-4adc-b5df-3124bbf56869', 'f4d992a3-8ca6-4930-b39c-1f9454bc4df1', 'b5c62afc-75c2-4dd3-a35a-037d7e1d4609', '6cd1c566-eee0-4bf4-b68b-c5d9ad24714c');
|
||||||
|
|
||||||
|
DELETE FROM agendador_solicitacoes WHERE id IN ('891b1bd8-05a6-4aac-8346-8aa99d889ebf', 'e80d1e72-dbdd-48c3-be6f-117d2f5f4267', 'db89a451-74ab-4b43-ad16-91b69172fff5');
|
||||||
|
|
||||||
|
DELETE FROM agenda_regras_semanais WHERE id IN ('815e4214-e3c6-4934-9a85-458d03ccb801', '9ab48292-3170-475f-b66b-7adf50477ae5', 'b9d6ec37-b290-4cd1-b82a-7e13c2cecdff', 'a9d2c596-8e9c-4f7b-b3d0-7078080f63be', '1ad0afcd-e59d-41d4-9e07-c53ec210e23e');
|
||||||
|
|
||||||
|
-- Descomente se quiser remover também as configurações de agenda:
|
||||||
|
-- DELETE FROM agenda_configuracoes WHERE owner_id = 'aaaaaaaa-0002-0002-0002-000000000002';
|
||||||
|
|
||||||
|
DELETE FROM patients WHERE id IN ('7910bd11-fdd3-4719-8d31-7352e33b0871', 'c23586a1-7f89-437b-9b15-0894c6f4e766', '56410d42-c489-47ef-a324-6177d0d54b6c', 'd7faec09-1ac0-47c6-bb75-ac3872188c66', '7d407456-ff3b-44ff-b8a6-f555987206a1', '86fcfccb-6230-42fa-a111-987d707fd4be', 'f408f949-c5cd-46c1-aa98-446eb378c653', '82b39d67-aac3-4117-b740-cdbec1d61ffc');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
SET session_replication_role = DEFAULT;
|
||||||
|
|
||||||
|
-- ─── Fim do cleanup ─────────────────────────────────────────────────────────
|
||||||
15
logs/simulation-log.txt
Normal file
15
logs/simulation-log.txt
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[INFO] 2026-03-11 16:48:50 Iniciando simulação...
|
||||||
|
[INFO] 2026-03-11 16:48:50 OWNER_ID: aaaaaaaa-0002-0002-0002-000000000002
|
||||||
|
[INFO] 2026-03-11 16:48:50 TENANT_ID: bbbbbbbb-0002-0002-0002-000000000002
|
||||||
|
[INFO] 2026-03-11 16:48:50 Período: 2026-02-09 → 2026-06-09
|
||||||
|
[INFO] 2026-03-11 16:48:50 ✔ 8 pacientes criados
|
||||||
|
[INFO] 2026-03-11 16:48:50 ✔ agenda_configuracoes inserida
|
||||||
|
[INFO] 2026-03-11 16:48:50 ✔ 5 regras semanais inseridas
|
||||||
|
[INFO] 2026-03-11 16:48:50 ✔ 2 eventos avulsos criados
|
||||||
|
[INFO] 2026-03-11 16:48:50 ✔ 6 regras de recorrência criadas
|
||||||
|
[INFO] 2026-03-11 16:48:50 ✔ Exceções: 6 faltou, 2 remarcado, 0 cancelado
|
||||||
|
[INFO] 2026-03-11 16:48:50 ✔ 32 sessões reais (passado) criadas
|
||||||
|
[INFO] 2026-03-11 16:48:50 ✔ 5 solicitações do agendador criadas
|
||||||
|
[INFO] 2026-03-11 16:48:50 ✔ Seed SQL: D:\leonohama\AgenciaPsi.com.br\Sistema\agenciapsi-primesakai\logs\simulation-seed.sql
|
||||||
|
[INFO] 2026-03-11 16:48:50 ✔ Cleanup SQL: D:\leonohama\AgenciaPsi.com.br\Sistema\agenciapsi-primesakai\logs\simulation-cleanup.sql
|
||||||
|
[INFO] 2026-03-11 16:48:50 ✔ Relatório: D:\leonohama\AgenciaPsi.com.br\Sistema\agenciapsi-primesakai\logs\simulation-report.txt
|
||||||
63
logs/simulation-report.txt
Normal file
63
logs/simulation-report.txt
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
============================================================
|
||||||
|
RELATÓRIO DE SIMULAÇÃO — AgenciaPsi
|
||||||
|
Gerado em: 11/03/2026, 13:48:50
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
OWNER_ID: aaaaaaaa-0002-0002-0002-000000000002
|
||||||
|
TENANT_ID: bbbbbbbb-0002-0002-0002-000000000002
|
||||||
|
|
||||||
|
─── Dados gerados ─────────────────────────────────────────
|
||||||
|
Pacientes: 8
|
||||||
|
Séries de recorrência: 6
|
||||||
|
Sessões reais (passado): 32
|
||||||
|
Eventos avulsos: 2
|
||||||
|
Exceções — faltou: 6
|
||||||
|
Exceções — remarcado: 2
|
||||||
|
Exceções — cancelado: 0
|
||||||
|
Solicitações agendador: 5
|
||||||
|
|
||||||
|
─── Pacientes criados ─────────────────────────────────────
|
||||||
|
[7910bd11-fdd3-4719-8d31-7352e33b0871] Isabela Carvalho — isabela.carvalho82@hotmail.com
|
||||||
|
[c23586a1-7f89-437b-9b15-0894c6f4e766] Ana Souza — ana.souza58@gmail.com
|
||||||
|
[56410d42-c489-47ef-a324-6177d0d54b6c] Helena Santos — helena.santos3@gmail.com
|
||||||
|
[d7faec09-1ac0-47c6-bb75-ac3872188c66] Daniel Carvalho — daniel.carvalho86@outlook.com
|
||||||
|
[7d407456-ff3b-44ff-b8a6-f555987206a1] Mariana Rocha — mariana.rocha79@gmail.com
|
||||||
|
[86fcfccb-6230-42fa-a111-987d707fd4be] Fernanda Silva — fernanda.silva87@hotmail.com
|
||||||
|
[f408f949-c5cd-46c1-aa98-446eb378c653] Juliana Almeida — juliana.almeida64@outlook.com
|
||||||
|
[82b39d67-aac3-4117-b740-cdbec1d61ffc] Karen Nunes — karen.nunes25@gmail.com
|
||||||
|
|
||||||
|
─── Séries de recorrência ─────────────────────────────────
|
||||||
|
[f94e562a-dee3-4b6c-9a63-62f54173f7e5]
|
||||||
|
Paciente: Isabela Carvalho
|
||||||
|
Tipo: weekly | Dias: Seg
|
||||||
|
Período: 2026-02-09 → 2026-06-09
|
||||||
|
[84614add-030b-4110-8542-fbb2af844648]
|
||||||
|
Paciente: Ana Souza
|
||||||
|
Tipo: weekly | Dias: Ter
|
||||||
|
Período: 2026-02-09 → 2026-06-09
|
||||||
|
[9e493b9e-06a5-4adc-b5df-3124bbf56869]
|
||||||
|
Paciente: Helena Santos
|
||||||
|
Tipo: weekly | Dias: Qui
|
||||||
|
Período: 2026-02-09 → 2026-06-09
|
||||||
|
[f4d992a3-8ca6-4930-b39c-1f9454bc4df1]
|
||||||
|
Paciente: Daniel Carvalho
|
||||||
|
Tipo: biweekly | Dias: Seg
|
||||||
|
Período: 2026-02-09 → 2026-06-09
|
||||||
|
[b5c62afc-75c2-4dd3-a35a-037d7e1d4609]
|
||||||
|
Paciente: Mariana Rocha
|
||||||
|
Tipo: custom_weekdays | Dias: Seg, Qua
|
||||||
|
Período: 2026-02-09 → 2026-06-09
|
||||||
|
[6cd1c566-eee0-4bf4-b68b-c5d9ad24714c]
|
||||||
|
Paciente: Fernanda Silva
|
||||||
|
Tipo: weekly | Dias: Qua
|
||||||
|
Período: 2026-02-09 → 2026-06-09
|
||||||
|
|
||||||
|
─── Como testar ───────────────────────────────────────────
|
||||||
|
1. Abra o Supabase SQL Editor
|
||||||
|
2. Cole e rode: logs/simulation-seed.sql
|
||||||
|
3. Acesse a agenda — os eventos devem aparecer
|
||||||
|
4. Acesse Pacientes — pacientes simulados aparecem na lista
|
||||||
|
5. Acesse Agendamentos Recebidos — solicitações pendentes
|
||||||
|
6. Quando terminar, rode: logs/simulation-cleanup.sql
|
||||||
|
|
||||||
|
============================================================
|
||||||
237
logs/simulation-seed.sql
Normal file
237
logs/simulation-seed.sql
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- SIMULAÇÃO AgenciaPsi — gerado por npm run simulate
|
||||||
|
-- Data: 11/03/2026, 13:48:50
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Desabilita triggers para permitir inserts de simulação
|
||||||
|
SET session_replication_role = replica;
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- ─── 1. Pacientes ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
INSERT INTO patients (id, tenant_id, owner_id, responsible_member_id, nome_completo, email_principal, telefone, cpf, patient_scope, status)
|
||||||
|
VALUES ('7910bd11-fdd3-4719-8d31-7352e33b0871', 'bbbbbbbb-0002-0002-0002-000000000002', 'aaaaaaaa-0002-0002-0002-000000000002',
|
||||||
|
(SELECT id FROM tenant_members WHERE user_id = 'aaaaaaaa-0002-0002-0002-000000000002' AND tenant_id = 'bbbbbbbb-0002-0002-0002-000000000002' LIMIT 1),
|
||||||
|
'Isabela Carvalho', 'isabela.carvalho82@hotmail.com', '(51) 93447-7268', '36694226368', 'clinic', 'Ativo');
|
||||||
|
INSERT INTO patients (id, tenant_id, owner_id, responsible_member_id, nome_completo, email_principal, telefone, cpf, patient_scope, status)
|
||||||
|
VALUES ('c23586a1-7f89-437b-9b15-0894c6f4e766', 'bbbbbbbb-0002-0002-0002-000000000002', 'aaaaaaaa-0002-0002-0002-000000000002',
|
||||||
|
(SELECT id FROM tenant_members WHERE user_id = 'aaaaaaaa-0002-0002-0002-000000000002' AND tenant_id = 'bbbbbbbb-0002-0002-0002-000000000002' LIMIT 1),
|
||||||
|
'Ana Souza', 'ana.souza58@gmail.com', '(11) 91789-2404', '76394836115', 'clinic', 'Ativo');
|
||||||
|
INSERT INTO patients (id, tenant_id, owner_id, responsible_member_id, nome_completo, email_principal, telefone, cpf, patient_scope, status)
|
||||||
|
VALUES ('56410d42-c489-47ef-a324-6177d0d54b6c', 'bbbbbbbb-0002-0002-0002-000000000002', 'aaaaaaaa-0002-0002-0002-000000000002',
|
||||||
|
(SELECT id FROM tenant_members WHERE user_id = 'aaaaaaaa-0002-0002-0002-000000000002' AND tenant_id = 'bbbbbbbb-0002-0002-0002-000000000002' LIMIT 1),
|
||||||
|
'Helena Santos', 'helena.santos3@gmail.com', '(11) 92531-4591', '98615983257', 'clinic', 'Ativo');
|
||||||
|
INSERT INTO patients (id, tenant_id, owner_id, responsible_member_id, nome_completo, email_principal, telefone, cpf, patient_scope, status)
|
||||||
|
VALUES ('d7faec09-1ac0-47c6-bb75-ac3872188c66', 'bbbbbbbb-0002-0002-0002-000000000002', 'aaaaaaaa-0002-0002-0002-000000000002',
|
||||||
|
(SELECT id FROM tenant_members WHERE user_id = 'aaaaaaaa-0002-0002-0002-000000000002' AND tenant_id = 'bbbbbbbb-0002-0002-0002-000000000002' LIMIT 1),
|
||||||
|
'Daniel Carvalho', 'daniel.carvalho86@outlook.com', '(11) 95077-3606', '58936177264', 'clinic', 'Ativo');
|
||||||
|
INSERT INTO patients (id, tenant_id, owner_id, responsible_member_id, nome_completo, email_principal, telefone, cpf, patient_scope, status)
|
||||||
|
VALUES ('7d407456-ff3b-44ff-b8a6-f555987206a1', 'bbbbbbbb-0002-0002-0002-000000000002', 'aaaaaaaa-0002-0002-0002-000000000002',
|
||||||
|
(SELECT id FROM tenant_members WHERE user_id = 'aaaaaaaa-0002-0002-0002-000000000002' AND tenant_id = 'bbbbbbbb-0002-0002-0002-000000000002' LIMIT 1),
|
||||||
|
'Mariana Rocha', 'mariana.rocha79@gmail.com', '(21) 92159-7795', '14818998161', 'clinic', 'Ativo');
|
||||||
|
INSERT INTO patients (id, tenant_id, owner_id, responsible_member_id, nome_completo, email_principal, telefone, cpf, patient_scope, status)
|
||||||
|
VALUES ('86fcfccb-6230-42fa-a111-987d707fd4be', 'bbbbbbbb-0002-0002-0002-000000000002', 'aaaaaaaa-0002-0002-0002-000000000002',
|
||||||
|
(SELECT id FROM tenant_members WHERE user_id = 'aaaaaaaa-0002-0002-0002-000000000002' AND tenant_id = 'bbbbbbbb-0002-0002-0002-000000000002' LIMIT 1),
|
||||||
|
'Fernanda Silva', 'fernanda.silva87@hotmail.com', '(21) 97345-9576', '15668565266', 'clinic', 'Ativo');
|
||||||
|
INSERT INTO patients (id, tenant_id, owner_id, responsible_member_id, nome_completo, email_principal, telefone, cpf, patient_scope, status)
|
||||||
|
VALUES ('f408f949-c5cd-46c1-aa98-446eb378c653', 'bbbbbbbb-0002-0002-0002-000000000002', 'aaaaaaaa-0002-0002-0002-000000000002',
|
||||||
|
(SELECT id FROM tenant_members WHERE user_id = 'aaaaaaaa-0002-0002-0002-000000000002' AND tenant_id = 'bbbbbbbb-0002-0002-0002-000000000002' LIMIT 1),
|
||||||
|
'Juliana Almeida', 'juliana.almeida64@outlook.com', '(71) 95914-2804', '54146327124', 'clinic', 'Ativo');
|
||||||
|
INSERT INTO patients (id, tenant_id, owner_id, responsible_member_id, nome_completo, email_principal, telefone, cpf, patient_scope, status)
|
||||||
|
VALUES ('82b39d67-aac3-4117-b740-cdbec1d61ffc', 'bbbbbbbb-0002-0002-0002-000000000002', 'aaaaaaaa-0002-0002-0002-000000000002',
|
||||||
|
(SELECT id FROM tenant_members WHERE user_id = 'aaaaaaaa-0002-0002-0002-000000000002' AND tenant_id = 'bbbbbbbb-0002-0002-0002-000000000002' LIMIT 1),
|
||||||
|
'Karen Nunes', 'karen.nunes25@gmail.com', '(31) 90712-9951', '78969125499', 'clinic', 'Ativo');
|
||||||
|
|
||||||
|
-- ─── 2. Configurações de agenda ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
INSERT INTO agenda_configuracoes (owner_id, tenant_id, session_duration_min, session_break_min, pausas_semanais, online_ativo, setup_clinica_concluido)
|
||||||
|
VALUES ('aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002',
|
||||||
|
50, 10, '[{"weekday":1,"start":"12:00","end":"13:00","label":"Almoço"},{"weekday":2,"start":"12:00","end":"13:00","label":"Almoço"},{"weekday":3,"start":"12:00","end":"13:00","label":"Almoço"},{"weekday":4,"start":"12:00","end":"13:00","label":"Almoço"},{"weekday":5,"start":"12:00","end":"13:00","label":"Almoço"}]'::jsonb, true, true)
|
||||||
|
ON CONFLICT (owner_id) DO NOTHING;
|
||||||
|
|
||||||
|
-- ─── 3. Regras semanais ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
INSERT INTO agenda_regras_semanais (id, owner_id, tenant_id, dia_semana, hora_inicio, hora_fim, ativo)
|
||||||
|
VALUES ('815e4214-e3c6-4934-9a85-458d03ccb801', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', 1, '08:00', '18:00', true);
|
||||||
|
INSERT INTO agenda_regras_semanais (id, owner_id, tenant_id, dia_semana, hora_inicio, hora_fim, ativo)
|
||||||
|
VALUES ('9ab48292-3170-475f-b66b-7adf50477ae5', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', 2, '08:00', '18:00', true);
|
||||||
|
INSERT INTO agenda_regras_semanais (id, owner_id, tenant_id, dia_semana, hora_inicio, hora_fim, ativo)
|
||||||
|
VALUES ('b9d6ec37-b290-4cd1-b82a-7e13c2cecdff', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', 3, '08:00', '18:00', true);
|
||||||
|
INSERT INTO agenda_regras_semanais (id, owner_id, tenant_id, dia_semana, hora_inicio, hora_fim, ativo)
|
||||||
|
VALUES ('a9d2c596-8e9c-4f7b-b3d0-7078080f63be', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', 4, '08:00', '18:00', true);
|
||||||
|
INSERT INTO agenda_regras_semanais (id, owner_id, tenant_id, dia_semana, hora_inicio, hora_fim, ativo)
|
||||||
|
VALUES ('1ad0afcd-e59d-41d4-9e07-c53ec210e23e', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', 5, '08:00', '18:00', true);
|
||||||
|
|
||||||
|
-- ─── 4. Eventos avulsos (passado) ──────────────────────────────────────────
|
||||||
|
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, titulo)
|
||||||
|
VALUES ('19a93b33-68be-4916-9069-48587ba5b11f', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '7910bd11-fdd3-4719-8d31-7352e33b0871',
|
||||||
|
'sessao', 'faltou', '2026-02-23T13:00:00', '2026-02-23T13:50:00', 'online', 'Sessão avulsa');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, titulo)
|
||||||
|
VALUES ('30d76055-1a5d-409a-b716-5e27aa109b0f', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '56410d42-c489-47ef-a324-6177d0d54b6c',
|
||||||
|
'sessao', 'agendado', '2026-02-16T13:00:00', '2026-02-16T13:50:00', 'presencial', 'Sessão avulsa');
|
||||||
|
|
||||||
|
-- ─── 5. Séries de recorrência ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
INSERT INTO recurrence_rules (id, owner_id, tenant_id, patient_id, type, weekdays, interval, start_date, end_date, status, start_time, end_time, modalidade)
|
||||||
|
VALUES ('f94e562a-dee3-4b6c-9a63-62f54173f7e5', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '7910bd11-fdd3-4719-8d31-7352e33b0871',
|
||||||
|
'weekly', ARRAY[1]::smallint[], 1, '2026-02-09', '2026-06-09',
|
||||||
|
'ativo', '09:00', '09:50', 'online');
|
||||||
|
INSERT INTO recurrence_rules (id, owner_id, tenant_id, patient_id, type, weekdays, interval, start_date, end_date, status, start_time, end_time, modalidade)
|
||||||
|
VALUES ('84614add-030b-4110-8542-fbb2af844648', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', 'c23586a1-7f89-437b-9b15-0894c6f4e766',
|
||||||
|
'weekly', ARRAY[2]::smallint[], 1, '2026-02-09', '2026-06-09',
|
||||||
|
'ativo', '10:00', '10:50', 'presencial');
|
||||||
|
INSERT INTO recurrence_rules (id, owner_id, tenant_id, patient_id, type, weekdays, interval, start_date, end_date, status, start_time, end_time, modalidade)
|
||||||
|
VALUES ('9e493b9e-06a5-4adc-b5df-3124bbf56869', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '56410d42-c489-47ef-a324-6177d0d54b6c',
|
||||||
|
'weekly', ARRAY[4]::smallint[], 1, '2026-02-09', '2026-06-09',
|
||||||
|
'ativo', '11:00', '11:50', 'presencial');
|
||||||
|
INSERT INTO recurrence_rules (id, owner_id, tenant_id, patient_id, type, weekdays, interval, start_date, end_date, status, start_time, end_time, modalidade)
|
||||||
|
VALUES ('f4d992a3-8ca6-4930-b39c-1f9454bc4df1', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', 'd7faec09-1ac0-47c6-bb75-ac3872188c66',
|
||||||
|
'biweekly', ARRAY[1]::smallint[], 1, '2026-02-09', '2026-06-09',
|
||||||
|
'ativo', '14:00', '14:50', 'online');
|
||||||
|
INSERT INTO recurrence_rules (id, owner_id, tenant_id, patient_id, type, weekdays, interval, start_date, end_date, status, start_time, end_time, modalidade)
|
||||||
|
VALUES ('b5c62afc-75c2-4dd3-a35a-037d7e1d4609', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '7d407456-ff3b-44ff-b8a6-f555987206a1',
|
||||||
|
'custom_weekdays', ARRAY[1,3]::smallint[], 1, '2026-02-09', '2026-06-09',
|
||||||
|
'ativo', '15:00', '15:50', 'online');
|
||||||
|
INSERT INTO recurrence_rules (id, owner_id, tenant_id, patient_id, type, weekdays, interval, start_date, end_date, status, start_time, end_time, modalidade)
|
||||||
|
VALUES ('6cd1c566-eee0-4bf4-b68b-c5d9ad24714c', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '86fcfccb-6230-42fa-a111-987d707fd4be',
|
||||||
|
'weekly', ARRAY[3]::smallint[], 1, '2026-02-09', '2026-06-09',
|
||||||
|
'ativo', '16:00', '16:50', 'online');
|
||||||
|
|
||||||
|
-- ─── 6. Exceções de recorrência ────────────────────────────────────────────
|
||||||
|
|
||||||
|
INSERT INTO recurrence_exceptions (id, recurrence_id, tenant_id, original_date, type, new_date)
|
||||||
|
VALUES ('036ec898-d994-4892-9f6d-d0367d941e7f', '9e493b9e-06a5-4adc-b5df-3124bbf56869', 'bbbbbbbb-0002-0002-0002-000000000002', '2026-02-19', 'patient_missed', NULL);
|
||||||
|
INSERT INTO recurrence_exceptions (id, recurrence_id, tenant_id, original_date, type, new_date)
|
||||||
|
VALUES ('153bbd3a-d6f8-4199-bd1b-f744ad400e50', '9e493b9e-06a5-4adc-b5df-3124bbf56869', 'bbbbbbbb-0002-0002-0002-000000000002', '2026-03-05', 'patient_missed', NULL);
|
||||||
|
INSERT INTO recurrence_exceptions (id, recurrence_id, tenant_id, original_date, type, new_date)
|
||||||
|
VALUES ('8e4af6a8-beba-4cae-86cf-be8bbc02f8a9', 'f4d992a3-8ca6-4930-b39c-1f9454bc4df1', 'bbbbbbbb-0002-0002-0002-000000000002', '2026-02-23', 'reschedule_session', '2026-02-27');
|
||||||
|
INSERT INTO recurrence_exceptions (id, recurrence_id, tenant_id, original_date, type, new_date)
|
||||||
|
VALUES ('a568ceac-0be6-4ec6-b4c8-1d666ff27fd5', 'b5c62afc-75c2-4dd3-a35a-037d7e1d4609', 'bbbbbbbb-0002-0002-0002-000000000002', '2026-02-09', 'patient_missed', NULL);
|
||||||
|
INSERT INTO recurrence_exceptions (id, recurrence_id, tenant_id, original_date, type, new_date)
|
||||||
|
VALUES ('37a01ea8-65d1-4810-b8d6-869100251f47', 'b5c62afc-75c2-4dd3-a35a-037d7e1d4609', 'bbbbbbbb-0002-0002-0002-000000000002', '2026-02-16', 'patient_missed', NULL);
|
||||||
|
INSERT INTO recurrence_exceptions (id, recurrence_id, tenant_id, original_date, type, new_date)
|
||||||
|
VALUES ('50c4b11e-a4ca-4f1f-ac09-05da87db3cd9', 'b5c62afc-75c2-4dd3-a35a-037d7e1d4609', 'bbbbbbbb-0002-0002-0002-000000000002', '2026-02-18', 'patient_missed', NULL);
|
||||||
|
INSERT INTO recurrence_exceptions (id, recurrence_id, tenant_id, original_date, type, new_date)
|
||||||
|
VALUES ('3721079d-4ee3-46e5-a873-a33fce0e8080', 'b5c62afc-75c2-4dd3-a35a-037d7e1d4609', 'bbbbbbbb-0002-0002-0002-000000000002', '2026-02-25', 'patient_missed', NULL);
|
||||||
|
INSERT INTO recurrence_exceptions (id, recurrence_id, tenant_id, original_date, type, new_date)
|
||||||
|
VALUES ('0e99db44-019d-40b5-a67e-cd55b4a096c2', '6cd1c566-eee0-4bf4-b68b-c5d9ad24714c', 'bbbbbbbb-0002-0002-0002-000000000002', '2026-02-25', 'reschedule_session', '2026-02-26');
|
||||||
|
|
||||||
|
-- ─── 7. Sessões reais (passado — realizado/faltou) ────────────────────────
|
||||||
|
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('aa3d5832-a462-4380-8d4c-390aff127952', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '7910bd11-fdd3-4719-8d31-7352e33b0871',
|
||||||
|
'sessao', 'realizado', '2026-02-09T09:00:00', '2026-02-09T09:50:00', 'online', 'f94e562a-dee3-4b6c-9a63-62f54173f7e5', '2026-02-09');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('32fc2417-ab45-4091-be6d-8b66b9231fc7', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '7910bd11-fdd3-4719-8d31-7352e33b0871',
|
||||||
|
'sessao', 'realizado', '2026-02-16T09:00:00', '2026-02-16T09:50:00', 'online', 'f94e562a-dee3-4b6c-9a63-62f54173f7e5', '2026-02-16');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('42bd5e8b-4bc3-4fa2-abad-5cc8c520d44d', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '7910bd11-fdd3-4719-8d31-7352e33b0871',
|
||||||
|
'sessao', 'realizado', '2026-02-23T09:00:00', '2026-02-23T09:50:00', 'online', 'f94e562a-dee3-4b6c-9a63-62f54173f7e5', '2026-02-23');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('7f180aac-b117-4237-a725-18333b3e6791', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '7910bd11-fdd3-4719-8d31-7352e33b0871',
|
||||||
|
'sessao', 'realizado', '2026-03-02T09:00:00', '2026-03-02T09:50:00', 'online', 'f94e562a-dee3-4b6c-9a63-62f54173f7e5', '2026-03-02');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('3122bb93-3cea-4cfc-a3a3-ce66be7ccb0d', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '7910bd11-fdd3-4719-8d31-7352e33b0871',
|
||||||
|
'sessao', 'realizado', '2026-03-09T09:00:00', '2026-03-09T09:50:00', 'online', 'f94e562a-dee3-4b6c-9a63-62f54173f7e5', '2026-03-09');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('bb8536db-35c5-47f4-88d6-c8bab4bfad8e', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', 'c23586a1-7f89-437b-9b15-0894c6f4e766',
|
||||||
|
'sessao', 'realizado', '2026-02-10T10:00:00', '2026-02-10T10:50:00', 'presencial', '84614add-030b-4110-8542-fbb2af844648', '2026-02-10');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('e10632b8-2a40-4fb6-a318-7e95db39b216', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', 'c23586a1-7f89-437b-9b15-0894c6f4e766',
|
||||||
|
'sessao', 'realizado', '2026-02-17T10:00:00', '2026-02-17T10:50:00', 'presencial', '84614add-030b-4110-8542-fbb2af844648', '2026-02-17');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('c8d815ea-9445-4c33-8777-9a4d0af7977e', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', 'c23586a1-7f89-437b-9b15-0894c6f4e766',
|
||||||
|
'sessao', 'realizado', '2026-02-24T10:00:00', '2026-02-24T10:50:00', 'presencial', '84614add-030b-4110-8542-fbb2af844648', '2026-02-24');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('0041d195-5203-4e83-9b18-c60b5ececa40', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', 'c23586a1-7f89-437b-9b15-0894c6f4e766',
|
||||||
|
'sessao', 'realizado', '2026-03-03T10:00:00', '2026-03-03T10:50:00', 'presencial', '84614add-030b-4110-8542-fbb2af844648', '2026-03-03');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('25672cbd-939d-4f64-bf3a-807cb206b0aa', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', 'c23586a1-7f89-437b-9b15-0894c6f4e766',
|
||||||
|
'sessao', 'realizado', '2026-03-10T10:00:00', '2026-03-10T10:50:00', 'presencial', '84614add-030b-4110-8542-fbb2af844648', '2026-03-10');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('94eeb1cb-2bc3-49c9-8394-1cda9ff51638', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '56410d42-c489-47ef-a324-6177d0d54b6c',
|
||||||
|
'sessao', 'realizado', '2026-02-12T11:00:00', '2026-02-12T11:50:00', 'presencial', '9e493b9e-06a5-4adc-b5df-3124bbf56869', '2026-02-12');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('a008de79-810b-4b6f-ad1f-313053955c09', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '56410d42-c489-47ef-a324-6177d0d54b6c',
|
||||||
|
'sessao', 'realizado', '2026-02-19T11:00:00', '2026-02-19T11:50:00', 'presencial', '9e493b9e-06a5-4adc-b5df-3124bbf56869', '2026-02-19');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('db00a8cc-44a8-4c23-a577-0eb75e750f18', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '56410d42-c489-47ef-a324-6177d0d54b6c',
|
||||||
|
'sessao', 'realizado', '2026-02-26T11:00:00', '2026-02-26T11:50:00', 'presencial', '9e493b9e-06a5-4adc-b5df-3124bbf56869', '2026-02-26');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('12efaf45-57a9-4c91-8020-d3bf6fbca1be', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '56410d42-c489-47ef-a324-6177d0d54b6c',
|
||||||
|
'sessao', 'realizado', '2026-03-05T11:00:00', '2026-03-05T11:50:00', 'presencial', '9e493b9e-06a5-4adc-b5df-3124bbf56869', '2026-03-05');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('0a5fd25c-d8c6-4f07-b88a-49db13881e09', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', 'd7faec09-1ac0-47c6-bb75-ac3872188c66',
|
||||||
|
'sessao', 'realizado', '2026-02-09T14:00:00', '2026-02-09T14:50:00', 'online', 'f4d992a3-8ca6-4930-b39c-1f9454bc4df1', '2026-02-09');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('64d0e09a-dd16-46cb-90dd-2973bae26327', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', 'd7faec09-1ac0-47c6-bb75-ac3872188c66',
|
||||||
|
'sessao', 'faltou', '2026-02-16T14:00:00', '2026-02-16T14:50:00', 'online', 'f4d992a3-8ca6-4930-b39c-1f9454bc4df1', '2026-02-16');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('49b90ec6-3136-446d-a6f0-c0a3ae06f4f8', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', 'd7faec09-1ac0-47c6-bb75-ac3872188c66',
|
||||||
|
'sessao', 'realizado', '2026-02-23T14:00:00', '2026-02-23T14:50:00', 'online', 'f4d992a3-8ca6-4930-b39c-1f9454bc4df1', '2026-02-23');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('f981e635-d536-409c-b6c6-ff6d9276394a', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', 'd7faec09-1ac0-47c6-bb75-ac3872188c66',
|
||||||
|
'sessao', 'realizado', '2026-03-02T14:00:00', '2026-03-02T14:50:00', 'online', 'f4d992a3-8ca6-4930-b39c-1f9454bc4df1', '2026-03-02');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('c609091f-0414-4bb6-99a5-0db40cf18148', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', 'd7faec09-1ac0-47c6-bb75-ac3872188c66',
|
||||||
|
'sessao', 'realizado', '2026-03-09T14:00:00', '2026-03-09T14:50:00', 'online', 'f4d992a3-8ca6-4930-b39c-1f9454bc4df1', '2026-03-09');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('7cdc08f3-8ae0-42f9-a324-4f751782ff2a', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '7d407456-ff3b-44ff-b8a6-f555987206a1',
|
||||||
|
'sessao', 'realizado', '2026-02-09T15:00:00', '2026-02-09T15:50:00', 'online', 'b5c62afc-75c2-4dd3-a35a-037d7e1d4609', '2026-02-09');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('90c2a326-d173-4ad2-b716-86db878d035e', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '7d407456-ff3b-44ff-b8a6-f555987206a1',
|
||||||
|
'sessao', 'realizado', '2026-02-11T15:00:00', '2026-02-11T15:50:00', 'online', 'b5c62afc-75c2-4dd3-a35a-037d7e1d4609', '2026-02-11');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('1bd0b8ff-e058-4fdc-905e-87f958a9b9cd', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '7d407456-ff3b-44ff-b8a6-f555987206a1',
|
||||||
|
'sessao', 'realizado', '2026-02-16T15:00:00', '2026-02-16T15:50:00', 'online', 'b5c62afc-75c2-4dd3-a35a-037d7e1d4609', '2026-02-16');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('049aac05-cfc2-4107-8e07-2178174a4c91', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '7d407456-ff3b-44ff-b8a6-f555987206a1',
|
||||||
|
'sessao', 'faltou', '2026-02-18T15:00:00', '2026-02-18T15:50:00', 'online', 'b5c62afc-75c2-4dd3-a35a-037d7e1d4609', '2026-02-18');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('f045c147-758e-4cc8-b4fd-633488e263e2', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '7d407456-ff3b-44ff-b8a6-f555987206a1',
|
||||||
|
'sessao', 'realizado', '2026-02-23T15:00:00', '2026-02-23T15:50:00', 'online', 'b5c62afc-75c2-4dd3-a35a-037d7e1d4609', '2026-02-23');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('b097f0ac-67fd-4c6e-8cab-6417772a24ff', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '7d407456-ff3b-44ff-b8a6-f555987206a1',
|
||||||
|
'sessao', 'realizado', '2026-02-25T15:00:00', '2026-02-25T15:50:00', 'online', 'b5c62afc-75c2-4dd3-a35a-037d7e1d4609', '2026-02-25');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('c0256c42-c125-4d17-ac8d-e210b50ca56f', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '7d407456-ff3b-44ff-b8a6-f555987206a1',
|
||||||
|
'sessao', 'realizado', '2026-03-02T15:00:00', '2026-03-02T15:50:00', 'online', 'b5c62afc-75c2-4dd3-a35a-037d7e1d4609', '2026-03-02');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('d43bd403-ac9a-4e3d-a00d-91b2fdaa8b84', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '7d407456-ff3b-44ff-b8a6-f555987206a1',
|
||||||
|
'sessao', 'realizado', '2026-03-04T15:00:00', '2026-03-04T15:50:00', 'online', 'b5c62afc-75c2-4dd3-a35a-037d7e1d4609', '2026-03-04');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('8515afad-90a0-4a7f-936a-9b6977759bf6', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '7d407456-ff3b-44ff-b8a6-f555987206a1',
|
||||||
|
'sessao', 'faltou', '2026-03-09T15:00:00', '2026-03-09T15:50:00', 'online', 'b5c62afc-75c2-4dd3-a35a-037d7e1d4609', '2026-03-09');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('a8f0bdb6-9502-49e5-8c2b-a00822113847', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '86fcfccb-6230-42fa-a111-987d707fd4be',
|
||||||
|
'sessao', 'realizado', '2026-02-11T16:00:00', '2026-02-11T16:50:00', 'online', '6cd1c566-eee0-4bf4-b68b-c5d9ad24714c', '2026-02-11');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('041a0295-4986-43ba-9ef1-49eb962a64e1', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '86fcfccb-6230-42fa-a111-987d707fd4be',
|
||||||
|
'sessao', 'faltou', '2026-02-18T16:00:00', '2026-02-18T16:50:00', 'online', '6cd1c566-eee0-4bf4-b68b-c5d9ad24714c', '2026-02-18');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('074d85af-d24e-4de6-8c90-0073f1411a0b', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '86fcfccb-6230-42fa-a111-987d707fd4be',
|
||||||
|
'sessao', 'faltou', '2026-02-25T16:00:00', '2026-02-25T16:50:00', 'online', '6cd1c566-eee0-4bf4-b68b-c5d9ad24714c', '2026-02-25');
|
||||||
|
INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)
|
||||||
|
VALUES ('a07e5625-6c44-4c05-9607-e60cea9ab226', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', '86fcfccb-6230-42fa-a111-987d707fd4be',
|
||||||
|
'sessao', 'realizado', '2026-03-04T16:00:00', '2026-03-04T16:50:00', 'online', '6cd1c566-eee0-4bf4-b68b-c5d9ad24714c', '2026-03-04');
|
||||||
|
|
||||||
|
-- ─── 8. Agendador Público — solicitações pendentes ─────────────────────────
|
||||||
|
|
||||||
|
INSERT INTO agendador_solicitacoes (id, owner_id, tenant_id, paciente_nome, paciente_sobrenome, paciente_email, paciente_celular, tipo, modalidade, data_solicitada, hora_solicitada, status)
|
||||||
|
VALUES ('891b1bd8-05a6-4aac-8346-8aa99d889ebf', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', 'Marcos', 'Costa', 'marcos.costa12@yahoo.com.br', '(51) 91799-2922',
|
||||||
|
'primeira', 'presencial', '2026-03-24', '09:00', 'recusado');
|
||||||
|
INSERT INTO agendador_solicitacoes (id, owner_id, tenant_id, paciente_nome, paciente_sobrenome, paciente_email, paciente_celular, tipo, modalidade, data_solicitada, hora_solicitada, status)
|
||||||
|
VALUES ('e80d1e72-dbdd-48c3-be6f-117d2f5f4267', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', 'Carla', 'Carvalho', 'carla.carvalho82@hotmail.com', '(91) 92340-8703',
|
||||||
|
'reagendar', 'presencial', '2026-03-23', '09:00', 'pendente');
|
||||||
|
INSERT INTO agendador_solicitacoes (id, owner_id, tenant_id, paciente_nome, paciente_sobrenome, paciente_email, paciente_celular, tipo, modalidade, data_solicitada, hora_solicitada, status)
|
||||||
|
VALUES ('db89a451-74ab-4b43-ad16-91b69172fff5', 'aaaaaaaa-0002-0002-0002-000000000002', 'bbbbbbbb-0002-0002-0002-000000000002', 'Sérgio', 'Martins', 'sergio.martins82@gmail.com', '(11) 93735-0761',
|
||||||
|
'reagendar', 'presencial', '2026-03-16', '08:00', 'pendente');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- Restaura comportamento normal dos triggers
|
||||||
|
SET session_replication_role = DEFAULT;
|
||||||
|
|
||||||
|
-- ─── Fim do seed ────────────────────────────────────────────────────────────
|
||||||
8
migrations/agenda_eventos_price.sql
Normal file
8
migrations/agenda_eventos_price.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
-- migrations/agenda_eventos_price.sql
|
||||||
|
-- Adiciona coluna price à agenda_eventos para registrar o valor da sessão
|
||||||
|
|
||||||
|
ALTER TABLE agenda_eventos
|
||||||
|
ADD COLUMN IF NOT EXISTS price numeric(10,2);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN agenda_eventos.price IS
|
||||||
|
'Valor da sessão em BRL. Preenchido automaticamente pela tabela professional_pricing do profissional.';
|
||||||
36
migrations/agendador_check_email.sql
Normal file
36
migrations/agendador_check_email.sql
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- agendador_check_email
|
||||||
|
-- Verifica se um e-mail já possui solicitação anterior para este agendador
|
||||||
|
-- SECURITY DEFINER → anon pode chamar sem burlar RLS diretamente
|
||||||
|
-- Execute no Supabase SQL Editor
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.agendador_check_email(
|
||||||
|
p_slug text,
|
||||||
|
p_email text
|
||||||
|
)
|
||||||
|
RETURNS boolean
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_owner_id uuid;
|
||||||
|
BEGIN
|
||||||
|
SELECT c.owner_id INTO v_owner_id
|
||||||
|
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 false; END IF;
|
||||||
|
|
||||||
|
RETURN EXISTS (
|
||||||
|
SELECT 1 FROM public.agendador_solicitacoes s
|
||||||
|
WHERE s.owner_id = v_owner_id
|
||||||
|
AND lower(s.paciente_email) = lower(trim(p_email))
|
||||||
|
LIMIT 1
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION public.agendador_check_email(text, text) TO anon, authenticated;
|
||||||
62
migrations/agendador_features.sql
Normal file
62
migrations/agendador_features.sql
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Feature keys do Agendador Online
|
||||||
|
-- Execute no Supabase SQL Editor
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- ── 1. Inserir as features ──────────────────────────────────────────────────
|
||||||
|
INSERT INTO public.features (key, name, descricao)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
'agendador.online',
|
||||||
|
'Agendador Online',
|
||||||
|
'Permite que pacientes solicitem agendamentos via link público. Inclui aprovação manual ou automática, controle de horários e notificações.'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'agendador.link_personalizado',
|
||||||
|
'Link Personalizado do Agendador',
|
||||||
|
'Permite que o profissional escolha um slug de URL próprio para o agendador (ex: /agendar/dra-ana-silva) em vez de um link gerado automaticamente.'
|
||||||
|
)
|
||||||
|
ON CONFLICT (key) DO UPDATE
|
||||||
|
SET name = EXCLUDED.name,
|
||||||
|
descricao = EXCLUDED.descricao;
|
||||||
|
|
||||||
|
-- ── 2. Vincular aos planos ──────────────────────────────────────────────────
|
||||||
|
-- ATENÇÃO: ajuste os filtros de plan key/name conforme seus planos reais.
|
||||||
|
-- Exemplo: agendador.online disponível para planos PRO e acima.
|
||||||
|
-- agendador.link_personalizado apenas para planos Elite/Superior.
|
||||||
|
|
||||||
|
-- agendador.online → todos os planos com target 'therapist' ou 'clinic'
|
||||||
|
-- (Adapte o WHERE conforme necessário)
|
||||||
|
INSERT INTO public.plan_features (plan_id, feature_id, enabled)
|
||||||
|
SELECT
|
||||||
|
p.id,
|
||||||
|
f.id,
|
||||||
|
true
|
||||||
|
FROM public.plans p
|
||||||
|
CROSS JOIN public.features f
|
||||||
|
WHERE f.key = 'agendador.online'
|
||||||
|
AND p.is_active = true
|
||||||
|
-- Comente a linha abaixo para liberar para TODOS os planos:
|
||||||
|
-- AND p.key IN ('pro', 'elite', 'clinic_pro', 'clinic_elite')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- agendador.link_personalizado → apenas planos superiores
|
||||||
|
-- Deixe comentado e adicione manualmente quando definir os planos:
|
||||||
|
-- INSERT INTO public.plan_features (plan_id, feature_id, enabled)
|
||||||
|
-- SELECT p.id, f.id, true
|
||||||
|
-- FROM public.plans p
|
||||||
|
-- CROSS JOIN public.features f
|
||||||
|
-- WHERE f.key = 'agendador.link_personalizado'
|
||||||
|
-- AND p.key IN ('elite', 'clinic_elite', 'pro_plus')
|
||||||
|
-- ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- ── 3. Verificação ─────────────────────────────────────────────────────────
|
||||||
|
SELECT
|
||||||
|
f.key,
|
||||||
|
f.name,
|
||||||
|
COUNT(pf.plan_id) AS planos_vinculados
|
||||||
|
FROM public.features f
|
||||||
|
LEFT JOIN public.plan_features pf ON pf.feature_id = f.id AND pf.enabled = true
|
||||||
|
WHERE f.key IN ('agendador.online', 'agendador.link_personalizado')
|
||||||
|
GROUP BY f.key, f.name
|
||||||
|
ORDER BY f.key;
|
||||||
221
migrations/agendador_fix_slots.sql
Normal file
221
migrations/agendador_fix_slots.sql
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- FIX: agendador_slots_disponiveis + agendador_dias_disponiveis
|
||||||
|
-- Usa agenda_online_slots como fonte de slots
|
||||||
|
-- Cruzamento com: agenda_eventos, recurrence_rules/exceptions, agendador_solicitacoes
|
||||||
|
-- Execute no Supabase SQL Editor
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.agendador_slots_disponiveis(
|
||||||
|
p_slug text,
|
||||||
|
p_data date
|
||||||
|
)
|
||||||
|
RETURNS TABLE (hora time, disponivel boolean)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = 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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
-- ── 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) ───────────────────────────────
|
||||||
|
-- Loop explícito para evitar erros de tipo no cálculo do ciclo semanal
|
||||||
|
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
|
||||||
|
-- Calcula a primeira ocorrência do dia-da-semana a partir do start_date
|
||||||
|
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;
|
||||||
|
|
||||||
|
-- Ocorrência válida: diff >= 0 e divisível pelo ciclo semanal
|
||||||
|
IF v_day_diff >= 0 AND v_day_diff % (7 * v_rule.interval) = 0 THEN
|
||||||
|
|
||||||
|
-- Verifica se há exceção para esta data
|
||||||
|
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;
|
||||||
|
|
||||||
|
-- Sem exceção, ou exceção que não cancela → bloqueia o slot
|
||||||
|
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; -- já basta uma regra que conflite
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ── Recorrências remarcadas para este dia (reschedule → new_date = p_data) ─
|
||||||
|
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;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION public.agendador_slots_disponiveis(text, date) TO anon, authenticated;
|
||||||
|
|
||||||
|
|
||||||
|
-- ── agendador_dias_disponiveis ───────────────────────────────────────────────
|
||||||
|
CREATE OR REPLACE FUNCTION public.agendador_dias_disponiveis(
|
||||||
|
p_slug text,
|
||||||
|
p_ano int,
|
||||||
|
p_mes int
|
||||||
|
)
|
||||||
|
RETURNS TABLE (data date, tem_slots boolean)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = 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;
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION public.agendador_dias_disponiveis(text, int, int) TO anon, authenticated;
|
||||||
170
migrations/agendador_online.sql
Normal file
170
migrations/agendador_online.sql
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Agendador Online — tabelas de configuração e solicitações
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- ── 1. agendador_configuracoes ──────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS "public"."agendador_configuracoes" (
|
||||||
|
"owner_id" "uuid" NOT NULL,
|
||||||
|
"tenant_id" "uuid",
|
||||||
|
|
||||||
|
-- PRO / Ativação
|
||||||
|
"ativo" boolean DEFAULT false NOT NULL,
|
||||||
|
"link_slug" "text",
|
||||||
|
|
||||||
|
-- Identidade Visual
|
||||||
|
"imagem_fundo_url" "text",
|
||||||
|
"imagem_header_url" "text",
|
||||||
|
"logomarca_url" "text",
|
||||||
|
"cor_primaria" "text" DEFAULT '#4b6bff',
|
||||||
|
|
||||||
|
-- Perfil Público
|
||||||
|
"nome_exibicao" "text",
|
||||||
|
"endereco" "text",
|
||||||
|
"botao_como_chegar_ativo" boolean DEFAULT true NOT NULL,
|
||||||
|
"maps_url" "text",
|
||||||
|
|
||||||
|
-- Fluxo de Agendamento
|
||||||
|
"modo_aprovacao" "text" DEFAULT 'aprovacao' NOT NULL,
|
||||||
|
"modalidade" "text" DEFAULT 'presencial' 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
|
||||||
|
"pagamento_obrigatorio" boolean DEFAULT false NOT NULL,
|
||||||
|
"pix_chave" "text",
|
||||||
|
"pix_countdown_minutos" integer DEFAULT 20 NOT NULL,
|
||||||
|
|
||||||
|
-- Triagem & Conformidade
|
||||||
|
"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,
|
||||||
|
|
||||||
|
-- Textos
|
||||||
|
"mensagem_boas_vindas" "text",
|
||||||
|
"texto_como_se_preparar" "text",
|
||||||
|
"texto_termos_lgpd" "text",
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "agendador_configuracoes_pkey" PRIMARY KEY ("owner_id"),
|
||||||
|
CONSTRAINT "agendador_configuracoes_owner_fk"
|
||||||
|
FOREIGN KEY ("owner_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "agendador_configuracoes_tenant_fk"
|
||||||
|
FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "agendador_configuracoes_modo_check"
|
||||||
|
CHECK ("modo_aprovacao" = ANY (ARRAY['automatico','aprovacao'])),
|
||||||
|
CONSTRAINT "agendador_configuracoes_modalidade_check"
|
||||||
|
CHECK ("modalidade" = ANY (ARRAY['presencial','online','ambos'])),
|
||||||
|
CONSTRAINT "agendador_configuracoes_duracao_check"
|
||||||
|
CHECK ("duracao_sessao_min" >= 10 AND "duracao_sessao_min" <= 240),
|
||||||
|
CONSTRAINT "agendador_configuracoes_antecedencia_check"
|
||||||
|
CHECK ("antecedencia_minima_horas" >= 0 AND "antecedencia_minima_horas" <= 720),
|
||||||
|
CONSTRAINT "agendador_configuracoes_reserva_check"
|
||||||
|
CHECK ("reserva_horas" >= 1 AND "reserva_horas" <= 48),
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE "public"."agendador_configuracoes" ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "agendador_cfg_select" ON "public"."agendador_configuracoes";
|
||||||
|
CREATE POLICY "agendador_cfg_select" ON "public"."agendador_configuracoes"
|
||||||
|
FOR SELECT USING (auth.uid() = owner_id);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "agendador_cfg_write" ON "public"."agendador_configuracoes";
|
||||||
|
CREATE POLICY "agendador_cfg_write" ON "public"."agendador_configuracoes"
|
||||||
|
USING (auth.uid() = owner_id)
|
||||||
|
WITH CHECK (auth.uid() = owner_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "agendador_cfg_tenant_idx"
|
||||||
|
ON "public"."agendador_configuracoes" ("tenant_id");
|
||||||
|
|
||||||
|
-- ── 2. agendador_solicitacoes ───────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS "public"."agendador_solicitacoes" (
|
||||||
|
"id" "uuid" DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"owner_id" "uuid" NOT NULL,
|
||||||
|
"tenant_id" "uuid",
|
||||||
|
|
||||||
|
-- Dados do paciente
|
||||||
|
"paciente_nome" "text" NOT NULL,
|
||||||
|
"paciente_sobrenome" "text",
|
||||||
|
"paciente_email" "text" NOT NULL,
|
||||||
|
"paciente_celular" "text",
|
||||||
|
"paciente_cpf" "text",
|
||||||
|
|
||||||
|
-- Agendamento solicitado
|
||||||
|
"tipo" "text" NOT NULL,
|
||||||
|
"modalidade" "text" NOT NULL,
|
||||||
|
"data_solicitada" date NOT NULL,
|
||||||
|
"hora_solicitada" time NOT NULL,
|
||||||
|
|
||||||
|
-- Reserva temporária
|
||||||
|
"reservado_ate" timestamp with time zone,
|
||||||
|
|
||||||
|
-- Triagem
|
||||||
|
"motivo" "text",
|
||||||
|
"como_conheceu" "text",
|
||||||
|
|
||||||
|
-- Pagamento
|
||||||
|
"pix_status" "text" DEFAULT 'pendente',
|
||||||
|
"pix_pago_em" timestamp with time zone,
|
||||||
|
|
||||||
|
-- Status geral
|
||||||
|
"status" "text" DEFAULT 'pendente' NOT NULL,
|
||||||
|
"recusado_motivo" "text",
|
||||||
|
|
||||||
|
-- Autorização
|
||||||
|
"autorizado_em" timestamp with time zone,
|
||||||
|
"autorizado_por" "uuid",
|
||||||
|
|
||||||
|
-- Vínculos internos
|
||||||
|
"user_id" "uuid",
|
||||||
|
"patient_id" "uuid",
|
||||||
|
"evento_id" "uuid",
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "agendador_solicitacoes_pkey" PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "agendador_sol_owner_fk"
|
||||||
|
FOREIGN KEY ("owner_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "agendador_sol_tenant_fk"
|
||||||
|
FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "agendador_sol_status_check"
|
||||||
|
CHECK ("status" = ANY (ARRAY['pendente','autorizado','recusado','expirado'])),
|
||||||
|
CONSTRAINT "agendador_sol_tipo_check"
|
||||||
|
CHECK ("tipo" = ANY (ARRAY['primeira','retorno','reagendar'])),
|
||||||
|
CONSTRAINT "agendador_sol_modalidade_check"
|
||||||
|
CHECK ("modalidade" = ANY (ARRAY['presencial','online'])),
|
||||||
|
CONSTRAINT "agendador_sol_pix_check"
|
||||||
|
CHECK ("pix_status" IS NULL OR "pix_status" = ANY (ARRAY['pendente','pago','expirado']))
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE "public"."agendador_solicitacoes" ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "agendador_sol_owner_select" ON "public"."agendador_solicitacoes";
|
||||||
|
CREATE POLICY "agendador_sol_owner_select" ON "public"."agendador_solicitacoes"
|
||||||
|
FOR SELECT USING (auth.uid() = owner_id);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "agendador_sol_owner_write" ON "public"."agendador_solicitacoes";
|
||||||
|
CREATE POLICY "agendador_sol_owner_write" ON "public"."agendador_solicitacoes"
|
||||||
|
USING (auth.uid() = owner_id)
|
||||||
|
WITH CHECK (auth.uid() = owner_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "agendador_sol_owner_idx"
|
||||||
|
ON "public"."agendador_solicitacoes" ("owner_id", "status");
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "agendador_sol_tenant_idx"
|
||||||
|
ON "public"."agendador_solicitacoes" ("tenant_id");
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "agendador_sol_data_idx"
|
||||||
|
ON "public"."agendador_solicitacoes" ("data_solicitada", "hora_solicitada");
|
||||||
20
migrations/agendador_pagamento_modo.sql
Normal file
20
migrations/agendador_pagamento_modo.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
-- migrations/agendador_pagamento_modo.sql
|
||||||
|
-- Adiciona suporte a modo de pagamento no agendador online
|
||||||
|
-- Execute no Supabase SQL Editor
|
||||||
|
|
||||||
|
ALTER TABLE agendador_configuracoes
|
||||||
|
ADD COLUMN IF NOT EXISTS pagamento_modo text NOT NULL DEFAULT 'sem_pagamento',
|
||||||
|
ADD COLUMN IF NOT EXISTS pagamento_metodos_visiveis text[] NOT NULL DEFAULT '{}';
|
||||||
|
|
||||||
|
-- Migração de dados existentes:
|
||||||
|
-- quem tinha pagamento_obrigatorio = true → pix_antecipado
|
||||||
|
UPDATE agendador_configuracoes
|
||||||
|
SET pagamento_modo = 'pix_antecipado'
|
||||||
|
WHERE pagamento_obrigatorio = true
|
||||||
|
AND pagamento_modo = 'sem_pagamento';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN agendador_configuracoes.pagamento_modo IS
|
||||||
|
'sem_pagamento | pagar_na_hora | pix_antecipado';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN agendador_configuracoes.pagamento_metodos_visiveis IS
|
||||||
|
'Métodos exibidos ao paciente quando pagamento_modo = pagar_na_hora. Ex: {pix, deposito, dinheiro, cartao, convenio}';
|
||||||
219
migrations/agendador_publico.sql
Normal file
219
migrations/agendador_publico.sql
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Agendador Online — acesso público (anon) + função de slots disponíveis
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- ── 1. Geração automática de slug ──────────────────────────────────────────
|
||||||
|
-- Cria slug único de 8 chars quando o profissional ativa sem link_personalizado
|
||||||
|
CREATE OR REPLACE 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;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS agendador_slug_trigger ON public.agendador_configuracoes;
|
||||||
|
CREATE TRIGGER agendador_slug_trigger
|
||||||
|
BEFORE INSERT OR UPDATE ON public.agendador_configuracoes
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION public.agendador_gerar_slug();
|
||||||
|
|
||||||
|
-- ── 2. Políticas públicas (anon) ────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Leitura pública da config pelo slug (só ativo)
|
||||||
|
DROP POLICY IF EXISTS "agendador_cfg_public_read" ON public.agendador_configuracoes;
|
||||||
|
CREATE POLICY "agendador_cfg_public_read" ON public.agendador_configuracoes
|
||||||
|
FOR SELECT TO anon
|
||||||
|
USING (ativo = true AND link_slug IS NOT NULL);
|
||||||
|
|
||||||
|
-- Inserção pública de solicitações (qualquer pessoa pode solicitar)
|
||||||
|
DROP POLICY IF EXISTS "agendador_sol_public_insert" ON public.agendador_solicitacoes;
|
||||||
|
CREATE POLICY "agendador_sol_public_insert" ON public.agendador_solicitacoes
|
||||||
|
FOR INSERT TO anon
|
||||||
|
WITH CHECK (true);
|
||||||
|
|
||||||
|
-- Leitura da própria solicitação (pelo paciente logado)
|
||||||
|
DROP POLICY IF EXISTS "agendador_sol_patient_read" ON public.agendador_solicitacoes;
|
||||||
|
CREATE POLICY "agendador_sol_patient_read" ON public.agendador_solicitacoes
|
||||||
|
FOR SELECT TO authenticated
|
||||||
|
USING (auth.uid() = user_id OR auth.uid() = owner_id);
|
||||||
|
|
||||||
|
-- ── 3. Função: retorna slots disponíveis para uma data ──────────────────────
|
||||||
|
-- Roda como SECURITY DEFINER (acessa agenda_regras e agenda_eventos sem RLS)
|
||||||
|
CREATE OR REPLACE FUNCTION public.agendador_slots_disponiveis(
|
||||||
|
p_slug text,
|
||||||
|
p_data date
|
||||||
|
)
|
||||||
|
RETURNS TABLE (hora time, disponivel boolean)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_owner_id uuid;
|
||||||
|
v_duracao int;
|
||||||
|
v_reserva int;
|
||||||
|
v_antecedencia int;
|
||||||
|
v_dia_semana int; -- 0=dom..6=sab (JS) → convertemos
|
||||||
|
v_db_dow int; -- 0=dom..6=sab no Postgres (extract dow)
|
||||||
|
v_inicio time;
|
||||||
|
v_fim time;
|
||||||
|
v_slot time;
|
||||||
|
v_slot_fim time;
|
||||||
|
v_agora timestamptz;
|
||||||
|
BEGIN
|
||||||
|
-- carrega config do agendador
|
||||||
|
SELECT
|
||||||
|
c.owner_id,
|
||||||
|
c.duracao_sessao_min,
|
||||||
|
c.reserva_horas,
|
||||||
|
c.antecedencia_minima_horas
|
||||||
|
INTO v_owner_id, v_duracao, v_reserva, 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; -- 0=dom..6=sab
|
||||||
|
|
||||||
|
-- regra semanal para o dia da semana
|
||||||
|
SELECT hora_inicio, hora_fim
|
||||||
|
INTO v_inicio, v_fim
|
||||||
|
FROM public.agenda_regras_semanais
|
||||||
|
WHERE owner_id = v_owner_id
|
||||||
|
AND dia_semana = v_db_dow
|
||||||
|
AND ativo = true
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF v_inicio IS NULL THEN
|
||||||
|
RETURN; -- profissional não atende nesse dia
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- itera slots de v_duracao em v_duracao dentro da jornada
|
||||||
|
v_slot := v_inicio;
|
||||||
|
WHILE v_slot + (v_duracao || ' minutes')::interval <= v_fim LOOP
|
||||||
|
v_slot_fim := v_slot + (v_duracao || ' minutes')::interval;
|
||||||
|
|
||||||
|
-- bloco temporário para verificar conflitos
|
||||||
|
DECLARE
|
||||||
|
v_ocupado boolean := false;
|
||||||
|
v_slot_ts timestamptz;
|
||||||
|
BEGIN
|
||||||
|
-- antecedência mínima (compara em horário de Brasília)
|
||||||
|
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;
|
||||||
|
|
||||||
|
-- conflito com eventos existentes na agenda
|
||||||
|
IF NOT v_ocupado THEN
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM public.agenda_eventos
|
||||||
|
WHERE owner_id = v_owner_id
|
||||||
|
AND status::text NOT IN ('cancelado', 'faltou')
|
||||||
|
AND inicio_em AT TIME ZONE 'America/Sao_Paulo' >= p_data::timestamp
|
||||||
|
AND inicio_em AT TIME ZONE 'America/Sao_Paulo' < p_data::timestamp + interval '1 day'
|
||||||
|
AND (inicio_em AT TIME ZONE 'America/Sao_Paulo')::time < v_slot_fim
|
||||||
|
AND (fim_em AT TIME ZONE 'America/Sao_Paulo')::time > v_slot
|
||||||
|
) INTO v_ocupado;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- conflito com solicitações pendentes (reservadas)
|
||||||
|
IF NOT v_ocupado THEN
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM public.agendador_solicitacoes
|
||||||
|
WHERE owner_id = v_owner_id
|
||||||
|
AND status = 'pendente'
|
||||||
|
AND data_solicitada = p_data
|
||||||
|
AND hora_solicitada = v_slot
|
||||||
|
AND (reservado_ate IS NULL OR reservado_ate > v_agora)
|
||||||
|
) INTO v_ocupado;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
hora := v_slot;
|
||||||
|
disponivel := NOT v_ocupado;
|
||||||
|
RETURN NEXT;
|
||||||
|
END;
|
||||||
|
|
||||||
|
v_slot := v_slot + (v_duracao || ' minutes')::interval;
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION public.agendador_slots_disponiveis(text, date) TO anon, authenticated;
|
||||||
|
|
||||||
|
-- ── 4. Função: retorna dias com disponibilidade no mês ─────────────────────
|
||||||
|
CREATE OR REPLACE FUNCTION public.agendador_dias_disponiveis(
|
||||||
|
p_slug text,
|
||||||
|
p_ano int,
|
||||||
|
p_mes int -- 1-12
|
||||||
|
)
|
||||||
|
RETURNS TABLE (data date, tem_slots boolean)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_owner_id uuid;
|
||||||
|
v_antecedencia int;
|
||||||
|
v_data date;
|
||||||
|
v_data_inicio date;
|
||||||
|
v_data_fim date;
|
||||||
|
v_agora timestamptz;
|
||||||
|
v_db_dow int;
|
||||||
|
v_tem_regra 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
|
||||||
|
-- não oferece dias no passado ou dentro da antecedência mínima
|
||||||
|
IF v_data::timestamptz + '23:59:59'::interval > v_agora + (v_antecedencia || ' hours')::interval THEN
|
||||||
|
v_db_dow := extract(dow from v_data::timestamp)::int;
|
||||||
|
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM public.agenda_regras_semanais
|
||||||
|
WHERE owner_id = v_owner_id AND dia_semana = v_db_dow AND ativo = true
|
||||||
|
) INTO v_tem_regra;
|
||||||
|
|
||||||
|
IF v_tem_regra THEN
|
||||||
|
data := v_data;
|
||||||
|
tem_slots := true;
|
||||||
|
RETURN NEXT;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
v_data := v_data + 1;
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION public.agendador_dias_disponiveis(text, int, int) TO anon, authenticated;
|
||||||
19
migrations/agendador_status_convertido.sql
Normal file
19
migrations/agendador_status_convertido.sql
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- FIX: adiciona status 'convertido' na constraint de agendador_solicitacoes
|
||||||
|
-- e adiciona coluna motivo_recusa (alias amigável de recusado_motivo)
|
||||||
|
-- Execute no Supabase SQL Editor
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- 1. Remove o CHECK existente e recria com os novos valores
|
||||||
|
ALTER TABLE public.agendador_solicitacoes
|
||||||
|
DROP CONSTRAINT IF EXISTS "agendador_sol_status_check";
|
||||||
|
|
||||||
|
ALTER TABLE public.agendador_solicitacoes
|
||||||
|
ADD CONSTRAINT "agendador_sol_status_check"
|
||||||
|
CHECK (status = ANY (ARRAY[
|
||||||
|
'pendente',
|
||||||
|
'autorizado',
|
||||||
|
'recusado',
|
||||||
|
'expirado',
|
||||||
|
'convertido'
|
||||||
|
]));
|
||||||
56
migrations/agendador_storage.sql
Normal file
56
migrations/agendador_storage.sql
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Storage bucket para imagens do Agendador Online
|
||||||
|
-- Execute no Supabase SQL Editor
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- ── 1. Criar o bucket ──────────────────────────────────────────────────────
|
||||||
|
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
||||||
|
VALUES (
|
||||||
|
'agendador',
|
||||||
|
'agendador',
|
||||||
|
true, -- público (URLs diretas sem assinar)
|
||||||
|
5242880, -- 5 MB
|
||||||
|
ARRAY['image/jpeg','image/png','image/webp','image/gif']
|
||||||
|
)
|
||||||
|
ON CONFLICT (id) DO UPDATE
|
||||||
|
SET public = true,
|
||||||
|
file_size_limit = 5242880,
|
||||||
|
allowed_mime_types = ARRAY['image/jpeg','image/png','image/webp','image/gif'];
|
||||||
|
|
||||||
|
-- ── 2. Políticas ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Leitura pública (anon e authenticated)
|
||||||
|
DROP POLICY IF EXISTS "agendador_storage_public_read" ON storage.objects;
|
||||||
|
CREATE POLICY "agendador_storage_public_read"
|
||||||
|
ON storage.objects FOR SELECT
|
||||||
|
USING (bucket_id = 'agendador');
|
||||||
|
|
||||||
|
-- Upload: apenas o dono da pasta (owner_id é o primeiro segmento do path)
|
||||||
|
DROP POLICY IF EXISTS "agendador_storage_owner_insert" ON storage.objects;
|
||||||
|
CREATE POLICY "agendador_storage_owner_insert"
|
||||||
|
ON storage.objects FOR INSERT
|
||||||
|
TO authenticated
|
||||||
|
WITH CHECK (
|
||||||
|
bucket_id = 'agendador'
|
||||||
|
AND (storage.foldername(name))[1] = auth.uid()::text
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Update/upsert pelo dono
|
||||||
|
DROP POLICY IF EXISTS "agendador_storage_owner_update" ON storage.objects;
|
||||||
|
CREATE POLICY "agendador_storage_owner_update"
|
||||||
|
ON storage.objects FOR UPDATE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
bucket_id = 'agendador'
|
||||||
|
AND (storage.foldername(name))[1] = auth.uid()::text
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Delete pelo dono
|
||||||
|
DROP POLICY IF EXISTS "agendador_storage_owner_delete" ON storage.objects;
|
||||||
|
CREATE POLICY "agendador_storage_owner_delete"
|
||||||
|
ON storage.objects FOR DELETE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
bucket_id = 'agendador'
|
||||||
|
AND (storage.foldername(name))[1] = auth.uid()::text
|
||||||
|
);
|
||||||
71
migrations/payment_settings.sql
Normal file
71
migrations/payment_settings.sql
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
-- migrations/payment_settings.sql
|
||||||
|
-- Tabela de configurações de formas de pagamento por terapeuta/owner
|
||||||
|
-- Execute no Supabase SQL Editor
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS payment_settings (
|
||||||
|
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||||
|
owner_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
tenant_id uuid REFERENCES tenants(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Pix
|
||||||
|
pix_ativo boolean NOT NULL DEFAULT false,
|
||||||
|
pix_tipo text NOT NULL DEFAULT 'cpf', -- cpf | cnpj | email | celular | aleatoria
|
||||||
|
pix_chave text NOT NULL DEFAULT '',
|
||||||
|
pix_nome_titular text NOT NULL DEFAULT '',
|
||||||
|
|
||||||
|
-- Depósito / TED
|
||||||
|
deposito_ativo boolean NOT NULL DEFAULT false,
|
||||||
|
deposito_banco text NOT NULL DEFAULT '',
|
||||||
|
deposito_agencia text NOT NULL DEFAULT '',
|
||||||
|
deposito_conta text NOT NULL DEFAULT '',
|
||||||
|
deposito_tipo_conta text NOT NULL DEFAULT 'corrente', -- corrente | poupanca
|
||||||
|
deposito_titular text NOT NULL DEFAULT '',
|
||||||
|
deposito_cpf_cnpj text NOT NULL DEFAULT '',
|
||||||
|
|
||||||
|
-- Dinheiro (espécie)
|
||||||
|
dinheiro_ativo boolean NOT NULL DEFAULT false,
|
||||||
|
|
||||||
|
-- Cartão (maquininha presencial)
|
||||||
|
cartao_ativo boolean NOT NULL DEFAULT false,
|
||||||
|
cartao_instrucao text NOT NULL DEFAULT '',
|
||||||
|
|
||||||
|
-- Plano de saúde / Convênio
|
||||||
|
convenio_ativo boolean NOT NULL DEFAULT false,
|
||||||
|
convenio_lista text NOT NULL DEFAULT '', -- texto livre com convênios aceitos
|
||||||
|
|
||||||
|
-- Observações gerais exibidas ao paciente
|
||||||
|
observacoes_pagamento text NOT NULL DEFAULT '',
|
||||||
|
|
||||||
|
created_at timestamptz DEFAULT now(),
|
||||||
|
updated_at timestamptz DEFAULT now(),
|
||||||
|
|
||||||
|
CONSTRAINT payment_settings_owner_id_key UNIQUE (owner_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índice por tenant
|
||||||
|
CREATE INDEX IF NOT EXISTS payment_settings_tenant_id_idx ON payment_settings(tenant_id);
|
||||||
|
|
||||||
|
-- RLS
|
||||||
|
ALTER TABLE payment_settings ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Owner lê e escreve os próprios dados
|
||||||
|
DROP POLICY IF EXISTS "payment_settings: owner full access" ON payment_settings;
|
||||||
|
CREATE POLICY "payment_settings: owner full access"
|
||||||
|
ON payment_settings
|
||||||
|
FOR ALL
|
||||||
|
USING (owner_id = auth.uid())
|
||||||
|
WITH CHECK (owner_id = auth.uid());
|
||||||
|
|
||||||
|
-- updated_at automático
|
||||||
|
CREATE OR REPLACE FUNCTION update_payment_settings_updated_at()
|
||||||
|
RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = now();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trg_payment_settings_updated_at ON payment_settings;
|
||||||
|
CREATE TRIGGER trg_payment_settings_updated_at
|
||||||
|
BEFORE UPDATE ON payment_settings
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_payment_settings_updated_at();
|
||||||
61
migrations/professional_pricing.sql
Normal file
61
migrations/professional_pricing.sql
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
-- migrations/professional_pricing.sql
|
||||||
|
-- Fase 1: Precificação — tabela de preços padrão por profissional
|
||||||
|
-- Execute no Supabase SQL Editor
|
||||||
|
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
-- 1. Campo price em agenda_eventos
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
ALTER TABLE agenda_eventos
|
||||||
|
ADD COLUMN IF NOT EXISTS price numeric(10,2);
|
||||||
|
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
-- 2. Tabela de preços por profissional
|
||||||
|
-- Chave: owner_id + determined_commitment_id (NULL = padrão)
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS professional_pricing (
|
||||||
|
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||||
|
owner_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
determined_commitment_id uuid REFERENCES determined_commitments(id) ON DELETE SET NULL,
|
||||||
|
-- NULL = preço padrão (fallback quando não há match por tipo)
|
||||||
|
price numeric(10,2) NOT NULL,
|
||||||
|
notes text,
|
||||||
|
created_at timestamptz DEFAULT now(),
|
||||||
|
updated_at timestamptz DEFAULT now(),
|
||||||
|
|
||||||
|
CONSTRAINT professional_pricing_owner_commitment_key
|
||||||
|
UNIQUE (owner_id, determined_commitment_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índice por tenant (listagens do admin)
|
||||||
|
CREATE INDEX IF NOT EXISTS professional_pricing_tenant_idx
|
||||||
|
ON professional_pricing (tenant_id);
|
||||||
|
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
-- 3. RLS
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
ALTER TABLE professional_pricing ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Terapeuta lê e escreve seus próprios preços
|
||||||
|
DROP POLICY IF EXISTS "professional_pricing: owner full access" ON professional_pricing;
|
||||||
|
CREATE POLICY "professional_pricing: owner full access"
|
||||||
|
ON professional_pricing
|
||||||
|
FOR ALL
|
||||||
|
USING (owner_id = auth.uid())
|
||||||
|
WITH CHECK (owner_id = auth.uid());
|
||||||
|
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
-- 4. updated_at automático
|
||||||
|
-- ─────────────────────────────────────────────────────────────
|
||||||
|
CREATE OR REPLACE FUNCTION update_professional_pricing_updated_at()
|
||||||
|
RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = now();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trg_professional_pricing_updated_at ON professional_pricing;
|
||||||
|
CREATE TRIGGER trg_professional_pricing_updated_at
|
||||||
|
BEFORE UPDATE ON professional_pricing
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_professional_pricing_updated_at();
|
||||||
6
migrations/recurrence_rules_price.sql
Normal file
6
migrations/recurrence_rules_price.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
-- migrations/recurrence_rules_price.sql
|
||||||
|
-- Adiciona campo price em recurrence_rules para herança nas ocorrências virtuais
|
||||||
|
-- Execute no Supabase SQL Editor
|
||||||
|
|
||||||
|
ALTER TABLE recurrence_rules
|
||||||
|
ADD COLUMN IF NOT EXISTS price numeric(10,2);
|
||||||
6
migrations/remove_session_start_offset.sql
Normal file
6
migrations/remove_session_start_offset.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
-- Migration: remove session_start_offset_min from agenda_configuracoes
|
||||||
|
-- This field is replaced by hora_inicio in agenda_regras_semanais (work schedule per day)
|
||||||
|
-- The first session slot is now derived directly from hora_inicio of the work rule.
|
||||||
|
|
||||||
|
ALTER TABLE public.agenda_configuracoes
|
||||||
|
DROP COLUMN IF EXISTS session_start_offset_min;
|
||||||
235
migrations/support_sessions.sql
Normal file
235
migrations/support_sessions.sql
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Support Sessions — Sessões de suporte técnico SaaS
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Permite que admins SaaS gerem tokens de acesso temporário para debug
|
||||||
|
-- de agendas de terapeutas, sem expor debug para usuários comuns.
|
||||||
|
--
|
||||||
|
-- SEGURANÇA:
|
||||||
|
-- - RLS: só saas_admin pode criar/listar sessões
|
||||||
|
-- - Token é opaco (gen_random_uuid) — não adivinhável
|
||||||
|
-- - expires_at com TTL máximo de 60 minutos
|
||||||
|
-- - validate_support_session() retorna apenas true/false + tenant_id
|
||||||
|
-- (não expõe dados do admin)
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- ── Tabela ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "public"."support_sessions" (
|
||||||
|
"id" uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"tenant_id" uuid NOT NULL,
|
||||||
|
"admin_id" uuid NOT NULL,
|
||||||
|
"token" text NOT NULL DEFAULT (replace(gen_random_uuid()::text, '-', '') || replace(gen_random_uuid()::text, '-', '')),
|
||||||
|
"expires_at" timestamp with time zone NOT NULL
|
||||||
|
DEFAULT (now() + interval '60 minutes'),
|
||||||
|
"created_at" timestamp with time zone NOT NULL DEFAULT now(),
|
||||||
|
|
||||||
|
CONSTRAINT "support_sessions_pkey" PRIMARY KEY ("id"),
|
||||||
|
|
||||||
|
CONSTRAINT "support_sessions_tenant_fk"
|
||||||
|
FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE,
|
||||||
|
|
||||||
|
CONSTRAINT "support_sessions_admin_fk"
|
||||||
|
FOREIGN KEY ("admin_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE,
|
||||||
|
|
||||||
|
CONSTRAINT "support_sessions_token_unique" UNIQUE ("token")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ── Índices ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "support_sessions_token_idx"
|
||||||
|
ON "public"."support_sessions" ("token");
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "support_sessions_tenant_idx"
|
||||||
|
ON "public"."support_sessions" ("tenant_id");
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "support_sessions_expires_idx"
|
||||||
|
ON "public"."support_sessions" ("expires_at");
|
||||||
|
|
||||||
|
-- ── RLS ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
ALTER TABLE "public"."support_sessions" ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Somente saas_admin pode ver suas próprias sessões de suporte
|
||||||
|
DROP POLICY IF EXISTS "support_sessions_saas_select" ON "public"."support_sessions";
|
||||||
|
CREATE POLICY "support_sessions_saas_select"
|
||||||
|
ON "public"."support_sessions"
|
||||||
|
FOR SELECT
|
||||||
|
USING (
|
||||||
|
auth.uid() = admin_id
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM public.profiles
|
||||||
|
WHERE id = auth.uid()
|
||||||
|
AND role = 'saas_admin'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Somente saas_admin pode criar sessões de suporte
|
||||||
|
DROP POLICY IF EXISTS "support_sessions_saas_insert" ON "public"."support_sessions";
|
||||||
|
CREATE POLICY "support_sessions_saas_insert"
|
||||||
|
ON "public"."support_sessions"
|
||||||
|
FOR INSERT
|
||||||
|
WITH CHECK (
|
||||||
|
auth.uid() = admin_id
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM public.profiles
|
||||||
|
WHERE id = auth.uid()
|
||||||
|
AND role = 'saas_admin'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Somente saas_admin pode deletar suas próprias sessões
|
||||||
|
DROP POLICY IF EXISTS "support_sessions_saas_delete" ON "public"."support_sessions";
|
||||||
|
CREATE POLICY "support_sessions_saas_delete"
|
||||||
|
ON "public"."support_sessions"
|
||||||
|
FOR DELETE
|
||||||
|
USING (
|
||||||
|
auth.uid() = admin_id
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM public.profiles
|
||||||
|
WHERE id = auth.uid()
|
||||||
|
AND role = 'saas_admin'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ── RPC: create_support_session ───────────────────────────────────────────────
|
||||||
|
-- Cria uma sessão de suporte para um tenant.
|
||||||
|
-- Apenas saas_admin pode chamar. TTL: 60 minutos (configurável via p_ttl_minutes).
|
||||||
|
-- Retorna: token, expires_at
|
||||||
|
|
||||||
|
DROP FUNCTION IF EXISTS public.create_support_session(uuid, integer);
|
||||||
|
CREATE OR REPLACE FUNCTION public.create_support_session(
|
||||||
|
p_tenant_id uuid,
|
||||||
|
p_ttl_minutes integer DEFAULT 60
|
||||||
|
)
|
||||||
|
RETURNS json
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = 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;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ── RPC: validate_support_session ────────────────────────────────────────────
|
||||||
|
-- Valida um token de suporte. Não requer autenticação (chamada pública).
|
||||||
|
-- Retorna: { valid: bool, tenant_id: uuid|null }
|
||||||
|
-- NUNCA retorna admin_id ou dados internos.
|
||||||
|
|
||||||
|
DROP FUNCTION IF EXISTS public.validate_support_session(text);
|
||||||
|
CREATE OR REPLACE FUNCTION public.validate_support_session(
|
||||||
|
p_token text
|
||||||
|
)
|
||||||
|
RETURNS json
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = 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;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ── RPC: revoke_support_session ───────────────────────────────────────────────
|
||||||
|
-- Revoga um token manualmente. Apenas o admin que criou pode revogar.
|
||||||
|
|
||||||
|
DROP FUNCTION IF EXISTS public.revoke_support_session(text);
|
||||||
|
CREATE OR REPLACE FUNCTION public.revoke_support_session(
|
||||||
|
p_token text
|
||||||
|
)
|
||||||
|
RETURNS boolean
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = 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;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ── Cleanup automático (opcional) ────────────────────────────────────────────
|
||||||
|
-- Sessões expiradas podem ser limpas periodicamente via pg_cron ou edge function.
|
||||||
|
-- DELETE FROM public.support_sessions WHERE expires_at < now();
|
||||||
34
migrations/unify_patient_id.sql
Normal file
34
migrations/unify_patient_id.sql
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Unifica paciente_id → patient_id em agenda_eventos
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Contexto:
|
||||||
|
-- Campo legado `paciente_id` (texto, sem FK) coexiste com `patient_id`
|
||||||
|
-- (uuid, com FK → patients.id). Eventos antigos têm `paciente_id` preenchido
|
||||||
|
-- mas `patient_id = null`. Esta migration corrige isso e remove a coluna legada.
|
||||||
|
--
|
||||||
|
-- SEGURANÇA:
|
||||||
|
-- Execute em transação. Verifique os counts antes do COMMIT.
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- 1. Copia paciente_id → patient_id onde patient_id ainda é null
|
||||||
|
-- paciente_id já é uuid no banco — sem necessidade de cast ou validação de regex
|
||||||
|
UPDATE public.agenda_eventos
|
||||||
|
SET patient_id = paciente_id
|
||||||
|
WHERE patient_id IS NULL
|
||||||
|
AND paciente_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- 2. Verificação: deve retornar 0
|
||||||
|
SELECT COUNT(*) AS "orfaos_restantes"
|
||||||
|
FROM public.agenda_eventos
|
||||||
|
WHERE patient_id IS NULL AND paciente_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- 3. Remove a coluna legada
|
||||||
|
ALTER TABLE public.agenda_eventos DROP COLUMN IF EXISTS paciente_id;
|
||||||
|
|
||||||
|
-- 4. Remove FK e coluna legada de terapeuta_id se existir equivalente
|
||||||
|
-- (opcional — remova o comentário se quiser limpar terapeuta_id também)
|
||||||
|
-- ALTER TABLE public.agenda_eventos DROP COLUMN IF EXISTS terapeuta_id;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user