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