feat(agenda): auditoria/autoria, faltas, a-reagendar, presença + RBAC por cargo

Agenda (multi-secretária):
- audit_log genérico + autoria (created/updated/canceled_by) + soft-delete + version (lock otimista)
- status novo vocabulário + índice único PARCIAL (libera slot ao cancelar/faltar)
- endpoints: falta, remarcar, restaurar, historico, pendencias (a reagendar), faltas, familia
- presença: heartbeat (DB última visita + Dragonfly ao vivo, janela 45s)
- frontend: StackModal, AgendaDetailModal, AgendaPresence; lista a-reagendar; pacienteId no fluxo de criação; chips de família + badge de faltas no modal

RBAC por cargo: funcoes.permissoes/nivel; gate de menu por cargo (array vazio herda role); hierarquia só-gerencia-abaixo no backend

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Builder
2026-06-09 16:10:58 +02:00
parent b7bb455f56
commit f084656712
47 changed files with 15732 additions and 1843 deletions
+211
View File
@@ -0,0 +1,211 @@
# ScoreOdonto — Pendências, Tutoria e Deploy (dev)
> Documento de handoff. Última atualização: 2026-06-08.
> Contexto completo da arquitetura de agenda/workspaces: ver memória do projeto
> `project-agenda-workspace-architecture.md`.
---
## 1. Deploy no container de dev (IMPORTANTE)
**`dev.scoreodonto.com` NÃO é Vite HMR.** É servido pelo container
`scoreodontocom-scoreodonto-frontend-1` (projeto compose `scoreodontocom`,
de `docker-compose.yml`) rodando **nginx sobre um `vite build` estático**.
Edições no fonte **não aparecem sozinhas** — exigem rebuild + recreate manual.
### Banco é de PRODUÇÃO (compartilhado)
- Backend de dev (VPS 4) e produção (VPS 1) usam o **MESMO Postgres na VPS 3**
(`10.99.0.3:5432/scoreodonto`). Não há banco de dev separado.
- Migrações aditivas idempotentes rodam no startup (`runMigrations()` em `server.js`).
- Qualquer dado criado "testando" em dev é **dado real de produção** → sempre limpar.
- A constraint `vinculos_role_check` é DROP+ADD a cada boot. Ao deployar na prod,
o `server.js` de lá PRECISA incluir `'protetico'`, senão o ADD falha se já houver
vínculo protético criado via dev.
### Comandos de deploy
```bash
cd /home/deploy/stack/scoreodonto.com
# Frontend (server.js NÃO é hot-mount; só plugins/sessions/media são):
docker compose build scoreodonto-frontend && docker compose up -d --force-recreate scoreodonto-frontend
# Backend:
docker compose build scoreodonto-backend && docker compose up -d --force-recreate scoreodonto-backend
```
- Verificar: o hash do bundle em `https://dev.scoreodonto.com/` (`/assets/index-*.js`) muda.
- `vite build` NÃO faz type-check bloqueante → erros TS não quebram o build, mas
podem estourar em runtime. Conferir após deploy.
- Logs de migração: `docker logs --tail 20 scoreodontocom-scoreodonto-backend-1 | grep -i migration`
(esperado: `[MIGRATION] all migrations OK`).
### Efeito colateral relevante
Quando esta sessão começou, o dev servia um **build antigo** (`index-DWdouzmk.js`).
Os rebuilds passaram a publicar o **working tree atual**, que inclui vários arquivos
**untracked** (WIP de outra sessão): `OrthoView`, `OrthoReports`, `OrthoPhotoWorkspace`,
`TutoriaSection`, `SuperAdminView`, `GestaoEquipeView`, `Tutor*View`, `plugins/`, etc.
→ Bugs nesses arquivos só passaram a **aparecer agora** por causa do deploy, não por edição.
---
## 2. TUTORIA no modal de tratamento de orto — ✅ CAUSA RAIZ RESOLVIDA
**Relato do usuário:** "ferrou com a tutoria no modal que administra o tratamento de orto."
**CAUSA RAIZ (confirmada):** as telas WIP (`TutoriaSection`, `OrthoPhotoWorkspace`,
`PhotoAnnotator`, `TutorPainelView`, `TutorCandidaturaView`, `AdminGestaoTutoresView`)
chamavam **24 métodos do `HybridBackend` que não existiam** em `services/backend.ts`
(só `getCurrentUserName` existia) → crash `HybridBackend.X is not a function` assim que
o rebuild publicou esses componentes. Além disso o **backend não tinha nenhuma tabela
nem endpoint** de orto/tutoria.
**CORRIGIDO nesta sessão:**
- `services/backend.ts`: adicionados os 24 métodos via helper `_ortoJson` (chama o
endpoint real e **degrada com fallback seguro** se a rota faltar → fim do crash).
- `backend/server.js`: **9 tabelas** (`ortho_photo_session`, `ortho_photo`,
`ortho_photo_annotation`, `ortho_tutor_config`, `ortho_tutor_candidatura`,
`ortho_tutor_profile`, `ortho_tutoring_request`, `ortho_tutoring_message`,
`ortho_tutoring_photo_package`) + **25 endpoints** + upload real de fotos via `multer`
no volume persistente `/app/media`, servidas por `/api/orto/fotos/:id/raw`.
- `backend/package.json`: + `multer`.
- Validado: migrations OK, 9 tabelas no banco, endpoints respondendo (200/401/404).
Frontend e backend rebuildados → live no dev.
**FEITO depois:**
- [x] `OrthoDetailPanel` agora renderiza `OrthoPhotoWorkspace` + `TutoriaSection`
(no lugar do `PhotoCompare` placeholder) — a tutoria/fotos vivem no modal do paciente.
- [x] **Sessões mensais**: orto é mensal → `ortho_photo_session` ganhou `rotulo`+`data_ref`;
o workspace cria sessão por mês (input `month`) + especiais (INICIAL/FINALIZAÇÃO/
PÓS-CONTENÇÃO); tabs dinâmicas ordenadas. Validado end-to-end.
- [x] **Exportar documentação**: botão EXPORTAR → modal seleciona "completa" (tudo) ou
fotos específicas (checkbox por foto, agrupado por sessão) → abre view de impressão
(navegador salva como PDF). Sem dependência nova.
**FEITO depois (cont.):**
- [x] **Tratamentos de orto persistem**: tabelas `ortho_tratamento` + `ortho_evolucao`
(triagem/diagnóstico/flags em JSONB); endpoints GET/POST/PUT + `/evolucoes` +
`/finalizar`. `backend.ts` troca o mock t1/t2 por chamadas reais (mappers snake↔camel).
Botão "NOVO TRATAMENTO" abre modal de criação (paciente/plano/diagnóstico/triagem).
Adicionar evolução atualiza `fio_atual`/`ultima_consulta`; finalizar grava contenção.
Validado end-to-end.
- [x] Comparador cross-sessão (mesmo slot, sessão A × B / mês a mês) com modos
**lado a lado / slider / sobreposição**.
**FEITO depois (cont.):**
- [x] **MercadoPago real**: `POST /tutoria/candidaturas/:id/pagamento` cria uma
preference (Checkout Pro) com a `taxa_inscricao` da config, `external_reference=cand:<id>`,
`back_urls` e `notification_url` → devolve `init_point`. `POST /tutoria/mp-webhook`
(público) consulta o pagamento e, se `approved`, ativa o tutor automaticamente
(`ativarCandidaturaTutor`, reaproveitado pelo confirmar-pagamento do admin).
Sem `mp_access_token` na config → fallback honesto (ativação manual). Admin configura
token/taxa em `AdminGestaoTutoresView`. **Caveat:** não testado contra a API real do MP
(sem credenciais); fallback e webhook-noop validados.
**FEITO depois (cont.):**
- [x] **GestaoEquipeView corrigida** (estava roteada em `/gestao-equipe` e quebrava igual a
tutoria — chamava 12 métodos inexistentes). Backend: tabelas `setores`/`funcoes` +
colunas `vinculos.setor_id/funcao_id`; endpoints membros (GET com nomes, POST com
setor/função, **+PUT**, DELETE) e setores/funções CRUD (todos `tenantGuard`).
`backend.ts`: os 12 métodos. Validado end-to-end + isolamento de tenant (403).
- [x] **PDF nativo** (`jspdf` adicionado às deps): o EXPORTAR agora gera um PDF real
(fotos agrupadas por sessão, 3/linha, legenda do slot) via canvas→jsPDF; botão
IMPRIMIR mantém o print→PDF como alternativa. **Caveat:** geração é client-side
(jsPDF+canvas), validei só install/tsc/build, não a renderização no navegador.
**FEITO depois (cont.):**
- [x] **Planejamento visual** no `PhotoAnnotator`: grupo PLANEJ. com **vetor de
movimentação** (haste + barra de origem + cabeça larga, arrastar da posição atual →
destino), **marca de extração** (X, clique no dente) e **linha média** (referência
vertical tracejada, clique). Persistem como anotações (JSONB) — sem mudança no backend.
**FEITO depois (cont.):**
- [x] **Type-gaps zerados — `tsc` do frontend agora passa com 0 erros.** Bug real
corrigido em `FinanceiroView` (`getPacientes` retorna `{data,total}` → usava o objeto
inteiro como lista, quebrando a busca → agora `results.data`); `TutorPainelView` ganhou
os campos financeiros na interface `Solicitacao`. Login/RxConfig limparam junto.
**FEITO depois (cont.):**
- [x] **Calibração de escala (mm)** no `PhotoAnnotator`: ferramenta ESCALA (arrastar sobre
distância conhecida → informa o valor real em mm) define mm/px; os **vetores de
movimentação passam a exibir a magnitude em mm**. Rótulos renderizados fora do SVG
(HTML posicionado por `imgDim`) p/ não distorcer com o viewBox. Persiste no JSONB.
- [x] `RxConfigView` usava props inexistentes do `PageHeader` (`subtitle`/`icon`) → trocado
por `description` (subtítulo agora aparece). **Resta 1 erro tsc pré-existente** em
`LoginView:490` (comparação morta no fluxo de cadastro/auth) — deixado de propósito:
é lógica de auth e mascarar esconderia a intenção do autor; funciona em runtime.
- [x] Comparador ganhou modo **EVOLUÇÃO**: mesmo slot em todas as sessões/meses lado a
lado (faixa horizontal cronológica) — além de lado a lado / slider / sobreposição.
- [x] Vínculo com o cadastro de pacientes: o modal de Novo Tratamento tem busca no
cadastro (`getPacientes`) que preenche `paciente_id` + nome + nascimento; digitar
livre cria paciente avulso (sem vínculo). Frontend-only, validado por tsc/build.
---
## 2b. Apuração original (histórico)
**Apuração (fatos):**
- Não foi editado nenhum arquivo de orto/tutoria nesta sessão. Todos estão **untracked**
(`git status``??`): `OrthoView.tsx`, `OrthoReports.tsx`, `TutoriaSection.tsx`,
`TutorPainelView.tsx`, `TutorCandidaturaView.tsx`, `AdminGestaoTutoresView.tsx`.
- `OrthoView.tsx` **não tem nenhuma referência a "tutor"**.
- `TutoriaSection.tsx` **não é importada em lugar nenhum** (código solto/WIP).
- Quem referencia tutor: **`OrthoReports.tsx`** (e os Tutor*View).
- Único toque indireto possível: `services/backend.ts` (eu alterei `getCurrentRole`,
mas só com fallback p/ superadmin quando não há workspace — não afeta dentista/tutoria;
`getCurrentUserName`, usado pela TutoriaSection, NÃO foi alterado).
- Provável causa: o rebuild publicou a versão WIP da tutoria que antes não estava no ar.
**PENDENTE — preciso do sintoma para diagnosticar:**
- [ ] Onde exatamente? (OrthoView do paciente / OrthoReports / "Meus Tratamentos" / outro)
- [ ] O que acontece? (crash/tela branca / botão sumiu / dado não carrega / erro console)
- [ ] Mensagem de erro no console do navegador?
→ Com o sintoma, achar a causa raiz em `OrthoReports.tsx` / `TutoriaSection.tsx` e corrigir.
---
## 3. Status das fases (arquitetura agenda/workspaces)
- [x] **Fase 0** — Cadastro: 5 cards de perfil com preço (Clínica R$499 separada). `LoginView.tsx`.
- [x] **Fase 1** — Workspace pessoal automático no `/api/register` (dentista/protético/paciente);
`clinicas.tipo`+`owner_id`; `protetico` adicionado ao `vinculos_role_check`. Fim do beco "aguardando-convite".
- [x] **Fase 2** — Identidade global: `dentistas.usuario_id` + backfill por email; criação seta usuario_id.
- [x] **Fase 3** — Anti-overbooking global: `POST /api/agendamentos` resolve identidade do
profissional e bloqueia sobreposição em QUALQUER clínica → 409 `{conflito, ocupado, podeRegistrarInteresse}`.
- [~] **Fase 4** — Fila de interesse:
- [x] Backend: tabela `interesses_agenda`, `POST/GET /api/interesses`, hook no DELETE de
agendamento que notifica interessados como `enviado_por='sistema'` e marca `liberado`.
- [x] Frontend Parte 1: AgendaView mostra painel "Horário ocupado" no 409 → registrar interesse (com minutos de espera).
- [x] Frontend Parte 2: dentista vê "Pedidos de interesse" (sino + badge + modal; GET retorna `clinica_nome`).
- [x] `POST /api/clinicas` passou a setar `owner_id`+`tipo`.
- [ ] **FALTA**: endpoint **"tentar liberar"** (clínica pede → dentista decide mover/cancelar o ocupante).
- [ ] **FALTA**: **re-notificação periódica** do sistema enquanto o dentista demora (job/cron),
sempre rotulada como "notificação do sistema" (impessoal).
- [ ] **Fase 5** — Odontodelivery: diretório (`disponivel_diretorio` já existe) → pedido de horário ao freelancer.
### Regras de negócio confirmadas
- Profissional não pode estar em 2 lugares ao mesmo tempo (invariante: 1 agendamento por slot).
- Conflito não é "não" seco: vira fila de INTERESSE (não trava o slot). Clínica pode: registrar
interesse com tempo de espera (expira), escolher outro horário, ou pedir pro dentista liberar.
- Todos têm workspace (pessoal automático no cadastro).
---
## 4. Bugs pré-existentes encontrados (não introduzidos nesta sessão)
- [ ] **`notificacoes.lida` é smallint**, mas `POST /api/superadmin/notificacoes` insere boolean
`false`**recurso de notificação do superadmin quebrado** (corrigir para `0`).
(No meu código novo já usei `0`.)
- [ ] **`POST /api/agendamentos` default `status='Agendado'`** viola `agendamentos_status_check`
(só aceita Confirmado/Pendente/Concluido/Faltou) → criar sem status explícito dá 500.
Frontend hoje sempre manda status válido. Não há status 'Cancelado' (cancelar = DELETE).
- [ ] **`PUT /api/agendamentos/:id`** (mover horário) **ainda não passa pelo check de overlap** da Fase 3.
---
## 5. Estado de versionamento
- **NADA foi commitado** nesta sessão. Tudo está no working tree, rodando contra o banco de prod.
- Regra do projeto: **commit/push só com autorização explícita** — o push dispara o pipeline de
produção via webhook do Gitea (pode sobrescrever trabalho em andamento).
- Arquivos que ESTA sessão editou: `backend/server.js`, `frontend/App.tsx`,
`frontend/components/Sidebar.tsx`, `frontend/services/backend.ts`, `frontend/views/AgendaView.tsx`,
`frontend/views/LoginView.tsx`, `frontend/views/LandingPage.tsx`.
(Demais arquivos modificados/untracked são WIP de sessões anteriores.)