Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4673f3c606 | |||
| dc5a2d89ae | |||
| 7671199420 | |||
| 6cd8c1f9e1 | |||
| c084ad553b | |||
| cd89ca245c | |||
| 07d798c370 | |||
| 6a88995146 | |||
| f5a6dc7ea0 | |||
| b5d093d2ce | |||
| 489d2753ee | |||
| d357f3a2e8 | |||
| 2e90a2ba5f | |||
| 53cf2c52e6 | |||
| 8040e70310 | |||
| 8552e87665 | |||
| 28dc27a289 | |||
| f0b7a45d86 | |||
| 672c3b01e5 | |||
| 5f4460e37d | |||
| e05231663f | |||
| 84f4a797d2 | |||
| ae48feaa8a | |||
| c8d679eb70 | |||
| 3ffaff9d7d | |||
| 7ae6993c4b | |||
| 9f103be397 | |||
| f4f433b14c | |||
| 8cf4a83161 | |||
| 623c9990d6 | |||
| b80d061a6f | |||
| 064f36d614 | |||
| 50e1483ad5 | |||
| a1c203ced5 | |||
| 9ed8bc25bd | |||
| 8676308325 | |||
| 983582f066 | |||
| 718a7886e1 | |||
| 9b0ba65eca | |||
| 553efb003b | |||
| 321ca7e127 | |||
| d005fe77a9 | |||
| 0ade8027f5 | |||
| 8f9177a1c1 | |||
| 715b2dc81e | |||
| 8a27f3bc7c | |||
| 6adf353403 | |||
| f22e30e6aa | |||
| e83af044e0 | |||
| 5b99292b2d | |||
| 06674563d5 | |||
| 0fc69bbcd3 | |||
| 16c13ad35d | |||
| 5b58a8a01c | |||
| 116200ccc6 | |||
| 9ad65c358f | |||
| f4e7fe37b1 | |||
| 1559387a7c | |||
| 3aa6a444dc | |||
| fcf2fb15ba | |||
| 7c5b199d0e | |||
| 4e00e28bc2 | |||
| fc006221de | |||
| d77679a849 | |||
| 93bf37340f | |||
| 471c2c2603 | |||
| 5049ce9b8b | |||
| 0f50835114 | |||
| f2eb90ccc8 | |||
| a991f1fe2c | |||
| 6e81718725 | |||
| deaddfbf68 | |||
| efa9553536 | |||
| 96b857e408 | |||
| a89eed3ba6 |
+68
-34
@@ -1,24 +1,28 @@
|
||||
name: build-and-push
|
||||
name: build-and-promote
|
||||
|
||||
# Etapa 5: a cada push na main (ou tag vX.Y.Z), builda as imagens com o carimbo
|
||||
# de versão e envia ao Container Registry do Gitea. NÃO faz deploy (Etapa 6 separada).
|
||||
# Passo 5 — Build Once → Promote → Deploy:
|
||||
# • push na main → job BUILD: builda 1x e publica por COMMIT (:<sha> + :latest). NÃO deploya.
|
||||
# • tag vX.Y.Z → job PROMOTE: re-tagueia a imagem do commit como :vX.Y.Z (SEM rebuild),
|
||||
# valida integridade e deploya na VPS1.
|
||||
# A versão semântica NÃO entra no build: nasce da tag e é injetada no deploy em runtime
|
||||
# (docker-compose.prod.yml). Não há mais APP_VERSION/constants.ts.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
|
||||
jobs:
|
||||
# ───────────────────────── BUILD (só em push de branch) ─────────────────────────
|
||||
build:
|
||||
# Roda direto no host VPS4 (runner registrado com label de host + acesso ao Docker).
|
||||
if: startsWith(github.ref, 'refs/heads/')
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Calcular versão/commit/data
|
||||
- name: Vars (commit + data)
|
||||
id: vars
|
||||
run: |
|
||||
echo "version=$(grep -oE 'V[0-9]+\.[0-9]+\.[0-9]+' frontend/constants.ts | head -1 | tr 'V' 'v')" >> "$GITHUB_OUTPUT"
|
||||
echo "commit=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
|
||||
echo "build_time=$(date -u +'%Y-%m-%d %H:%M UTC')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -27,65 +31,95 @@ jobs:
|
||||
if [ -n "${{ secrets.REGISTRY_TOKEN }}" ]; then
|
||||
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.clube67.com -u ruicesar --password-stdin
|
||||
else
|
||||
echo "REGISTRY_TOKEN não definido — usando a credencial já presente no host (~/.docker/config.json)."
|
||||
echo "REGISTRY_TOKEN ausente — usando a credencial já presente no host."
|
||||
fi
|
||||
|
||||
- name: Build (compose, com carimbo de versão)
|
||||
- name: Build (carimba commit/data; versão vem da tag no deploy, não aqui)
|
||||
env:
|
||||
# Fixa o nome do projeto p/ a imagem buildada bater com o passo de tag
|
||||
# (em modo host o diretório do checkout muda o project name por padrão).
|
||||
COMPOSE_PROJECT_NAME: scoreodontocom
|
||||
APP_VERSION: ${{ steps.vars.outputs.version }}
|
||||
GIT_COMMIT: ${{ steps.vars.outputs.commit }}
|
||||
BUILD_TIME: ${{ steps.vars.outputs.build_time }}
|
||||
APP_ENV: PROD
|
||||
# SCOREO_DB_PASS tem fail-fast `:?` no compose (runtime). No build é só interpolado,
|
||||
# nunca entra na imagem (é `environment:`, não build-arg) — placeholder satisfaz o compose.
|
||||
SCOREO_DB_PASS: build-placeholder
|
||||
run: docker compose build scoreodonto-backend scoreodonto-frontend
|
||||
|
||||
- name: Tag + push (com retry p/ 499 transitório)
|
||||
- name: Publicar por COMMIT (:<sha> + :latest), com retry p/ 499
|
||||
run: |
|
||||
set -e
|
||||
V="${{ steps.vars.outputs.version }}"
|
||||
REG=git.clube67.com/ruicesar
|
||||
C="${{ steps.vars.outputs.commit }}"
|
||||
for app in backend frontend; do
|
||||
SRC="scoreodontocom-scoreodonto-${app}:latest"
|
||||
DST="git.clube67.com/ruicesar/scoreodonto-${app}"
|
||||
docker tag "$SRC" "$DST:$V"
|
||||
DST="${REG}/scoreodonto-${app}"
|
||||
docker tag "$SRC" "$DST:$C"
|
||||
docker tag "$SRC" "$DST:latest"
|
||||
for tag in "$V" "$C" "latest"; do
|
||||
for tag in "$C" "latest"; do
|
||||
n=0
|
||||
until docker push "$DST:$tag"; do
|
||||
n=$((n+1)); [ $n -ge 4 ] && { echo "push falhou após 4 tentativas: $DST:$tag"; exit 1; }
|
||||
n=$((n+1)); [ $n -ge 4 ] && { echo "push falhou: $DST:$tag"; exit 1; }
|
||||
echo "retry $n (499 transitório) em $DST:$tag"; sleep 3
|
||||
done
|
||||
done
|
||||
done
|
||||
echo "Publicado: scoreodonto-{backend,frontend}:$C (+ :latest). Push NÃO deploya."
|
||||
|
||||
- name: Resumo
|
||||
run: |
|
||||
echo "Imagens publicadas:"
|
||||
echo " git.clube67.com/ruicesar/scoreodonto-backend:${{ steps.vars.outputs.version }}"
|
||||
echo " git.clube67.com/ruicesar/scoreodonto-frontend:${{ steps.vars.outputs.version }}"
|
||||
echo "Deploy só acontece em TAG (job deploy)."
|
||||
|
||||
# Deploy automático SÓ em tags vX.Y.Z (a tag = 'aprovado para produção').
|
||||
# Push na main NÃO deploya — só builda. Etapa 6.
|
||||
deploy:
|
||||
needs: build
|
||||
# ─────────────────────── PROMOTE (só em tag vX.Y.Z) ───────────────────────
|
||||
promote:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Checkout (na tag)
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Promover a tag para produção (VPS1)
|
||||
- name: Vars (tag + commit)
|
||||
id: vars
|
||||
run: |
|
||||
echo "version=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "commit=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Login no registry
|
||||
run: |
|
||||
if [ -n "${{ secrets.REGISTRY_TOKEN }}" ]; then
|
||||
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.clube67.com -u ruicesar --password-stdin
|
||||
fi
|
||||
|
||||
- name: Integridade — a imagem do commit existe? (nunca buildar na tag)
|
||||
run: |
|
||||
set -e
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
echo "Promovendo $TAG para a produção (VPS1)..."
|
||||
# Garante que o mecanismo de deploy está atualizado na VPS1
|
||||
REG=git.clube67.com/ruicesar
|
||||
C="${{ steps.vars.outputs.commit }}"
|
||||
for app in backend frontend; do
|
||||
if ! docker buildx imagetools inspect "${REG}/scoreodonto-${app}:$C" >/dev/null 2>&1; then
|
||||
echo "ERRO: ${REG}/scoreodonto-${app}:$C não existe no registry."
|
||||
echo " Faça 'git push origin main' (que builda o commit $C) ANTES de criar a tag."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "OK: backend:$C e frontend:$C presentes — mesmo commit $C (front+back atômicos)."
|
||||
|
||||
- name: Promover — re-tag commit → versão (SEM rebuild) + conferir digest
|
||||
run: |
|
||||
set -e
|
||||
REG=git.clube67.com/ruicesar
|
||||
C="${{ steps.vars.outputs.commit }}"; V="${{ steps.vars.outputs.version }}"
|
||||
for app in backend frontend; do
|
||||
DST="${REG}/scoreodonto-${app}"
|
||||
docker buildx imagetools create -t "$DST:$V" "$DST:$C"
|
||||
dC=$(docker buildx imagetools inspect "$DST:$C" --format '{{.Manifest.Digest}}')
|
||||
dV=$(docker buildx imagetools inspect "$DST:$V" --format '{{.Manifest.Digest}}')
|
||||
[ "$dC" = "$dV" ] || { echo "ERRO: digest divergiu em $app ($V=$dV != $C=$dC)"; exit 1; }
|
||||
echo "$app: $V -> $C (digest $dV)"
|
||||
done
|
||||
echo "Promovido sem rebuild: a imagem :$V E a imagem :$C (mesmo artefato)."
|
||||
|
||||
- name: Deploy na VPS1 (pull :versao + up --no-build; versao injetada em runtime)
|
||||
run: |
|
||||
set -e
|
||||
V="${{ steps.vars.outputs.version }}"
|
||||
scp -o StrictHostKeyChecking=accept-new \
|
||||
docker-compose.prod.yml deploy-prod.sh \
|
||||
deploy@10.99.0.1:/home/deploy/stack/scoreodonto.com/
|
||||
ssh -o StrictHostKeyChecking=accept-new deploy@10.99.0.1 \
|
||||
"/home/deploy/stack/scoreodonto.com/deploy-prod.sh $TAG"
|
||||
"/home/deploy/stack/scoreodonto.com/deploy-prod.sh $V"
|
||||
|
||||
+13
@@ -12,8 +12,21 @@ backend/test-db-users.js
|
||||
backend/test-db-users.cjs
|
||||
backend/pg-convert.js
|
||||
|
||||
# Segredos de ambiente — NÃO versionar (qualquer nível)
|
||||
.env
|
||||
**/.env
|
||||
backend/.env
|
||||
.secrets
|
||||
**/.secrets
|
||||
*.dump
|
||||
backend/server.js.bak
|
||||
|
||||
# Dependências e caches locais
|
||||
node_modules/
|
||||
frontend/node_modules/
|
||||
backend/node_modules/
|
||||
.claude/
|
||||
|
||||
# backups de env (segredos) — nunca versionar
|
||||
*.env.bak*
|
||||
backend/.env.bak*
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# LEIA
|
||||
|
||||
A documentação deste projeto **NÃO fica aqui**. Fonte única (em português):
|
||||
**`git.clube67.com/ruicesar/Documentos-Unicos`** · local: `/home/deploy/stack/documentos-unicos/`
|
||||
Comece pelo `LEIA-PRIMEIRO.md`.
|
||||
|
||||
- Universal (vale para todos): `deploy/` e `infra/`
|
||||
- Específico deste projeto, **só se diferir** do universal: `projetos/scoreodonto.com/`
|
||||
|
||||
## Regras-chave
|
||||
- **Sempre testar em `https://dev.<dominio>`** antes de qualquer coisa.
|
||||
- **Pedir aprovação** do administrador antes de **produção** ou de **novas melhorias/implementações**.
|
||||
- **Em dúvida, consultar `desuso/`** (só consulta/histórico) — instrução nova vai **sempre** na doc oficial.
|
||||
- **Não duplicar**: se o tema já existe, não recriar. Preferir doc **universal**; o que diferir vai em `projetos/scoreodonto.com/` (citando o que difere).
|
||||
- **Nunca** criar documentação solta no projeto. **Tudo em português.**
|
||||
@@ -0,0 +1,35 @@
|
||||
# SCOREODONTO ENTERPRISE SKILLS
|
||||
|
||||
## REGRA Nº 1 — NÃO QUEBRAR O QUE JÁ FUNCIONA
|
||||
|
||||
Antes de qualquer alteração:
|
||||
|
||||
- Auditar impacto.
|
||||
- Identificar dependências.
|
||||
- Identificar telas afetadas.
|
||||
- Identificar APIs afetadas.
|
||||
- Identificar tabelas afetadas.
|
||||
- Identificar permissões afetadas.
|
||||
- Identificar integrações afetadas.
|
||||
|
||||
PROIBIDO:
|
||||
|
||||
- Refatorar sem auditoria.
|
||||
- Remover funcionalidades existentes sem validação.
|
||||
- Alterar identidade visual aprovada.
|
||||
- Alterar fluxo financeiro sem mapeamento.
|
||||
- Alterar entidades centrais sem documentação.
|
||||
|
||||
Toda implementação deve seguir:
|
||||
|
||||
Auditoria → Planejamento → Aprovação → Implementação → Validação.
|
||||
|
||||
Arquitetura oficial:
|
||||
|
||||
Pessoa → Vínculo → Workspace
|
||||
|
||||
Workspaces:
|
||||
- Clínica
|
||||
- Consultório
|
||||
- Laboratório
|
||||
- Sala
|
||||
@@ -0,0 +1,69 @@
|
||||
# SKILL — Modelagem Mestra
|
||||
|
||||
> **Skill raiz.** Todas as demais skills herdam destas regras. Baseado em evidências reais do
|
||||
> `backend/server.js` e do schema. Antes de qualquer alteração: **Auditoria → Planejamento → Aprovação → Implementação → Validação.**
|
||||
|
||||
## Conceito de negócio
|
||||
O ScoreOdonto **não é um sistema de clínica**; é uma **plataforma de pessoas** que atuam dentro de
|
||||
workspaces. A clínica é apenas **um tipo de workspace**. A arquitetura oficial é:
|
||||
|
||||
```
|
||||
Pessoa → Vínculo → Workspace → Serviços → Marketplace → Financeiro
|
||||
```
|
||||
|
||||
## Objetivo da skill
|
||||
Fixar o modelo mental único do sistema, para que nenhum módulo volte ao paradigma legado
|
||||
"Clínica → tudo". Toda entidade nova deve se posicionar nessa cadeia.
|
||||
|
||||
## Entidades centrais (fonte da verdade)
|
||||
| Camada | Tabela real | Papel |
|
||||
|---|---|---|
|
||||
| Pessoa | `usuarios` | Identidade única (login). Âncora de tudo. |
|
||||
| Vínculo | `vinculos` | Liga pessoa↔workspace com papel e RBAC. |
|
||||
| Workspace | `clinicas` (`tipo` ∈ pessoal/consultorio/clinica) + `salas` | Contexto operacional. |
|
||||
| Capacidades | `usuario_plugins`, `usuario_areas` | Atributos da pessoa (não do workspace). |
|
||||
|
||||
## Relacionamentos
|
||||
- `usuarios (1) → (N) vinculos (N) → (1) clinicas` — uma pessoa, vários workspaces, papel por workspace.
|
||||
- `clinicas.owner_id → usuarios.id` — todo workspace tem dono.
|
||||
- Ao registrar, cria-se automaticamente um **workspace pessoal** (`clinicas.tipo='pessoal'`).
|
||||
|
||||
## Regras obrigatórias
|
||||
1. **A pessoa (`usuarios`) é a âncora.** Capacidades (áreas, plugins, tutor) pertencem à pessoa.
|
||||
2. **Clínica/Consultório/Sala/Laboratório são `tipo` de workspace**, nunca entidades de topo.
|
||||
3. **Todo dado operacional é escopado por workspace** (`clinica_id`) via `tenantGuard`.
|
||||
4. **Nunca duplicar a pessoa** para representá-la em outro workspace — use `vinculos`.
|
||||
5. **Migração aditiva**: colunas novas via `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` (padrão atual).
|
||||
|
||||
## Casos de uso
|
||||
- Dentista atende em 3 clínicas → 1 `usuarios` + 3 `vinculos`.
|
||||
- Profissional autônomo sem clínica → `usuarios` + workspace `tipo='pessoal'`.
|
||||
- Locador de sala → `usuarios` + `salas` (workspace `tipo='sala'`).
|
||||
|
||||
## Anti-padrões (PROIBIDO)
|
||||
- ❌ Criar tabela-entidade nova "espelhando" a pessoa por workspace (vide débito de `dentistas`).
|
||||
- ❌ Tratar `clinica` como raiz hierárquica do sistema.
|
||||
- ❌ Guardar identidade/capacidade no workspace em vez de na pessoa.
|
||||
- ❌ Escrever dado operacional sem `clinica_id`.
|
||||
|
||||
## Impactos em outros módulos
|
||||
Esta skill governa **todas**: Pessoa, Workspace, Vínculos, Multiempresa, RBAC e todos os módulos
|
||||
de negócio. Qualquer conflito com ela é defeito de modelagem (gravidade ALTA).
|
||||
|
||||
## Débitos conhecidos (legado clínica-cêntrico ainda vivo)
|
||||
- `pacientes` (sem `usuario_id`, preso à clínica) e `dentistas` (tabela paralela a `usuarios`).
|
||||
- Workspace dividido em 2 tabelas (`clinicas` + `salas`).
|
||||
> Ver `../FASE_08_AUDITORIA/SKILL_Entidades.md`.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
P[Pessoa · usuarios] --> V[Vínculo · vinculos]
|
||||
V --> W{Workspace}
|
||||
W -->|tipo=clinica| C[Clínica]
|
||||
W -->|tipo=consultorio| K[Consultório]
|
||||
W -->|tipo=sala| S[Sala]
|
||||
W -->|tipo=pessoal| PP[Workspace Pessoal]
|
||||
P --> CAP[Capacidades · areas/plugins]
|
||||
W --> OP[Operação · pacientes/agenda/financeiro]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,48 @@
|
||||
# SKILL — Multiempresa (Tenancy)
|
||||
|
||||
> Herda de `SKILL_ModelagemMestra.md`. Regra de isolamento de dados entre workspaces.
|
||||
|
||||
## Conceito de negócio
|
||||
Cada workspace é um inquilino (tenant). Dados de uma clínica **nunca** podem vazar para outra.
|
||||
A mesma pessoa pode transitar entre tenants via vínculos, mas os **dados ficam no tenant**.
|
||||
|
||||
## Objetivo
|
||||
Garantir isolamento por `clinica_id` em 100% da operação e impedir queries sem escopo.
|
||||
|
||||
## Entidades envolvidas
|
||||
- Toda tabela operacional tem `clinica_id` (`pacientes`, `agendamentos`, `financeiro`,
|
||||
`procedimentos_realizados`, `orcamentos`, `contratos`, `repasses`, `glosas`, `protese_os`…).
|
||||
- `audit_log` por `clinica_id`.
|
||||
|
||||
## Relacionamentos / mecanismo real
|
||||
- **`tenantGuard`** (middleware) injeta `req.clinicaId` e é exigido na maioria das 277 rotas.
|
||||
- **`authGuard`** é o plano de identidade (rotas `me/*`) — sem tenant.
|
||||
- O cliente envia o workspace ativo; o backend valida o vínculo antes de servir dados.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. **Toda query operacional filtra `clinica_id = $req.clinicaId`.** Sem exceção.
|
||||
2. Rota que toca dado de clínica usa `tenantGuard`; rota de identidade usa `authGuard`.
|
||||
3. `superadmin` é o único plano transversal (gestão global) — auditar com cuidado.
|
||||
4. Marketplace cross-tenant (salas/propostas) é **acoplamento intencional** e explícito.
|
||||
|
||||
## Casos de uso
|
||||
- Listar pacientes → sempre do `clinica_id` ativo.
|
||||
- Trocar workspace → recomputar tudo no novo `clinica_id`.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ `SELECT ... FROM pacientes` sem `clinica_id`.
|
||||
- ❌ Confiar em `clinica_id` vindo do corpo do request sem validar o vínculo.
|
||||
- ❌ Agregar entre clínicas fora dos fluxos de marketplace/superadmin.
|
||||
|
||||
## Impactos
|
||||
Base de segurança de TODOS os módulos. Falha aqui = vazamento de dados (gravidade ALTA) + risco LGPD.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
REQ[Request] --> AG{Guard}
|
||||
AG -->|authGuard| ID[Identidade · me/*]
|
||||
AG -->|tenantGuard| TEN[req.clinicaId]
|
||||
TEN --> Q[Query SEMPRE com clinica_id]
|
||||
Q --> DB[(Postgres)]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,48 @@
|
||||
# SKILL — Pessoa
|
||||
|
||||
> Herda de `SKILL_ModelagemMestra.md`. A Pessoa é a **âncora única** do sistema.
|
||||
|
||||
## Conceito de negócio
|
||||
Toda interação no ScoreOdonto parte de uma **pessoa** (humano com login). Papéis (dentista,
|
||||
biomédico, protético, paciente, tutor, dono) são **facetas** da mesma pessoa, não entidades separadas.
|
||||
|
||||
## Objetivo
|
||||
Garantir que identidade, capacidades e histórico fiquem na pessoa — nunca duplicados por workspace.
|
||||
|
||||
## Entidades envolvidas
|
||||
- `usuarios` — identidade (login, `role` global, `is_tutor`).
|
||||
- `usuario_areas` — áreas de atuação (multi-área; ref. `areas`).
|
||||
- `usuario_plugins` — capacidades/add-ons por pessoa (`me/plugins`).
|
||||
- `vinculos` — onde a pessoa atua e com qual papel.
|
||||
|
||||
## Relacionamentos
|
||||
- `usuarios (1) → (N) vinculos` — pessoa em vários workspaces.
|
||||
- `usuarios (1) → (N) usuario_areas` e `(N) usuario_plugins`.
|
||||
- Marketplace de profissionais: `propostas_profissional.profissional_id → usuarios.id`.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. **Uma pessoa = um `usuarios`** (idealmente deduplicado por CPF/e-mail).
|
||||
2. **Capacidades vivem na pessoa**, não no workspace.
|
||||
3. `role` global em `usuarios` só para `superadmin`; os demais papéis vêm de `vinculos.role`.
|
||||
4. Toda rota de identidade usa `authGuard` (não `tenantGuard`).
|
||||
|
||||
## Casos de uso
|
||||
- Pessoa é dentista numa clínica e dono de sala em outra → 2 vínculos, papéis distintos.
|
||||
- Pessoa ativa o plugin de Salas → grava em `usuario_plugins` (vale em qualquer dispositivo).
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Criar `dentistas`/`proteticos` como tabela de identidade paralela (débito atual de `dentistas`).
|
||||
- ❌ Persistir capacidade só no `localStorage` sem refletir em `usuario_plugins` (risco de divergência).
|
||||
|
||||
## Impactos em outros módulos
|
||||
Base de Vínculos, RBAC, Marketplace, Tutoria, Pessoas (Fase 3). Mudou a pessoa → reflete em todos.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
U[usuarios] --> A[usuario_areas]
|
||||
U --> PL[usuario_plugins]
|
||||
U --> V[vinculos]
|
||||
U --> PR[propostas_profissional]
|
||||
V --> WS[Workspaces]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,47 @@
|
||||
# SKILL — RBAC (Controle de Acesso)
|
||||
|
||||
> Herda de `SKILL_Vinculos.md`. Define quem vê e faz o quê, por papel e função, no workspace ativo.
|
||||
|
||||
## Conceito de negócio
|
||||
Acesso é decidido pelo **papel da pessoa no workspace ativo** + permissões finas da `funcao`.
|
||||
O frontend espelha o backend: menu e telas seguem a mesma matriz de papéis.
|
||||
|
||||
## Objetivo
|
||||
Padronizar a decisão de acesso e manter front/back coerentes (sem permissão hardcoded).
|
||||
|
||||
## Entidades / mecanismo real
|
||||
- **Backend:** `tenantGuard`/`authGuard`; `vinculos.role` + `funcoes` (permissões); `usuarios.role` só p/ `superadmin`.
|
||||
- **Frontend:** `App.tsx → isViewAllowed(view, role)` e o filtro de menu em `components/Sidebar.tsx`.
|
||||
- Papéis: `superadmin` (global), `admin`/`donoclinica`/`donoconsultorio` (amplo), `funcionario`,
|
||||
`dentista`, `biomedico`, `protetico`, `paciente`, `donosala`.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. **Decisão por papel/função**, nunca por identidade individual.
|
||||
2. Quando a `funcao` define permissões explícitas (`permissoes[]`), elas **prevalecem** sobre o default do papel.
|
||||
3. Telas `superadmin*` e o catálogo de `plugins` são **exclusivos do superadmin**.
|
||||
4. Workspace `tipo='sala'` restringe o menu a painel da sala/conta/notificações.
|
||||
5. **Mudou regra de acesso no backend → atualizar `isViewAllowed` e `Sidebar` no mesmo PR.**
|
||||
|
||||
## Casos de uso
|
||||
- Biomédico não vê Ortodontia/RX (não-odonto) — regra explícita no menu.
|
||||
- Cargo customizado com `permissoes:['agenda','financeiro']` → menu reduzido a isso.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Liberar tela no front sem proteger a rota no back (e vice-versa).
|
||||
- ❌ Checar `if (email === 'x')`.
|
||||
- ❌ Esquecer de espelhar a permissão nos dois lados (front/back divergentes).
|
||||
|
||||
## Impactos
|
||||
Governa todo o menu, todas as rotas e a experiência por papel. Ver `../FASE_08_AUDITORIA/SKILL_Rotas.md`.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
P[Pessoa] --> V[vinculo.role no workspace ativo]
|
||||
V --> F{tem funcao com permissoes?}
|
||||
F -->|sim| PERM[usa permissoes da funcao]
|
||||
F -->|não| DEF[usa default do papel]
|
||||
PERM --> UI[Sidebar + isViewAllowed]
|
||||
DEF --> UI
|
||||
UI --> API[rota protegida por guard]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,46 @@
|
||||
# SKILL — Vínculos
|
||||
|
||||
> Herda de `SKILL_ModelagemMestra.md`. O vínculo é a **ponte** pessoa↔workspace e o lugar do papel.
|
||||
|
||||
## Conceito de negócio
|
||||
Uma pessoa não "é" dentista no sistema — ela **tem um vínculo de papel dentista** num workspace.
|
||||
Mudou de workspace, muda o papel. É o que permite multi-clínica sem duplicar a pessoa.
|
||||
|
||||
## Objetivo
|
||||
Centralizar papel + RBAC na relação, nunca na identidade nem no workspace.
|
||||
|
||||
## Entidades envolvidas
|
||||
- `vinculos(id, usuario_id, clinica_id, role, setor_id, funcao_id)`.
|
||||
- `funcoes` (cargos com permissões) e `setores` — por workspace.
|
||||
- Constraint real de papéis (`vinculos_role_check`):
|
||||
`paciente, dentista, biomedico, protetico, funcionario, donoclinica, donoconsultorio, donosala, admin`.
|
||||
|
||||
## Relacionamentos
|
||||
- `vinculos.usuario_id → usuarios.id`; `vinculos.clinica_id → clinicas.id`.
|
||||
- `vinculos.funcao_id → funcoes.id` (permissões finas); `vinculos.setor_id → setores.id`.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. **Papel operacional vem do vínculo** (`vinculos.role`), não de `usuarios` (exceto `superadmin`).
|
||||
2. **N vínculos por pessoa** — multi-workspace é nativo. Nunca duplicar a pessoa.
|
||||
3. A permissão efetiva = papel do vínculo **+** permissões da `funcao` (quando houver).
|
||||
4. Remover acesso = remover/alterar o vínculo, jamais apagar a pessoa.
|
||||
|
||||
## Casos de uso
|
||||
- Convidar membro: cria `vinculos` com `role` + `funcao_id`.
|
||||
- Profissional sai da clínica: deleta o vínculo; histórico e identidade permanecem.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Guardar papel em tabela-recurso (débito de `dentistas`).
|
||||
- ❌ Permissão hardcoded por e-mail/nome em vez de papel/função.
|
||||
|
||||
## Impactos
|
||||
Base direta de RBAC, Multiempresa, Gestão de Equipe e do menu/`isViewAllowed` do frontend.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
U[usuarios] --> VN[vinculos: role, setor, funcao]
|
||||
VN --> CL[clinicas/workspace]
|
||||
VN --> FU[funcoes: permissões]
|
||||
VN --> SE[setores]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,50 @@
|
||||
# SKILL — Workspace
|
||||
|
||||
> Herda de `SKILL_ModelagemMestra.md`. Workspace é o **contexto operacional** onde a pessoa atua.
|
||||
|
||||
## Conceito de negócio
|
||||
Todo dado operacional (pacientes, agenda, financeiro) pertence a um **workspace**, nunca diretamente
|
||||
à pessoa. A clínica é o workspace mais comum, mas é só um `tipo`.
|
||||
|
||||
## Objetivo
|
||||
Padronizar o conceito de workspace e impedir o retorno ao modelo clínica-cêntrico.
|
||||
|
||||
## Entidades envolvidas
|
||||
- `clinicas` — tabela de workspace. Colunas-chave: `tipo` (`pessoal`|`consultorio`|`clinica`),
|
||||
`owner_id`, `cor`, `geo` (PostGIS). **Hoje "clinicas" É a tabela de workspaces.**
|
||||
- `salas` — workspace `tipo='sala'` (tabela separada — débito de unificação).
|
||||
- `vinculos` — quem pertence ao workspace e com qual papel.
|
||||
|
||||
## Relacionamentos
|
||||
- `clinicas.owner_id → usuarios.id`.
|
||||
- Operação: `pacientes.clinica_id`, `agendamentos.clinica_id`, `financeiro.clinica_id`, etc.
|
||||
- Agregação no app: `GET /api/me/workspaces` → `listarWorkspaces()` reúne clínicas (via vínculo) + salas.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. **Tudo que é operacional carrega `clinica_id`** e passa por `tenantGuard`.
|
||||
2. O **workspace ativo** (cacheado no cliente) governa menu, permissões e dados visíveis.
|
||||
3. `tipo='sala'` **isola o contexto** (menu reduzido: painel da sala/conta/notificações).
|
||||
4. Workspace pessoal (`tipo='pessoal'`) é criado automaticamente no registro.
|
||||
|
||||
## Casos de uso
|
||||
- Trocar de workspace no seletor → recarrega contexto (cor de marca, menu, dados).
|
||||
- Sala como workspace → usuário cai em `sala-home`, sem acesso à operação clínica.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Ler/gravar operação sem `clinica_id`.
|
||||
- ❌ Tratar `salas` e `clinicas` como conceitos distintos na lógica de negócio (são ambos workspace).
|
||||
- ❌ Vazar dados entre workspaces (sempre filtrar por `req.clinicaId`).
|
||||
|
||||
## Impactos
|
||||
Governa Multiempresa, RBAC e todos os módulos de Workspace (Fase 2) e operação.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
WS[Workspace] -->|clinicas.tipo| T1[clinica]
|
||||
WS --> T2[consultorio]
|
||||
WS --> T3[pessoal]
|
||||
WS -->|salas| T4[sala]
|
||||
WS --> OP[pacientes / agenda / financeiro · clinica_id]
|
||||
WS --> VN[vinculos · membros + papéis]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,42 @@
|
||||
# SKILL — Clínicas (Workspace)
|
||||
|
||||
> Herda de `SKILL_Workspace.md`. Tipo de workspace mais completo do sistema.
|
||||
|
||||
## O que é?
|
||||
Workspace `clinicas.tipo='clinica'` — a empresa odontológica com equipe, pacientes e operação plena.
|
||||
|
||||
## Quem usa?
|
||||
Donos (`donoclinica`/`admin`), funcionários, dentistas, biomédicos, protéticos vinculados.
|
||||
|
||||
## Quais módulos possui?
|
||||
Dashboard, Agenda, Pacientes, Ortodontia, Próteses/GTO, Financeiro, Comissões, Repasses, Glosas,
|
||||
Orçamentos, Contratos, Relatórios, Leads, Equipe/RBAC, Planos/Procedimentos/Especialidades,
|
||||
RX (plugin), Configurações. (Menu completo — ver `../FASE_08_AUDITORIA/SKILL_Rotas.md`.)
|
||||
|
||||
## Quais receitas gera?
|
||||
Produção de procedimentos (RECEITA), contratos/cobrança, recuperação de glosas. Paga repasses (DESPESA).
|
||||
> Detalhe do dinheiro em `../FASE_04_FINANCEIRO/`.
|
||||
|
||||
## Quais permissões possui?
|
||||
Donos/admin: acesso amplo (menos telas `superadmin`). Demais papéis: subconjunto por papel/função (RBAC).
|
||||
|
||||
## Entidades envolvidas
|
||||
`clinicas` + toda a operação por `clinica_id` + `vinculos`/`funcoes`/`setores`.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. Operação sempre escopada por `clinica_id` (`tenantGuard`).
|
||||
2. A clínica é **um tipo de workspace**, não a raiz do sistema.
|
||||
3. Equipe entra por vínculo; permissões por papel/função.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Assumir "uma clínica por usuário" (a pessoa pode ter várias).
|
||||
- ❌ Tratar clínica como dona da pessoa.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
CL[Clínica] --> EQ[Equipe via vinculos]
|
||||
CL --> PAC[Pacientes]
|
||||
CL --> AG[Agenda]
|
||||
CL --> FIN[Financeiro → Comissão → Repasse]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,37 @@
|
||||
# SKILL — Consultórios (Workspace)
|
||||
|
||||
> Herda de `SKILL_Workspace.md`. Variante enxuta da clínica.
|
||||
|
||||
## O que é?
|
||||
Workspace `clinicas.tipo='consultorio'` — operação individual/pequena de um profissional-dono.
|
||||
Mesma tabela e mecânica da clínica; muda o `tipo` e o papel do dono (`donoconsultorio`).
|
||||
|
||||
## Quem usa?
|
||||
`donoconsultorio` (profissional que opera por conta própria) e eventuais membros.
|
||||
|
||||
## Quais módulos possui?
|
||||
Os mesmos da clínica (herda a operação), normalmente com equipe menor. O `tipo` distingue rótulo/UX,
|
||||
não um conjunto de tabelas separado.
|
||||
|
||||
## Quais receitas gera?
|
||||
Iguais às da clínica (produção, contratos), em escala individual.
|
||||
|
||||
## Quais permissões possui?
|
||||
`donoconsultorio` tem acesso amplo (não-`superadmin`), idêntico ao dono de clínica.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. **Consultório = `tipo` de workspace**, não um módulo nem uma entidade separada.
|
||||
2. `donoconsultorio` é o **papel do dono**; o `tipo='consultorio'` é o **workspace**.
|
||||
3. Não criar tabelas próprias de "consultório" — reutilizar a operação por `clinica_id`.
|
||||
|
||||
## Anti-padrões / Débito conhecido
|
||||
- ⚠️ Acoplamento **role↔tipo** (`donoconsultorio`⇄`tipo='consultorio'`): decisão duplicada.
|
||||
Ao ramificar lógica, prefira o `tipo` do workspace, não a role.
|
||||
- ❌ Duplicar telas/tabelas só para "consultório".
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
K[Consultório · tipo=consultorio] --> OWN[Dono · donoconsultorio]
|
||||
K --> OP[Operação reutilizada por clinica_id]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,43 @@
|
||||
# SKILL — Delivery (Profissionais temporários) — ALVO / ROADMAP
|
||||
|
||||
> Herda de `SKILL_Marketplace.md`. **ATENÇÃO: não existe no código hoje.** Esta skill descreve o conceito-alvo
|
||||
> e as **sementes reais** sobre as quais ele se apoiaria — sem inventar funcionalidade.
|
||||
|
||||
## O que NÃO existe hoje (estado real)
|
||||
Não há nenhuma implementação de "delivery"/plantão/diária de profissional: sem contratação temporária,
|
||||
sem vínculo por período, sem pagamento de profissional avulso. Buscas por `delivery/temporario/plantao/
|
||||
freela/substituto` no `server.js` **não retornam feature** — apenas os fragmentos abaixo.
|
||||
|
||||
## Sementes reais já existentes (base para o alvo)
|
||||
1. **Marketplace de Profissionais**: `profissionais-disponiveis`, `profissionais/marketplace`, `propostas`
|
||||
(clínica→profissional). Hoje só **mensagem/conexão**, status `pendente`.
|
||||
2. **Modelos de período** (vindos das Salas): `RESERVA_MODELOS = ['hora','turno','diaria','semanal','mensal']`
|
||||
e `valor_diaria`/`diaria_consulta` — o conceito de **diária** já está modelado para salas.
|
||||
3. **Vínculos temporários**: `vinculos` já suporta papel por workspace — a base para um vínculo com prazo.
|
||||
|
||||
## Conceito-alvo (Delivery)
|
||||
Profissional aceita uma **escala temporária** numa clínica (ex.: diária/turno), com início/fim, valor e
|
||||
pagamento — unindo a proposta de profissional ao modelo de período e a um vínculo com prazo.
|
||||
|
||||
## O que faltaria (lacunas — descrever, NÃO implementar)
|
||||
- Evoluir `propostas_profissional` de mensagem → **acordo com período + valor + aceite**.
|
||||
- Vínculo temporário (`vinculos` com janela de validade) e seu encerramento automático.
|
||||
- Captura de pagamento (mesma lacuna de Salas) e repasse ao profissional avulso.
|
||||
|
||||
## Regras obrigatórias (se um dia evoluir)
|
||||
1. Construir **sobre as sementes** (propostas + modelos de período + vínculos), sem entidade paralela nova.
|
||||
2. Manter Pessoa→Vínculo→Workspace: delivery é **vínculo temporário**, não nova classe de pessoa.
|
||||
3. Seguir Auditoria → Planejamento → **Aprovação** antes de qualquer implementação.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Documentar/assumir Delivery como se existisse.
|
||||
- ❌ Criar tabela/role nova "delivery" em vez de reusar `vinculos` + `propostas`.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
PROF[Profissional · perfil] --> PROP[Proposta hoje: mensagem]
|
||||
PROP -. alvo .-> ACORDO[Acordo: período + valor + aceite]
|
||||
ACORDO -. alvo .-> VT[vinculo temporário]
|
||||
VT -. alvo .-> PAY[(pagamento + repasse)]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,46 @@
|
||||
# SKILL — Laboratórios (Workspace-alvo)
|
||||
|
||||
> Herda de `SKILL_Workspace.md`. **Atenção:** o estado ATUAL difere da arquitetura-alvo.
|
||||
|
||||
## O que é? (estado real hoje)
|
||||
Hoje o laboratório é um **cadastro interno por clínica** (`protese_laboratorios`), usado nas Ordens de
|
||||
Serviço de prótese e no **custo lab** que abate comissão. **Não** tem identidade própria nem login,
|
||||
**não** cruza clínicas. (Modelo "A — cadastro auxiliar".)
|
||||
|
||||
## Arquitetura-alvo (declarada no LEIA-PRIMEIRO)
|
||||
O laboratório é listado como **tipo de workspace** (Clínica/Consultório/Laboratório/Sala). Ou seja,
|
||||
a visão oficial é elevá-lo a **participante/operador do ecossistema**, não apenas lookup.
|
||||
|
||||
## Quem usa?
|
||||
Protéticos/laboratórios e as clínicas que enviam OS. (O protético-pessoa é tratado em
|
||||
`../FASE_03_PESSOAS/SKILL_Proteticos.md` — não confundir pessoa com laboratório.)
|
||||
|
||||
## Quais módulos possui? (hoje)
|
||||
Cadastro de laboratório + produtos (`protese_produtos`) + OS (`protese_os`, `protese_os_evento`,
|
||||
`protese_os_foto`). Integra com Comissões via custo lab.
|
||||
|
||||
## Quais receitas gera?
|
||||
Indireta hoje (operacional: custo lab compõe a margem). Como workspace, poderia faturar produção própria.
|
||||
|
||||
## Quais permissões possui?
|
||||
Hoje: dentro do RBAC da clínica que cadastra. Como workspace: teria papéis/vínculos próprios.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. **Não confundir** laboratório (fornecedor/operador) com protético (pessoa).
|
||||
2. Enquanto for cadastro, manter `clinica_id` e custo lab consistentes com Comissões.
|
||||
3. Qualquer evolução para workspace é decisão arquitetural → seguir Auditoria→Aprovação (não implementar aqui).
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Tratar o laboratório como pessoa (login) sendo cadastro, ou vice-versa.
|
||||
- ❌ Recadastrar o mesmo laboratório em cada clínica como se fossem distintos (limitação atual a registrar).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Hoje
|
||||
LAB[protese_laboratorios · cadastro por clínica] --> OS[protese_os] --> COM[custo lab → Comissão]
|
||||
end
|
||||
subgraph Alvo
|
||||
LABW[Laboratório · workspace] --> OPR[Opera no ecossistema]
|
||||
end
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,44 @@
|
||||
# SKILL — Marketplace
|
||||
|
||||
> Herda de `../FASE_01_ARQUITETURA/SKILL_Workspace.md` e `SKILL_Pessoa.md`. O marketplace só existe
|
||||
> porque a base é **Pessoa → Vínculo → Workspace** (atores cruzam fronteiras de clínica).
|
||||
|
||||
## Conceito
|
||||
O marketplace conecta **pessoas/workspaces** entre si, cross-tenant. São **três frentes** reais hoje,
|
||||
em maturidades diferentes:
|
||||
|
||||
| Frente | O que existe | Receita |
|
||||
|---|---|---|
|
||||
| **Salas (locação)** | CRUD + `salas/marketplace` + reservas (`sala_reservas`), modelos `hora/turno/diaria/semanal/mensal` com `valor_*` | `valor` registrado, **sem gateway** |
|
||||
| **Profissionais** | `profissionais/perfil`, `profissionais/marketplace`, `profissionais-disponiveis`, `propostas` (clínica→profissional) | **Só mensagem**, sem cobrança |
|
||||
| **Tutoria orto** | candidatura paga (MercadoPago) + solicitações | ✅ **única automatizada** (`comissao_pct`) |
|
||||
|
||||
## Entidades / rotas reais
|
||||
- Salas: `salas`, `sala_reservas` · `GET/POST/PUT /api/salas*`, `/api/sala-reservas*`.
|
||||
- Profissionais: `propostas_profissional` (`status='pendente'`, dup-check por clínica+profissional) ·
|
||||
`/api/profissionais/*`, `/api/propostas*`. Perfil = `usuarios` (nível pessoa, `authGuard`).
|
||||
- Tutoria: ver `../FASE_03_PESSOAS/SKILL_Tutores.md`.
|
||||
|
||||
## Permissões
|
||||
- Salas: plugin `locacao-salas` (sempre ON), todos menos `superadmin`.
|
||||
- Profissionais: plugin `profissionais-marketplace`, clínicos exceto `paciente`.
|
||||
- O catálogo de plugins é exclusivo do `superadmin`.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. Marketplace é **cross-tenant intencional** — mas cada lado continua isolado por `clinica_id`/workspace.
|
||||
2. **Sinalizar lacunas de pagamento** (salas/profissionais), nunca presumir transação concluída.
|
||||
3. Proposta de profissional hoje é **conexão (mensagem)**, não contratação — não inventar fluxo inexistente.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Acoplar marketplace à clínica (Modelo A) — ele depende da pessoa/workspace (Modelo B).
|
||||
- ❌ Tratar reserva/proposta como paga.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
P[Pessoa/Workspace] --> S[Salas: reserva valor/modelo]
|
||||
P --> PR[Profissionais: perfil + proposta]
|
||||
P --> T[Tutoria: MercadoPago]
|
||||
S -. falta .-> PAY[(gateway)]
|
||||
PR -. falta .-> CONTR[(contratação/cobrança)]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,43 @@
|
||||
# SKILL — Salas (Workspace + Marketplace)
|
||||
|
||||
> Herda de `SKILL_Workspace.md`. Único workspace que já nasce no modelo novo + marketplace.
|
||||
|
||||
## O que é?
|
||||
Workspace `tipo='sala'` (tabela `salas`) que pode ser **locado** a terceiros. Plugin `locacao-salas`
|
||||
é **sempre ativo** (capability de plataforma).
|
||||
|
||||
## Quem usa?
|
||||
- **Locador** (`donosala` ou qualquer conta): cadastra e administra salas ("Minhas Salas").
|
||||
- **Locatário** (qualquer profissional): busca e reserva ("Alugar Salas").
|
||||
|
||||
## Quais módulos possui?
|
||||
Painel da sala (`sala-home`), marketplace de salas, reservas (`sala_reservas`), geolocalização (PostGIS `geo`).
|
||||
|
||||
## Quais receitas gera?
|
||||
**Potencial.** `sala_reservas` já tem `valor`, `modelo` e workflow de status (`pendente`→`aceita`),
|
||||
mas **não há captura de pagamento** (gateway) — o fluxo financeiro **não fecha** hoje.
|
||||
> É a alavanca de receita mais próxima de destravar. Ver `../FASE_04_FINANCEIRO/SKILL_Faturamento.md`.
|
||||
|
||||
## Quais permissões possui?
|
||||
Workspace `tipo='sala'` **isola** o contexto: menu só painel da sala/conta/notificações.
|
||||
Os itens "Minhas Salas"/"Alugar Salas" são de plugin (todos menos `superadmin`).
|
||||
|
||||
## Entidades envolvidas
|
||||
`salas`, `sala_reservas` (cross-tenant: locação entre workspaces distintos).
|
||||
|
||||
## Regras obrigatórias
|
||||
1. Reserva é **cross-tenant intencional** — validar disponibilidade (anti-overbooking de sala).
|
||||
2. Contexto de sala isola da clínica (não misturar operação clínica no workspace de sala).
|
||||
3. `valor` da reserva é registrado; **pagamento real é externo** (lacuna a sinalizar, não assumir).
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Assumir que a reserva foi paga (não há gateway).
|
||||
- ❌ Vazar dados da clínica para o contexto de sala.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
SAL[Sala · workspace] --> MKT[Marketplace de salas]
|
||||
MKT --> RES[sala_reservas: valor, status]
|
||||
RES -. falta .-> PAY[(Pagamento/gateway)]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,39 @@
|
||||
# SKILL — Biomédicos (Pessoa)
|
||||
|
||||
> Herda de `../FASE_01_ARQUITETURA/SKILL_Pessoa.md`. Profissional de **biomedicina estética**, não-odonto.
|
||||
|
||||
## Perfil
|
||||
Pessoa (`usuarios`) com vínculo `biomedico`. Tem **catálogo próprio** (especialidades/procedimentos de
|
||||
biomedicina) — semeável via `POST /api/me/seed-biomedicina`. Multi-área pelo perfil (`usuario_areas`/`areas`).
|
||||
|
||||
## Fluxos
|
||||
- Agenda própria + Tratamentos + lançamento de procedimentos do seu catálogo.
|
||||
- **Sem** itens odontológicos (Ortodontia, RX, tutoria orto) — separação explícita no menu.
|
||||
|
||||
## Permissões (RBAC)
|
||||
Menu: `dashboard, agenda, tratamentos, especialidades, procedimentos, clinicas, notificacoes,
|
||||
configuracoes, diretorio-profissionais`. Não vê Ortodontia/RX/tutoria.
|
||||
|
||||
## Marketplace
|
||||
Publica perfil no diretório de Profissionais; recebe propostas; pode locar/alugar salas.
|
||||
|
||||
## Financeiro
|
||||
Produção gera RECEITA/Comissão no workspace onde atua (mesma mecânica de `comissao_regras`/Repasse).
|
||||
|
||||
## Regras obrigatórias
|
||||
1. Catálogo de biomedicina é **da área**, não misturar com catálogo odonto.
|
||||
2. Identidade/áreas na pessoa; papel no vínculo.
|
||||
3. Não expor telas odonto ao papel `biomedico` (regra de menu já existente).
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Vazar Ortodontia/RX para o menu do biomédico.
|
||||
- ❌ Duplicar a pessoa por local de atuação.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
B[Biomédico · usuarios] --> V[vinculo role=biomedico]
|
||||
B --> AR[usuario_areas: biomedicina]
|
||||
V --> AG[Agenda] --> PR[Procedimento área] --> COM[Comissão→Repasse]
|
||||
B --> MKT[Diretório/Propostas]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,45 @@
|
||||
# SKILL — Dentistas (Pessoa)
|
||||
|
||||
> Herda de `../FASE_01_ARQUITETURA/SKILL_Pessoa.md` e `SKILL_Vinculos.md`.
|
||||
|
||||
## Perfil
|
||||
Pessoa (`usuarios`) com vínculo de papel `dentista` num workspace. É o clínico odontológico completo.
|
||||
**Débito conhecido:** existe também a tabela `dentistas` (recurso de agenda por clínica, com `usuario_id`
|
||||
opcional) — a MESMA pessoa pode aparecer como `usuarios` + `dentistas`. Tratar `dentistas` como
|
||||
**projeção de agenda**, não como identidade. Ver `../FASE_08_AUDITORIA/SKILL_Entidades.md`.
|
||||
|
||||
## Fluxos
|
||||
- Atende na Agenda → registra Procedimento Realizado → gera RECEITA/Comissão.
|
||||
- Ortodontia completa (triagem, plano, evoluções, fotos, contenção).
|
||||
- Pode pedir tutoria de orto e/ou ser tutor (ver `SKILL_Tutores.md`).
|
||||
|
||||
## Permissões (RBAC)
|
||||
Menu odonto: `dashboard, agenda, ortodontia, proteses, tratamentos, clinicas, configuracoes,
|
||||
candidatura-tutor, diretorio-profissionais` (+ RX e tutoria-painel conforme plugin/aprovação).
|
||||
**NÃO** vê telas administrativas globais nem `superadmin`.
|
||||
|
||||
## Marketplace
|
||||
- Publica perfil no diretório de Profissionais (`profissionais/perfil`, nível pessoa via `authGuard`).
|
||||
- Recebe propostas de clínicas (`propostas_profissional`).
|
||||
- Pode locar/alugar salas (plugin `locacao-salas`).
|
||||
|
||||
## Financeiro
|
||||
Gera comissão sobre procedimentos realizados (regra `comissao_regras`); recebe via Repasse.
|
||||
Ver `../FASE_04_FINANCEIRO/SKILL_Comissoes.md` e `SKILL_Repasses.md`.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. Identidade e capacidades na pessoa; papel no vínculo (multi-clínica nativo).
|
||||
2. Não duplicar a pessoa para representá-la em outra clínica — novo `vinculo`.
|
||||
3. `dentistas` é recurso de agenda, não fonte de identidade.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Hardcodar permissão por e-mail. ❌ Criar identidade do dentista fora de `usuarios`.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
D[Dentista · usuarios] --> V[vinculo role=dentista]
|
||||
V --> AG[Agenda] --> PR[Procedimento] --> COM[Comissão→Repasse]
|
||||
D --> MKT[Diretório/Propostas]
|
||||
D --> TUT[Tutoria orto]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,41 @@
|
||||
# SKILL — Pacientes (Pessoa — com débito de modelagem)
|
||||
|
||||
> Herda de `../FASE_01_ARQUITETURA/SKILL_Pessoa.md`. **Atenção:** estado atual contraria a arquitetura-alvo.
|
||||
|
||||
## Perfil (estado real hoje)
|
||||
O paciente é registrado em `pacientes` com `clinica_id` e **sem `usuario_id`** — ou seja, **pertence à
|
||||
clínica**, não ao ecossistema. O mesmo CPF em 2 clínicas vira **2 registros** (sem prontuário único).
|
||||
Há resíduo legado: existem INSERTs com e sem `clinica_id`. Ver `../FASE_08_AUDITORIA/SKILL_Entidades.md`.
|
||||
|
||||
## Fluxos
|
||||
- Origem: cadastro direto **ou** conversão de Lead (`leads/:id/converter` cria paciente + agendamento).
|
||||
- Agenda, Orçamento, Procedimentos, Ortodontia, RX, Receita/Exame, Score, Faltas, Família.
|
||||
|
||||
## Permissões (RBAC)
|
||||
- Operadores (funcionário/donos) gerenciam pacientes da clínica ativa.
|
||||
- O **paciente-usuário** (papel `paciente`, quando existe login) vê só: `tratamentos, notificacoes, configuracoes`.
|
||||
|
||||
## Privacidade / LGPD
|
||||
O modelo atual (paciente preso à clínica) **favorece isolamento** — cada clínica é controladora dos
|
||||
seus dados. Compartilhar histórico entre clínicas exigiria base legal/consentimento. **Não** presumir
|
||||
"paciente do ecossistema" sem decisão arquitetural + jurídica.
|
||||
|
||||
## Marketplace / Financeiro
|
||||
Não participa de marketplace. É **origem de RECEITA** (procedimentos, contratos), nunca recebe repasse.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. Paciente sempre escopado por `clinica_id` (tenancy).
|
||||
2. **Não** unificar pacientes entre clínicas sem aprovação (impacto LGPD direto).
|
||||
3. Conversão de lead é anti-overbooking — preservar a checagem.
|
||||
|
||||
## Anti-padrões / Débito
|
||||
- ⚠️ Falta de identidade de pessoa no paciente é o maior atrito com o modelo Pessoa→Workspace (gravidade ALTA).
|
||||
- ❌ Deduplicar/“mesclar” pacientes entre clínicas sem base legal.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
L[Lead] -->|converter| PAC[Paciente · clinica_id]
|
||||
PAC --> AG[Agenda] --> PR[Procedimento] --> FIN[RECEITA]
|
||||
PAC --> ORC[Orçamento] --> CON[Contrato]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,42 @@
|
||||
# SKILL — Protéticos (Pessoa)
|
||||
|
||||
> Herda de `../FASE_01_ARQUITETURA/SKILL_Pessoa.md`. **Não confundir** com o Laboratório (entidade/fornecedor):
|
||||
> ver `../FASE_02_NEGOCIO/SKILL_Laboratorios.md`.
|
||||
|
||||
## Perfil
|
||||
Pessoa (`usuarios`) com vínculo `protetico` — profissional do laboratório. É **profissional do
|
||||
ecossistema** (modelo B da auditoria), não um "fornecedor" abstrato. O fornecedor é o **laboratório**.
|
||||
|
||||
## Fluxos
|
||||
- Agenda própria + Próteses (OS de laboratório: `protese_os`, produtos, materiais, custo lab, entrega).
|
||||
- Lança GTO (guia de tratamento) quando aplicável.
|
||||
|
||||
## Permissões (RBAC)
|
||||
Menu: `dashboard, agenda, proteses, tratamentos, lancar-gto, clinicas, notificacoes, configuracoes,
|
||||
diretorio-profissionais`. Sem Ortodontia/RX/pacientes clínicos.
|
||||
|
||||
## Marketplace
|
||||
Perfil no diretório de Profissionais; propostas; salas (plugin).
|
||||
|
||||
## Financeiro
|
||||
O **custo lab** das próteses **abate a base de comissão** do procedimento (entra em `computeComissoes`).
|
||||
Ver `../FASE_04_FINANCEIRO/SKILL_Comissoes.md`.
|
||||
|
||||
## Conflito conceitual a vigiar (gravidade baixa)
|
||||
A faceta "fornecedor" pertence ao **laboratório** (cadastro `protese_laboratorios`), não à pessoa.
|
||||
Manter pessoa (protético) e laboratório (entidade) **separados e bem ligados**.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. Protético = pessoa/profissional; Laboratório = entidade/fornecedor. Não fundir.
|
||||
2. Custo lab consistente com Comissões.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Tratar o protético como cadastro de fornecedor (ou o laboratório como login).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
PT[Protético · usuarios] --> V[vinculo role=protetico]
|
||||
V --> OS[Próteses/OS] --> CL[custo lab → Comissão]
|
||||
PT -. usa .-> LAB[Laboratório · fornecedor]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,34 @@
|
||||
# SKILL — Secretárias / Recepção (Pessoa)
|
||||
|
||||
> Herda de `../FASE_01_ARQUITETURA/SKILL_Pessoa.md` e `SKILL_RBAC.md`.
|
||||
> **Importante:** no código **não existe role `secretaria`** — a secretária é o papel `funcionario`
|
||||
> (eventualmente com `funcao` customizada para refinar permissões).
|
||||
|
||||
## Perfil
|
||||
Pessoa (`usuarios`) com vínculo `funcionario`. Opera o front-office da clínica (não-clínico).
|
||||
|
||||
## Fluxos
|
||||
- Leads (captar/converter), Pacientes, Agenda, Financeiro, Orçamentos, Contratos, Relatórios, Glosas.
|
||||
|
||||
## Permissões (RBAC)
|
||||
Menu padrão do `funcionario`: `dashboard, leads, pacientes, agenda, financeiro, tratamentos,
|
||||
lancar-gto, proteses, relatorios, glosas, orcamentos, contratos, clinicas, notificacoes, configuracoes,
|
||||
diretorio-profissionais`. Permissões podem ser **estreitadas por `funcao`** (RBAC fino) no workspace.
|
||||
|
||||
## Marketplace / Financeiro
|
||||
Não participa de marketplace como profissional. Opera o financeiro da clínica, mas **não recebe comissão/repasse**.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. "Secretária" = papel `funcionario` + `funcao` opcional. Não criar role nova sem decisão arquitetural.
|
||||
2. Para restringir acesso, use **`funcao` com `permissoes[]`** (prevalece sobre o default do papel).
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Inventar role `secretaria` no código (quebra a constraint `vinculos_role_check`).
|
||||
- ❌ Dar a funcionário telas de superadmin/admin global.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
S[Secretária · usuarios] --> V[vinculo role=funcionario + funcao]
|
||||
V --> FO[Leads/Agenda/Pacientes/Financeiro]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,42 @@
|
||||
# SKILL — Tutores de Ortodontia (Pessoa)
|
||||
|
||||
> Herda de `../FASE_01_ARQUITETURA/SKILL_Pessoa.md`. Única faceta com **receita de plataforma automatizada**.
|
||||
|
||||
## Perfil
|
||||
Pessoa (`usuarios`) com flag `is_tutor=true` após **candidatura paga** (MercadoPago). Não é uma role de
|
||||
vínculo — é uma **capacidade da pessoa** (atributo), liberada pelo painel de tutoria.
|
||||
|
||||
## Fluxos
|
||||
- Candidatura (`tutoria/candidaturas`) → pagamento (`preferences` MercadoPago) → `mp-webhook` ativa o tutor.
|
||||
- Tutor resolve solicitações de orto (`ortho_tutoring_request`), troca mensagens/pacotes de fotos.
|
||||
- Gestão pelo superadmin (`admin/candidaturas`, `admin/repasses`, `admin/tutor-config`).
|
||||
|
||||
## Permissões (RBAC)
|
||||
- `candidatura-tutor`: disponível a dentistas.
|
||||
- `tutoria-painel`: **só para tutores aprovados** (`is_tutor`).
|
||||
- `admin-gestao-tutores`: **só superadmin**.
|
||||
|
||||
## Marketplace / Financeiro (modelo de receita)
|
||||
- Configuração global em `ortho_tutor_config` (`comissao_pct`, default **20%**, `taxa_inscricao`, chave PIX,
|
||||
credenciais MercadoPago).
|
||||
- Cada consultoria registra `valor_consultoria`, `comissao_pct`, `valor_tutor`, `status_repasse`.
|
||||
- **A plataforma fica com a comissão (`comissao_pct`)**; o tutor recebe `valor_tutor` (repasse próprio,
|
||||
distinto do repasse de clínica em `../FASE_04_FINANCEIRO/SKILL_Repasses.md`).
|
||||
|
||||
## Regras obrigatórias
|
||||
1. `is_tutor` é capacidade da pessoa — não criar role de vínculo "tutor".
|
||||
2. Pagamento via MercadoPago é idempotente (webhook). Não duplicar ativação.
|
||||
3. Repasse de tutoria é fluxo próprio (não confundir com repasse de comissão de clínica).
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Ativar tutor sem confirmar pagamento (webhook/`confirmar-pagamento`).
|
||||
- ❌ Misturar `comissao_pct` da tutoria com `comissao_regras` da clínica.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
D[Dentista] --> CAND[Candidatura]
|
||||
CAND -->|MercadoPago| WH[mp-webhook] --> T[is_tutor=true]
|
||||
T --> REQ[Solicitações orto]
|
||||
REQ --> $[valor_tutor + comissão plataforma]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,34 @@
|
||||
# SKILL — Assinaturas (da plataforma / SaaS)
|
||||
|
||||
> Herda de `SKILL_Financeiro.md`. **Receita da PLATAFORMA** (não da clínica). Hoje é manual.
|
||||
|
||||
## Conceito
|
||||
Assinatura é a relação de **cobrança da plataforma com a pessoa/conta** — a monetização do SaaS.
|
||||
**Distinta** de `contrato_assinaturas` (aceite de contratos da clínica). Não confundir.
|
||||
|
||||
## Entidades / mecanismo real
|
||||
- `assinaturas(id, usuario_id, status, valor, validade, created_at)`.
|
||||
- Gestão pelo **superadmin**: `PUT /api/superadmin/assinaturas/:userId` (define status/valor/validade) e
|
||||
`GET|PUT /api/superadmin/pricing` (tabela de preços).
|
||||
- **Estado atual: cobrança MANUAL** (superadmin define) — **não há gateway/recorrência automática**.
|
||||
|
||||
## Receita
|
||||
É a alavanca de **receita recorrente da plataforma** mais direta (base multiempresa + pricing já existem),
|
||||
porém ainda não automatizada. Ver auditoria estratégica (`documentação/AUDITORIA-ESTRATEGICA-MODELAGEM.md`).
|
||||
|
||||
## Regras obrigatórias
|
||||
1. Assinatura é **da pessoa/conta** (`usuario_id`), coerente com Pessoa→Workspace.
|
||||
2. Só `superadmin` gerencia (`superadminGuard`).
|
||||
3. Não confundir com assinatura de contrato nem com repasse.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Tratar `assinaturas` como assinatura de documento.
|
||||
- ❌ Assumir recorrência/gateway que não existe (cobrança é manual hoje).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
SA[superadmin] --> PRC[pricing]
|
||||
SA --> ASS[assinaturas: usuario_id, status, valor, validade]
|
||||
ASS -. falta .-> GW[(gateway/recorrência)]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,38 @@
|
||||
# SKILL — Comissões
|
||||
|
||||
> Herda de `SKILL_Financeiro.md`. Cálculo da remuneração do profissional sobre a produção.
|
||||
|
||||
## Conceito
|
||||
Comissão = **(base − custo_lab) × percentual**. A base é a produção do profissional no período; o custo
|
||||
lab (próteses) é abatido antes do percentual.
|
||||
|
||||
## Entidades / mecanismo real
|
||||
- `comissao_regras(id, clinica_id, procedimento_id, profissional_id, percentual, custo_lab)`.
|
||||
- Função `computeComissoes(pool, clinicaId, {inicio, fim, base, profissionalId})` agrega a produção.
|
||||
- Três níveis de regra (precedência): **override por profissional** > **por procedimento** > **padrão da clínica**.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. Custo lab **sempre abatido** antes do percentual.
|
||||
2. Lançamentos com `sem_comissao=true` (garantia/reabertura do mesmo profissional, despesa de repasse) **não entram**.
|
||||
3. Base configurável (produção × recebido) — respeitar o parâmetro `base`.
|
||||
4. Mudou regra de comissão → auditar impacto em Repasses (não recalcular repasses já fechados).
|
||||
|
||||
## Fluxos
|
||||
Produção (`procedimentos_realizados` → `financeiro` RECEITA) → `computeComissoes` → **Repasse** (fechar).
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Ignorar `custo_lab` no cálculo. ❌ Comissionar lançamento `sem_comissao`.
|
||||
- ❌ Alterar percentual e recalcular retroativamente repasses fechados (snapshot é imutável).
|
||||
|
||||
## Permissões
|
||||
Definição de regras: donos/admin. Ver `../FASE_01_ARQUITETURA/SKILL_RBAC.md`.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
PROD[Produção do período] --> BASE[base]
|
||||
LAB[custo_lab próteses] --> CALC
|
||||
BASE --> CALC[(base − custo_lab) × %]
|
||||
REG[comissao_regras: prof > proc > clínica] --> CALC
|
||||
CALC --> REP[Repasse]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,36 @@
|
||||
# SKILL — Contratos
|
||||
|
||||
> Herda de `SKILL_Financeiro.md`. Formalização de acordos (paciente/profissional ↔ clínica) e cobrança.
|
||||
|
||||
## Conceito
|
||||
Contrato transforma um acordo (orçamento, remuneração, prestação) em documento com **modelo + variáveis +
|
||||
partes + assinaturas**, podendo **gerar cobrança** no financeiro.
|
||||
|
||||
## Entidades / fluxo real
|
||||
- `contrato_modelos` (modelos com variáveis `{{...}}`, categorias: remuneração, prestação, etc.).
|
||||
- `contratos` / instâncias (`status`: rascunho → aguardando_assinatura → assinado → vigente → encerrado/cancelado).
|
||||
- `contrato_partes` (as partes do contrato) e `contrato_assinaturas` (aceite por parte: assinado_em, ip,
|
||||
user_agent, hash) — **assinatura eletrônica de contrato** (≠ assinatura de plataforma, ver `SKILL_Assinaturas.md`).
|
||||
- Ações: `enviar` (cria assinaturas pendentes), `assinar`, `gerar-cobranca` (→ `financeiro`), `cancelar`, `duplicar`.
|
||||
|
||||
## Variáveis de remuneração (já modeladas)
|
||||
`{{PERCENTUAL}}`, `{{PERCENTUAL_REPASSE}}`, `{{VALOR}}`, `{{BASE_REPASSE}}`, `{{PRAZO_REPASSE}}` — contratos
|
||||
de comissão/percentual referenciam o mesmo conceito de Repasse.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. Respeitar a máquina de estados (não pular rascunho→vigente sem assinatura).
|
||||
2. `gerar-cobranca` cria lançamento em `financeiro` (`origem='contrato'`, `contrato_id`).
|
||||
3. Assinatura grava trilha (ip/user_agent/hash) — não remover (valor probatório).
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Confundir `contrato_assinaturas` (assinatura do documento) com `assinaturas` (mensalidade da plataforma).
|
||||
- ❌ Encerrar/cancelar sem auditoria.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
M[contrato_modelos] --> I[contratos: rascunho]
|
||||
I --> E[enviar → assinaturas pendentes]
|
||||
E --> A[assinar → assinado/vigente]
|
||||
A --> COB[gerar-cobranca → financeiro]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,49 @@
|
||||
# SKILL — Faturamento (visão consolidada do dinheiro)
|
||||
|
||||
> Herda de `SKILL_Financeiro.md`. Mapa de **onde o dinheiro nasce, para onde vai e quem é o responsável.**
|
||||
|
||||
## As três camadas de receita (estado real)
|
||||
1. **Receita da clínica** (operacional): produção de procedimentos, contratos/cobrança, recuperação de glosas.
|
||||
Responsável: a clínica. Tabela: `financeiro` (RECEITA).
|
||||
2. **Receita do profissional** (repasse): comissão sobre a produção. Responsável: clínica paga; profissional recebe.
|
||||
Tabelas: `comissao_regras` → `repasses` → `financeiro` (DESPESA).
|
||||
3. **Receita da plataforma**: (a) **Tutoria** — `comissao_pct` via MercadoPago (**automatizada**);
|
||||
(b) **Assinaturas** — SaaS por conta (**manual**, sem gateway); (c) **Locação de salas** — `valor` na
|
||||
reserva, **sem captura de pagamento** (lacuna).
|
||||
|
||||
## Mapa origem → destino → responsável
|
||||
| Origem | Tabela | Destino | Responsável |
|
||||
|---|---|---|---|
|
||||
| Procedimento (RECEITA) | `financeiro` | caixa da clínica | Clínica |
|
||||
| Contrato (cobrança) | `financeiro` | caixa da clínica | Clínica |
|
||||
| Convênio − Glosa | `glosas`/`financeiro` | recupera receita | Clínica |
|
||||
| Comissão → Repasse pago | `repasses`→`financeiro` DESPESA | profissional | Clínica paga |
|
||||
| Tutoria | `ortho_tutoring_request` + MercadoPago | plataforma (`comissao_pct`) + tutor (`valor_tutor`) | Plataforma |
|
||||
| Assinatura SaaS | `assinaturas` | plataforma | Plataforma (manual) |
|
||||
| Locação de sala | `sala_reservas.valor` | locador | — (sem gateway) |
|
||||
|
||||
## Regras obrigatórias
|
||||
1. Não misturar as três camadas: clínica ≠ profissional ≠ plataforma.
|
||||
2. Toda lacuna de captura (salas, assinaturas) deve ser **sinalizada, não presumida como paga**.
|
||||
3. Mudança em qualquer camada → mapear efeito nas demais antes (ver `../FASE_08_AUDITORIA/SKILL_Fluxos.md`).
|
||||
|
||||
## Oportunidades (já mapeadas, não implementar aqui)
|
||||
- Gateway na **Locação de Salas** (receita mais próxima) e **recorrência das Assinaturas**.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph Clínica
|
||||
PR[Procedimento] --> R[financeiro RECEITA]
|
||||
CON[Contrato] --> R
|
||||
R --> GL[Glosas]
|
||||
end
|
||||
subgraph Profissional
|
||||
R --> COM[Comissão] --> REP[Repasse] --> D[financeiro DESPESA]
|
||||
end
|
||||
subgraph Plataforma
|
||||
TUT[Tutoria · MercadoPago] --> PLT[comissao_pct + valor_tutor]
|
||||
ASS[Assinaturas · manual] --> PLT
|
||||
SAL[Salas · valor] -. falta gateway .-> PLT
|
||||
end
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,41 @@
|
||||
# SKILL — Financeiro (núcleo do caixa)
|
||||
|
||||
> Herda de `../FASE_01_ARQUITETURA/SKILL_Multiempresa.md`. Tabela central do dinheiro: `financeiro`.
|
||||
|
||||
## Conceito
|
||||
`financeiro` registra **RECEITA** e **DESPESA** da clínica (escopo `clinica_id`). É o caixa unificado de
|
||||
onde derivam comissões, repasses e glosas.
|
||||
|
||||
## Entidades / colunas-chave (reais)
|
||||
`financeiro(id, clinica_id, descricao, valor, tipo['RECEITA'|'DESPESA'], status['Pago'|'Pendente'|...],
|
||||
formapagamento, datavencimento, data_competencia, pago_em, paciente_id, profissional_id, procedimento_id,
|
||||
agendamento_id, realizado_id, contrato_id, origem, centro_custo, fornecedor, sem_comissao, deleted_at)`.
|
||||
|
||||
## Origens (origem=...)
|
||||
- `procedimento` — gerado ao lançar Procedimento Realizado (RECEITA, se `valor>0`).
|
||||
- `contrato` — cobrança de contrato (`gerar-cobranca`).
|
||||
- `repasse` — DESPESA ao pagar repasse (`centro_custo='COMISSÕES'`, `sem_comissao=true`).
|
||||
- Lançamento manual (avulso) — RECEITA/DESPESA direto na tela.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. Todo lançamento carrega `clinica_id` e `tipo`.
|
||||
2. **`sem_comissao`** marca o que NÃO deve gerar repasse (garantia, despesa de repasse).
|
||||
3. **`deleted_at`** = soft-delete (estorno reversível). Nunca DELETE físico de lançamento conciliado.
|
||||
4. RECEITA de procedimento é **opcional** (`valor>0` e `gerarLancamento!==false`) — alterar muda o caixa.
|
||||
|
||||
## Fluxos a jusante
|
||||
RECEITA → base de **Comissão**; DESPESA de repasse fecha o ciclo. Recebíveis de convênio → **Glosas**.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Apagar lançamento fisicamente. ❌ Lançar sem `clinica_id`/`tipo`.
|
||||
- ❌ Recriar comissão a partir de despesa de repasse (use `sem_comissao`).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
PR[Procedimento] -->|origem=procedimento| R[financeiro RECEITA]
|
||||
CON[Contrato] -->|gerar-cobranca| R
|
||||
RP[Repasse pago] -->|origem=repasse| D[financeiro DESPESA]
|
||||
R --> COM[Comissão]
|
||||
R --> GL[Glosas]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,35 @@
|
||||
# SKILL — Repasses
|
||||
|
||||
> Herda de `SKILL_Comissoes.md`. Fechamento e pagamento da comissão ao profissional.
|
||||
|
||||
## Conceito
|
||||
Repasse é o **snapshot imutável** da comissão de um profissional num período, que vira **DESPESA** quando pago.
|
||||
|
||||
## Entidades / fluxo real
|
||||
- `repasses(id, clinica_id, profissional_id, inicio, fim, base, base_valor, custo_lab_total,
|
||||
comissao_valor, qtd_lancamentos, detalhes(JSON), status['fechado'|'pago'|'cancelado'], pago_em,
|
||||
financeiro_id, comprovante_*)`.
|
||||
- `POST /repasses/fechar` → calcula via `computeComissoes` e grava `status='fechado'` (snapshot em `detalhes`).
|
||||
- `POST /repasses/:id/pagar` → cria `financeiro` DESPESA (`origem='repasse'`, `centro_custo='COMISSÕES'`,
|
||||
`sem_comissao=true`) e marca `status='pago'` + `financeiro_id`.
|
||||
- `POST /repasses/:id/cancelar` → se estava pago, **estorna** a DESPESA (soft-delete) e zera `financeiro_id`.
|
||||
- Comprovante: `POST /repasses/:id/comprovante`.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. Só paga repasse **fechado**. O snapshot (`detalhes`) é **imutável** após fechar.
|
||||
2. DESPESA de repasse nasce com `sem_comissao=true` (não recomissionar).
|
||||
3. Cancelamento é **reversível** (estorno por soft-delete) — preservar.
|
||||
4. Toda transição registra `audit_log` (`registrarAudit`). Manter.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Editar valores de um repasse fechado (refazer = cancelar + novo fechamento).
|
||||
- ❌ Pagar sem gerar a DESPESA correspondente (quebra a conciliação).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
COM[Comissão do período] --> F[fechar → repasses status=fechado]
|
||||
F --> P[pagar → financeiro DESPESA]
|
||||
P --> C{cancelar?}
|
||||
C -->|sim| EST[estorna DESPESA soft-delete]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,25 @@
|
||||
# SKILL — Acessibilidade
|
||||
|
||||
> Base mínima de a11y, sem regredir a identidade visual. Pragmático para o estado atual.
|
||||
|
||||
## Estado atual (honesto)
|
||||
O app prioriza densidade/visual; a11y é **básica**. Esta skill fixa o mínimo a manter/melhorar sem
|
||||
quebrar o design.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. **Contraste**: texto sobre a cor de marca deve ser legível (a marca é escura → texto branco). Evitar
|
||||
cinza-claro sobre branco em informação essencial.
|
||||
2. **Alvos de toque** no mobile ≥ ~40px (os botões do bottom nav/menu já respeitam).
|
||||
3. **Foco**: usar `focus:ring-brand` (já existe) em controles interativos; não remover outline sem substituto.
|
||||
4. **Semântica**: `<button>` para ação, `<a>` para navegação; ícone-só precisa de `aria-label`/título.
|
||||
5. **Estados**: feedback de loading/disabled (`disabled:opacity-60`) e toasts para resultado de ação.
|
||||
|
||||
## Como auditar
|
||||
```bash
|
||||
grep -rnE "onClick" frontend/views/<View>.tsx | grep -ivE "aria-label|title|>.*<" # ícones-botão sem rótulo
|
||||
```
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Remover outline de foco sem alternativa. ❌ Ícone clicável sem rótulo acessível.
|
||||
- ❌ Texto essencial com contraste insuficiente. ❌ Sacrificar a identidade visual aprovada por a11y (equilibrar).
|
||||
</content>
|
||||
@@ -0,0 +1,39 @@
|
||||
# SKILL — Containers & Identidade Visual (REGRA RÍGIDA)
|
||||
|
||||
> Herda de `SKILL_DesktopWidth.md`. Preservar a identidade visual aprovada. **Não inventar design.**
|
||||
|
||||
## Padrão real de container (não regredir)
|
||||
- **Wrapper de página:** `max-w-7xl mx-auto` + `p-4 md:p-8` (definido em `App.tsx`).
|
||||
- **Cards:** fundo branco, bordas suaves (`border-gray-200`), cantos arredondados (`rounded-xl`/`2xl`),
|
||||
sombra leve. Tipografia de rótulo em caixa-alta, peso forte (`font-bold/black uppercase` em labels/menus).
|
||||
- **Sidebar:** `w-64`, branca, item ativo pintado com a **cor de marca**.
|
||||
|
||||
## Cor de marca (token oficial — NÃO hardcodar)
|
||||
- Variável CSS `--brand-color`, vinda de `activeWorkspace.cor` (default `#2563eb`; superadmin `#dc2626`).
|
||||
- Classes utilitárias existentes: `bg-brand`, `text-brand`, `border-brand`, `hover:bg-brand`, `focus:ring-brand`.
|
||||
- **Sempre** usar o token/classe de marca para realce — nunca cravar um hex novo.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. **Preservar cores, estilos e identidade visual** aprovados — alterações visuais exigem aprovação.
|
||||
2. Realce/seleção usa **a cor do workspace** (`--brand-color`), não cor fixa.
|
||||
3. Reutilizar componentes existentes (`PageHeader`, `Toast`, `StackModal`, `EmptyState`, `Header`) — não recriar.
|
||||
4. Espaçamento e raio consistentes com o resto do app (não introduzir novo sistema de spacing).
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Hardcodar `#2563eb` (ou qualquer hex) em vez de `var(--brand-color)`/classes `*-brand`.
|
||||
- ❌ Criar um novo "design system" paralelo ou trocar a tipografia/raio/sombra padrão.
|
||||
- ❌ Container de página fora do `max-w-7xl mx-auto`.
|
||||
- ❌ Recriar modal/toast/header quando já existe componente.
|
||||
|
||||
## Validação
|
||||
- A tela nova é **indistinguível em estilo** das telas existentes? Se não, parar.
|
||||
- Trocar de workspace muda o realce conforme a cor da clínica? (token funcionando)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
WS[workspace.cor] --> VAR[--brand-color]
|
||||
VAR --> BR[classes *-brand / realces]
|
||||
COMP[Componentes existentes] --> UI[Tela consistente]
|
||||
WRAP[max-w-7xl mx-auto] --> UI
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,22 @@
|
||||
# SKILL — Dashboard
|
||||
|
||||
> Tela inicial por papel. Herda de `SKILL_GridSystem.md` + `SKILL_DesktopWidth.md`.
|
||||
|
||||
## O que é
|
||||
Visão consolidada (KPIs) de entrada. Dados via `GET /api/dashboard/stats`. View: `views/Dashboard.tsx`.
|
||||
Cada papel tem seu ponto de entrada (`getDefaultViewForRole`): superadmin → overview; sala → painel da sala;
|
||||
paciente → tratamentos; demais → dashboard.
|
||||
|
||||
## Padrão de layout (real)
|
||||
- KPIs em grid `grid-cols-2 md:grid-cols-4` (cards compactos), dentro do wrapper `max-w-7xl mx-auto`.
|
||||
- Realce com a cor do workspace; números grandes + rótulo caixa-alta.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. KPIs vêm do backend (`/api/dashboard/stats`) — não recalcular no cliente de forma divergente.
|
||||
2. Respeitar o teto de largura (`max-w-7xl`) e o grid mobile-first (ver GridSystem).
|
||||
3. Conteúdo do dashboard segue o papel/permissões (não mostrar KPI que o papel não pode ver).
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Dashboard full-width esticado. ❌ KPI financeiro para papel sem acesso a financeiro.
|
||||
- ❌ Recriar cards fora do padrão visual.
|
||||
</content>
|
||||
@@ -0,0 +1,29 @@
|
||||
# SKILL — Design System (REGRA RÍGIDA)
|
||||
|
||||
> Tokens e padrões visuais REAIS do app. **Preservar a identidade aprovada. Não inventar.**
|
||||
> Herda de `SKILL_Containers.md`.
|
||||
|
||||
## Stack
|
||||
React 19 + **Tailwind 4** + lucide-react (ícones). Sem biblioteca de componentes externa.
|
||||
|
||||
## Cor de marca (token oficial)
|
||||
- `--brand-color` ← `activeWorkspace.cor` (default `#2563eb`; superadmin `#dc2626`).
|
||||
- Classes utilitárias: `bg-brand`, `text-brand`, `border-brand`, `hover:bg-brand`, `focus:ring-brand`.
|
||||
- **Sempre** o token/classe — nunca um hex novo cravado.
|
||||
|
||||
## Tipografia & forma (observado)
|
||||
- Rótulos/menus/badges: **caixa-alta**, peso forte (`font-bold`/`font-black uppercase`, `tracking`).
|
||||
- Cantos arredondados (`rounded-xl`/`2xl`), bordas suaves (`border-gray-200`), sombra leve.
|
||||
- Ícones lucide-react com tamanho consistente (`size={16..22}`).
|
||||
|
||||
## Componentes a reutilizar (não recriar)
|
||||
`PageHeader`, `Header`, `Sidebar`, `Toast`/`ToastContainer`, `StackModal`, `EmptyState`, `ConfirmContext`(`useConfirm`), `ToothIcon`.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. Realce/seleção usa a cor do workspace (`--brand-color`).
|
||||
2. Reutilizar os componentes acima; modal/toast/confirm já existem.
|
||||
3. Manter spacing/raio/sombra consistentes — não introduzir novo sistema.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Hex hardcoded. ❌ Novo design system paralelo. ❌ Recriar modal/toast/header. ❌ Trocar tipografia/raio padrão.
|
||||
</content>
|
||||
@@ -0,0 +1,44 @@
|
||||
# SKILL — Desktop Width (REGRA RÍGIDA)
|
||||
|
||||
> Objetivo desta skill: **impedir que qualquer agente estique divs, quebre o desktop ou invente layout.**
|
||||
> Tudo abaixo reflete a convenção REAL do código (`frontend/App.tsx`, Tailwind 4). Não inventar valores.
|
||||
|
||||
## Regra de ouro
|
||||
**O conteúdo de toda página vive dentro do wrapper canônico:**
|
||||
```
|
||||
<div className="max-w-7xl mx-auto"> ... </div>
|
||||
```
|
||||
Esse wrapper já existe globalmente em `App.tsx` (a área de conteúdo é `max-w-7xl mx-auto` com `p-4 md:p-8`).
|
||||
`max-w-7xl` = **1280px** centralizado. **NUNCA** remover, sobrepor ou furar esse limite.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. **PROIBIDO `w-screen`, `100vw`, `max-w-none` ou `max-w-full`** em containers de página no desktop.
|
||||
2. **PROIBIDO full-width**: nada de conteúdo encostando nas bordas da viewport em telas largas.
|
||||
3. **Largura máxima de conteúdo = `max-w-7xl` (1280px)**, centralizado com `mx-auto`. É o padrão do app.
|
||||
4. Subseções podem ser mais estreitas (`max-w-2xl`, `max-w-3xl`, `max-w-md` para formulários) — **nunca mais largas** que o wrapper.
|
||||
5. **Sidebar fixa `w-64` (256px)** — não alterar sem auditoria de UX.
|
||||
6. Em ultrawide (>1280px) a sobra vira **margem lateral** (centralização), nunca esticão de conteúdo.
|
||||
|
||||
## Padrão real observado (não regredir)
|
||||
- Wrapper de página: `max-w-7xl mx-auto` + `p-4 md:p-8`.
|
||||
- Larguras de cards/formulários: `max-w-md / 2xl / 3xl` conforme densidade.
|
||||
- Breakpoints usados: `sm:` `md:` `lg:` (o `xl:` é raro — **não** introduzir layouts dependentes de `xl/2xl` sem necessidade).
|
||||
|
||||
## Anti-padrões (o que QUEBRA o desktop)
|
||||
- ❌ Trocar `max-w-7xl` por `max-w-full`/`w-full` no nível da página.
|
||||
- ❌ Criar grids que se expandem indefinidamente em telas largas.
|
||||
- ❌ Remover o `mx-auto` (conteúdo cola à esquerda).
|
||||
- ❌ Inventar nova paleta/identidade (ver Containers/DesignSystem).
|
||||
|
||||
## Validação obrigatória antes de entregar
|
||||
- Conferir em **desktop largo** (≥1920px): conteúdo centralizado, margens laterais, sem esticão.
|
||||
- Conferir em **mobile**: `p-4`, coluna única, sem overflow horizontal.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
V[Viewport larga] --> M1[margem]
|
||||
V --> C[max-w-7xl · 1280px · mx-auto]
|
||||
V --> M2[margem]
|
||||
C --> CONT[Conteúdo da página]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,40 @@
|
||||
# SKILL — Grid System (REGRA RÍGIDA)
|
||||
|
||||
> Herda de `SKILL_DesktopWidth.md`. Layout responsivo **mobile-first**, como já feito no app.
|
||||
|
||||
## Padrão real (não regredir)
|
||||
O app é **mobile-first**: começa em coluna única e cresce por breakpoint. Convenção observada:
|
||||
```
|
||||
grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 (cards/listas)
|
||||
grid grid-cols-2 md:grid-cols-4 (KPIs/contadores)
|
||||
```
|
||||
Breakpoints em uso: `sm:` `md:` `lg:` (predominantes); `xl:` é raro — **não** depender dele.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. **Mobile-first sempre:** definir o estado base (1 coluna) e escalar com `md:`/`lg:`.
|
||||
2. **Teto de colunas em `lg:`** (até `lg:grid-cols-4`). Não criar grids que multiplicam colunas em ultrawide.
|
||||
3. Usar `gap-*` consistente com o app; não misturar grid com float/posicionamento absoluto para layout.
|
||||
4. Tabelas largas: scroll interno (`overflow-x-auto`), **nunca** estourar o `max-w-7xl`.
|
||||
5. Formulários: largura contida (`max-w-md/2xl`) e centralizada, não full-width.
|
||||
|
||||
## Casos de uso
|
||||
- Dashboard de KPIs: `grid-cols-2 md:grid-cols-4`.
|
||||
- Lista de cards: `grid-cols-1 md:grid-cols-2 lg:grid-cols-3`.
|
||||
- Form de cadastro: `max-w-2xl mx-auto`.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ `grid-cols-6`/`grid-cols-12` que esticam em telas largas sem teto.
|
||||
- ❌ Layout que só foi testado em desktop (quebra no mobile).
|
||||
- ❌ Conteúdo de grid ultrapassando o wrapper `max-w-7xl`.
|
||||
- ❌ Introduzir dependência de `xl:`/`2xl:` para o layout funcionar.
|
||||
|
||||
## Validação
|
||||
- Mobile (1 coluna, sem overflow), tablet (`md:`), desktop (`lg:`), ultrawide (centralizado, sem esticão).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
B[grid-cols-1 · base/mobile] --> M[md:grid-cols-2/3]
|
||||
M --> L[lg:grid-cols-3/4 · TETO]
|
||||
L --> X[ultrawide: centraliza, não estica]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,23 @@
|
||||
# SKILL — Responsividade (REGRA RÍGIDA)
|
||||
|
||||
> Mobile-first, como o app já é. Herda de `SKILL_GridSystem.md`.
|
||||
|
||||
## Padrão real (não regredir)
|
||||
- **Mobile-first**: estado base = mobile; cresce com `sm:`/`md:`/`lg:` (uso: `sm:`≈238, `md:`≈186, `lg:`≈100; `xl:` raro≈8).
|
||||
- **Sidebar**: `w-64` fixa no desktop; off-canvas no mobile (drawer com overlay `bg-black/50 md:hidden`).
|
||||
- **Bottom nav mobile** por papel (`BOTTOM_TABS` em `App.tsx`) com `+ Mais`.
|
||||
- Padding de página `p-4 md:p-8`; safe-area no bottom nav (`env(safe-area-inset-bottom)`).
|
||||
|
||||
## Regras obrigatórias
|
||||
1. Toda tela nova **funciona no mobile** (coluna única, sem overflow horizontal) E no desktop.
|
||||
2. Não depender de `xl:`/`2xl:` para o layout funcionar.
|
||||
3. Sidebar drawer no mobile; conteúdo dentro de `max-w-7xl mx-auto`.
|
||||
4. Tabelas largas → `overflow-x-auto` (scroll interno), nunca estourar o container.
|
||||
|
||||
## Validação (obrigatória antes de entregar)
|
||||
- Mobile (≤640): 1 coluna, sem scroll horizontal, bottom nav ok.
|
||||
- Tablet (`md`), Desktop (`lg`), Ultrawide (centraliza, sem esticar).
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Testar só no desktop. ❌ Overflow horizontal no mobile. ❌ Layout que quebra sem `xl:`.
|
||||
</content>
|
||||
@@ -0,0 +1,28 @@
|
||||
# SKILL — Módulo Agenda
|
||||
|
||||
> Coração operacional. RBAC: capability `'agenda'` (staff clínico, bloqueia paciente).
|
||||
|
||||
## O que é
|
||||
Agendar/confirmar/remarcar consultas, controle de presença, anti-overbooking e pendências de reagendamento.
|
||||
|
||||
## Entidades
|
||||
`agendamentos`, `agenda_presenca`, `agenda_pendencias`, `interesses_agenda`, `dentistas`+`dentistas_horarios`.
|
||||
|
||||
## Rotas principais
|
||||
- `GET/POST /api/agendamentos`, `PUT/DELETE /api/agendamentos/:id`
|
||||
- Ações `/:id`: `confirmar`, `falta`, `remarcar`, `restaurar`, `reencaminhar`, `solicitar-avaliacao`
|
||||
- `GET/POST /api/agenda/presenca`, `GET /api/agenda/{pendencias,atividade}`, `POST /api/agenda/pendencias/:id/resolver`
|
||||
- `POST /api/agenda/importar-google`, horários: `GET /api/dentistas/:id/horarios`, `POST/DELETE /api/dentistas/horarios`
|
||||
|
||||
## Regras de negócio (reais)
|
||||
- **Anti-overbooking**: checa conflito ANTES de criar (ex.: conversão de lead não cria paciente se o slot está ocupado).
|
||||
- **Falta** abre pendência e impacta o **score** do paciente.
|
||||
- **Presença/confirmação** libera o lançamento financeiro a jusante.
|
||||
- Tempo real via WebSocket (`wsBroadcast('agenda', ...)`) → refetch instantâneo na clínica.
|
||||
|
||||
## Impactos a jusante
|
||||
Procedimento realizado → Financeiro → Comissão. Integra Google Calendar (importar).
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Remover a checagem de overbooking. ❌ Mutação de agenda sem `registrarAudit`. ❌ Expor agenda ao paciente.
|
||||
</content>
|
||||
@@ -0,0 +1,28 @@
|
||||
# SKILL — Módulo CRM (Leads)
|
||||
|
||||
> Topo do funil comercial. RBAC: capability `'leads'` (funcionário + dono).
|
||||
|
||||
## O que é
|
||||
Captar interessados e **converter** em paciente + agendamento (início do ciclo clínico).
|
||||
|
||||
## Entidades
|
||||
`leads` (status: Novo|Convertido|Arquivado), `interesses_agenda`, gera `pacientes` + `agendamentos`.
|
||||
|
||||
## Rotas
|
||||
`GET/POST /api/leads`, `POST /api/leads/:id/converter`. Origem pública: `PublicContactForm`/`/api/interesses`.
|
||||
|
||||
## Regra de negócio central (verificada)
|
||||
`POST /api/leads/:id/converter`:
|
||||
1. valida que o lead não foi convertido (409 se já foi);
|
||||
2. checa **anti-overbooking** do slot ANTES de criar (evita paciente órfão);
|
||||
3. cria o **paciente**; 4) cria o **agendamento** inicial (se slot válido e livre).
|
||||
|
||||
## Impactos a jusante
|
||||
Paciente → Agenda → atendimento → Financeiro.
|
||||
|
||||
## Oportunidade (do mapa funcional)
|
||||
CRM é a entrada; integrar com WhatsApp/origem de marketing é alavanca de crescimento (hoje básico).
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Converter sem anti-overbooking. ❌ Duplicar paciente na reconversão. ❌ Expor leads a papel clínico/paciente.
|
||||
</content>
|
||||
@@ -0,0 +1,25 @@
|
||||
# SKILL — Módulo Documentos (Receitas, Exames, Contratos, Mídia)
|
||||
|
||||
> Geração e guarda de documentos clínicos/jurídicos e mídia. Cruza Prontuário e Contratos.
|
||||
|
||||
## O que é
|
||||
Receitas, pedidos de exame, contratos (com assinatura eletrônica) e arquivos (fotos, comprovantes, PDFs).
|
||||
|
||||
## Entidades
|
||||
`modelos_receita`/`items_receita`, `pedidos_exame`/`items_pedido_exame`,
|
||||
`contratos`/`contrato_modelos`/`contrato_partes`/`contrato_assinaturas`, e fotos por entidade
|
||||
(`*_foto`). PDFs no frontend via `jspdf`.
|
||||
|
||||
## Acesso à mídia (padrão real)
|
||||
Arquivos são servidos por **URL assinada**: `/api/.../raw?t=signMedia(id)` — sem token de sessão.
|
||||
A URL só é entregue a quem já passou pelo guard da listagem. **Não** gatear o `/raw` por capability
|
||||
(quebraria a imagem); a proteção é o token assinado + a lista gateada.
|
||||
|
||||
## Contratos (assinatura)
|
||||
`contrato_assinaturas` grava trilha probatória (ip, user_agent, hash do corpo). É **registro de aceite** —
|
||||
não remover. Capability `'contratos'` (funcionário+dono). Ver `../FASE_04_FINANCEIRO/SKILL_Contratos.md`.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Servir mídia sem URL assinada. ❌ Gatear `/raw` por capability. ❌ Apagar trilha de assinatura.
|
||||
- ❌ Confundir assinatura de contrato (`contrato_assinaturas`) com assinatura de plataforma (`assinaturas`).
|
||||
</content>
|
||||
@@ -0,0 +1,28 @@
|
||||
# SKILL — Módulo Prontuário (Paciente + Ortodontia)
|
||||
|
||||
> O histórico clínico do paciente. Detalhes acessíveis a staff clínico (capability `'agenda'`).
|
||||
|
||||
## O que é
|
||||
Prontuário do paciente: dados, família, score, faltas, procedimentos realizados, receitas, pedidos de exame,
|
||||
e o tratamento **ortodôntico** completo.
|
||||
|
||||
## Entidades
|
||||
`pacientes`, `procedimentos_realizados`(+foto), `modelos_receita`/`items_receita`, `pedidos_exame`/`items_pedido_exame`,
|
||||
`avaliacoes`/`avaliacao_foto`; **orto**: `ortho_tratamento`, `ortho_evolucao`, `ortho_photo`(+session/annotation).
|
||||
|
||||
## Rotas
|
||||
`GET/POST/PUT /api/pacientes[...]`, `GET /api/pacientes/:id/{score,faltas,familia}`,
|
||||
`GET/POST /api/orto/tratamentos[/:id/{evolucoes,finalizar}]`, `/api/orto/{sessoes,fotos}`, `/api/avaliacoes`.
|
||||
|
||||
## Regras de negócio
|
||||
- **Ortodontia** tem triagem, diagnóstico, plano, evoluções, fotos e contenção (vide `frontend/types.ts`).
|
||||
- `procedimentos_realizados` gera RECEITA (opcional por valor) e alimenta comissão.
|
||||
- **`OrthoView` (dentista) cria/edita paciente** → por isso o cap de mutação de paciente é `'agenda'` (clínico), não `'pacientes'`.
|
||||
|
||||
## Privacidade/LGPD
|
||||
Paciente é **da clínica** (escopo `clinica_id`), não do ecossistema. Não compartilhar histórico entre clínicas
|
||||
sem base legal — ver `../FASE_03_PESSOAS/SKILL_Pacientes.md`.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Vazar prontuário entre clínicas. ❌ Gatear mutação de paciente a `'pacientes'` (bloquearia o dentista do Ortho).
|
||||
</content>
|
||||
@@ -0,0 +1,25 @@
|
||||
# SKILL — Módulo Próteses / GTO
|
||||
|
||||
> Controle de laboratório. Liga-se à Comissão pelo **custo lab**. Ver `../FASE_03_PESSOAS/SKILL_Proteticos.md`.
|
||||
|
||||
## O que é
|
||||
Ordens de Serviço de prótese (produto, dentes, material, custo lab, prazo, entrega) e GTO (guia de tratamento).
|
||||
|
||||
## Entidades
|
||||
`protese_os`, `protese_os_evento`, `protese_os_foto`, `protese_produtos`, `protese_laboratorios`;
|
||||
GTO legado: `gto_builders`/`gto_items`, `guias_odontologicas`.
|
||||
|
||||
## Rotas
|
||||
`GET/POST /api/protese/os`, `GET /api/protese/os/:id`, `POST /api/protese/os/:id/{status,foto}`,
|
||||
`GET/POST/PUT/DELETE /api/protese/produtos`, `GET /api/protese/laboratorios[/:id/produtos]`, `GET /api/protese/bancada`.
|
||||
|
||||
## Regra de negócio central
|
||||
O **custo lab** da prótese **abate a base de comissão** do procedimento (`computeComissoes` =
|
||||
`(base − custo_lab) × %`). Mexer aqui afeta o repasse — ver `../FASE_04_FINANCEIRO/SKILL_Comissoes.md`.
|
||||
|
||||
## Papéis
|
||||
Protético (pessoa, profissional do ecossistema) opera; laboratório = fornecedor (cadastro hoje).
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Alterar custo lab sem considerar o efeito na comissão. ❌ Confundir protético (pessoa) com laboratório (fornecedor).
|
||||
</content>
|
||||
@@ -0,0 +1,24 @@
|
||||
# SKILL — Módulo Radiologia (RX)
|
||||
|
||||
> Plugin `rx-scoreodonto`. Radiografias são ODONTOLÓGICAS (papéis odonto, não bio/protético).
|
||||
|
||||
## O que é
|
||||
Captura/gestão de radiografias e dos dispositivos (PCs/sensores) por clínica. Integra com o produto externo
|
||||
**rx.scoreodonto.com** (server + client.exe) — ver `documentos-unicos/projetos/rx.scoreodonto.com*`.
|
||||
|
||||
## Rotas
|
||||
`GET /api/rx/{config,devices,images,patients,health}`, `PUT /api/rx/config`, `POST /api/rx/devices`.
|
||||
Views: `plugins/RXPlugin.tsx` (RX/Radiografias), `RxConfigView` (Dispositivos RX).
|
||||
|
||||
## Permissões (do Sidebar real)
|
||||
- **RX/Radiografias**: dentista + donos/funcionário (papéis odonto). **Biomédico e protético NÃO veem RX.**
|
||||
- **Dispositivos RX**: admin/donos + superadmin.
|
||||
- Requer o plugin `rx-scoreodonto` ativo na conta.
|
||||
|
||||
## Integração
|
||||
O RX é um sistema próprio (auto-update do client via `/updates/latest.yml`) — **não quebrar** esse mecanismo.
|
||||
Aqui é só a ponte (config/devices/images) dentro do ScoreOdonto.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Expor RX a biomédico/protético. ❌ Acoplar a lógica do client.exe aqui. ❌ Mexer no `/updates/` do RX por engano.
|
||||
</content>
|
||||
@@ -0,0 +1,26 @@
|
||||
# SKILL — Módulo Tutoria de Ortodontia
|
||||
|
||||
> **Única receita de plataforma automatizada hoje** (MercadoPago). Ver `../FASE_03_PESSOAS/SKILL_Tutores.md`.
|
||||
|
||||
## O que é
|
||||
Dentista vira **tutor** (pagando inscrição) e atende solicitações de orto de outros dentistas; a plataforma
|
||||
fica com uma comissão sobre a consultoria.
|
||||
|
||||
## Entidades
|
||||
`ortho_tutor_config` (`comissao_pct` default 20, `taxa_inscricao`, chave PIX, credenciais MP),
|
||||
`ortho_tutor_profile`, `ortho_tutor_candidatura`, `ortho_tutoring_request`/`message`/`photo_package`.
|
||||
|
||||
## Rotas
|
||||
`POST /api/tutoria/candidaturas[/:id/pagamento]`, `POST /api/tutoria/mp-webhook` (público),
|
||||
`GET/POST /api/tutoria/solicitacoes[...]`, admin: `/api/admin/{candidaturas,repasses,tutor-config}`.
|
||||
|
||||
## Fluxo de receita (verificado)
|
||||
Candidatura → **MercadoPago** (`checkout/preferences`) → `mp-webhook` (idempotente) ativa `is_tutor` →
|
||||
solicitações registram `valor_consultoria`, `comissao_pct`, `valor_tutor`, `status_repasse`.
|
||||
|
||||
## Permissões
|
||||
`candidatura-tutor` (dentistas), `tutoria-painel` (só tutores aprovados, `is_tutor`), `admin-gestao-tutores` (superadmin).
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Ativar tutor sem confirmar pagamento. ❌ Misturar `comissao_pct` da tutoria com a comissão de clínica.
|
||||
</content>
|
||||
@@ -0,0 +1,33 @@
|
||||
# SKILL — Build Once → Promote → Deploy
|
||||
|
||||
> Orientação de infra oficial. Fonte: `documentos-unicos/deploy/`. Implementação de referência: scoreodonto.com.
|
||||
|
||||
## Conceito
|
||||
**Push** na `main` builda a imagem **uma vez** e publica por **commit** (`:<sha>` + `:latest`) — **sem deploy**.
|
||||
A **tag `vX.Y.Z`** **re-tagueia** essa MESMA imagem (mesmo digest, sem rebuild) e deploya. **Produção nunca builda.**
|
||||
|
||||
## Garantia central
|
||||
A imagem em produção é **byte-idêntica** à que foi buildada e testada. A tag é só um rótulo de release
|
||||
sobre um artefato imutável. Validado em scoreodonto (`:v1.0.16` == `:471c2c2`, mesmo digest).
|
||||
|
||||
## Fluxo real
|
||||
```
|
||||
PUSH main (commit C) → CI build 1x → publica :C + :latest (NÃO deploya)
|
||||
TAG vX.Y.Z (em C) → CI re-tag :C → :vX.Y.Z (sem rebuild) + deploy (up --no-build)
|
||||
```
|
||||
|
||||
## Regras obrigatórias
|
||||
1. **Produção só executa imagem pronta** (`pull` + `up -d --no-build`). Nunca `build` na VPS1.
|
||||
2. **Tag exige commit já publicado** (push antes). Sem isso o CI **falha de propósito** (não builda na tag).
|
||||
3. Versão nasce da **tag git**, injetada em runtime (`APP_VERSION`), não escrita à mão.
|
||||
4. Push e tag exigem **autorização explícita do administrador**.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Buildar em produção. ❌ Tag sem push. ❌ Versão hardcoded em arquivo.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
PUSH[push main · commit C] --> B[CI build 1x] --> REG[registry :C + :latest]
|
||||
TAG[tag vX.Y.Z] --> PROM[CI re-tag :C→:vX.Y.Z] --> DEP[VPS1 up --no-build]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,32 @@
|
||||
# SKILL — CI/CD (Gitea Actions)
|
||||
|
||||
> Herda de `SKILL_BuildOnce.md`. Automação de build/promote/deploy por evento git.
|
||||
|
||||
## Conceito
|
||||
O pipeline roda no **Gitea Actions**, runner `vps4-builder` (VPS4, rootless, **instance-scoped** — atende
|
||||
todos os repos). Workflow: `.gitea/workflows/build.yml`.
|
||||
|
||||
## Gatilhos reais
|
||||
- **Push na `main`** → job build: builda `scoreodonto-*:<sha>` e **publica no registry** (não deploya).
|
||||
- **Tag `v*`** → job promote/deploy: re-tag (digest idêntico) → `deploy-prod.sh` na VPS1.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. O runner builda; **produção só consome**. Não mover build para a VPS1.
|
||||
2. Segredos do CI vêm de **secrets do Gitea**, nunca commitados no repo. (Ver `⚠️ SKILL_PostgreSQL`/Registry.)
|
||||
3. Tag só promove se o commit já está no registry (senão falha proposital).
|
||||
4. Manter o pipeline idempotente e auditável (logs do run).
|
||||
|
||||
## Pré-requisitos
|
||||
- Gitea Actions habilitado no repo; runner `vps4-builder` online; `docker login` no registry.
|
||||
- SSH VPS4→VPS1 para o passo de deploy (rsync/`deploy-prod.sh`).
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Credencial em texto no `build.yml`. ❌ Deploy manual fora do pipeline sem registrar.
|
||||
- ❌ Pipeline que builda na tag (quebra o Build-Once).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
PUSH[push main] --> RUN[vps4-builder] --> REG[(registry)]
|
||||
TAG[tag v*] --> RUN2[vps4-builder promote] --> SSH[VPS1 deploy-prod.sh]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,33 @@
|
||||
# SKILL — Deploy Imutável & Rollback
|
||||
|
||||
> Herda de `SKILL_BuildOnce.md`. Como promover e reverter sem rebuild.
|
||||
|
||||
## Conceito
|
||||
O deploy troca **qual imagem** roda, não o código no servidor. Reverter = apontar para outra imagem.
|
||||
|
||||
## Fluxo real
|
||||
- Promover: `deploy-prod.sh <tag>` na VPS1 → `pull` + `docker compose -f docker-compose.prod.yml up -d --no-build`.
|
||||
- `docker-compose.prod.yml` usa `image:` do registry, `APP_VERSION`/`GIT_COMMIT` em runtime.
|
||||
- Rollback: `deploy-prod.sh <ref-anterior>` (segundos/minutos, só troca imagem).
|
||||
|
||||
## Regras obrigatórias
|
||||
1. **Referência confiável = commit (`:<sha>`)**. Tags antigas (pré-Build-Once) podem ter sido republicadas.
|
||||
2. **Migrações são aditivas** (`ADD COLUMN IF NOT EXISTS`) → voltar o código é seguro (schema extra fica inerte).
|
||||
3. **Backup do banco antes de release sensível** (container `pg_dump -Fc` → `/home/deploy/backups/`).
|
||||
4. Validar `/api/version` e `/api/health` após deploy.
|
||||
|
||||
## Pré-requisitos de PROD (uma vez, por projeto)
|
||||
- Arquivo de segredos presente na VPS1 (env_file do compose) — **copiado manualmente, fora do git** (chmod 600).
|
||||
- `docker login git.clube67.com` na VPS1 (para o `pull`).
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Reverter via rebuild. ❌ Migração destrutiva (DROP/ALTER que apaga) — quebra rollback.
|
||||
- ❌ Confiar em tag `:vX.Y.Z` como imutável se for anterior ao Build-Once.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
REG[(registry)] --> PULL[VPS1 pull :ref] --> UP[up -d --no-build]
|
||||
UP --> CHK[/api/version + /api/health/]
|
||||
CHK -->|quebrou| RB[deploy-prod.sh ref-anterior]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,34 @@
|
||||
# SKILL — Docker Rootless & Roteamento
|
||||
|
||||
> Herda de `SKILL_DeployImutavel.md`. Como os containers rodam e são expostos.
|
||||
|
||||
## Topologia real
|
||||
- **Rede privada WireGuard** `10.99.0.0/24`.
|
||||
- **VPS1 (produção):** Docker **rootless** — comandos usam `DOCKER_HOST=unix:///run/user/1000/docker.sock`.
|
||||
Roda Traefik (borda TLS) + apps (scoreodonto, rx, newwhats, subdomínio, mercado).
|
||||
- **VPS4 (dev+builder):** Docker rootless; roda `dev.<projeto>` (imagem buildada, **não** HMR) + runner.
|
||||
- **Roteamento (Traefik na VPS1):** termina TLS e roteia por host:
|
||||
`scoreodonto.com` → containers de produção locais; `dev.scoreodonto.com` → `10.99.0.4:8020` (VPS4) via WireGuard.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. Na VPS1, sempre exportar `DOCKER_HOST` rootless antes de comandos `docker`.
|
||||
2. Exposição pública **só via Traefik** (TLS `certresolver=le`); apps não publicam porta na internet.
|
||||
3. `dev.<projeto>` é descartável (VPS4); produção é a real (VPS1) — **confirmar DEV ou PROD antes de agir**.
|
||||
4. Editar dev exige **rebuild + recreate** (não há hot-reload no container atual).
|
||||
|
||||
## ⚠️ Para abrir produção
|
||||
- Garantir que apenas Traefik exponha portas; conferir labels de roteador/host por app.
|
||||
- Rede WireGuard fechada; serviços de dados (VPS3) **nunca** expostos à internet.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ `docker` na VPS1 sem `DOCKER_HOST` rootless. ❌ Publicar porta de app direto na internet.
|
||||
- ❌ Confundir `dev.` (VPS4) com produção (VPS1).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
NET[Internet] -->|TLS| TR[Traefik VPS1]
|
||||
TR -->|host scoreodonto.com| APP[containers prod]
|
||||
TR -->|host dev.*| WG[WireGuard → VPS4:8020]
|
||||
APP --> DADOS[(VPS3 dados · só rede privada)]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,30 @@
|
||||
# SKILL — Gitea (código + registry + CI)
|
||||
|
||||
> Herda de `SKILL_CICD.md`. A VPS3 concentra a "fonte da verdade" do código.
|
||||
|
||||
## Papel real
|
||||
- **Código:** Gitea na **VPS3** é a fonte da verdade (repo `ruicesar/scoreodonto.com`).
|
||||
- **Registry:** Container Registry embutido do Gitea (ver `SKILL_Registry.md`).
|
||||
- **CI:** Gitea Actions + runner `vps4-builder` (ver `SKILL_CICD.md`).
|
||||
- Doc oficial da stack: repo `ruicesar/Documentos-Unicos` (local `documentos-unicos/`).
|
||||
|
||||
## Regras obrigatórias
|
||||
1. **Nunca `git push --force` na `main`.** Push/tag exigem autorização do administrador.
|
||||
2. **Segredos via Gitea Secrets / webhooks**, nunca commitados.
|
||||
3. Idioma português em commits/docs (regra da stack).
|
||||
4. Webhooks/Actions antigos desligados não devem ser reativados sem auditoria.
|
||||
|
||||
## ⚠️ Higiene de repositório (para abrir produção)
|
||||
- **Não versionar `.env`/segredos.** Hoje `backend/.env` está rastreado — corrigir e **rotacionar** as
|
||||
credenciais expostas (ver `SKILL_PostgreSQL`). Confirmar `.gitignore` cobrindo `**/.env` e `.secrets`.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ `push --force` na main. ❌ Commitar segredo. ❌ Reativar webhook legado sem auditoria.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
DEV[VPS4 dev] -->|push| GITEA[Gitea VPS3]
|
||||
GITEA --> ACT[Actions · vps4-builder]
|
||||
GITEA --> REG[(Registry)]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,25 @@
|
||||
# SKILL — PostGIS (geolocalização)
|
||||
|
||||
> Herda de `SKILL_PostgreSQL.md`. Extensão geoespacial usada por busca por proximidade.
|
||||
|
||||
## Onde é usado (real)
|
||||
- Coluna `geo geography(Point,4326)` em `clinicas` e `salas` (e `cep_geo` para CEP→coordenada).
|
||||
- Suporta o **marketplace de salas** (achar salas próximas) e geocódigo de endereço.
|
||||
- O container `pgdev` é uma imagem **PostGIS** (dev espelha a capacidade da produção).
|
||||
|
||||
## Regras obrigatórias
|
||||
1. Usar `geography(Point,4326)` (lat/long padrão) — não inventar SRID.
|
||||
2. Índice espacial (GIST) quando houver busca por distância em volume.
|
||||
3. Geocódigo (`cep_geo`) é cache — tratar ausência com fallback, não quebrar o cadastro.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Guardar lat/long como texto solto em vez de tipo geográfico.
|
||||
- ❌ Busca por proximidade sem índice (varredura completa).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
END[Endereço/CEP] --> GEO[cep_geo → ponto]
|
||||
GEO --> SAL[salas.geo]
|
||||
SAL --> PROX[busca por proximidade · marketplace]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,32 @@
|
||||
# SKILL — PostgreSQL (dados)
|
||||
|
||||
> Herda de `../FASE_01_ARQUITETURA/SKILL_Multiempresa.md`. Banco de produção e isolamento de dev.
|
||||
|
||||
## Topologia real
|
||||
- **Produção:** PostgreSQL no host da **VPS3** (`10.99.0.3:5432`), banco `scoreodonto`, user `scoreodonto_user`.
|
||||
- **DEV isolado:** container **`pgdev`** (PostGIS) na VPS4, banco `scoreodonto_dev`, user `devadmin` —
|
||||
**não toca produção**. `pg-dev-refresh.sh` / `pg-dev-reset.sh` para repuxar/limpar.
|
||||
- Acesso via WireGuard (`10.99.0.0/24`). Sem shell na VPS3 → operar por container (`pg_dump`/`pg_restore`).
|
||||
|
||||
## Regras obrigatórias
|
||||
1. **Toda query operacional filtra `clinica_id`** (tenancy — ver Multiempresa).
|
||||
2. **Migrações aditivas** no boot (`CREATE TABLE/ADD COLUMN IF NOT EXISTS`). Nunca DROP/ALTER destrutivo.
|
||||
3. **Backup antes de release sensível**: `pg_dump -Fc` → `/home/deploy/backups/`.
|
||||
4. Testar migração arriscada **sempre em `pgdev`** antes da produção.
|
||||
|
||||
## ⚠️ SEGREDOS (crítico para abrir produção)
|
||||
- **A senha do banco NÃO pode ficar em arquivo versionado nem como default em `docker-compose.yml`.**
|
||||
Hoje há default hardcoded no compose (`SCOREO_DB_PASS:-...`) e `backend/.env` está **rastreado no git** —
|
||||
isso vaza credenciais. Segredos devem vir de **env_file fora do git** (chmod 600) ou secrets do CI.
|
||||
- Rotacionar qualquer credencial que tenha entrado no histórico do git.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Query sem `clinica_id`. ❌ Migração destrutiva. ❌ Senha no compose/`.env` versionado.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
APP[Backend VPS1] -->|WireGuard| PG[(Postgres VPS3 · scoreodonto)]
|
||||
DEVAPP[Backend DEV VPS4] --> PGDEV[(pgdev · scoreodonto_dev)]
|
||||
PG --> BKP[/home/deploy/backups · pg_dump -Fc/]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,25 @@
|
||||
# SKILL — Registry de Imagens
|
||||
|
||||
> Herda de `SKILL_BuildOnce.md`. Onde os artefatos imutáveis vivem.
|
||||
|
||||
## Conceito
|
||||
Registry = **Container Registry embutido do Gitea** (`git.clube67.com`, namespace `ruicesar/`).
|
||||
Não há `registry:2` separado. Imagens: `git.clube67.com/ruicesar/scoreodonto-{backend,frontend}:<tag>`.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. Imagem por **commit** (`:<sha>`) é a fonte; `:vX.Y.Z` é alias imutável do mesmo digest.
|
||||
2. `docker login git.clube67.com` na VPS1 (pull) e no runner (push) — credenciais via secrets, não no git.
|
||||
3. **Imagem enxuta**: `.dockerignore` deve excluir `node_modules`, `data/`, dumps, mídia pesada.
|
||||
(Lição do RX: camadas grandes davam PUT 500/499 no registry.)
|
||||
4. Não sobrescrever uma tag de release com código diferente (quebra a imutabilidade).
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Republicar `:vX.Y.Z` apontando para outro código. ❌ Empurrar imagem gigante (sem `.dockerignore`).
|
||||
- ❌ Guardar credencial do registry em arquivo versionado.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
RUN[runner build] -->|push :sha| REG[(Gitea Registry · ruicesar/)]
|
||||
REG -->|pull :ref| VPS1[Produção]
|
||||
```
|
||||
</content>
|
||||
@@ -0,0 +1,28 @@
|
||||
# SKILL — Auditoria de APIs
|
||||
|
||||
> Como mapear e evoluir a API sem quebrar consumidores. Complementa `SKILL_Rotas.md`.
|
||||
|
||||
## Realidade
|
||||
- **~277 rotas** REST `/api/*` num único `backend/server.js` (Express monolítico, ~516 KB). Sem camada de
|
||||
controllers/services separada.
|
||||
- Guards: `authGuard` (identidade), `tenantGuard` (tenant), `requireCapability*` (cargo), `superadminGuard`.
|
||||
- Endpoints transversais: `GET /api/version`, `GET /api/health` (ver FASE_09); mídia por **URL assinada**
|
||||
(`/raw?t=signMedia(...)`).
|
||||
- Frontend consome via `services/backend.ts` (`HybridBackend`/`apiFetch`).
|
||||
|
||||
## Como auditar
|
||||
```bash
|
||||
grep -oE "app\.(get|post|put|delete|patch)\('[^']+" backend/server.js | sort -u # todas as rotas
|
||||
grep -rn "API_URL}/<recurso>" frontend/services/backend.ts # consumidores
|
||||
```
|
||||
|
||||
## Regras obrigatórias
|
||||
1. **Nunca** mudar o caminho/contrato de uma rota sem achar TODOS os consumidores (`services/backend.ts` + views).
|
||||
2. Toda rota nova: definir guard correto (identidade vs tenant vs cargo) — ver `SKILL_Permissoes.md`.
|
||||
3. Erros consistentes (`{ error }` / `{ success:false, message }` conforme o padrão vizinho).
|
||||
4. Rota que toca dado de clínica → `clinica_id` + `tenantGuard`.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Quebrar contrato sem mapear consumidores. ❌ Rota sensível sem guard de cargo.
|
||||
- ❌ Mídia sem token assinado. ❌ Inventar convenção de resposta diferente da do arquivo.
|
||||
</content>
|
||||
@@ -0,0 +1,27 @@
|
||||
# SKILL — Auditoria de Banco
|
||||
|
||||
> Como inspecionar e mexer no banco sem quebrar. Herda de `../FASE_07_INFRA/SKILL_PostgreSQL.md`.
|
||||
|
||||
## Realidade
|
||||
- **PostgreSQL** (PostGIS) na VPS3 (`10.99.0.3`), banco `scoreodonto`. ~70 tabelas acessadas por **SQL direto**
|
||||
(`pg`), via helpers `buildInsert`/`buildUpdate`. **Prisma é vestigial** (4 modelos GTO legados).
|
||||
- DEV isolado: container `pgdev` (banco `scoreodonto_dev`). Mexer aqui tem zero impacto em produção.
|
||||
- **Migrações aditivas no boot**: blocos `CREATE TABLE IF NOT EXISTS` / `ALTER TABLE ... ADD COLUMN IF NOT EXISTS`.
|
||||
|
||||
## Regras obrigatórias
|
||||
1. **Só aditivo** (add tabela/coluna). Nunca DROP/ALTER destrutivo → quebra rollback (ver Deploy Imutável).
|
||||
2. **Toda query operacional filtra `clinica_id`** (tenancy).
|
||||
3. Validar migração arriscada **no `pgdev`** antes; **backup `pg_dump -Fc`** antes de release sensível.
|
||||
4. Sem shell na VPS3 → operar por container (`pg_dump`/`pg_restore`/`psql`).
|
||||
|
||||
## Como auditar (via container, com creds do `.env`)
|
||||
```bash
|
||||
# tabelas e colunas
|
||||
grep -niE "CREATE TABLE IF NOT EXISTS|ADD COLUMN IF NOT EXISTS" backend/server.js
|
||||
# índices declarados
|
||||
grep -niE "CREATE INDEX" backend/server.js
|
||||
```
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Migração destrutiva. ❌ Query sem `clinica_id`. ❌ Mexer direto na produção sem testar no `pgdev`/backup.
|
||||
</content>
|
||||
@@ -0,0 +1,28 @@
|
||||
# SKILL — Auditoria de Botões
|
||||
|
||||
> Como rastrear o que cada botão faz (rota → tabela → fluxo) antes de mexer. Base:
|
||||
> `documentação/MAPA-FUNCIONAL-COMPLETO.md` (Etapa 3).
|
||||
|
||||
## Método
|
||||
Para cada botão: **rótulo → handler no front → método HTTP/rota → tabela impactada → fluxo a jusante**.
|
||||
|
||||
## Exemplos reais (verificados)
|
||||
| Botão (tela) | Rota | Tabela | Próximo fluxo |
|
||||
|---|---|---|---|
|
||||
| Novo Agendamento (Agenda) | `POST /api/agendamentos` | `agendamentos` | Procedimento → Financeiro |
|
||||
| Confirmar Presença (Agenda) | `POST /api/agenda/presenca` | `agenda_presenca` | libera lançamento |
|
||||
| Lançar Procedimento (Pacientes) | `POST /api/procedimentos-realizados` | `procedimentos_realizados` (+`financeiro`) | Comissão → Repasse |
|
||||
| Fechar Repasse (Comissões) | `POST /api/repasses/fechar` | `repasses` | Pagar → DESPESA |
|
||||
| Gerar Contrato (Contratos) | `POST /api/contratos/instancias` | `contratos` | gerar-cobrança → Financeiro |
|
||||
| Converter (Leads) | `POST /api/leads/:id/converter` | `pacientes` + `agendamentos` | atendimento |
|
||||
|
||||
## Como auditar
|
||||
```bash
|
||||
grep -oE ">[A-ZÀ-Ú][a-zà-úA-ZÀ-Ú /]{3,28}<" frontend/views/<View>.tsx # rótulos
|
||||
# depois localize o onClick → método do HybridBackend → rota em services/backend.ts
|
||||
```
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Alterar a ação de um botão sem mapear a tabela e o fluxo a jusante (sobretudo financeiro).
|
||||
- ❌ Botão que muta sem confirmação onde o padrão da tela usa `useConfirm`.
|
||||
</content>
|
||||
@@ -0,0 +1,29 @@
|
||||
# SKILL — Auditoria de Entidades
|
||||
|
||||
> Como verificar se as entidades estão bem classificadas. Herda de
|
||||
> `../FASE_01_ARQUITETURA/SKILL_ModelagemMestra.md`. Detalhe em `documentação/AUDITORIA-ESTRATEGICA-MODELAGEM.md`.
|
||||
|
||||
## Entidades-âncora (corretas)
|
||||
- `usuarios` (Pessoa) · `vinculos(usuario_id, clinica_id, role, setor_id, funcao_id)` (Vínculo+RBAC)
|
||||
- `clinicas(tipo: pessoal|consultorio|clinica, owner_id)` + `salas(tipo='sala')` (Workspaces)
|
||||
- `usuario_areas`, `usuario_plugins`, `funcoes` (capacidades/cargo)
|
||||
|
||||
## Débitos de modelagem (gravidade ALTA — vigiar)
|
||||
- **`dentistas`**: tabela paralela a `usuarios` (mesma pessoa duplicada por clínica, `usuario_id` opcional)
|
||||
→ tratar como **projeção de agenda**, não identidade.
|
||||
- **`pacientes`**: tem `clinica_id` e **não** `usuario_id` (preso à clínica, não-portável); há 2 variantes de
|
||||
INSERT (com/sem `clinica_id`) — resíduo legado.
|
||||
- **MÉDIA**: workspace dividido em 2 tabelas (`clinicas` + `salas`); acoplamento role↔tipo (`donoconsultorio`).
|
||||
|
||||
## Como auditar
|
||||
```bash
|
||||
grep -niE "CREATE TABLE" backend/server.js | sort # tabelas reais (banco é SQL puro, Prisma vestigial)
|
||||
grep -oiE "(FROM|JOIN|INTO|UPDATE) [a-z_]+" backend/server.js | awk '{print $2}' | sort | uniq -c | sort -rn
|
||||
```
|
||||
Para cada entidade, pergunte: é **Pessoa**, **Vínculo**, **Workspace**, **capacidade** ou **dado operacional**?
|
||||
Se não encaixa, é candidata a má-classificação.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Criar tabela-entidade que espelha a pessoa por workspace (repetir o erro de `dentistas`).
|
||||
- ❌ Dado operacional sem `clinica_id`. ❌ Identidade fora de `usuarios`.
|
||||
</content>
|
||||
@@ -0,0 +1,47 @@
|
||||
# SKILL — Auditoria de Fluxos
|
||||
|
||||
> Objetivo: ensinar a **mapear o fluxo de negócio ponta a ponta antes de alterar** — sobretudo o do dinheiro.
|
||||
> Regra Nº1: não quebrar o que funciona. Toda mudança em fluxo financeiro exige mapeamento prévio.
|
||||
|
||||
## Fluxo principal (núcleo — verificado no código)
|
||||
```
|
||||
Lead ─converter→ Paciente + Agendamento ─presença→ Procedimento Realizado
|
||||
→ Financeiro RECEITA (origem='procedimento', flag sem_comissao)
|
||||
→ Comissão = (base − custo_lab) × % [comissao_regras + computeComissoes]
|
||||
→ Repasse FECHADO → pagar → Financeiro DESPESA (origem='repasse', centro_custo='COMISSÕES')
|
||||
```
|
||||
Ramos: Orçamento → Contrato → `gerar-cobranca` → Financeiro; Glosas sobre recebíveis de convênio.
|
||||
|
||||
## Pontos sensíveis (não quebrar)
|
||||
1. **RECEITA é opcional** no procedimento (`valor>0` e `gerarLancamento!==false`) — alterar isso muda o caixa.
|
||||
2. **`sem_comissao`** propaga garantia/reabertura e o repasse pago (evita recomissionar). Manter.
|
||||
3. **Repasse cancelado estorna** a despesa (soft-delete). Preservar a reversibilidade.
|
||||
4. **Anti-overbooking** na conversão de lead e na agenda — não remover a checagem.
|
||||
5. **Marketplace**: só Tutoria captura pagamento (MercadoPago); Salas tem `valor` sem gateway.
|
||||
|
||||
## Como auditar um fluxo (comandos)
|
||||
```bash
|
||||
# Achar o handler de uma etapa
|
||||
grep -n "repasses/fechar\|procedimentos-realizados\|leads/:id/converter" backend/server.js
|
||||
# Ver o que cada etapa INSERE/ATUALIZA
|
||||
grep -nE "INSERT INTO (financeiro|repasses|procedimentos_realizados)" backend/server.js
|
||||
# Conferir auditoria
|
||||
grep -n "registrarAudit" backend/server.js | head
|
||||
```
|
||||
|
||||
## Checklist antes de alterar um fluxo
|
||||
1. Onde começa, onde termina, o que cada etapa grava (tabela + colunas)?
|
||||
2. Há `audit_log` (`registrarAudit`) na etapa? Manter.
|
||||
3. A mudança afeta comissão/repasse/caixa? Mapear o impacto a jusante.
|
||||
4. Existe reversão (estorno/soft-delete)? Não quebrá-la.
|
||||
5. Validar em `dev.scoreodonto.com` com banco `pgdev` antes de qualquer promoção.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Alterar geração de RECEITA/DESPESA sem mapear o efeito em Comissões/Repasses.
|
||||
- ❌ Remover flags `sem_comissao`/`deleted_at` (quebram não-duplicação e estorno).
|
||||
- ❌ Mexer no fluxo sem reproduzir ponta a ponta em DEV.
|
||||
|
||||
## Entregável
|
||||
Diagrama Mermaid do fluxo + tabela etapa→tabela→efeito + riscos. (Modelo:
|
||||
`documentação/MAPA-FUNCIONAL-COMPLETO.md`, Etapas 4 e 8.)
|
||||
</content>
|
||||
@@ -0,0 +1,28 @@
|
||||
# SKILL — Auditoria de Performance
|
||||
|
||||
> Onde olhar antes de otimizar. Sem micro-otimização cega — medir primeiro.
|
||||
|
||||
## Pontos de atenção reais
|
||||
- **`server.js` monolítico** (~516 KB): carga cognitiva alta, mas não é gargalo de runtime por si.
|
||||
- **Guards fazem queries por request**: `tenantGuard` (1 SELECT em `vinculos`) + `requireCapability` (1–2
|
||||
SELECTs em `usuarios`/`vinculos`/`funcoes`). Aceitável; se virar gargalo, considerar cache curto do papel.
|
||||
- **Catálogos/listas**: `GET /api/{procedimentos,especialidades,planos,dentistas,pacientes,agendamentos}` —
|
||||
garantir índices em `clinica_id` e filtros corretos.
|
||||
- **PostGIS**: busca por proximidade (salas) precisa de índice GIST em `geo`.
|
||||
- **Cache DragonflyDB** (`/1`): usado para acelerar; `/api/health` reporta latência de banco e cache.
|
||||
|
||||
## Como auditar
|
||||
```bash
|
||||
grep -niE "CREATE INDEX" backend/server.js # índices existentes
|
||||
grep -niE "SELECT .* FROM (pacientes|agendamentos)" backend/server.js | head # queries quentes
|
||||
curl -s https://<projeto>/api/health # latency_ms de database/cache
|
||||
```
|
||||
|
||||
## Regras obrigatórias
|
||||
1. **Medir** (latências do `/api/health`, EXPLAIN) antes de otimizar.
|
||||
2. Índice em toda coluna usada em filtro frequente (`clinica_id`, FKs, status).
|
||||
3. Não introduzir N+1 em loop de query; preferir `IN`/JOIN.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Otimizar sem medir. ❌ Remover guard "para acelerar" (quebra segurança). ❌ Lista sem filtro por `clinica_id`.
|
||||
</content>
|
||||
@@ -0,0 +1,36 @@
|
||||
# SKILL — Auditoria de Permissões (RBAC)
|
||||
|
||||
> Como o controle de acesso REAL funciona hoje e como auditá-lo. Herda de
|
||||
> `../FASE_01_ARQUITETURA/SKILL_RBAC.md`. Implementado no backend nesta stack.
|
||||
|
||||
## Três planos de autorização (backend)
|
||||
1. **Identidade** — `authGuard`: valida o JWT (assinado com `JWT_SECRET`), popula `req.authUser.userId`.
|
||||
2. **Tenant** — `tenantGuard`: valida que o usuário tem **vínculo** com a `clinica_id` pedida (ou é dono da
|
||||
sala); seta `req.clinicaId`. Sem vínculo → 403 `[TENANT VIOLATION]`.
|
||||
3. **Cargo (capability)** — `requireCapability(cap)` / `requireCapabilityRow(cap, tabela)` /
|
||||
`requireCapabilityFromOrders(cap, tabela)`: exige a capability no workspace ativo. **Superadmin** → `superadminGuard` (checa `usuarios.role` no banco).
|
||||
|
||||
## `capabilityCore` (espelha o `isViewAllowed` do frontend)
|
||||
- superadmin global → ok;
|
||||
- dono (`donoclinica`/`donoconsultorio`/`admin`, `CAP_OWNER_ROLES`) → ok;
|
||||
- cargo com `funcoes.permissoes` (JSONB) explícitas → ok se inclui a capability;
|
||||
- senão, **mapa padrão por papel** (`CAP_DEFAULTS`, cópia fiel do frontend).
|
||||
|
||||
## Cobertura (99 rotas gateadas)
|
||||
financeiro (`'financeiro'`/`'comissoes'`/`'glosas'`), equipe (`'gestao-equipe'`), cadastros
|
||||
(`'dentistas'`/`'especialidades'`/`'procedimentos'`/`'planos'`, incl. `/reorder`), operação clínica
|
||||
(`'agenda'`/`'leads'`), orçamentos/contratos. Marketplace (salas/propostas) = autorização por **posse** no handler.
|
||||
|
||||
## Como auditar
|
||||
```bash
|
||||
grep -nE "tenantGuard|authGuard|requireCapability|superadminGuard" backend/server.js # guards por rota
|
||||
grep -nE "CAP_DEFAULTS|CAP_OWNER_ROLES" backend/server.js # matriz de papéis
|
||||
```
|
||||
Regra de ouro: **a capability do backend tem que casar com o `isViewAllowed`/Sidebar do frontend**.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Rota sensível só com `tenantGuard` (membersia ≠ cargo).
|
||||
- ❌ `authGuard` puro em rota que toca dado de clínica (falta tenant).
|
||||
- ❌ Capability no backend divergente do menu do frontend.
|
||||
- ❌ Confiar em estado do cliente (`localStorage`/preview) para autorizar.
|
||||
</content>
|
||||
@@ -0,0 +1,43 @@
|
||||
# SKILL — Auditoria de Rotas
|
||||
|
||||
> Objetivo: ensinar qualquer agente a **mapear e entender as rotas antes de alterar** — front e back.
|
||||
> Regra Nº1: não quebrar o que funciona. Sempre Auditoria → Planejamento → Aprovação.
|
||||
|
||||
## Onde a verdade está
|
||||
- **Frontend (navegação):** `frontend/App.tsx` — `ROUTE_MAP` (URL→view), `VIEW_TO_ROUTE` (view→URL),
|
||||
`renderView()` (view→componente), `isViewAllowed()` (RBAC), e o filtro de menu em `components/Sidebar.tsx`.
|
||||
Não há `react-router` — o roteamento é próprio.
|
||||
- **Backend (API):** `backend/server.js` — **277 rotas** `app.<método>('/api/...')`, quase todas com `tenantGuard`.
|
||||
|
||||
## Como mapear (comandos de auditoria)
|
||||
```bash
|
||||
# Rotas de API (método + caminho)
|
||||
grep -oE "app\.(get|post|put|delete|patch)\('[^']+" backend/server.js | sort -u
|
||||
# Rotas do frontend (URL → view)
|
||||
sed -n '/ROUTE_MAP/,/};/p' frontend/App.tsx
|
||||
# Permissões por papel
|
||||
sed -n '/isViewAllowed/,/^ };/p' frontend/App.tsx
|
||||
sed -n '/menuItems/,/];/p' frontend/components/Sidebar.tsx
|
||||
```
|
||||
|
||||
## Checklist antes de mexer numa rota
|
||||
1. A URL existe em **ambos** `ROUTE_MAP` e `VIEW_TO_ROUTE`? (senão deep-link quebra)
|
||||
2. Qual `isViewAllowed`/menu libera essa view e para quais papéis?
|
||||
3. Qual rota de API ela consome? Tem `tenantGuard` (escopo de clínica)?
|
||||
4. Quais tabelas a rota toca? (ver `SKILL_Banco.md`/`SKILL_Entidades.md`)
|
||||
5. Alterou acesso no back? **Espelhe** em `isViewAllowed` + `Sidebar` no mesmo PR.
|
||||
|
||||
## Débitos conhecidos a vigiar (já auditados)
|
||||
- `/comissoes`, `/glosas`, `/cadastro-dentista`: existem em `VIEW_TO_ROUTE` mas **faltam em `ROUTE_MAP`**
|
||||
→ deep-link/refresh cai em `dashboard`. (Documentado em `MAPA-FUNCIONAL-COMPLETO.md`.)
|
||||
- `/admin/sync`: rota existe, mas `isViewAllowed` retorna `false` sempre (tela inacessível).
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Adicionar view sem registrar nas DUAS direções (`ROUTE_MAP` + `VIEW_TO_ROUTE`).
|
||||
- ❌ Liberar tela no menu sem proteger a rota de API correspondente.
|
||||
- ❌ Alterar caminho de API sem buscar todos os consumidores no frontend (`services/backend.ts`).
|
||||
|
||||
## Entregável da auditoria
|
||||
Tabela URL → view → componente → papéis → rota(s) de API → tabelas, + riscos. (Modelo:
|
||||
`documentação/MAPA-FUNCIONAL-COMPLETO.md`, Etapa 1.)
|
||||
</content>
|
||||
@@ -0,0 +1,28 @@
|
||||
# SKILL — Diagnósticos (runbook de incidente)
|
||||
|
||||
> Passo a passo quando algo quebra. Combina Health, Logs, Deploy Imutável.
|
||||
|
||||
## Triagem rápida
|
||||
1. `GET /api/health` — `database down` (503)? `cache degraded`? `version` esperada?
|
||||
2. `GET /api/version` — bate com a tag que deveria estar no ar?
|
||||
3. `docker ps` (rootless) — container `Up`? reiniciando (`RestartCount`)?
|
||||
4. `docker logs --tail 80 <container>` — erro no boot? `[PG]`/`[DRAGONFLY]` conectaram?
|
||||
|
||||
## Causas comuns (reais nesta stack)
|
||||
- **`env file .secrets/.env not found`** → falta o env_file no host (VPS1). Copiar manualmente (fora do git).
|
||||
- **`database down`** → Postgres VPS3 inacessível (WireGuard) ou credencial errada.
|
||||
- **`cache down`** → Dragonfly/`ping()` (degradável; não derruba).
|
||||
- **Migração** falhou no boot → ver log `[MIGRATION]`.
|
||||
|
||||
## Recuperação
|
||||
- App quebrado pós-deploy → **rollback por imagem** (segundos): `deploy-prod.sh <ref-anterior>` (ver Deploy Imutável).
|
||||
- Dado corrompido → restaurar `pg_dump` de `/home/deploy/backups/` (caso extremo).
|
||||
|
||||
## Regras obrigatórias
|
||||
1. Diagnosticar com `/api/health` + logs **antes** de mexer.
|
||||
2. Preferir **rollback de imagem** a "consertar em produção no quente".
|
||||
3. Reproduzir/testar a correção no `dev` (`pgdev`) antes de promover.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Editar código direto na VPS1. ❌ Mexer no banco de produção sem backup. ❌ Deploy às cegas sem health.
|
||||
</content>
|
||||
@@ -0,0 +1,26 @@
|
||||
# SKILL — Health Checks
|
||||
|
||||
> Como saber se o app está vivo e saudável, sem SSH. Fonte: `documentos-unicos/deploy/versionamento.md`.
|
||||
|
||||
## Endpoint real
|
||||
`GET /api/health` → `{ status, version, commit, environment, uptime_s, checks }`:
|
||||
- **`database`** é crítico: se cair → `status: down` + **HTTP 503**.
|
||||
- **`cache`** (DragonflyDB) é opcional: se cair → `status: degraded` (HTTP 200); `disabled` se desligado.
|
||||
- Cada check traz `latency_ms` — útil para perceber lentidão de banco/cache.
|
||||
|
||||
## Como usar
|
||||
```bash
|
||||
curl -s https://<projeto>/api/health # produção
|
||||
curl -s https://dev.<projeto>/api/health # dev
|
||||
```
|
||||
|
||||
## Regras obrigatórias
|
||||
1. **Validar `/api/health` após todo deploy** (e na rotina de monitoramento).
|
||||
2. `database down` (503) é incidente → investigar conexão VPS1→VPS3 / Postgres.
|
||||
3. `cache down/degraded` não derruba o app, mas registre (ex.: pré-existente no RX — `redis.ping()`).
|
||||
4. Não remover/alterar o contrato do `/api/health` (monitor externo depende dele).
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Deploy sem checar health. ❌ Tornar o cache crítico (deve ser degradável).
|
||||
- ❌ Vazar segredo/detalhe interno no payload do health.
|
||||
</content>
|
||||
@@ -0,0 +1,28 @@
|
||||
# SKILL — Logs & Auditoria
|
||||
|
||||
> Onde está o rastro do que aconteceu. Dois níveis: log de aplicação e trilha de auditoria persistida.
|
||||
|
||||
## Auditoria de negócio (persistida)
|
||||
- Tabela **`audit_log`** (`clinica_id, entidade, entidade_id, acao, actor_id, actor_nome, detalhes, at`),
|
||||
gravada por `registrarAudit(...)` em mutações sensíveis (procedimento, repasse, comissão, contrato, equipe…).
|
||||
- **Imutável** — é o rastro de quem fez o quê. Mutações financeiras/RBAC devem registrar.
|
||||
- `login_log` registra acessos (visível em SuperAdmin → Acessos).
|
||||
|
||||
## Log de aplicação (runtime)
|
||||
- `console.*` no `server.js` (boot, `[PG] Connected`, `[DRAGONFLY] Connected`, `[WS] broadcast`,
|
||||
`[TENANT VIOLATION]`, `[CLINICA] Created`…). Visível em `docker logs <container>` (rootless).
|
||||
|
||||
## Como usar
|
||||
```bash
|
||||
DOCKER_HOST=unix:///run/user/1000/docker.sock docker logs --tail 50 <container>
|
||||
grep -n "registrarAudit" backend/server.js # pontos auditados
|
||||
```
|
||||
|
||||
## Regras obrigatórias
|
||||
1. Toda mutação sensível (dinheiro, cargo, contrato) chama `registrarAudit`. **Não** remover.
|
||||
2. **Nunca** logar segredo/credencial/token.
|
||||
3. `[TENANT VIOLATION]` no log = tentativa de acesso cross-tenant → investigar.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Mutação sensível sem auditoria. ❌ Logar dado pessoal/segredo. ❌ Apagar `audit_log`.
|
||||
</content>
|
||||
@@ -0,0 +1,26 @@
|
||||
# SKILL — Versionamento em Runtime
|
||||
|
||||
> De onde vem a versão exibida e por que ela é confiável. Herda de `../FASE_07_INFRA/SKILL_BuildOnce.md`.
|
||||
|
||||
## Como funciona (real)
|
||||
- A versão **nasce da tag git** (`vX.Y.Z`) e é **injetada no deploy em runtime** (`APP_VERSION`), não escrita à
|
||||
mão (não existe mais `constants.ts`/`update-version.js`).
|
||||
- `commit` e `build` são **carimbados na imagem** (imutáveis, propriedade do artefato).
|
||||
- Em **DEV** aparece `dev`; o número só existe em produção.
|
||||
- Exposição: `GET /api/version` → `{ name, version, commit, build, environment }`. O frontend **não embute**
|
||||
versão — lê de `/api/version` (fonte única = backend). Mostrada no rodapé (canto inf. direito) e em
|
||||
Configurações → "Sobre o sistema".
|
||||
|
||||
## Como verificar
|
||||
```bash
|
||||
curl -s https://<projeto>/api/version # → vX.Y.Z · <commit> · PROD
|
||||
```
|
||||
|
||||
## Regras obrigatórias
|
||||
1. **Não** hardcodar versão em arquivo — só a tag define.
|
||||
2. A MESMA imagem promovida sob a tag = `commit`/`build` imutáveis + `version`/`environment` em runtime.
|
||||
3. Conferir `/api/version` após deploy (bate com a tag promovida?).
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Escrever versão manualmente. ❌ Frontend embutir versão. ❌ Rebuild na tag (quebra o build-once).
|
||||
</content>
|
||||
@@ -0,0 +1,26 @@
|
||||
# SKILL — Backlog (Roadmap)
|
||||
|
||||
> Itens reais levantados nas auditorias e na sessão de hardening. Não inventar; manter atualizado.
|
||||
|
||||
## Segurança / produção (parcialmente feito em DEV)
|
||||
- ✅ JWT rotacionado (dev+prod); `.env` fora do git; `.dockerignore`; senhas fora do compose; RBAC por cargo (99 rotas).
|
||||
- ⬜ Provisionar `SCOREO_DB_PASS` na VPS1; push imagem limpa `v1.0.19`; rotacionar DB/cache/API keys.
|
||||
|
||||
## Correções (do MAPA-FUNCIONAL)
|
||||
- ⬜ Deep-links residuais já tratados (`/comissoes`, `/glosas`); revisar outros se surgirem.
|
||||
- ⬜ RECEITA de procedimento opcional → decidir se vira padrão (risco de caixa).
|
||||
- ⬜ Estado de plugins divergente cliente/servidor (`localStorage` vs `usuario_plugins`).
|
||||
|
||||
## Modelagem (do AUDITORIA-ESTRATEGICA)
|
||||
- ⬜ `dentistas` como projeção da pessoa; `pacientes` com identidade (LGPD/jurídico antes).
|
||||
- ⬜ Unificar workspace (`clinicas`+`salas`); entidade "rede" acima da clínica.
|
||||
|
||||
## Receita (do SKILL_Monetizacao)
|
||||
- ⬜ Gateway na Locação de Salas; recorrência das Assinaturas; marketplace premium.
|
||||
|
||||
## Limpeza
|
||||
- ⬜ Órfã `OrthoReports.tsx`; `SyncView` desativada; `server.js.bak`/scripts legados.
|
||||
|
||||
## Regra
|
||||
Backlog reflete a **realidade**; ao concluir, marcar ✅ com referência ao commit/PR.
|
||||
</content>
|
||||
@@ -0,0 +1,24 @@
|
||||
# SKILL — Escalabilidade (Roadmap)
|
||||
|
||||
> O que o modelo suporta em 5 anos e onde estão os gargalos. Base: `AUDITORIA-ESTRATEGICA-MODELAGEM.md`.
|
||||
|
||||
## O que JÁ escala (base Pessoa→Vínculo→Workspace)
|
||||
- Pessoa em **múltiplos workspaces** (N `vinculos`), workspace pessoal automático, salas/tutoria/marketplace.
|
||||
|
||||
## Gargalos conhecidos
|
||||
| Cenário | Suporta? | Gargalo |
|
||||
|---|---|---|
|
||||
| Dentista em várias clínicas | parcial | `dentistas` **duplica a pessoa** por clínica (agenda/horários presos ao `dentistas.id`) |
|
||||
| Paciente entre clínicas | não | `pacientes` preso à clínica, sem identidade de pessoa (não-portável) |
|
||||
| Redes de clínicas | não | **não existe entidade "rede/grupo"** acima da clínica (workspaces são planos) |
|
||||
| Visão consolidada da pessoa | não | tenancy de `clinica_id` único impede agregar cross-clínica |
|
||||
| Profissionais temporários (delivery) | não | sem contrato/pagamento/vínculo temporário (ver `../FASE_02_NEGOCIO/SKILL_Delivery.md`) |
|
||||
| Locação/Tutoria/Marketplace | sim/parcial | falta captura de pagamento (salas) |
|
||||
|
||||
## Direção (sem propor implementação)
|
||||
Consolidar a **Pessoa** como âncora única (unificar `dentistas`→projeção; paciente→identidade) destrava
|
||||
multi-clínica, redes e portabilidade. Coexistir os 2 modelos é o que gera as contradições de gravidade ALTA.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Duplicar a pessoa por workspace. ❌ Tratar clínica como raiz. ❌ Operação sem `clinica_id`.
|
||||
</content>
|
||||
@@ -0,0 +1,25 @@
|
||||
# SKILL — Monetização (Roadmap)
|
||||
|
||||
> O que gera (ou pode gerar) receita. Base: `documentação/MAPA-FUNCIONAL-COMPLETO.md` + `AUDITORIA-ESTRATEGICA-MODELAGEM.md`.
|
||||
|
||||
## Três camadas de receita (estado real)
|
||||
1. **Receita da clínica** (operacional): produção, contratos/cobrança, recuperação de glosas.
|
||||
2. **Receita do profissional** (repasse de comissão).
|
||||
3. **Receita da PLATAFORMA**:
|
||||
- **Tutoria** — MercadoPago + `comissao_pct`: **única automatizada hoje**. ✅
|
||||
- **Assinaturas SaaS** (`assinaturas`/`superadmin/pricing`): existe, mas **manual** (sem gateway/recorrência).
|
||||
- **Locação de salas** (`sala_reservas.valor`): registra valor, **sem captura de pagamento**.
|
||||
|
||||
## Alavancas (ordem de retorno mais rápido)
|
||||
1. **Gateway na Locação de Salas** — reserva + `valor` já existem; falta cobrança + taxa da plataforma.
|
||||
2. **Recorrência das Assinaturas SaaS** — base multiempresa + pricing prontos; falta gateway/automação.
|
||||
3. **Marketplace de Profissionais premium** — taxa de match/destaque (hoje só mensagem).
|
||||
4. **Escalar Tutoria** (já fatura).
|
||||
|
||||
## Regras
|
||||
- Toda lacuna de captura deve ser **sinalizada, não presumida como paga**.
|
||||
- Não misturar as 3 camadas (clínica ≠ profissional ≠ plataforma).
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Assumir que reserva/assinatura foi paga. ❌ Construir cobrança fora do modelo Pessoa→Workspace.
|
||||
</content>
|
||||
@@ -0,0 +1,22 @@
|
||||
# SKILL — Priorização (Roadmap)
|
||||
|
||||
> Como decidir a ordem. Critérios concretos para esta stack.
|
||||
|
||||
## Critérios (em ordem)
|
||||
1. **Não quebrar o que funciona** (REGRA Nº 1) — auditar impacto antes.
|
||||
2. **Segurança / pré-produção** primeiro (abrir a usuários reais exige base segura) — ver `SKILL_Backlog`.
|
||||
3. **Receita mais rápida com menor esforço** — alavanca onde a infra já existe (ex.: gateway de salas).
|
||||
4. **Destravar arquitetura** (unificar Pessoa) — alto valor, maior esforço/risco → deliberado, não no susto.
|
||||
5. **Esforço × risco × reversibilidade** — preferir mudanças aditivas e reversíveis (build-once/rollback).
|
||||
|
||||
## Fluxo obrigatório por item
|
||||
`Auditoria → Planejamento → Aprovação → Implementação (em DEV/`pgdev`) → Validação → Promoção`.
|
||||
|
||||
## Recomendação estratégica única (da auditoria)
|
||||
Assumir a **Pessoa como âncora** e **parar de manter o legado clínica-cêntrico em paralelo** — é a coexistência
|
||||
dos dois modelos que gera as contradições graves. Mas: só após segurança/produção estável e com aprovação.
|
||||
|
||||
## Anti-padrões
|
||||
- ❌ Implementar sem auditoria/aprovação. ❌ Priorizar features novas sobre segurança pré-lançamento.
|
||||
- ❌ Refator arquitetural grande sem janela e rollback.
|
||||
</content>
|
||||
@@ -0,0 +1,28 @@
|
||||
# Segredos e ambiente — NUNCA entram na imagem
|
||||
.env
|
||||
.env.*
|
||||
*.secret
|
||||
.secrets
|
||||
|
||||
# Dependências e caches (reinstalados no build)
|
||||
node_modules
|
||||
npm-debug.log
|
||||
|
||||
# Artefatos locais / backups / dados que não pertencem à imagem
|
||||
*.bak
|
||||
server.js.bak
|
||||
sessions/
|
||||
media/
|
||||
data/
|
||||
bkp/
|
||||
*.dump
|
||||
|
||||
# Diagnóstico temporário
|
||||
test-db-users.js
|
||||
test-db-users.cjs
|
||||
pg-convert.js
|
||||
|
||||
# Diversos
|
||||
.git
|
||||
.DS_Store
|
||||
*.log
|
||||
@@ -1,45 +0,0 @@
|
||||
# ─────────────────────────────────────────────
|
||||
# Server
|
||||
# ─────────────────────────────────────────────
|
||||
PORT=8018
|
||||
NODE_ENV=development
|
||||
JWT_SECRET=scoreodonto_jwt_secret_forte_2026
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# PostgreSQL (Prisma)
|
||||
# ─────────────────────────────────────────────
|
||||
DATABASE_URL="postgresql://scoreodonto_user:clube67_scoreodonto_pass_9903@10.99.0.3:5432/scoreodonto?schema=public"
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# DragonflyDB (compatível com Redis)
|
||||
# ─────────────────────────────────────────────
|
||||
DRAGONFLY_URL=redis://:clube67_dragonfly_pass_9903@10.99.0.3:6379/1
|
||||
DRAGONFLY_TTL_SECONDS=86400
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# NATS JetStream
|
||||
# ─────────────────────────────────────────────
|
||||
NATS_URL=nats://10.99.0.3:4222
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Temporal.io
|
||||
# ─────────────────────────────────────────────
|
||||
TEMPORAL_ADDRESS=10.99.0.3:7233
|
||||
TEMPORAL_NAMESPACE=default
|
||||
TEMPORAL_TASK_QUEUE=scoreodonto-queue
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# IA (OpenAI + Google Gemini)
|
||||
# ─────────────────────────────────────────────
|
||||
OPENAI_API_KEY=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
|
||||
GEMINI_API_KEY=AIza...
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# WhatsApp / Baileys
|
||||
# ─────────────────────────────────────────────
|
||||
BAILEYS_SESSIONS_PATH=/home/deploy/scoreodonto-sessions
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Frontend (CORS)
|
||||
# ─────────────────────────────────────────────
|
||||
FRONTEND_URL=http://localhost:3013
|
||||
@@ -0,0 +1,26 @@
|
||||
# Modelo de variáveis de ambiente do backend (SEM segredos).
|
||||
# Copie para .env e preencha com valores REAIS. O .env NÃO é versionado.
|
||||
PORT=8018
|
||||
NODE_ENV=production
|
||||
|
||||
# Segredo de assinatura dos tokens JWT — gerar com: openssl rand -hex 48
|
||||
JWT_SECRET=
|
||||
|
||||
# Banco e cache (não versionar valores reais)
|
||||
DATABASE_URL=postgresql://USER:SENHA@HOST:5432/BANCO?schema=public
|
||||
DRAGONFLY_URL=redis://:SENHA@HOST:6379/1
|
||||
DRAGONFLY_TTL_SECONDS=3600
|
||||
|
||||
# Mensageria / workflows
|
||||
NATS_URL=nats://HOST:4222
|
||||
TEMPORAL_ADDRESS=HOST:7233
|
||||
TEMPORAL_NAMESPACE=default
|
||||
TEMPORAL_TASK_QUEUE=scoreodonto
|
||||
|
||||
# Chaves de IA (rotacionáveis apenas nos painéis dos provedores)
|
||||
OPENAI_API_KEY=
|
||||
GEMINI_API_KEY=
|
||||
|
||||
# Caminhos / front
|
||||
BAILEYS_SESSIONS_PATH=/app/sessions
|
||||
FRONTEND_URL=https://scoreodonto.com
|
||||
+1756
-108
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -24,8 +24,9 @@ FILES="-f docker-compose.yml -f docker-compose.prod.yml"
|
||||
|
||||
echo "──────────────────────────────────────────────────────────────"
|
||||
echo " Deploy ScoreOdonto → PRODUÇÃO tag: $TAG"
|
||||
echo " Versão exibida (/api/version) será injetada em runtime = $TAG (commit/build vêm da imagem)"
|
||||
echo "──────────────────────────────────────────────────────────────"
|
||||
echo "⚠️ Recomendado: backup do banco antes (dev/prod compartilham o banco):"
|
||||
echo "⚠️ Recomendado: backup do banco de PRODUÇÃO antes (migrações rodam no boot do backend):"
|
||||
echo " ssh deploy@10.99.0.3 'pg_dump -Fc scoreodonto -f ~/backups/scoreodonto_\$(date +%Y%m%d_%H%M).dump'"
|
||||
echo
|
||||
|
||||
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Build + sobe o ambiente de DEV (porta 8020 / dev.scoreodonto.com) carimbando
|
||||
# a VERSÃO a partir do git — para que /api/version (e o rótulo no app) deixe de
|
||||
# mostrar apenas "dev".
|
||||
#
|
||||
# Uso (NA VPS): ./dev-build.sh
|
||||
#
|
||||
# Diferença p/ produção: a prod consome imagem versionada e injeta a tag em
|
||||
# runtime (deploy-prod.sh). Em DEV buildamos local, então derivamos a versão do
|
||||
# git AQUI (no host, onde o .git existe) e passamos como build-args do compose.
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
export DOCKER_HOST=unix:///run/user/1000/docker.sock
|
||||
|
||||
# Versão semântica + commit + carimbo de build, derivados do git.
|
||||
# APP_VERSION -> ex.: v1.0.18-16-g6adf353 (tag mais próxima + distância + sha)
|
||||
# GIT_COMMIT -> sha curto
|
||||
export APP_VERSION="$(git describe --tags --always --dirty 2>/dev/null || echo dev)"
|
||||
export GIT_COMMIT="$(git rev-parse --short HEAD 2>/dev/null || echo dev)"
|
||||
export BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
export APP_ENV="DEV"
|
||||
|
||||
echo "──────────────────────────────────────────────────────────────"
|
||||
echo " Build ScoreOdonto → DEV (porta 8020)"
|
||||
echo " APP_VERSION = $APP_VERSION"
|
||||
echo " GIT_COMMIT = $GIT_COMMIT"
|
||||
echo " BUILD_TIME = $BUILD_TIME"
|
||||
echo "──────────────────────────────────────────────────────────────"
|
||||
|
||||
docker compose up -d --build scoreodonto-backend scoreodonto-frontend nginx
|
||||
|
||||
echo
|
||||
echo -n " versão no ar (DEV): "
|
||||
sleep 8
|
||||
curl -s --max-time 10 http://localhost:8020/api/version || echo "(ainda subindo — confira em alguns segundos)"
|
||||
echo
|
||||
echo "✅ DEV buildado com versão carimbada."
|
||||
@@ -0,0 +1,47 @@
|
||||
# Agenda & Coleta do laboratório
|
||||
|
||||
> Desenho + implementação (24/jun/2026). Gap apontado na auditoria: o lab não tem agenda de
|
||||
> produção nem coleta logística (só "próximas entregas" por prazo e custódia de posse).
|
||||
|
||||
## Conceito
|
||||
- **Coleta** = o lab **busca** o trabalho na clínica (trabalho vai p/ o laboratório).
|
||||
- **Entrega** = o lab **leva** o trabalho de volta (trabalho volta p/ a clínica).
|
||||
- Cada agendamento tem data/hora, endereço, responsável, observação e status
|
||||
(`agendada | realizada | cancelada`). Ligado a uma OS.
|
||||
- **Integra com a custódia:** ao marcar `realizada`, a custódia muda automaticamente
|
||||
(coleta→`laboratorio`, entrega→`clinica`) e entra no histórico da OS. Sem dado duplicado.
|
||||
|
||||
## Modelo
|
||||
```sql
|
||||
CREATE TABLE protese_coleta (
|
||||
id TEXT PRIMARY KEY,
|
||||
os_id TEXT NOT NULL,
|
||||
clinica_id TEXT,
|
||||
laboratorio_id TEXT,
|
||||
protetico_id TEXT,
|
||||
tipo TEXT NOT NULL, -- coleta | entrega
|
||||
data_hora TIMESTAMPTZ,
|
||||
endereco TEXT,
|
||||
responsavel TEXT,
|
||||
observacao TEXT,
|
||||
status TEXT DEFAULT 'agendada', -- agendada | realizada | cancelada
|
||||
created_by TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
- `POST /api/protese/os/:id/coleta` — agenda (clínica ou lab).
|
||||
- `GET /api/protese/lab/agenda` — coletas/entregas do lab (cronológica) + próximas entregas por prazo.
|
||||
- `POST /api/protese/coleta/:id/status` — `realizada` (move a custódia) | `cancelada`.
|
||||
- GET detalhe da OS passa a trazer `coletas[]`.
|
||||
|
||||
## UI
|
||||
- **Lab — tela Agenda** (`lab-agenda`, novo item de menu): linha do tempo de coletas/entregas
|
||||
agendadas + entregas previstas (prazo da OS), com ação "marcar realizada".
|
||||
- **OS (aba Monitoramento)** — agendar coleta/entrega ao lado da custódia (mesmo contexto físico).
|
||||
|
||||
## Decisões
|
||||
- 1 coleta por OS (MVP). Agrupar várias OS numa rota/motoboy fica como evolução.
|
||||
- `realizada` é a fonte única do movimento físico (atualiza custódia) — evita registrar custódia
|
||||
e coleta em separado.
|
||||
@@ -0,0 +1,158 @@
|
||||
# Análise — Área da Recepção / Coordenação de Tratamentos (Treatment Coordinator)
|
||||
|
||||
> Gatilho (23/jun/2026): `recep.consulttclinic@gmail.com` lançou uma prótese mas **não tinha
|
||||
> a aba para acompanhá-la**. Análise do gap + recomendação fundamentada em modelos validados
|
||||
> de software odontológico. Conecta com `FASE2-PROTETICO-AREA.md` (o outro lado do fluxo).
|
||||
|
||||
---
|
||||
|
||||
## 1. Diagnóstico (DEV `scoreodonto_dev`)
|
||||
|
||||
| Entidade | Achado |
|
||||
|---|---|
|
||||
| Dono | `consulttclinic@gmail.com` → *Rui Cesar*, `donoclinica` |
|
||||
| Funcionária | `recep.consulttclinic@gmail.com` (`u_1780962424310_cqsxn`), role `funcionario`, vínculo na **CONSULTT CLINIC LTDA** (`c_1780871001549_koqa3o`) |
|
||||
| Cargo | função "Recepcionista" (`fnc_1780962382528_m28go`) |
|
||||
| Permissões (antes) | `["agenda","ortodontia","contratos","tratamentos","pacientes","lancar-gto"]` |
|
||||
| Prótese lançada | OS `pos_1782228267898_zgfzn` — Prótese Total, paciente ABIGAIR, R$600, protético "Zeca Andrade Fake", status `solicitado`, **via GTO** (`gto_item_id=QOJWR6D`) |
|
||||
|
||||
**Gap exato:** o cargo tinha `lancar-gto` (lançar) mas **não** `proteses` (acompanhar). O RBAC da
|
||||
`Sidebar.tsx` é literal (`return wsPerms.includes(item.id)`), então a OS criada por ela ficava
|
||||
**invisível** para ela própria. A Fase 1 (GTO→OS) funcionou; o problema era de **configuração de
|
||||
cargo**, não de arquitetura.
|
||||
|
||||
---
|
||||
|
||||
## 2. Modelos validados (referência externa)
|
||||
|
||||
- **Papel = "Treatment Coordinator" + Front Desk.** É a ponte entre paciente e equipe clínica:
|
||||
sequencia planos multi-etapa, conduz o aceite do caso (case acceptance), arranjo financeiro e
|
||||
follow-up até a conclusão — exatamente "gerir tratamentos" de ponta a ponta.
|
||||
- **RBAC por papel é o padrão.** Permissões mapeadas a papéis, não a indivíduos; o front desk
|
||||
recebe agenda/cadastro/cobrança, tipicamente **sem notas clínicas**. O ScoreOdonto já faz isso
|
||||
via `funcoes.permissoes[]`.
|
||||
- **Prótese = "Lab Case", acompanhada pelo front desk.** No Dentrix Lab Case Manager: clínico cria
|
||||
a prescrição → status *Sent* → **o front desk muda para *Received* quando volta do lab** →
|
||||
*Finished* ao assentar. Ou seja: acompanhar a OS de prótese é, por modelo consagrado, **atribuição
|
||||
da recepção** — o acesso que faltava.
|
||||
|
||||
Fontes:
|
||||
- The Role of a Treatment Coordinator — https://www.dentalmanagers.com/blog/role-of-treatment-coordinator-in-dental-practice/
|
||||
- What Does a Treatment Plan Coordinator Do? — https://www.teero.com/blog/treatment-plan-coordinator
|
||||
- Dental Software User Permissions (Curve) — https://www.curvedental.com/blog/dental-software-user-permissions
|
||||
- RBAC in Healthcare — https://www.accountablehq.com/post/role-based-access-control-rbac-in-healthcare-benefits-examples-and-best-practices
|
||||
- Manage Your Lab Cases in Dentrix — https://magazine.dentrix.com/manage-your-lab-cases-in-dentrix/
|
||||
- Open Dental — Lab Cases — https://www.opendental.com/manual/labcasemanage.html
|
||||
|
||||
---
|
||||
|
||||
## 3. Ação aplicada — Nível 1 (configuração, sem código)
|
||||
|
||||
✅ **Aplicado no DEV em 23/jun/2026.** Função "Recepcionista" da CONSULTT CLINIC LTDA atualizada:
|
||||
|
||||
```sql
|
||||
UPDATE funcoes
|
||||
SET permissoes = '["agenda","pacientes","tratamentos","ortodontia","proteses","lancar-gto","orcamentos","contratos"]'
|
||||
WHERE id = 'fnc_1780962382528_m28go'; -- UPDATE 1
|
||||
```
|
||||
|
||||
Acréscimos: **`proteses`** (acompanhar a OS que ela lança) e **`orcamentos`** (case acceptance —
|
||||
coração do papel TC). Efeito imediato: a OS `pos_1782228267898_zgfzn` passa a aparecer para ela.
|
||||
|
||||
> Escopo: alteração **só no DEV**. Não há mudança de código nem migração — é dado de configuração.
|
||||
> Para refletir em outras clínicas, ajustar o cargo equivalente de cada uma (ou rever o template
|
||||
> de cargos padrão, se existir).
|
||||
|
||||
---
|
||||
|
||||
## 3b. Causa raiz (investigação 23/jun) — `proteses` não é concedível pela UI
|
||||
|
||||
A correção de dado acima trata o sintoma. A **causa raiz** é um bug de configuração de RBAC:
|
||||
|
||||
- Cargos **não** têm seed no backend (signup não cria funções). São criados manualmente pelo
|
||||
dono em Gestão de Equipe (`POST → INSERT INTO funcoes`, `server.js:806`), partindo de 4
|
||||
templates do frontend (`GestaoEquipeView.tsx:96`).
|
||||
- **Nenhum** template inclui `proteses`: RECEPÇÃO (`dashboard,agenda,pacientes,leads,orcamentos`),
|
||||
AUXILIAR (`...,ortodontia,tratamentos`), FINANCEIRO (`...,financeiro,relatorios,lancar-gto`),
|
||||
GERENTE (`[...TODAS_PAGINAS]`).
|
||||
- **Raiz:** `proteses` **não está em `PAGINAS_RBAC`** (`GestaoEquipeView.tsx:57`), a lista mestre
|
||||
de páginas atribuíveis. Como `TODAS_PAGINAS = PAGINAS_RBAC.flatMap(...)`, **nem o GERENTE /
|
||||
"MARCAR TODAS"** concede prótese. ⇒ **É impossível um dono dar `proteses` a um funcionário
|
||||
pela interface** — só via UPDATE direto no banco (o que foi feito na §3).
|
||||
- Também ausentes da lista atribuível, embora em uso: **`contratos`** (a função tinha — legado),
|
||||
`comissoes`, `glosas`.
|
||||
|
||||
**Resposta à pergunta "o cargo Recepcionista padrão nasce sem proteses?":** Sim — e mais,
|
||||
*nenhum* cargo consegue tê-la pela UI hoje.
|
||||
|
||||
### Fix de raiz (código) — ✅ aplicado e validado no DEV (23/jun)
|
||||
Em `frontend/views/GestaoEquipeView.tsx`:
|
||||
- `PAGINAS_RBAC`, grupo `ATENDIMENTO`: + `{ key: 'proteses', label: 'PRÓTESES' }` e
|
||||
`{ key: 'contratos', label: 'CONTRATOS' }` → agora concedíveis pela UI (e entram em
|
||||
`TODAS_PAGINAS`/GERENTE/"MARCAR TODAS").
|
||||
- Templates: RECEPÇÃO ganhou `contratos, lancar-gto, proteses`; AUXILIAR ganhou `proteses`
|
||||
(alinhado ao papel TC).
|
||||
|
||||
**Validação:** `vite build` transformou 2382 módulos OK; container DEV rebuildado e no ar; o
|
||||
bundle servido pelo nginx DEV (`index-*.js`, alvo de `dev.scoreodonto.com`) contém
|
||||
`key:"proteses",label:"PRÓTESES"` e a string `proteses` nos 3 pontos editados. Cargos padrão
|
||||
passam a nascer coerentes — sem UPDATE manual por clínica.
|
||||
|
||||
> ⚠️ Pendente de commit + deploy à produção (a mudança está só no DEV). Os fixes de **dado**
|
||||
> (§3, função da CONSULTT CLINIC) continuam sendo só-DEV e não se propagam por este fix de código.
|
||||
|
||||
## 4. Recomendação — Nível 2 (estrutural, "Fase 3")
|
||||
|
||||
Em vez de abas soltas (`tratamentos`, `proteses`, `ortodontia`), um **hub de Coordenação de
|
||||
Tratamentos por paciente**: pipeline único com o status de cada frente (clínico / prótese-lab /
|
||||
orto), espelhando o "lab case visível para toda a equipe" do Dentrix. View nova que **agrega o que
|
||||
já existe** (`tratamentos` + `protese_os` + ortodontia por paciente) — sem dado novo.
|
||||
|
||||
## 5. Conexão com a Fase 2
|
||||
Dois lados do mesmo fluxo, unidos pela mesma `protese_os`:
|
||||
- **Fase 2** → casa do **protético** (quem produz; avança status na Bancada).
|
||||
- **Esta análise** → **recepção/TC** (quem encomenda e acompanha; lê o status no hub).
|
||||
O status que o lab avança é o mesmo que a recepção lê — o "Lab Case Manager" repartido entre os papéis.
|
||||
|
||||
---
|
||||
|
||||
## 6. Salas: acesso do funcionário corrigido + bloqueio de manutenção (23/jun)
|
||||
|
||||
Gatilho: a mesma recepcionista aparecia com **MINHAS SALAS** e **ALUGAR SALAS** (gestão de
|
||||
locação, de titular) e deveria ter apenas a **agenda da sala** e o **bloqueio para manutenção**.
|
||||
|
||||
### 6a. Bug — vazamento dos menus de locação ✅ corrigido (`Sidebar.tsx`)
|
||||
`MINHAS SALAS` (locador) e `ALUGAR SALAS` (locatário) eram empurrados a "todos menos
|
||||
superadmin" (`Sidebar.tsx:42`), vazando para **funcionário e paciente** — papéis que não são
|
||||
titulares de sala. Passaram a exibir apenas a `PAPEIS_LOCACAO`
|
||||
(`donosala/donoclinica/donoconsultorio/admin/dentista/biomedico/protetico`). O backend já era
|
||||
seguro por ownership; era vazamento só de UI.
|
||||
|
||||
### 6b. Feature — acesso operacional + manutenção ✅ implementada e validada no DEV
|
||||
Não existia caminho para "funcionário vê só a agenda" nem "bloqueio para manutenção" — foram
|
||||
criados (modelos de mercado: *Dentrix/Open Dental Lab... não; aqui é gestão de espaço* — o
|
||||
padrão é **front desk opera a agenda da sala sem tocar no financeiro do locador**).
|
||||
|
||||
- **Acesso operacional** (`server.js` `listarWorkspaces`): sala cuja `clinica_id` é uma clínica
|
||||
onde o usuário tem vínculo (e ele **não** é o dono) entra como workspace `tipo='sala'` com
|
||||
`papel_sala='operador'`. O `SalaHomeView` detecta o modo operador e **esconde recebíveis,
|
||||
relatório e baixa** — mostra só a **agenda** + botão de bloqueio.
|
||||
- **Bloqueio de manutenção** (`POST /api/salas/:id/manutencao`): reserva `tipo='manutencao'`
|
||||
(coluna nova, default `'locacao'`), `valor=0`, nasce `'confirmada'` (sem aprovação) e **não
|
||||
gera recebível**. Acesso: dono **ou** membro com vínculo na clínica da sala. Reaproveita o
|
||||
anti-conflito de horário (409) e o cancelamento por solicitante/unidade. A agenda renderiza
|
||||
o bloqueio em **cinza** (legenda "Manutenção").
|
||||
|
||||
**Validação ponta a ponta (DEV, JWT real da recepcionista, sala de teste vinculada à clínica):**
|
||||
| Caso | Resultado |
|
||||
|---|---|
|
||||
| Sala da clínica vira workspace operador dela | ✅ query retorna a sala |
|
||||
| POST bloqueio | ✅ `status:confirmada, tipo:manutencao` |
|
||||
| GET agenda mostra bloqueio + motivo | ✅ `valor:0, motivo:"Conserto do equipo"` |
|
||||
| Conflito de horário | ✅ HTTP 409 |
|
||||
| Não gera financeiro | ✅ 0 lançamentos |
|
||||
|
||||
Commits: `fix(salas)` (menu) + `feat(salas)` (operador + manutenção). Backend e frontend no ar
|
||||
no DEV; dados de teste limpos.
|
||||
|
||||
> ⚠️ Pendente de push + deploy (produção segue em v1.0.19).
|
||||
@@ -0,0 +1,160 @@
|
||||
# Estratégia de Banco DEV — Relatório + Plano de Implementação
|
||||
|
||||
> Documento de **decisão e plano** (16/jun/2026). A 1ª parte é o relatório técnico baseado em dados reais; a 2ª é o plano de implementação profissional, escalável para **todos os projetos atuais e futuros**.
|
||||
> Relacionado: `arquitetura.md`, `PENDENCIAS.md` (P0 isolar dev), `VERDADE-HOJE.md`.
|
||||
|
||||
---
|
||||
|
||||
# PARTE 1 — Relatório técnico (dados reais, read-only)
|
||||
|
||||
**Servidor (VPS3):** PostgreSQL **18.4** · **PostGIS 3.6.2** (suíte completa disponível).
|
||||
|
||||
**Bancos (8 no total, ~175 MB somados):**
|
||||
|
||||
| Banco | Tamanho | Projeto | DEV na VPS4? | Tabelas | Índices | PostGIS |
|
||||
|-------|--------:|---------|:------------:|--------:|--------:|:-------:|
|
||||
| newwhats | 91 MB | NewWhats (VPS1) | não | — | — | — |
|
||||
| giteadb | 22 MB | Gitea (infra) | não | — | — | não |
|
||||
| **scoreodonto** | **21 MB** | **ScoreOdonto** | **sim** | 104 | 168 | **SIM (3 geog)** |
|
||||
| mercado | 13 MB | Mercado (VPS1) | não | — | 187 | não |
|
||||
| **dental_images** | **11 MB** | **RX** | **sim** | 8 | 25 | não |
|
||||
| clube67 | 8.8 MB | Clube67 | não | — | — | — |
|
||||
| **subdominio** | **8 MB** | **Subdomínio** | **sim** | 3 | 7 | não |
|
||||
| postgres | 7.7 MB | default | — | — | — | — |
|
||||
|
||||
**Conclusões:**
|
||||
- Os 3 bancos de DEV (scoreodonto, dental_images, subdominio) somam **~40 MB**. São minúsculos.
|
||||
- **Só o ScoreOdonto usa PostGIS.** RX e Subdomínio não.
|
||||
- Sem dependências entre bancos. Sem dados de crescimento histórico (não medível sem superuser); porte indica crescimento lento.
|
||||
|
||||
**Veredito:** a proposta "mirror + dev por projeto" (6 containers PostgreSQL/PostGIS) é **viável mas exagerada** para esse porte. Um "mirror" *rodando* não agrega sobre um **template database** ou um **dump**; é só RAM e complexidade a mais (~300–600 MB RAM e 6× manutenção).
|
||||
|
||||
**Recomendação final:** **um único container PostGIS na VPS4** (rootless), multi-banco, com `*_ref` (referência/template, restaurado da VPS3) + `*_dev` (de trabalho, criado via `CREATE DATABASE ... TEMPLATE`). Reset instantâneo, custo ~50–100 MB RAM, manutenção mínima, e **escala para qualquer projeto novo**.
|
||||
|
||||
---
|
||||
|
||||
# PARTE 2 — Plano de implementação (profissional, escalável)
|
||||
|
||||
> Objetivo: desgrudar **todos** os ambientes DEV do banco de produção (VPS3), com um padrão único que serve projetos atuais e futuros. **Produção (VPS3/VPS1) nunca é alterada — só lida (pg_dump).**
|
||||
|
||||
## 2.1 Arquitetura
|
||||
|
||||
```
|
||||
VPS3 (produção, fonte da verdade)
|
||||
│ pg_dump (somente leitura, one-way)
|
||||
▼
|
||||
┌──────────────── VPS4 (rootless) — container único "pgdev" ───────────────┐
|
||||
│ postgis/postgis 18-3.6 (porta interna, rede 'soc', sem exposição pública) │
|
||||
│ │
|
||||
│ scoreodonto_ref ──TEMPLATE──► scoreodonto_dev ◄── backend dev ScoreOdonto │
|
||||
│ dental_images_ref ─TEMPLATE──► dental_images_dev ◄── backend dev RX │
|
||||
│ subdominio_ref ──TEMPLATE──► subdominio_dev ◄── backend dev Subdomínio │
|
||||
│ <novo>_ref ──TEMPLATE──► <novo>_dev ◄── (projeto futuro) │
|
||||
└────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **`*_ref`**: cópia fiel da produção (restaurada por `pg_dump`). Não se mexe — é a referência/"mirror".
|
||||
- **`*_dev`**: banco de trabalho, recriável do `_ref` em segundos via `TEMPLATE`. É onde rodam migrações e testes.
|
||||
- **Um container só** atende todos (Postgres lida com dezenas de bancos pequenos sem esforço). PostGIS coberto para quem precisa (ScoreOdonto); os demais simplesmente não usam.
|
||||
|
||||
## 2.2 Componentes (a criar em `/home/deploy/stack/pg-dev/`)
|
||||
|
||||
**a) `docker-compose.yml`** (container do banco DEV):
|
||||
```yaml
|
||||
services:
|
||||
pgdev:
|
||||
image: postgis/postgis:18-3.6 # confirmar tag p/ PG18+PostGIS3.6 (alt.: imresamu/postgis:18-3.6)
|
||||
container_name: pgdev
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: devadmin
|
||||
POSTGRES_PASSWORD: ${PGDEV_PASSWORD}
|
||||
volumes:
|
||||
- pgdev-data:/var/lib/postgresql/data
|
||||
networks: [soc] # mesma rede dos backends de dev (acesso por DNS 'pgdev')
|
||||
# SEM ports: publicado → só acessível internamente (segurança)
|
||||
volumes:
|
||||
pgdev-data:
|
||||
networks:
|
||||
soc:
|
||||
external: true
|
||||
```
|
||||
|
||||
**b) `pg-dev-projects.conf`** (manifesto — adicionar 1 linha por projeto novo):
|
||||
```
|
||||
# nome_projeto : banco_origem_VPS3 : usuario_origem
|
||||
scoreodonto : scoreodonto : scoreodonto_user
|
||||
rx : dental_images : clube67
|
||||
subdominio : subdominio : mercado_admin
|
||||
```
|
||||
|
||||
**c) `.env` / `secrets`** (gitignored, fora do repo): senha do `pgdev` (`PGDEV_PASSWORD`) e as senhas de origem da VPS3. **Nunca** versionar.
|
||||
|
||||
**d) `pg-dev-refresh.sh <projeto>`** — atualiza o `_ref` a partir da VPS3 e recria o `_dev`:
|
||||
```
|
||||
1. pg_dump -Fc da VPS3 (banco_origem) usando a credencial do manifesto (read-only).
|
||||
2. DROP/CREATE <projeto>_ref no pgdev; pg_restore do dump → _ref.
|
||||
3. DROP <projeto>_dev; CREATE DATABASE <projeto>_dev TEMPLATE <projeto>_ref.
|
||||
```
|
||||
|
||||
**e) `pg-dev-reset.sh <projeto>`** — reset rápido (sem ir à VPS3):
|
||||
```
|
||||
DROP <projeto>_dev; CREATE DATABASE <projeto>_dev TEMPLATE <projeto>_ref;
|
||||
```
|
||||
|
||||
## 2.3 Apontar os backends de DEV para o pgdev (sem afetar produção)
|
||||
|
||||
Parametrizar a `DATABASE_URL` no compose de cada projeto para usar variáveis com **default = produção**, e na VPS4 criar um `.env` (gitignored) que sobrescreve só no dev:
|
||||
```
|
||||
# no docker-compose.yml (default mantém produção intacta):
|
||||
DATABASE_URL=postgresql://${DB_USER}:${DB_PASS}@${DB_HOST:-10.99.0.3}:5432/${DB_NAME:-scoreodonto}
|
||||
|
||||
# .env SÓ na VPS4 (dev):
|
||||
DB_HOST=pgdev
|
||||
DB_NAME=scoreodonto_dev
|
||||
DB_USER=devadmin
|
||||
DB_PASS=<senha pgdev>
|
||||
```
|
||||
- Na VPS1 (produção) não existe esse `.env` → resolve para `10.99.0.3/scoreodonto`. **Produção nunca muda.**
|
||||
- Rebuild/recreate do backend de dev → passa a falar com `pgdev`.
|
||||
|
||||
## 2.4 Passos de implantação (quando aprovado)
|
||||
|
||||
1. Criar `/home/deploy/stack/pg-dev/` com o compose + scripts + `.env` (secrets).
|
||||
2. `docker compose up -d pgdev` (rootless) + adicionar `pgdev` à rede `soc`.
|
||||
3. Rodar `pg-dev-refresh.sh scoreodonto` (e rx, subdominio) → cria os `_ref` e `_dev`.
|
||||
4. Criar o `.env` de dev em cada projeto e recriar os backends de dev.
|
||||
5. Verificar: `dev.scoreodonto.com/api/version` sobe; conferir que escrita em dev cai em `scoreodonto_dev` (e produção em `scoreodonto` permanece intacta).
|
||||
6. (privacidade) opcional: script de anonimização de dados sensíveis nos `_dev`.
|
||||
|
||||
## 2.5 Operação do dia a dia
|
||||
|
||||
- **Quer dados atualizados da produção?** `pg-dev-refresh.sh <projeto>`.
|
||||
- **Sujou o dev e quer voltar ao limpo?** `pg-dev-reset.sh <projeto>` (segundos, via TEMPLATE).
|
||||
- **Projeto novo?** adiciona 1 linha no `pg-dev-projects.conf` + `pg-dev-refresh.sh <novo>`. Pronto — sem novo container.
|
||||
|
||||
## 2.6 Custo e ganho
|
||||
|
||||
- **Custo:** 1 container (~50–100 MB RAM, ~100 MB disco). Disco da VPS4 hoje: ~64 GB livres → folga total.
|
||||
- **Ganho:** dev deixa de poder corromper produção; resets instantâneos; PostGIS coberto; padrão único que escala para N projetos sem multiplicar containers.
|
||||
|
||||
## 2.7 O que este plano deliberadamente NÃO faz
|
||||
- Não cria 2 containers por projeto (over-engineering p/ bancos de 8–21 MB).
|
||||
- Não usa réplica streaming (exigiria config superuser na VPS3 + WAL; canhão p/ 40 MB).
|
||||
- Não toca em produção (VPS3/VPS1) — refresh é one-way, só `pg_dump`.
|
||||
|
||||
---
|
||||
|
||||
# PARTE 3 — Status de implementação (16/jun/2026)
|
||||
|
||||
**Implementado em `/home/deploy/stack/pg-dev/`** (Docker rootless da VPS4):
|
||||
- ✅ Container **`pgdev`** (`postgis/postgis:18-3.6`) na rede `soc`, sem porta pública, volume `pgdev-data` em `/var/lib/postgresql`.
|
||||
- ✅ Scripts `pg-dev-refresh.sh` / `pg-dev-reset.sh` + manifesto `pg-dev-projects.conf` + `secrets.env` (gitignored).
|
||||
- ✅ Bancos criados (ref+dev): **scoreodonto** (20 MB, PostGIS 3.6.3, 1750 pacientes), **rx** (dental_images), **subdominio**.
|
||||
|
||||
**Backends apontados (via `.env` gitignored + `DATABASE_URL`/`PG_*` parametrizados com default=produção):**
|
||||
- ✅ **ScoreOdonto** → `pgdev/scoreodonto_dev` (validado: backend conecta, migrações OK; `dev.scoreodonto.com` no ar). Exigiu ajuste de SSL no backend (não força SSL p/ host `@pgdev`).
|
||||
- ✅ **Subdomínio** → `pgdev/subdominio_dev` (env confirmado, site no ar).
|
||||
- ✅ **RX** → `pgdev/rx_dev` (`rx.scoreodonto.com` no ar). Solução: app adicionado à rede `soc` + `DB_HOST/DB_NAME` parametrizados; role `clube67` criado no pgdev com a senha do `.secrets` (assim DB_USER/senha não mudam).
|
||||
|
||||
**Produção (VPS3/VPS1) intocada.** Para um projeto novo: adicionar linha no `pg-dev-projects.conf`, `SRC_<proj>_PASS` no `secrets.env`, rodar `./pg-dev-refresh.sh <proj>` e parametrizar o compose do projeto + `.env`.
|
||||
@@ -0,0 +1,217 @@
|
||||
# Fase 2 — Detalhamento de implementação: passos 2.0 e 2.1
|
||||
|
||||
> ✅ **IMPLEMENTADO e validado em DEV (23/jun/2026).** Commits `feat(protese): Fase 2 backend`
|
||||
> + `frontend`. Validações: registro de protético nasce `laboratorio`; backfill preenche
|
||||
> `laboratorio_id`; bancada roteia por lab (fallback por pessoa); entrega gera **par espelhado**
|
||||
> DESPESA(clínica)+RECEITA(lab) R$600, idempotente (409 na re-entrega); menu do lab no bundle.
|
||||
>
|
||||
> ⚠️ **Bug pré-existente achado e corrigido junto:** o INSERT da DESPESA de prótese (Fase 1, já
|
||||
> em produção v1.0.20) usava `paciente_nome` — coluna inexistente em `financeiro` (é
|
||||
> `pacientenome`). A entrega de OS com `custo>0` **abortava** em produção. Corrigido no mesmo
|
||||
> commit de backend. **Recomenda-se priorizar o deploy** por causa disso.
|
||||
|
||||
|
||||
> Companheiro de `FASE2-PROTETICO-AREA.md` (o modelo). Aqui está o **como**, a nível de
|
||||
> arquivo/função, fiel ao código atual de `backend/server.js` e `frontend/`.
|
||||
> Decisões aplicadas: **1 lab por protético**, **só terceirizado**, **receita espelhada na 2.1**.
|
||||
> Disciplina herdada da Fase 1: migrações aditivas/idempotentes; blocos novos em `try/catch`
|
||||
> que nunca derrubam o fluxo principal; `protetico_id` sempre preservado (fallback).
|
||||
|
||||
---
|
||||
|
||||
# PASSO 2.0 — Dados invisíveis (backfill `laboratorio_id`)
|
||||
|
||||
Nenhuma mudança de UI. Objetivo: todo protético tem um workspace `tipo='laboratorio'` e toda
|
||||
OS/produto carrega `laboratorio_id`. Validar 100% em DEV (`pgdev`) antes de seguir.
|
||||
|
||||
## 2.0.a — Migrações (em `runMigrations()`, junto às da Fase 1, ~linha 5663)
|
||||
|
||||
```js
|
||||
// ── FASE 2.0: Laboratório como workspace de 1ª classe ──────────────────────
|
||||
`ALTER TABLE protese_os ADD COLUMN IF NOT EXISTS financeiro_lab_id TEXT`, // RECEITA espelhada (2.1)
|
||||
`ALTER TABLE protese_produtos ADD COLUMN IF NOT EXISTS laboratorio_id TEXT`,
|
||||
// laboratorio_id em protese_os JÁ EXISTE (coluna reservada na criação da tabela) — não recriar.
|
||||
|
||||
// re-tag: o workspace pessoal do protético passa a ser 'laboratorio'
|
||||
`UPDATE clinicas SET tipo='laboratorio'
|
||||
WHERE tipo='pessoal'
|
||||
AND owner_id IN (SELECT id FROM usuarios WHERE role='protetico')`,
|
||||
|
||||
// backfill: cada OS herda o laboratorio do workspace do seu protético
|
||||
`UPDATE protese_os o SET laboratorio_id = c.id
|
||||
FROM clinicas c
|
||||
WHERE o.laboratorio_id IS NULL AND c.tipo='laboratorio' AND c.owner_id = o.protetico_id`,
|
||||
|
||||
// backfill: catálogo passa a ser do laboratório (mantém protetico_id p/ compat)
|
||||
`UPDATE protese_produtos p SET laboratorio_id = c.id
|
||||
FROM clinicas c
|
||||
WHERE p.laboratorio_id IS NULL AND c.tipo='laboratorio' AND c.owner_id = p.protetico_id`,
|
||||
|
||||
// índice por lab (o de protetico_id já existe; adiciona o do lab)
|
||||
`CREATE INDEX IF NOT EXISTS idx_protese_os_labid ON protese_os (laboratorio_id, status)`,
|
||||
```
|
||||
> O `UPDATE especialidades … area='protese'` da Fase 1 já está logo acima — não duplicar.
|
||||
|
||||
## 2.0.b — Helper: resolver o laboratório de um protético
|
||||
|
||||
Novo helper perto de `proteseOsAccesso` (~linha 3772). 1 lab por protético ⇒ `LIMIT 1`.
|
||||
|
||||
```js
|
||||
// O workspace 'laboratorio' do protético (1 por protético nesta fase).
|
||||
async function labDoProtetico(proteticoId) {
|
||||
if (!proteticoId) return null;
|
||||
const { rows } = await pool.query(
|
||||
"SELECT id FROM clinicas WHERE owner_id=$1 AND tipo='laboratorio' AND COALESCE(deleted_at IS NULL, true) LIMIT 1",
|
||||
[proteticoId]);
|
||||
return rows[0]?.id || null;
|
||||
}
|
||||
```
|
||||
> Se `clinicas` não tiver `deleted_at`, usar só `WHERE owner_id=$1 AND tipo='laboratorio'`.
|
||||
|
||||
## 2.0.c — Gravar `laboratorio_id` na CRIAÇÃO da OS (2 pontos)
|
||||
|
||||
**(i) `POST /api/protese/os`** (~linha 3893): resolver o lab e incluí-lo no INSERT.
|
||||
```js
|
||||
const labId = await labDoProtetico(b.protetico_id); // antes do INSERT
|
||||
// no INSERT: acrescentar a coluna laboratorio_id e o valor labId
|
||||
// ... protetico_id, laboratorio_id, protetico_nome, ...
|
||||
// VALUES ($1,$2,$3,$LAB,$4,...)
|
||||
```
|
||||
|
||||
**(ii) Bloco GTO→protese_os (Fase 1)** em `PUT /api/gto-builder/:id/finalizar`
|
||||
(o INSERT em `protese_os` que você acabou de escrever): mesma adição.
|
||||
```js
|
||||
const labId = await labDoProtetico(it.protetico_id); // dentro do for, antes do INSERT
|
||||
// incluir laboratorio_id = labId no INSERT da OS gerada por GTO
|
||||
```
|
||||
> Assim, OS novas já nascem com `laboratorio_id`; as antigas foram cobertas pelo backfill.
|
||||
|
||||
## 2.0.d — Bancada roteia por laboratório (com fallback por pessoa)
|
||||
|
||||
`GET /api/protese/bancada` (~linha 3926). Hoje: `WHERE protetico_id=$1`. Passa a aceitar OS
|
||||
do(s) laboratório(s) do usuário, mantendo fallback para OS ainda sem `laboratorio_id`:
|
||||
|
||||
```js
|
||||
const uid = req.authUser.userId;
|
||||
const labId = await labDoProtetico(uid); // o lab do protético logado
|
||||
const vals = [uid, labId];
|
||||
let where = `( (laboratorio_id IS NOT NULL AND laboratorio_id=$2)
|
||||
OR (laboratorio_id IS NULL AND protetico_id=$1) ) AND deleted_at IS NULL`;
|
||||
if (req.query.status && PROTESE_STATUS.includes(req.query.status)) {
|
||||
vals.push(req.query.status); where += ` AND status=$${vals.length}`;
|
||||
}
|
||||
// resto idêntico (ORDER BY / LIMIT / map)
|
||||
```
|
||||
> `proteseOsAccesso` **não muda na 2.0** (1 lab por protético ⇒ `protetico_id===uid` ainda
|
||||
> identifica o lab). O `OR por vínculo no lab` entra só na 2.3 (equipe/`tecnico_protese`).
|
||||
|
||||
## 2.0.e — Novos protéticos já nascem com `tipo='laboratorio'`
|
||||
|
||||
No `POST /api/register` (~linha 625), o INSERT do workspace usa `tipo='pessoal'` para todos.
|
||||
Diferenciar o protético:
|
||||
```js
|
||||
const wsTipo = userRole === 'protetico' ? 'laboratorio' : 'pessoal';
|
||||
`INSERT INTO clinicas (id, nome_fantasia, tipo, owner_id) VALUES ($1,$2,$3,$4)` // $3 = wsTipo
|
||||
```
|
||||
> O seed de catálogo (`seedProteseProdutos(userId)`) continua por `protetico_id` (owner) —
|
||||
> sem mudança. (O `laboratorio_id` do catálogo é resolvido pelo backfill / por leitura.)
|
||||
|
||||
## ✅ Critérios de aceite 2.0 (validar em DEV)
|
||||
- `SELECT count(*) FROM protese_os WHERE laboratorio_id IS NULL` → **0** (após backfill).
|
||||
- Toda `clinicas` de protético tem `tipo='laboratorio'`.
|
||||
- Criar OS nova (via Nova OS **e** via finalizar GTO) → `laboratorio_id` preenchido.
|
||||
- Bancada continua mostrando exatamente as mesmas OS de antes (fallback cobre o legado).
|
||||
- **Zero** mudança visível na UI.
|
||||
|
||||
---
|
||||
|
||||
# PASSO 2.1 — Casa própria + receita espelhada
|
||||
|
||||
Agora o protético entra numa **área de Laboratório** (menu próprio) e a entrega passa a
|
||||
lançar a receita no workspace do lab.
|
||||
|
||||
## 2.1.a — BACKEND: receita espelhada na entrega
|
||||
|
||||
Em `POST /api/protese/os/:id/status`, dentro do `if (novo === 'entregue') { ... }`, logo após
|
||||
o bloco que cria a **DESPESA** (`finId`). Mesma condição (`custo>0 && tipo_os≠'garantia'`):
|
||||
|
||||
```js
|
||||
// Espelho: RECEITA no workspace do laboratório (clinica_id = laboratorio_id). Idempotente.
|
||||
let finLabId = os.financeiro_lab_id || null;
|
||||
try {
|
||||
const labId = os.laboratorio_id || await labDoProtetico(os.protetico_id);
|
||||
if (!finLabId && labId && Number(os.custo) > 0 && os.tipo_os !== 'garantia') {
|
||||
finLabId = `fin_lab_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
||||
await pool.query(
|
||||
`INSERT INTO financeiro (id, clinica_id, descricao, valor, tipo, status, datavencimento, data_competencia, paciente_id, paciente_nome, origem, centro_custo, sem_comissao)
|
||||
VALUES ($1,$2,$3,$4,'RECEITA','Pendente',CURRENT_DATE,CURRENT_DATE,$5,$6,'protese','PRÓTESE',true)`,
|
||||
[finLabId, labId, `PRÓTESE: ${os.tipo} (cliente ${clinicaNome || ''})`.trim(), Number(os.custo),
|
||||
os.paciente_id || null, os.paciente_nome || null]);
|
||||
}
|
||||
} catch (e) { console.error('[protese entrega→receita lab]', e.message); }
|
||||
```
|
||||
E no `UPDATE protese_os` da entrega, persistir o id:
|
||||
```js
|
||||
// ...financeiro_id=COALESCE($3,financeiro_id), financeiro_lab_id=COALESCE($N,financeiro_lab_id)...
|
||||
```
|
||||
> `clinicaNome` = nome da clínica de origem (1 query opcional, ou usar `os.clinica_id`).
|
||||
> **Estorno** (cancelar entrega) é tarefa à parte: reverter os dois `financeiro` — fica para
|
||||
> a sub-fase de cancelamento; por ora a entrega é terminal (já é hoje: 409 se `entregue`).
|
||||
|
||||
## 2.1.b — FRONTEND: menu da área de Laboratório (`Sidebar.tsx`)
|
||||
|
||||
**(i)** Acrescentar os itens ao array `menuItems` (~linha 53):
|
||||
```js
|
||||
{ id: 'lab-home', label: 'PAINEL DO LAB', icon: LayoutDashboard },
|
||||
{ id: 'lab-bancada', label: 'BANCADA', icon: FlaskConical },
|
||||
```
|
||||
(catálogo/clientes/agenda/financeiro entram na 2.2/2.3 — não criar itens mortos agora.)
|
||||
|
||||
**(ii)** Isolar o menu quando o workspace ativo é laboratório — **espelhar** o bloco que já
|
||||
existe para `tipo==='sala'` (~linha 98). Inserir antes dele:
|
||||
```js
|
||||
// Contexto de LABORATÓRIO: área própria do protético (não é clínica).
|
||||
if ((activeWorkspace as any)?.tipo === 'laboratorio')
|
||||
return ['lab-home', 'lab-bancada', 'notificacoes', 'configuracoes'].includes(item.id);
|
||||
```
|
||||
> Como o protético tem **1** workspace e ele é `tipo='laboratorio'`, esse branch governa o
|
||||
> menu dele. O antigo filtro `currentRole==='protetico'` (linha ~131) deixa de ser alcançado
|
||||
> para o caso comum, mas **mantê-lo** como rede de segurança (protético sem workspace lab).
|
||||
|
||||
## 2.1.c — FRONTEND: roteamento das telas novas (`App.tsx`)
|
||||
|
||||
No switch/router de `view` do `App.tsx`, mapear:
|
||||
```jsx
|
||||
case 'lab-bancada': return <ProteseView />; // já renderiza modo 'bancada' p/ role protetico
|
||||
case 'lab-home': return <LabHomeView />; // novo painel (abaixo)
|
||||
```
|
||||
> `ProteseView` já decide `modo='bancada'` por `getCurrentRole()==='protetico'` — reaproveita
|
||||
> 100%. Só muda o **id de menu** que a abre.
|
||||
|
||||
## 2.1.d — FRONTEND: `LabHomeView` (painel mínimo)
|
||||
|
||||
Nova view `frontend/views/LabHomeView.tsx`. Cards a partir de dados que já existem:
|
||||
- **Fila por status** e **atrasadas**: derivar de `getProteseBancada()` (já retorna `atrasada`).
|
||||
- **Faturamento do mês**: somar `financeiro` RECEITA `origem='protese'` do workspace lab
|
||||
(rota de financeiro existente, escopada por `clinicaId` = id do lab).
|
||||
- **Prazo médio / entregues no mês**: agregação simples sobre a bancada.
|
||||
|
||||
Sem rota nova obrigatória na 2.1 (reusa `bancada` + `financeiro`); se quiser um resumo de 1
|
||||
chamada, criar `GET /api/protese/lab/resumo` depois.
|
||||
|
||||
## ✅ Critérios de aceite 2.1
|
||||
- Logado como protético, a Sidebar mostra **PAINEL DO LAB** + **BANCADA** (+ notificações/config).
|
||||
- A clínica continua com a tela PRÓTESES (modo `clinica`) **intacta**.
|
||||
- Entregar uma OS com `custo>0` (não-garantia) cria **2** lançamentos: DESPESA na clínica e
|
||||
RECEITA no lab (mesmo valor `custo`); reentregar/reprocessar **não** duplica (`COALESCE`).
|
||||
- OS de garantia entregue → **nenhum** lançamento (nos dois lados).
|
||||
- `LabHomeView` mostra fila + faturamento do mês do laboratório.
|
||||
|
||||
---
|
||||
|
||||
# Ordem de execução e deploy
|
||||
1. Branch a partir da `main` (que já tem a Fase 1 não-commitada — **commitar a Fase 1 antes**).
|
||||
2. 2.0 (backend só) → subir em DEV (`dev.scoreodonto.com`/`pgdev`), rodar os SELECTs de aceite.
|
||||
3. 2.1 (backend receita + front) → validar aceite em DEV.
|
||||
4. Commit por sub-fase; push → CI builda por commit; `git tag vX.Y.Z` promove à produção.
|
||||
> Migrações aditivas ⇒ rollback de código é seguro (padrão já validado no projeto).
|
||||
@@ -0,0 +1,220 @@
|
||||
# Fase 2 — Área do Protético (Laboratório como workspace de 1ª classe)
|
||||
|
||||
> Desenho de modelo. Continuação da **Fase 1 (GTO → Prótese)**, onde finalizar uma GTO
|
||||
> abre automaticamente uma OS na Bancada (`protese_os.gto_item_id`).
|
||||
> **Objetivo da Fase 2:** o protético deixa de ser "um papel que só vê a Bancada" e passa
|
||||
> a ter uma **área de gestão própria** — o Laboratório — equivalente ao que a Clínica é para
|
||||
> o dentista. Ele gere catálogo, equipe, clientes (clínicas), financeiro e agenda de entregas.
|
||||
|
||||
---
|
||||
|
||||
## 1. Onde estamos hoje (não reinventar)
|
||||
|
||||
O alicerce já existe — a Fase 2 **promove**, não cria do zero:
|
||||
|
||||
| Já existe | Onde |
|
||||
|---|---|
|
||||
| Role `protetico` (válido em `usuarios` e `vinculos`) | `server.js` VALID_ROLES / constraints |
|
||||
| Workspace pessoal "LABORATÓRIO DE …" criado no signup | `clinicas (tipo='pessoal')` + `vinculos (role='protetico')` |
|
||||
| Catálogo de produtos (preço fixo + garantia) | `protese_produtos (protetico_id)` |
|
||||
| Bancada (fila de OS do protético) | `GET /api/protese/bancada` (roteia por `protetico_id`) |
|
||||
| OS com eventos, fotos, status, garantia, financeiro | `protese_os`, `protese_os_evento`, `protese_os_foto` |
|
||||
| `ProteseView` com `modo: 'bancada' | 'clinica'` por role | `frontend/views/ProteseView.tsx` |
|
||||
| **`protese_os.laboratorio_id`** — reservado, hoje `null` | comentário: "workspace destino (futuro)" |
|
||||
| Menu filtrado por role (protético = subset) | `Sidebar.tsx` linha ~131 |
|
||||
|
||||
**Lacuna que a Fase 2 fecha:** o Laboratório não é uma *entidade gerenciável*. O protético
|
||||
roteia OS por `protetico_id` (a pessoa), não por um laboratório (a empresa). Não há painel,
|
||||
equipe, visão de clientes, nem financeiro do lado do laboratório. A OS conhece a *pessoa*,
|
||||
não a *bancada/empresa*.
|
||||
|
||||
---
|
||||
|
||||
## 2. Decisão de modelo: Laboratório = workspace `tipo='laboratorio'`
|
||||
|
||||
O sistema já trata **workspace** de forma polimórfica: `clinicas` é a tabela-base e `tipo`
|
||||
distingue (`clinica` | `pessoal` | `sala`). A Fase 2 introduz **`tipo='laboratorio'`** em vez
|
||||
de criar uma tabela nova. Isso reaproveita: vínculos, troca de workspace, RBAC por cargo,
|
||||
financeiro, notificações e o seletor de workspace da Sidebar.
|
||||
|
||||
```
|
||||
usuarios (role='protetico')
|
||||
│ vinculos (role='protetico' | 'tecnico_protese')
|
||||
▼
|
||||
clinicas (tipo='laboratorio') ◄── "LABORATÓRIO DE X" deixa de ser 'pessoal'
|
||||
│
|
||||
┌───────────┼─────────────────────────────┐
|
||||
▼ ▼ ▼
|
||||
protese_produtos protese_os (laboratorio_id) equipe (vinculos do lab)
|
||||
(catálogo) ▲ roteamento por LAB,
|
||||
│ não mais só por pessoa
|
||||
clínicas-cliente enviam OS ──┘
|
||||
```
|
||||
|
||||
### Migração do dado existente (aditiva, idempotente)
|
||||
```sql
|
||||
-- 1) re-tag do workspace pessoal do protético → laboratorio
|
||||
UPDATE clinicas SET tipo='laboratorio'
|
||||
WHERE tipo='pessoal'
|
||||
AND owner_id IN (SELECT id FROM usuarios WHERE role='protetico');
|
||||
|
||||
-- 2) backfill: toda OS legada ganha o laboratorio_id do workspace do seu protetico
|
||||
UPDATE protese_os o SET laboratorio_id = c.id
|
||||
FROM clinicas c
|
||||
WHERE o.laboratorio_id IS NULL
|
||||
AND c.tipo='laboratorio'
|
||||
AND c.owner_id = o.protetico_id;
|
||||
|
||||
-- 3) o catálogo passa a ser do laboratório (mantém protetico_id p/ compat)
|
||||
ALTER TABLE protese_produtos ADD COLUMN IF NOT EXISTS laboratorio_id TEXT;
|
||||
UPDATE protese_produtos p SET laboratorio_id = c.id
|
||||
FROM clinicas c
|
||||
WHERE p.laboratorio_id IS NULL AND c.tipo='laboratorio' AND c.owner_id = p.protetico_id;
|
||||
```
|
||||
> `protetico_id` **continua** populado em todas as tabelas (compatibilidade da Fase 1 e do
|
||||
> roteamento por pessoa). `laboratorio_id` passa a ser a **chave de escopo** da área.
|
||||
|
||||
---
|
||||
|
||||
## 3. A Área do Laboratório (navegação)
|
||||
|
||||
Quando o `activeWorkspace.tipo === 'laboratorio'`, a Sidebar troca para um menu próprio
|
||||
(análogo ao `tipo==='sala'` que já isola o menu). Itens:
|
||||
|
||||
| Item | Conteúdo | Reaproveita |
|
||||
|---|---|---|
|
||||
| `lab-home` | **Painel** do laboratório: OS por status, atrasadas, faturamento do mês, prazo médio | novo dashboard + agregações de `protese_os` |
|
||||
| `lab-bancada` | **Bancada** (fila de produção, kanban por status) | `ProteseView` modo bancada (já existe) |
|
||||
| `lab-catalogo` | **Catálogo** de produtos (preço/garantia) — vira tela, não modal | `ProdutosModal` → view |
|
||||
| `lab-clientes` | **Clínicas-cliente**: quem envia OS, volume, ticket, pendências | distinct de `protese_os.clinica_id` |
|
||||
| `lab-agenda` | **Agenda de entregas** por `prazo_entrega` | AgendaView adaptada / nova |
|
||||
| `lab-financeiro` | **A receber** das clínicas (a OS gera DESPESA na clínica = RECEITA no lab) | `protese_os.financeiro_id` (elo) |
|
||||
| `lab-equipe` | **Técnicos** do laboratório (RBAC por cargo já existe) | `vinculos` + permissões de workspace |
|
||||
| `notificacoes` / `configuracoes` | base | já existe |
|
||||
|
||||
A tela "PRÓTESES" da **clínica** (modo `clinica`) não muda — ela continua **enviando** OS.
|
||||
O que muda é o **lado de quem recebe**, que ganha uma casa própria.
|
||||
|
||||
### Roteamento de OS: pessoa → laboratório
|
||||
- Hoje: `bancada` filtra `WHERE protetico_id = me`.
|
||||
- Fase 2: `bancada` filtra `WHERE laboratorio_id = activeWorkspace.id` (com fallback
|
||||
`OR (laboratorio_id IS NULL AND protetico_id = me)` durante a transição).
|
||||
- Ganho imediato: **mais de um técnico** pode atender a mesma bancada (equipe), e o protético
|
||||
dono pode ter **mais de um laboratório**.
|
||||
|
||||
---
|
||||
|
||||
## 4. Modelo de dados — deltas da Fase 2
|
||||
|
||||
Tudo aditivo (migrações no padrão `ALTER TABLE … IF NOT EXISTS`, como a Fase 1):
|
||||
|
||||
```sql
|
||||
-- workspace
|
||||
clinicas.tipo -- novo valor possível: 'laboratorio'
|
||||
|
||||
-- escopo por laboratório (a empresa), além de protetico_id (a pessoa)
|
||||
protese_os ADD laboratorio_id TEXT -- (já reservado; passa a ser populado/backfill)
|
||||
protese_os ADD financeiro_lab_id TEXT -- RECEITA espelhada no lab (§9; idempotência)
|
||||
protese_produtos ADD laboratorio_id TEXT
|
||||
|
||||
-- equipe do laboratório: novo role de vínculo (técnico não-dono)
|
||||
vinculos.role -- incluir 'tecnico_protese' no CHECK constraint
|
||||
|
||||
-- relação clínica↔laboratório (opcional, p/ "meus laboratórios favoritos"/preços negociados)
|
||||
CREATE TABLE IF NOT EXISTS lab_clientes (
|
||||
id TEXT PRIMARY KEY,
|
||||
laboratorio_id TEXT NOT NULL,
|
||||
clinica_id TEXT NOT NULL,
|
||||
apelido TEXT,
|
||||
desconto_pct NUMERIC DEFAULT 0, -- preço negociado (futuro)
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE (laboratorio_id, clinica_id)
|
||||
);
|
||||
```
|
||||
|
||||
Índices: `idx_protese_os_lab` passa a ser `(laboratorio_id, status)`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Permissões (RBAC)
|
||||
|
||||
Reaproveita o RBAC por cargo que já existe na Sidebar (`wsRole`/`wsPerms`):
|
||||
|
||||
| Papel no laboratório | Pode |
|
||||
|---|---|
|
||||
| `protetico` (dono do lab) | tudo: catálogo, equipe, financeiro, status de qualquer OS |
|
||||
| `tecnico_protese` (membro) | bancada + avançar status + fotos; **sem** financeiro/equipe/catálogo |
|
||||
| clínica-cliente (`isClinic` em `proteseOsAccesso`) | criar OS, ver suas OS, entregar/cancelar, **sem** ver custo/bancada de outros |
|
||||
|
||||
`proteseOsAccesso(os, userId)` ganha o caso "membro do laboratório dono da OS"
|
||||
(`vinculo em laboratorio_id`), além do dono atual.
|
||||
|
||||
---
|
||||
|
||||
## 6. Plano de implementação incremental
|
||||
|
||||
Cada passo é deployável e reversível (migrações aditivas → rollback de código é seguro).
|
||||
|
||||
- **2.0 — Dados (invisível):** migrações + backfill `laboratorio_id`; `bancada` lê por lab com
|
||||
fallback por pessoa. Nada muda na UI. *Risco mínimo, valida o backfill em DEV (`pgdev`).*
|
||||
- **2.1 — Casa própria + receita espelhada:** `tipo='laboratorio'` re-tagueado; Sidebar com
|
||||
menu do laboratório; `lab-home` (painel) + `lab-bancada`; **espelho de RECEITA na entrega**
|
||||
(§9) — o financeiro mínimo do lab já nesta fase, conforme decisão 3.
|
||||
- **2.2 — Catálogo e clientes:** `ProdutosModal` vira `lab-catalogo`; tela `lab-clientes`.
|
||||
- **2.3 — Financeiro completo e equipe:** `lab-financeiro` (extrato/a-receber por cliente,
|
||||
sobre as receitas já lançadas na 2.1) + `tecnico_protese` (convite de membro reusando o
|
||||
fluxo de equipe existente).
|
||||
- **2.4 — Agenda de entregas + multi-laboratório** (expansão; modelo já suporta, sem migração).
|
||||
|
||||
---
|
||||
|
||||
## 7. O que **não** muda (contrato preservado)
|
||||
- O lado **clínica** de `ProteseView` (modo `clinica`) e o envio de OS.
|
||||
- A **Fase 1**: GTO finalizada continua abrindo OS (`gto_item_id`), agora já com
|
||||
`laboratorio_id` preenchido na criação.
|
||||
- `protetico_id` permanece em todas as tabelas — roteamento por pessoa segue funcionando
|
||||
como fallback; nenhuma OS legada fica órfã.
|
||||
|
||||
---
|
||||
|
||||
## 8. Decisões de produto (fechadas em 23/jun/2026)
|
||||
1. **Um laboratório por protético.** A UI assume 1 lab. O modelo de dados já suporta N
|
||||
(`laboratorio_id` por OS/produto), então multi-lab é expansão futura **sem migração**.
|
||||
2. **Laboratório terceirizado** (empresa separada que cobra da clínica). A flag `interno`
|
||||
de `getProteseLaboratorios` fica como gancho futuro, mas **não** é tratada agora —
|
||||
todo lab gera receita/despesa cruzada entre tenants.
|
||||
3. **Financeiro espelhado já na 2.1.** A OS entregue, que hoje lança **DESPESA na clínica**
|
||||
(`protese_os.financeiro_id`), passa a lançar simultaneamente uma **RECEITA no workspace
|
||||
do laboratório**. Ver §9.
|
||||
|
||||
---
|
||||
|
||||
## 9. Financeiro espelhado (decisão 3) — DESPESA na clínica ↔ RECEITA no lab
|
||||
|
||||
Hoje a entrega da OS gera um lançamento de **DESPESA** na clínica de origem
|
||||
(`protese_os.financeiro_id`), no valor de **`os.custo`** (o preço de catálogo do lab — o que a
|
||||
clínica paga ao laboratório; `os.valor` é o preço ao paciente, markup da clínica, e **não**
|
||||
entra aqui). Como o laboratório agora é um workspace de 1ª classe, o mesmo evento de entrega
|
||||
lança um **par espelhado**, ambos sobre `os.custo`:
|
||||
|
||||
```
|
||||
entrega da OS (status='entregue', custo>0, tipo_os≠'garantia')
|
||||
│
|
||||
├─► financeiro (DESPESA) clinica_id = os.clinica_id valor = os.custo [já existe]
|
||||
└─► financeiro (RECEITA) clinica_id = os.laboratorio_id valor = os.custo [novo]
|
||||
```
|
||||
|
||||
- Mesma **condição** da despesa já existente: só lança se `custo > 0` e `tipo_os ≠ 'garantia'`
|
||||
(OS de garantia refaz sem cobrar). Sai no mesmo `if (novo === 'entregue')`.
|
||||
- Idempotente, no padrão da Fase 1: novo `protese_os.financeiro_lab_id` guarda o id do
|
||||
lançamento de receita; se já existe (`COALESCE`), não duplica.
|
||||
- Isolado em `try/catch` para **nunca** derrubar a transição de status (mesma disciplina do
|
||||
bloco GTO→protese_os da Fase 1).
|
||||
- Estorno: cancelar/estornar a entrega deve reverter **os dois** lançamentos.
|
||||
|
||||
Por isso o **passo 2.1 já inclui o financeiro mínimo** (apenas o espelho de receita na
|
||||
entrega); o item de menu `lab-financeiro` completo (extrato, a-receber por cliente) continua
|
||||
na 2.3.
|
||||
|
||||
> ⚠️ Pré-requisito: `laboratorio_id` precisa estar populado **antes** de ligar o espelho
|
||||
> (senão a receita nasce sem destino). Por isso a ordem **2.0 (backfill) → 2.1** é obrigatória.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Marketplace transacional de Prótese
|
||||
|
||||
> Desenho (24/jun/2026). O último grande item do backlog do Provedor de Prótese.
|
||||
> Princípio: **transformar o diretório (descoberta) em jornada de compra** — descobrir → ver
|
||||
> serviços/preços → solicitar → cotar → contratar → vira OS. Reaproveita ao máximo o que já existe.
|
||||
|
||||
## O que já existe (não recriar)
|
||||
- **Descoberta:** `/api/profissionais/marketplace` (busca por proximidade/especialidade) **+ Nota
|
||||
ScoreOdonto** no card (feito).
|
||||
- **Catálogo:** `protese_produtos` (nome, preço, garantia) + `GET /api/protese/laboratorios/:id/produtos`
|
||||
— já acessível a quem está no **diretório** (`proteticoAcessivelPelaClinica` aceita `disponivel_diretorio`).
|
||||
- **Propostas:** `propostas_profissional` (clínica→profissional: mensagem, `pendente→aceita/recusada`).
|
||||
- **Contratação:** a clínica **já cria OS para qualquer protético do diretório** (Nova OS).
|
||||
- **Pós-venda:** OS completa (etapas, custódia, ocorrências), financeiro, score — tudo pronto.
|
||||
|
||||
**Conclusão:** o "transacional" não exige nova fundação — falta **costurar a jornada** dentro do
|
||||
marketplace (hoje a clínica vê nome/nota/distância, mas não os serviços, e contrata por outra tela).
|
||||
|
||||
## Fatias
|
||||
|
||||
### ✅ Fatia 1 — Vitrine (catálogo no marketplace) — IMPLEMENTADA
|
||||
No card do protético no marketplace, **"Ver catálogo"** expande os produtos (nome, preço, garantia)
|
||||
via `getLaboratorioProdutos(id)` (reuso — zero endpoint novo). A clínica passa a **decidir vendo
|
||||
serviços + preço + garantia + ★nota** antes de contratar. Botão de contato vira **"Solicitar
|
||||
prótese"** (proposta com contexto de prótese).
|
||||
|
||||
### ✅ Fatia 2 — Solicitação → OS direto — IMPLEMENTADA
|
||||
"Solicitar prótese" (card do protético) abre o `SolicitarProteseModal` — fluxo enxuto com o lab
|
||||
**pré-selecionado**: produto do catálogo (ou descrição livre) + paciente (busca) + dentista + dentes
|
||||
+ prazo + observações → **cria a OS** via `createProteseOS` (que já aceita lab do diretório, sem
|
||||
vínculo prévio). Tela de confirmação com CTA para a Bancada. Optei por um modal próprio e auto-contido
|
||||
(reusa getLaboratorioProdutos/getDentistas/getPacientes/createProteseOS) em vez de extrair a Nova OS —
|
||||
menor risco. Validado em DEV: OS criada para lab do diretório (Coroa de Zircônia, status solicitado).
|
||||
|
||||
### ✅ Fatia 3 — Cotação (RFQ) — IMPLEMENTADA
|
||||
Entidades novas `protese_rfq` + `protese_rfq_cotacao`. Fluxo: a clínica descreve o trabalho e
|
||||
**convida N labs** → cada lab **cota** (valor/prazo/obs) → a clínica **compara** (ordenado por preço)
|
||||
e **escolhe** → vira **OS** (custo = valor da cotação) e a RFQ fecha (demais cotações recusadas).
|
||||
Endpoints: `POST/GET /protese/rfq`, `GET /protese/rfq/recebidas`, `POST /protese/rfq/cotacao/:id`,
|
||||
`POST /protese/rfq/:id/escolher`. UI: lado lab = tela **Cotações** (`lab-cotacoes`, responde); lado
|
||||
clínica = botão **Cotações** na tela de Prótese (pedir multi-lab + comparar + escolher). Validado em
|
||||
DEV ponta a ponta (pedido → cotação R$420 → escolha → OS criada → RFQ fechada).
|
||||
|
||||
### ✅ Fatia 4 — Reputação (avaliação + selo) — IMPLEMENTADA
|
||||
A clínica avalia o laboratório (1-5★ + comentário) **após a entrega** (`protese_avaliacao`, 1 por OS,
|
||||
reavaliável). A satisfação **entra na Nota ScoreOdonto**: `0.6·operacional + 0.4·(estrelas/5·10)` (sem
|
||||
avaliações = só operacional). **Selo "Verificado"** com ≥ 5 entregas. Exposto no marketplace (★nota ·
|
||||
nº avaliações · Verificado) e nos indicadores do lab (KPI Satisfação). Endpoint `POST
|
||||
/protese/os/:id/avaliar`. Validado em DEV: 6 entregas + avaliações 5★/3★ → nota **9.2**, satisfação 4★,
|
||||
verificado, em marketplace e indicadores.
|
||||
|
||||
**Marketplace transacional concluído (Fatias 1-4).**
|
||||
|
||||
## Decisões de modelagem
|
||||
- **Catálogo público = catálogo atual** (`protese_produtos`); a vitrine só o exibe — sem duplicar.
|
||||
- **Solicitação reusa a OS** (não cria entidade paralela): toda contratação termina em `protese_os`,
|
||||
preservando rastreabilidade/financeiro/score.
|
||||
- **Cotação (Fatia 3)** é o único caso que precisa de entidade nova (pré-OS multi-lab).
|
||||
@@ -0,0 +1,117 @@
|
||||
# Modo 1 — Provedor de Prótese SEM conta (link seguro `/p/TOKEN`)
|
||||
|
||||
> Desenho (24/jun/2026). Evolução registrada como "fora de escopo" em `PROVEDOR-PROTESE-DECISAO.md`.
|
||||
> Princípio (decisão arquitetural): **a clínica opera 100% da OS; o protético não precisa ter conta.**
|
||||
> O link é a porta de entrada do ecossistema — acompanha hoje, vira conta amanhã.
|
||||
|
||||
## Objetivo
|
||||
A clínica cria/gere a OS normalmente. O protético recebe um **link seguro** (WhatsApp/e-mail) e,
|
||||
**sem login**, acompanha e atua na OS dele. Com CTA para criar conta grátis (conversão).
|
||||
|
||||
```
|
||||
Clínica cria OS ──► gera link /p/TOKEN ──► WhatsApp/e-mail ──► Protético abre (sem conta)
|
||||
├─ vê a OS (sem CPF/valores do paciente)
|
||||
├─ confirma recebimento/devolução
|
||||
├─ avança etapa / anexa foto
|
||||
└─ responde ocorrência
|
||||
"Crie sua conta grátis" ──► cadastro (card Protético)
|
||||
```
|
||||
|
||||
## 1. Escopo do token: **por OS** (não por protético)
|
||||
Escopo mínimo = menor superfície de risco. Um token dá acesso a **uma OS**. Vantagens: revogável
|
||||
individualmente, expira por trabalho, não expõe a carteira inteira do protético. (Evolução: um token
|
||||
"carteira" por protético-na-clínica que lista várias OS — fica para depois.)
|
||||
|
||||
## 2. Modelo de dados (revogável e auditável)
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS protese_os_link (
|
||||
token TEXT PRIMARY KEY, -- aleatório longo (≥ 32 bytes base62), NÃO sequencial
|
||||
os_id TEXT NOT NULL,
|
||||
protetico_id TEXT, -- a quem se destina (rastreio)
|
||||
created_by TEXT, -- quem na clínica gerou
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ, -- opcional (ex.: +30 dias); null = sem expirar
|
||||
revoked_at TIMESTAMPTZ, -- revogação manual
|
||||
last_access_at TIMESTAMPTZ,
|
||||
acessos INTEGER DEFAULT 0
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_protese_os_link_os ON protese_os_link (os_id);
|
||||
```
|
||||
> Preferir tabela a JWT puro: permite **revogar** e **auditar acessos**. O token nunca contém dados.
|
||||
|
||||
## 3. Rotas públicas (sem `authGuard`; o token é a credencial)
|
||||
Todas resolvem `os_id` pelo token (valida não-revogado, não-expirado) e aplicam a **mesma blindagem
|
||||
do provedor**: sem `paciente_id`/CPF, sem valores do paciente (reusa a lógica `soLab`).
|
||||
|
||||
| Rota | Ação |
|
||||
|---|---|
|
||||
| `GET /api/p/:token` | OS: nº, clínica, dentista, paciente (nome, conforme permissão), prazo, status, observações, fotos, componentes, ocorrências, custódia. **Sem CPF/valores do paciente.** |
|
||||
| `POST /api/p/:token/custodia` | confirmar recebimento / devolução (clínica↔lab) |
|
||||
| `POST /api/p/:token/status` | avançar a produção (recebido→…→finalizado) |
|
||||
| `POST /api/p/:token/foto` | anexar foto do trabalho (+ ocorrência) |
|
||||
| `POST /api/p/:token/ocorrencias/:id/resposta` | responder ocorrência (contraditório) |
|
||||
|
||||
Cada ação registra no histórico da OS um **actor "Protético (link)"** com o `protetico_id` do token —
|
||||
mantendo a auditoria. Mesmas validações de status/foto já existentes.
|
||||
|
||||
## 4. Geração e envio (lado clínica)
|
||||
Na OS (modal, aba Etapas ou Monitoramento): botão **"Enviar ao protético"** →
|
||||
`POST /api/protese/os/:id/link` gera/recupera o token e devolve a URL
|
||||
`https://scoreodonto.com/p/TOKEN`. Atalhos: **WhatsApp** (`https://wa.me/?text=…`) e **e-mail**
|
||||
(`mailto:`). Botão **"Revogar link"** para invalidar.
|
||||
|
||||
## 5. Frontend público `/p/:token`
|
||||
Rota **fora do app autenticado** (sem Sidebar/login). Página enxuta que reusa as seções do
|
||||
`OSDetailModal` em modo "público/lab" (etapas, fotos, componentes, custódia, ocorrências — sem
|
||||
financeiro/clientes). Topo com a marca + rodapé fixo:
|
||||
|
||||
> *"Gostou de acompanhar por aqui? Crie sua conta grátis e gerencie todos os seus laboratórios."*
|
||||
> → leva ao cadastro **já com o card Protético** (adicionado) e e-mail pré-preenchido.
|
||||
|
||||
## 6. Segurança (checklist)
|
||||
- Token aleatório ≥ 32 bytes, base62, **não sequencial**; comparação em tempo constante.
|
||||
- Escopo de **1 OS**; expiração opcional; **revogável**; rate-limit por token/IP.
|
||||
- **Sem CPF e sem valores do paciente** (mesma blindagem `soLab` da Fase 1).
|
||||
- Ações registram actor no histórico (auditoria); `last_access_at`/`acessos` para detectar abuso.
|
||||
- OS `entregue`/`cancelado` → link vira **somente leitura**.
|
||||
|
||||
## 7. Conversão (o "porquê de negócio")
|
||||
O link é aquisição: o protético experimenta o produto sem fricção e, ao criar conta, **herda o
|
||||
histórico** (as OS daquela clínica já estão ligadas ao `protetico_id` dele). Liga direto ao
|
||||
marketplace/score (Fase 5) — quanto mais clínicas o usam, mais OS e melhor o score do protético.
|
||||
|
||||
## 8. Faseamento sugerido
|
||||
- **✅ 1a (núcleo) — IMPLEMENTADA 24/jun:** tabela `protese_os_link` (token aleatório 24 bytes
|
||||
base64url, expira em 60 dias, revogável); `POST /api/protese/os/:id/link` (gera/recupera) +
|
||||
`POST …/link/revogar`; `GET /api/p/:token` (leitura pública curada — sem CPF, sem valor do
|
||||
paciente; conta acessos). Página pública `/p/:token` (`ProtesePublicView`, mobile-first 320px+,
|
||||
interceptada no `index.tsx` antes do app autenticado); botão "Gerar link / WhatsApp / Copiar" na
|
||||
aba Monitoramento (modo clínica). CTA "Criar conta grátis". Validado em DEV (leitura, blindagem,
|
||||
token inválido→404, revogação→404).
|
||||
- **✅ 1b (ação) — IMPLEMENTADA 24/jun:** o protético, pelo link, **confirma recebimento/devolução**
|
||||
(`POST /api/p/:token/custodia`), **avança a produção** (`/status`, só `recebido…finalizado` — nunca
|
||||
entregue/cancelado, que são da clínica) e **anexa foto** (`/foto`). Tudo auditado como
|
||||
`"<nome> (link)"`. Página pública ganha a seção "Atualizar status" (botões de custódia, etapa e foto)
|
||||
enquanto a OS não está finalizada. Validado em DEV: recebimento, avanço, foto, e `entregue` pelo link
|
||||
→ 400 (bloqueado).
|
||||
- **✅ 1c (gestão) — IMPLEMENTADA 24/jun:** UI da clínica (aba Monitoramento) gere o link:
|
||||
`GET /api/protese/os/:id/link/status` traz **link ativo, nº de acessos, último acesso e expiração**;
|
||||
botões **Copiar / WhatsApp / Revogar**. Validado em DEV: acessos contados, expiração 60 dias,
|
||||
revogação → status inativo + leitura pública 404.
|
||||
> Conversão por herança: nativa — a OS já aponta para o `protetico_id` do destinatário, então ao
|
||||
> entrar/criar conta ele já é dono do histórico. (Vincular um cadastro NOVO com e-mail diferente às
|
||||
> OS é caso de borda — fica para evolução, evitando duplicar usuário.)
|
||||
|
||||
**Modo 1 concluído (1a leitura + 1b ação + 1c gestão).**
|
||||
|
||||
### ✅ Extras (24/jun)
|
||||
- **Carteira pela link** (`GET /api/p/:token/carteira`): botão "Meus trabalhos" na página pública →
|
||||
trabalhos em andamento + finalizados + **mini financeiro mensal** (valores pagos ao protético).
|
||||
Escopo = a relação clínica↔protético da OS do token (não vaza outras clínicas; sem dados do paciente).
|
||||
- **Protético INTERNO (sem conta)** (`POST /api/protese/proteticos-internos`): a clínica cadastra um
|
||||
protético que não usa a plataforma — cria um usuário "mudo" (e-mail placeholder `..@interno.local`,
|
||||
senha-fantasma; `usuarios.email` é NOT NULL/UNIQUE) + vínculo interno (fora do diretório). Botão
|
||||
"Cadastrar protético interno" na Nova OS. A partir daí recebe OS e acompanha pelo link, sem conta.
|
||||
|
||||
> Nenhuma refatoração: reusa OS/eventos/custódia/ocorrências/blindagem já existentes. Só adiciona a
|
||||
> camada de token público.
|
||||
@@ -0,0 +1,121 @@
|
||||
# Decisão arquitetural — Provedor de Prótese (módulo de Prótese)
|
||||
|
||||
> Decisão de negócio/interface (24/jun/2026). Base: auditoria do código (`AUDITORIA-PROTESE.md`).
|
||||
> Princípio-guia: **a Clínica controla 100% do fluxo, mesmo que o Protético nunca crie conta.**
|
||||
> O login do Provedor é **alavanca de crescimento** (marketplace/ecossistema), não pré-requisito.
|
||||
|
||||
## Conceito
|
||||
"**Provedor de Prótese**" é rótulo de **negócio/interface**. Internamente **NÃO se renomeia** o
|
||||
role `protetico` (aparece 87× no backend + 15 arquivos no front — refatorar = risco alto, ganho
|
||||
zero). As três formas são variações do **mesmo** workspace `tipo='laboratorio'`, diferenciadas por
|
||||
metadados (futuros `pessoa_tipo`, `tem_equipe`), não por novos roles:
|
||||
|
||||
```
|
||||
PROVEDOR DE PRÓTESE (role=protetico, workspace tipo='laboratorio')
|
||||
├── Profissional Individual (PF)
|
||||
├── Laboratório (PJ)
|
||||
└── Laboratório com Equipe (PJ + técnicos)
|
||||
```
|
||||
|
||||
## Modos de operação
|
||||
- **Modo 1 — Protético SEM conta:** a Clínica opera tudo (OS, etapas, envio/recebimento/devolução,
|
||||
pagamentos, componentes, ocorrências, encerramento). O Protético acompanha por **link seguro**
|
||||
(`/p/TOKEN` via WhatsApp/e-mail), podendo confirmar recebimento/devolução, anexar fotos, responder
|
||||
ocorrências. CTA: "crie sua conta ScoreOdonto".
|
||||
- **Modo 2 — Protético COM conta:** Dashboard, Ordens, Produção, Entregas, Clientes, Financeiro,
|
||||
Catálogo, Notificações, Perfil.
|
||||
- **Modo 3 — Laboratório:** + Equipe, Relatórios, Marketplace, Configurações.
|
||||
|
||||
## Cadastro/Login
|
||||
Card próprio **[ PROTÉTICO / LABORATÓRIO ]** com opção PF (Protético Individual) / PJ (Laboratório).
|
||||
Campos futuros (não obrigatórios no MVP): Nome, Nome Fantasia, CPF, CNPJ, CROT, Responsável Técnico,
|
||||
Cidade, Estado, Especialidades, Telefone, E-mail. (Campo `clinicas.documento` já existe para CPF/CNPJ.)
|
||||
|
||||
## Rastreabilidade de componentes (modelo)
|
||||
Uma tabela (`protese_os_componente`) com eixo de direção/conferência, não duas:
|
||||
`enviado pela clínica → recebido pelo protético → devolvido pelo protético → conferido pela clínica`.
|
||||
A **ocorrência referencia o componente** (FK opcional).
|
||||
|
||||
## Ocorrências + Contraditório
|
||||
Tabela própria. Categorias: não devolvido, danificado, incompatível, parafuso substituído/desgastado,
|
||||
peça incorreta, ajuste inadequado, falha de fabricação, retrabalho, outro. Campos: autor, data,
|
||||
categoria, descrição, fotos, evidências, anexos, responsável, status.
|
||||
**Fluxo com contraditório** (proteção dos dois lados): `Aberta → Respondida → Em análise → Resolvida`
|
||||
ou `Contestada`. **Só ocorrências resolvidas** alimentam indicadores — nunca automático.
|
||||
|
||||
## Abas da OS (alvo)
|
||||
`Dados · Fotos · Componentes · Pagamentos · Custódia · Histórico · Ocorrências` (nova).
|
||||
|
||||
## Indicadores (POR ÚLTIMO)
|
||||
Capturar primeiro (eventos, componentes, ocorrências, evidências, histórico); só depois derivar
|
||||
Score/ranking/marketplace. Não codar indicador antes de existir dado.
|
||||
|
||||
---
|
||||
|
||||
## Roadmap (5 fases)
|
||||
|
||||
### ✅ FASE 1 — Origem da OS + LGPD (IMPLEMENTADA 24/jun)
|
||||
- **Clínica de origem** na bancada e no detalhe (`clinica_nome` via subquery) — visível ao protético
|
||||
(badge na lista e no modal). Destrava o cenário multi-clínica.
|
||||
- **Blindagem de CPF:** confirmado que `pacientes.id` **É o CPF**. O protético (lab) deixa de receber
|
||||
`paciente_id` (detalhe e bancada zeram), mantendo `paciente_nome`. A clínica continua vendo o CPF.
|
||||
- Validado em DEV: protético vê `clinica_nome` + `paciente_id:null`; clínica vê tudo.
|
||||
|
||||
### ✅ FASE 2 — Rastreabilidade & Ocorrências (IMPLEMENTADA 24/jun)
|
||||
- **Componentes** ganham `direcao` (enviado/devolvido) e `status_conferencia` (pendente/ok/divergente/
|
||||
ausente, com `conferido_nome/_em`). Endpoint `POST .../componentes/:id/conferir`.
|
||||
- **Ocorrências** (`protese_os_ocorrencia`): 10 categorias, descrição, responsável, autor, status.
|
||||
Evidências via `protese_os_foto.ocorrencia_id` (reusa a infra de mídia).
|
||||
- **Contraditório**: `aberta → respondida → em_analise → resolvida | contestada`; `resolvida` é final
|
||||
(409 ao remexer). Endpoints `/ocorrencias`, `/ocorrencias/:id/resposta`, `/ocorrencias/:id/status`.
|
||||
- **UI**: nova aba **Ocorrências** no modal (com contador) + conferência inline nos componentes.
|
||||
- Validado em DEV: componente `divergente`; ocorrência `parafuso_substituido` percorrendo todo o
|
||||
contraditório até `resolvida`; resolvida final (409). Indicadores ficam para a Fase 5.
|
||||
|
||||
### ✅ FASE 3 — Operação multi-clínica (IMPLEMENTADA 24/jun)
|
||||
- **Clientes** (tela `lab-clientes`): tabela das clínicas que enviam OS, com OS totais, ativas,
|
||||
atrasadas, entregues, faturado e a-receber por cliente. Endpoint `GET /protese/lab/clientes`.
|
||||
- **Financeiro do lab** (tela `lab-financeiro`): KPIs (faturado/recebido/a-receber) + extrato de
|
||||
recebimentos (pagamentos lado lab) com clínica/paciente/forma/data. Endpoint `GET /protese/lab/pagamentos`.
|
||||
Regra: faturado = custo das OS entregues (exceto garantia); a-receber = faturado − recebido.
|
||||
- Menu do laboratório agora: Painel · Bancada · **Clientes** · **Financeiro** · Notificações · Config.
|
||||
- Histórico completo: a Bancada já lista entregues/cancelados com filtro de status (mantida).
|
||||
- Validado em DEV: agregação por clínica (faturado 600 / recebido 250 / a-receber 350) + extrato.
|
||||
|
||||
### ✅ FASE 4 — Empresa de verdade (IMPLEMENTADA 24/jun)
|
||||
- **PF/PJ + documento**: `clinicas.pessoa_tipo` (PF=individual | PJ=laboratório); `documento` = CPF/CNPJ
|
||||
(já existia, agora editável). Tela **Laboratório & Equipe** (`lab-equipe`): GET/PATCH `/protese/lab`.
|
||||
- **Equipe/Técnicos**: novo `vinculos.role='tecnico_protese'`. O dono convida por e-mail
|
||||
(`ensureUsuarioPorEmail` → credencial temporária); endpoints `GET/POST/DELETE /protese/lab/equipe`.
|
||||
- **Acesso estendido**: `proteseOsAccesso` reconhece o técnico (vínculo no `laboratorio_id`); a bancada
|
||||
usa `labDoUsuario` (dono **ou** técnico). Técnico opera a produção; **não** vê clientes/financeiro
|
||||
(menu reduzido + endpoints retornam vazio/403). Gestão é exclusiva do dono.
|
||||
- Validado em DEV: dados PJ+CNPJ; técnico convidado acessa a bancada (vê clínica de origem), não vê
|
||||
clientes (0) e leva 403 ao tentar gerenciar equipe.
|
||||
|
||||
### ✅ FASE 5 — Indicadores & Score (IMPLEMENTADA 24/jun)
|
||||
- **Indicadores derivados** (`GET /protese/lab/indicadores`, só dono) calculados sobre a captura das
|
||||
fases 2-4: entregues, tempo médio (solicitado→entregue), atraso %, retrabalho % (garantia),
|
||||
ocorrências resolvidas %, componentes ausentes. Tela `lab-indicadores`.
|
||||
- **Nota ScoreOdonto** (0-10): `10 − (atraso%×3 + retrabalho%×4 + ocorrência%×3)`, só com **mínimo de
|
||||
3 entregas** (senão "score em formação"). Só ocorrências **resolvidas** entram (contraditório respeitado).
|
||||
- **Ranking no marketplace**: `getProteseLaboratorios` passa a devolver a `nota` por protético e ordena
|
||||
internos→maior nota→nome; a clínica vê `★ nota` no dropdown de Nova OS e do Lançar GTO.
|
||||
- Validado em DEV: 4 entregas (1 atrasada, 1 garantia) → atraso 25% / retrab 25% / **nota 8.3**; a
|
||||
clínica vê a nota no dropdown; "score em formação" abaixo do mínimo.
|
||||
|
||||
> Acoplamentos respeitados: PF/PJ veio com a equipe (Fase 4); Score só após a captura (Fases 2-4).
|
||||
|
||||
### Pós-roadmap (entregue depois das Fases 1-5)
|
||||
- **✅ Modo 1** (protético sem conta via link `/p/TOKEN`) — completo (1a leitura, 1b ação, 1c gestão).
|
||||
Ver `MODO1-LINK-SEGURO-PROTESE.md`.
|
||||
- **✅ Higiene** — menu legado do protético removido (Sidebar/App): o protético opera só no workspace
|
||||
laboratório; fallback mínimo fora dele.
|
||||
- **✅ Score público no marketplace** — `GET /api/profissionais/marketplace` traz a `nota` ScoreOdonto
|
||||
dos protéticos (helper `notasScorePorProtetico`, mesma fórmula da Fase 5), exibida como ★ no card.
|
||||
Ordem mantida por proximidade; a nota é critério de comparação visível.
|
||||
|
||||
### Ainda em aberto (futuro)
|
||||
Marketplace **transacional** (anúncio de serviços + solicitação/contratação/propostas dentro da
|
||||
plataforma); **agenda de produção/coleta**; **multi-unidade** do laboratório; envio automático do
|
||||
link (e-mail/WhatsApp API) e herança de cadastro novo com e-mail diferente.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Qual é a verdade hoje? — Auditoria de infra ScoreOdonto
|
||||
|
||||
> Resposta direta às 6 perguntas essenciais, **verificada em 23/jun/2026**. Somente fatos do estado atual.
|
||||
> **Documento principal: em caso de divergência entre docs, este prevalece.**
|
||||
> Como operar → `DEPLOY-PRODUCAO.md` (runbook). Para onde vamos → `deploy.md` (arquitetura-alvo).
|
||||
|
||||
---
|
||||
|
||||
### 1. Onde está o código?
|
||||
- **Fonte da verdade:** Gitea na **VPS3**, repo `ruicesar/scoreodonto.com`.
|
||||
- **Dev (trabalho):** VPS4 em `/home/deploy/stack/scoreodonto.com`.
|
||||
- **Produção:** VPS1 roda **imagens versionadas do registry** (não código-fonte). No ar: **v1.0.30 · commit `6cd8c1f`**.
|
||||
- **Documentação:** centralizada em `scoreodonto.com/doc/` (pasta única).
|
||||
|
||||
### 2. Onde está o banco?
|
||||
- **Produção:** PostgreSQL no host da **VPS3** (`10.99.0.3:5432`), banco `scoreodonto`, user `scoreodonto_user`.
|
||||
- ✅ **DEV ISOLADO:** a VPS4 usa um container PostGIS próprio **`pgdev`** (banco `scoreodonto_dev`), **NÃO mais o banco de produção**. Dev e prod **não compartilham** mais. (Ver `BANCO-DEV-ESTRATEGIA.md`.)
|
||||
- Cache **DragonflyDB** (`10.99.0.3:6379`, índice `/1`). **NATS** (`:4222`) e **Temporal** (`:7233`).
|
||||
|
||||
### 3. Onde estão os containers?
|
||||
- **Produção (VPS1, docker rootless):** `scoreodontocom-scoreodonto-{frontend,backend}-1` rodam a **imagem do registry** (`git.clube67.com/ruicesar/scoreodonto-*:<tag>`). Produção **não builda**. Também na VPS1: `traefik` (borda TLS), rx, newwhats, subdominio, mercado.
|
||||
- **Dev (VPS4, docker rootless):** `nginx-1` (`8020→80`, alvo do Traefik para `dev.`), `frontend-1` (`:3013`), `backend-1` (`:8018`) — **buildados localmente** e apontados ao `pgdev`.
|
||||
- **Roteamento:** Traefik na VPS1 → `scoreodonto.com` (containers locais) e `dev.scoreodonto.com` → `http://10.99.0.4:8020`.
|
||||
|
||||
### 4. Quem faz deploy?
|
||||
- **CI/CD por tag (Gitea Actions).** Push na `main` → o runner `act_runner` "vps4-builder" (VPS4, rootless) builda e **publica no registry** (NÃO deploya). `git tag vX.Y.Z && git push` → CI + job `deploy` promove para a VPS1 (`deploy-prod.sh`).
|
||||
- **Produção consome imagem** (`pull` + `up -d --no-build`).
|
||||
- ✅ **Build Once → Promote (Passo 5, validado em produção):** push builda 1× e publica por **commit** (`:<sha>`); a **tag re-tagueia** esse artefato como `:vX.Y.Z` (sem rebuild — digest idêntico) e deploya. Provado em v1.0.16 (`:v1.0.16` == `:471c2c2`, mesmo digest).
|
||||
|
||||
### 5. Como versiona?
|
||||
- A versão exibida vem do **backend em runtime**: `GET /api/version` (de `process.env`), mostrada em **Configurações → "Sobre o sistema"** e nas telas públicas (o frontend consome via `useAppVersion()`, não embute mais a versão). No ar: **v1.0.30 · `6cd8c1f` · PROD**.
|
||||
- A versão **nasce da tag git** e é **injetada no deploy em runtime** (`APP_VERSION`=tag, `APP_ENV=PROD`); `commit`/`build` vêm carimbados na imagem.
|
||||
- ✅ **`constants.ts`/`update-version.js` foram removidos** (Passo 5) — não há mais versão manual.
|
||||
|
||||
### 6. Como volta uma versão (rollback)?
|
||||
- **Por imagem, sem rebuild:** na VPS1, `./deploy-prod.sh <ref-anterior>` → `pull` + `up -d --no-build`.
|
||||
- ⚠️ A referência confiável é o **commit** (`:<sha>`): tags `:vX.Y.Z` podem ter sido **republicadas** com código diferente (colisão de tags pré-Passo 5 — confirmado: `:v1.0.14` do registry ≠ o que rodava). Migrações são **aditivas** (voltar o código é seguro). Dump em `/home/deploy/backups/` só em caso extremo.
|
||||
|
||||
---
|
||||
|
||||
## Veredito de uma linha
|
||||
> O código vive no Gitea (VPS3); a **produção (VPS1) roda imagens versionadas do registry** (hoje **v1.0.30**), sem buildar; o **DEV está isolado no `pgdev`** (não toca o banco de produção). **CI/CD por tag ativo** no modelo **Build Once → Promote → Deploy** (push builda por commit; tag re-tagueia sem rebuild e deploya — validado em v1.0.16), com **versão visível em runtime** (`/api/version` → Configurações) nascendo da tag git.
|
||||
+15
-4
@@ -1,14 +1,25 @@
|
||||
# Override de PRODUÇÃO (VPS1): roda IMAGENS VERSIONADAS do registry em vez de buildar.
|
||||
# A produção não builda — só consome a imagem pronta publicada pela CI (Etapa 5).
|
||||
# A produção não builda — só consome a imagem pronta publicada pela CI.
|
||||
#
|
||||
# Uso (na VPS1), via deploy-prod.sh <tag>, que faz:
|
||||
# SCOREODONTO_TAG=v1.0.13 docker compose -f docker-compose.yml -f docker-compose.prod.yml pull ...
|
||||
# Uso (na VPS1), via deploy-prod.sh <tag>, que exporta SCOREODONTO_TAG e faz:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.prod.yml pull ...
|
||||
# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-build ...
|
||||
#
|
||||
# O --no-build garante que o build: do compose base seja ignorado (usa a imagem puxada).
|
||||
#
|
||||
# VERSÃO EM RUNTIME (Passo 3): a versão semântica EXIBIDA (/api/version → footer/Configurações)
|
||||
# vem da TAG promovida, injetada via `environment` abaixo — NÃO do build. `commit` e `build`
|
||||
# continuam vindo do ENV carimbado na imagem (imutáveis, propriedade do artefato). Assim a
|
||||
# MESMA imagem pode ser promovida sob qualquer tag sem rebuild — pré-requisito do
|
||||
# "build once, promote the same artifact". O frontend não precisa de versão aqui: é estático
|
||||
# e lê tudo de /api/version (backend) em runtime.
|
||||
services:
|
||||
scoreodonto-backend:
|
||||
image: git.clube67.com/ruicesar/scoreodonto-backend:${SCOREODONTO_TAG:-latest}
|
||||
# Mescla com o environment do compose base (por chave). Sobrescreve o APP_VERSION/APP_ENV
|
||||
# que vieram embutidos na imagem pelo build, fixando a versão = tag e o ambiente = PROD.
|
||||
environment:
|
||||
- APP_VERSION=${SCOREODONTO_TAG:-dev}
|
||||
- APP_ENV=PROD
|
||||
|
||||
scoreodonto-frontend:
|
||||
image: git.clube67.com/ruicesar/scoreodonto-frontend:${SCOREODONTO_TAG:-latest}
|
||||
|
||||
+5
-2
@@ -37,8 +37,11 @@ services:
|
||||
- BAILEYS_ENGINE=infinite
|
||||
- PORT=8018
|
||||
- NODE_ENV=production
|
||||
- DATABASE_URL=postgresql://scoreodonto_user:clube67_scoreodonto_pass_9903@10.99.0.3:5432/scoreodonto?schema=public
|
||||
- DRAGONFLY_URL=redis://:clube67_dragonfly_pass_9903@10.99.0.3:6379/1
|
||||
# Senha do banco vem do ambiente (.env raiz em DEV; provisionado fora do git em PROD).
|
||||
# `:?` falha o `up` se a senha não estiver definida — evita subir com senha vazia/errada.
|
||||
# Host/usuário/banco mantêm default (não são segredo); só a senha é obrigatória via env.
|
||||
- DATABASE_URL=postgresql://${SCOREO_DB_USER:-scoreodonto_user}:${SCOREO_DB_PASS:?defina SCOREO_DB_PASS no .env}@${SCOREO_DB_HOST:-10.99.0.3}:5432/${SCOREO_DB_NAME:-scoreodonto}?schema=public
|
||||
# DRAGONFLY_URL (com senha) vem do env_file backend/.env — não fica em texto no compose.
|
||||
- NATS_URL=nats://10.99.0.3:4222
|
||||
- TEMPORAL_ADDRESS=10.99.0.3:7233
|
||||
- BAILEYS_SESSIONS_PATH=/app/sessions
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
# AUDITORIA ESTRATÉGICA DE MODELAGEM — ScoreOdonto
|
||||
|
||||
> **Análise crítica da modelagem e da visão de negócio.** Não altera código, banco, documentação ou migrations.
|
||||
> Baseada exclusivamente em evidências reais: `backend/server.js`, schema real (tabelas), fluxos existentes e
|
||||
> `MAPA-FUNCIONAL-COMPLETO.md`. Estado verificado: produção `v1.0.18` · DEV `dev`. Data: 19/jun/2026.
|
||||
|
||||
---
|
||||
|
||||
## Evidências-âncora (o que o código realmente diz)
|
||||
|
||||
| Camada | Evidência no código | Tabela/linha |
|
||||
|---|---|---|
|
||||
| **Identidade (Pessoa)** | `usuarios` é o ponto único de login; perfil multi-área (`usuario_areas`), capacidades por pessoa (`usuario_plugins`), `is_tutor` | `usuarios`, `me/areas`, `me/plugins` |
|
||||
| **Vínculo (join)** | `vinculos(usuario_id, clinica_id, role, setor_id, funcao_id)` + `CHECK role IN (paciente, dentista, biomedico, protetico, funcionario, donoclinica, donoconsultorio, donosala, admin)` | `server.js:4851` |
|
||||
| **Workspace** | `clinicas` ganhou `tipo ∈ {pessoal, consultorio, clinica}` + `owner_id`; comentário literal **"Clinicas → workspaces (Fase 1)"** | `server.js:4822-4824` |
|
||||
| **Workspace pessoal automático** | No registro, cria `clinicas(tipo='pessoal', owner_id=usuario)` — **toda pessoa nasce com um workspace** | `server.js:537,550` |
|
||||
| **Agregação** | `me/workspaces` → `listarWorkspaces(userId)` reúne clínicas (via vínculo) + salas | `server.js:5680` |
|
||||
| **Marketplace = pessoa** | `propostas_profissional.profissional_id` → `JOIN usuarios u` | `server.js:6307` |
|
||||
| **Legado clínica-cêntrico** | `pacientes` tem `clinica_id` e **não tem `usuario_id`**; `dentistas` é tabela à parte com `usuario_id` opcional; tudo passa por `tenantGuard`+`clinica_id` | `pacientes`, `dentistas:1542` |
|
||||
|
||||
**Leitura imediata:** a *espinha de identidade e acesso* **já é** Pessoa → Vínculo → Workspace (e isso está em migração ativa, declarada como "Fase 1"). Os *dados clínicos/operacionais* **ainda são** Clínica → Pessoa. O sistema é **híbrido, com a direção já apontada pelo próprio código**.
|
||||
|
||||
---
|
||||
|
||||
## ANÁLISE 1 — Pessoa × Workspace
|
||||
|
||||
1. **Centrado em clínica ou pessoa?** Híbrido. **Autenticação/autorização/workspaces:** centrado em **pessoa**. **Operação clínica e financeira:** centrado em **clínica** (`tenantGuard` + `clinica_id`).
|
||||
2. **A pessoa é entidade principal?** **Sim, há evidência forte:** `usuarios` é a âncora; áreas, plugins e tutor são da pessoa; o profissional do marketplace é a pessoa; e cada pessoa recebe um **workspace pessoal** ao se cadastrar.
|
||||
3. **A clínica é apenas um workspace?** **Sim, há evidência explícita:** `clinicas.tipo` pode ser `pessoal | consultorio | clinica`, com `owner_id`. A clínica é hoje **um tipo de workspace** dentro da própria tabela `clinicas` (que virou, de fato, a tabela de workspaces).
|
||||
4. **Módulos que favorecem Pessoa→Vínculo→Workspace:** login/identidade, `vinculos`+RBAC (`funcoes`/`setores`), `me/workspaces`, `usuario_plugins`, `usuario_areas`, Salas (workspace `tipo='sala'`), Marketplace de Profissionais, Tutoria.
|
||||
5. **Módulos que favorecem Clínica→Pessoa:** Pacientes, Dentistas (tabela-recurso), Agenda, Financeiro/Comissão/Repasse, Orçamentos/Contratos, Glosas — **toda a operação** ainda é escopada por `clinica_id`.
|
||||
6. **Mais sustentável em 5 anos:** **Pessoa → Vínculo → Workspace.** Justificativa técnica: a tabela `vinculos` é N:N (uma pessoa, vários workspaces, papéis distintos por workspace) — é o único modelo que comporta dentista em várias clínicas, locação de salas, tutoria e marketplace **sem duplicar a pessoa**. O modelo clínica-cêntrico exige duplicar o humano por clínica (é o que `dentistas` já faz, e é exatamente o gargalo).
|
||||
|
||||
---
|
||||
|
||||
## ANÁLISE 2 — Consultório
|
||||
|
||||
**Hoje é simultaneamente (A) Papel e (C) Workspace:** existe a *role* `donoconsultorio` em `vinculos` **e** o `tipo='consultorio'` em `clinicas` (`server.js:590` decide o `tipoWs` a partir da role do dono).
|
||||
|
||||
**Classificação ideal:** **(C) Workspace** — um `tipo` de workspace, sendo `donoconsultorio` apenas o *papel do dono* dentro dele. O sistema já está ~80% nesse modelo.
|
||||
|
||||
**Impactos:** baixo do ponto de vista de dados (já existe `tipo='consultorio'`). O resíduo é **semântico**: a role codifica o tipo do workspace (acoplamento role↔tipo). Enquanto persistir, decisões de UI/permissão ramificam em dois lugares ("é consultório?" pode ser perguntado pela role *ou* pelo tipo), gerando divergência.
|
||||
|
||||
---
|
||||
|
||||
## ANÁLISE 3 — Protéticos
|
||||
|
||||
**Hoje o protético é:** **Usuário/Profissional.** É `usuarios` com role `protetico` em `vinculos`, recebe workspace pessoal, e produz trabalho via `protese_os`/`protese_produtos`.
|
||||
|
||||
**Existe conflito conceitual? Sim.** O protético-pessoa está modelado como **profissional do ecossistema**, mas o **laboratório** (onde o trabalho/custo realmente reside, `protese_laboratorios` + custo lab que abate comissão) é um **cadastro interno separado**. Ou seja, a faceta "fornecedor" está **descolada** da pessoa: a pessoa é profissional, o fornecedor é um cadastro à parte.
|
||||
|
||||
**Modelo que faz mais sentido: (B) Protético = profissional do ecossistema** — e o **laboratório** é que ocupa o papel de *fornecedor*. Justificativa: o protético, como humano, se encaixa perfeitamente em Pessoa→Vínculo→Workspace (igual a dentista/biomédico); empurrá-lo para "fornecedor" o trataria como entidade não-humana e quebraria o login/marketplace/áreas que já funcionam para ele. A natureza "fornecimento" pertence ao **laboratório**, não ao indivíduo (ver Análise 4). (C) só seria verdade se um protético autônomo *fosse* o próprio laboratório — caso que o modelo atual não distingue.
|
||||
|
||||
---
|
||||
|
||||
## ANÁLISE 4 — Laboratórios
|
||||
|
||||
**Hoje aparecem como:** **(A) Cadastro interno** — `protese_laboratorios` escopado por clínica, alimentando OS e custo lab. Não há identidade própria nem login; não cruzam clínicas.
|
||||
|
||||
| Classificação | Vantagens | Desvantagens |
|
||||
|---|---|---|
|
||||
| **A) Cadastro auxiliar** (estado atual) | Simples; isolado por clínica; zero atrito | Cada clínica recadastra o mesmo laboratório; sem visão consolidada; sem relação direta com a pessoa protético |
|
||||
| **B) Participante do ecossistema** (entidade própria) | Laboratório único reaproveitável; liga custo lab à entidade real; base para reputação/histórico | Exige resolver identidade/duplicidade e governança de dados entre clínicas |
|
||||
| **C) Workspace independente** | Coerente com a linha Pessoa/Workspace; laboratório operaria como ator (recebe OS de várias clínicas) | Maior complexidade de tenancy; só se justifica se o laboratório for *operar dentro* da plataforma |
|
||||
|
||||
**Análise (sem assumir locação):** hoje é **A**, e isso é consistente com o legado clínica-cêntrico. A tensão é que o laboratório é a **única entidade "fornecedor" real** do sistema e está modelada como simples lookup — o que limita qualquer evolução para histórico/qualidade/integração entre clínicas. **B** é o ponto de equilíbrio entre o estado atual e a direção arquitetural; **C** só se sustenta se o laboratório virar *operador* na plataforma.
|
||||
|
||||
---
|
||||
|
||||
## ANÁLISE 5 — Pacientes
|
||||
|
||||
**Hoje o paciente pertence à CLÍNICA.** Evidências: `pacientes.clinica_id`, **ausência de `usuario_id`**, e até **dois variantes de INSERT** (um com `clinica_id`, outro sem — resíduo legado). Não há identidade de paciente no ecossistema; o mesmo CPF em duas clínicas vira **dois registros**.
|
||||
|
||||
- **Compartilhamento entre clínicas:** inexistente (duplicação por clínica).
|
||||
- **Histórico:** preso à clínica; não há prontuário único da pessoa.
|
||||
- **Multiempresa:** o isolamento é forte (bom para tenancy), mas impede portabilidade.
|
||||
- **Privacidade/LGPD:** o modelo atual é, na verdade, **mais seguro por isolamento** (cada clínica é controladora dos seus dados). Um paciente "do ecossistema" exigiria base legal/consentimento para compartilhar histórico entre controladores distintos — não trivial.
|
||||
|
||||
**Abordagem mais coerente com a arquitetura ATUAL:** **o paciente pertence à clínica.** É o que o código faz e o que o LGPD-por-isolamento favorece. **Porém** isso é a **maior contradição** com a direção Pessoa→Workspace: o paciente é a única "pessoa" do sistema que **não** é tratada como pessoa. É coerente com o presente e **incoerente com o futuro declarado**.
|
||||
|
||||
---
|
||||
|
||||
## ANÁLISE 6 — Marketplace
|
||||
|
||||
| Módulo | Encaixa em | Evidência |
|
||||
|---|---|---|
|
||||
| Salas | **Modelo B** | workspace `tipo='sala'`, reservas cross-clínica |
|
||||
| Profissionais | **Modelo B** | `profissional_id` → `usuarios`; cruza clínicas |
|
||||
| Tutoria | **Modelo B** | candidatura/pagamento ligados à pessoa (`is_tutor`, MercadoPago) |
|
||||
| Próteses | **Modelo A** (hoje) | `protese_os` por `clinica_id` |
|
||||
| Laboratórios | **Modelo A** (hoje) | cadastro interno por clínica |
|
||||
|
||||
**Conclusão:** o marketplace **só faz sentido no Modelo B (Pessoa→Vínculo→Workspace→Marketplace)**. Salas, Profissionais e Tutoria já estão lá porque dependem da **pessoa cruzando fronteiras de clínica** — algo que o Modelo A (Clínica→Tudo) torna impossível sem duplicação. Próteses/Laboratórios são os **retardatários** ainda presos ao Modelo A; são a fronteira onde os dois modelos colidem.
|
||||
|
||||
---
|
||||
|
||||
## ANÁLISE 7 — Escalabilidade (horizonte 5 anos)
|
||||
|
||||
| Cenário | Suporta hoje? | Gargalo |
|
||||
|---|---|---|
|
||||
| Dentista em múltiplas clínicas | **Parcial** | `vinculos` suporta a identidade, mas `dentistas` **duplica a pessoa por clínica** (agenda/horários presos a `dentistas.id`); não há visão unificada do profissional |
|
||||
| Biomédico em vários locais | Parcial | mesma duplicação via tabela-recurso |
|
||||
| Profissionais temporários (Delivery) | **Não** | marketplace só tem proposta-mensagem; sem contrato/pagamento/vínculo temporário |
|
||||
| Locação de salas | **Sim (estrutura)** | falta captura de pagamento (fluxo financeiro aberto) |
|
||||
| Tutoria | **Sim** | escala de oferta de tutores |
|
||||
| Marketplace | Parcial | só Tutoria fatura |
|
||||
| Protéticos independentes | Parcial | pessoa ok, mas laboratório-fornecedor é cadastro preso à clínica |
|
||||
| Redes de clínicas | **Não** | **não existe entidade "rede/grupo" acima da clínica**; workspaces são planos, sem hierarquia |
|
||||
|
||||
**O modelo escala?** A **camada de identidade sim**; a **camada operacional não**. Gargalos críticos: (1) **duplicação da pessoa** em `dentistas`/recursos; (2) **paciente não-portável**; (3) **ausência de hierarquia** (rede→clínica); (4) **tenancy de `clinica_id` único** impede consolidação cross-clínica (uma pessoa ver tudo o que é seu em todas as clínicas).
|
||||
|
||||
---
|
||||
|
||||
## ANÁLISE 8 — Contradições (por gravidade)
|
||||
|
||||
| Gravidade | Contradição | Evidência |
|
||||
|---|---|---|
|
||||
| **ALTA** | `dentistas` é entidade paralela a `usuarios` (mesmo humano duplicado por clínica, com `usuario_id` *opcional*) → identidade fragmentada | `dentistas:1542`, `:328` |
|
||||
| **ALTA** | `pacientes` é entidade clínica-presa sem identidade de pessoa, com **dois INSERTs** (com/sem `clinica_id`) → ambiguidade legada + bloqueio de portabilidade | INSERTs de `pacientes` |
|
||||
| **MÉDIA** | O "workspace" está **dividido em duas tabelas**: `clinicas` (pessoal/consultorio/clinica) **e** `salas` — conceito único, dois lugares | `clinicas:4823`, `salas` |
|
||||
| **MÉDIA** | Acoplamento **role ↔ tipo de workspace** (`donoconsultorio`⇄`tipo='consultorio'`) | `server.js:590` |
|
||||
| **MÉDIA** | Estado de capacidades em **dois lugares**: `usuario_plugins` (servidor) vs `localStorage` (cliente) → divergência entre dispositivos | `pluginRegistry.ts`, `me/plugins` |
|
||||
| **BAIXA** | Protético-pessoa (profissional) × laboratório (fornecedor) descolados | `protese_laboratorios` |
|
||||
| **BAIXA** | `superadmin` fora de `vinculos` (é `usuarios.role`) → plano de auth separado (aceitável, mas assimétrico) | `me/workspaces:5682` |
|
||||
|
||||
---
|
||||
|
||||
## RESULTADO FINAL
|
||||
|
||||
**1. Resumo executivo.** O ScoreOdonto é um ERP odontológico multiempresa em **transição arquitetural declarada no próprio código** ("Fase 1: Clinicas → workspaces"). A **identidade, o acesso e os workspaces já operam no modelo Pessoa→Vínculo→Workspace**; a **operação clínica/financeira permanece clínica-cêntrica**. As três frentes de marketplace já vivem no modelo novo; pacientes, dentistas-recurso e laboratórios são o legado que ainda não migrou. As contradições graves são todas de **identidade duplicada** (a mesma pessoa existindo como `usuarios` + `dentistas`, e o paciente sem identidade de pessoa).
|
||||
|
||||
**2. Modelo que melhor representa o sistema HOJE:** **híbrido** — `Pessoa → Vínculo → Workspace` na espinha; `Clínica → Pacientes/Dentistas/Financeiro` na operação.
|
||||
|
||||
**3. Modelo que melhor representa o sistema NO FUTURO:** `Pessoa → Vínculo → Workspace → Serviços → Marketplace → Financeiro` — e o código já está pavimentado para ele.
|
||||
|
||||
**4. Entidades CORRETAMENTE classificadas:** `usuarios` (Pessoa), `vinculos` (Vínculo+RBAC), Clínica/Consultório/Sala como **workspaces**, Dentista/Biomédico/Protético/Tutor como **papéis da pessoa**, Profissional do marketplace e capacidades (`areas`/`plugins`) como atributos da pessoa.
|
||||
|
||||
**5. Entidades MAL classificadas:**
|
||||
- **`dentistas`** — modelada como entidade própria; é, na verdade, uma **projeção da pessoa por workspace** (duplica o humano).
|
||||
- **`pacientes`** — entidade presa à clínica; é a única "pessoa" do sistema **não tratada como pessoa**.
|
||||
- **Laboratórios** — cadastro auxiliar; é o **único fornecedor real** e está subdimensionado.
|
||||
- **Consultório** — vive como role **e** tipo (redundância).
|
||||
|
||||
**6. Riscos arquiteturais:** identidade fragmentada (dentistas); paciente não-portável e com resíduo de schema; conceito de workspace partido em duas tabelas; ausência de hierarquia "rede→clínica"; tenancy de `clinica_id` único barrando visão consolidada da pessoa; estado de plugins divergente cliente/servidor.
|
||||
|
||||
**7. Oportunidades arquiteturais:** a "Fase 1" **já entregou o mais difícil** (identidade + vínculo + workspace + workspace pessoal automático). O ecossistema (marketplace, salas, tutoria) **só existe** porque essa base existe. Unificar as entidades-recurso (dentista) e a entidade-paciente sob a pessoa **destravaria de uma vez** multi-clínica, portabilidade de histórico, redes e o marketplace — sem reescrever o que já está certo.
|
||||
|
||||
**8. Recomendação estratégica única:** **assumir e consolidar a PESSOA como âncora única do sistema, reconhecendo que a clínica já é — no código — apenas um tipo de workspace.** A decisão estratégica de fundo não é "se" migrar para Pessoa→Vínculo→Workspace (o sistema já escolheu isso na Fase 1), e sim **parar de manter o legado clínica-cêntrico em paralelo** nas duas entidades que ainda o sustentam (paciente e dentista-recurso) — pois é a coexistência dos dois modelos, não a escolha de um, que gera todas as contradições de gravidade ALTA.
|
||||
|
||||
---
|
||||
|
||||
> *Nota de método: tudo acima decorre de leitura de `backend/server.js`, do schema real (tabelas `usuarios`,
|
||||
> `vinculos`, `clinicas`, `pacientes`, `dentistas`, `salas`, `protese_*`, `propostas_profissional`) e do
|
||||
> `MAPA-FUNCIONAL-COMPLETO.md`. Onde o código tinha ambiguidade (ex.: dois INSERTs de `pacientes`, role↔tipo),
|
||||
> descreveu-se o fato observado, sem inferir intenção. Conforme solicitado, não há propostas de implementação,
|
||||
> migrations, código ou tarefas.*
|
||||
</content>
|
||||
@@ -1,60 +0,0 @@
|
||||
# Checklist de Deploy para Produção — Pacote Financeiro + Salas (jun/2026)
|
||||
|
||||
> Consolida a rodada: **Financeiro V1 (Épicos 1–4)**, **V2 Comissão/Repasse**, **Glosas**, **GTO→Financeiro**, **modelo de contrato p/ convênio** e o **redesenho de Salas** (perfil Dono de Sala, sala como workspace, financeiro de locação, painel/relatório/agenda).
|
||||
> O **procedimento canônico de deploy** é o `DEPLOY-PRODUCAO.md` — este arquivo é só o checklist desta entrega.
|
||||
|
||||
## 0. Arquitetura (sem ambiente isolado)
|
||||
- **VPS4** = **desenvolvimento + builder**. O ambiente de teste é **`dev.scoreodonto.com`** (servido pela VPS4 via Traefik da VPS1) e é onde se **buildam** as imagens que vão pra produção.
|
||||
- **VPS1** = **produção** (`scoreodonto.com`) — só recebe a imagem pronta e sobe.
|
||||
- **VPS3** = **dados** (Postgres `scoreodonto`, Dragonfly, Gitea). **Dev e prod apontam para o MESMO banco `scoreodonto`.**
|
||||
- ⚠️ Consequência (confirmada): **dev e prod compartilham o banco** → as migrações desta entrega **já foram aplicadas** ao banco real durante o desenvolvimento (rodam no boot do backend de dev). Promover é, na prática, **levar o CÓDIGO** para a VPS1 — o schema já está migrado. **Não há ambiente nem banco isolado** entre dev e produção.
|
||||
|
||||
## 1. Fluxo de promoção (resumo do DEPLOY-PRODUCAO.md §2)
|
||||
1. `git push origin main` (na VPS4) — já feito (commit `3e64514`).
|
||||
2. Bump de versão (`update-version.js` → `frontend/constants.ts`).
|
||||
3. **Build da imagem Docker na VPS4** (`npm install` + `vite build` dentro do Dockerfile) — peso fora da produção.
|
||||
4. **Envia a imagem pronta para a VPS1** (registry privado pela WireGuard — recomendado — ou `docker save | ssh | docker load`).
|
||||
5. **VPS1:** `docker compose up -d` (troca o container pela imagem nova). Backend leva ~10–30s no boot (reconecta Postgres/Dragonfly + roda migrações).
|
||||
- *(Os scripts `deploy-prod-builder.sh` e `deploy-to-prod.sh` estão **DEPRECADOS** — ver seção "Fluxo ANTIGO" do `DEPLOY-PRODUCAO.md`. Não usar.)*
|
||||
|
||||
## 2. Pré-requisitos
|
||||
- [ ] **Backup do banco** VPS3 (`pg_dump scoreodonto` — custom format `-Fc`). Como dev/prod compartilham o banco, esse backup é a **única rede de segurança**.
|
||||
- [ ] Código em `origin/main` (Gitea) — ✅ `3e64514`.
|
||||
- [ ] `node --check backend/server.js` OK; build do frontend OK.
|
||||
- [ ] Validado em `dev.scoreodonto.com`.
|
||||
|
||||
## 3. Mudanças de banco (migrações idempotentes no boot — todas ADITIVAS)
|
||||
> Já aplicadas no banco (dev=prod). Listadas para auditoria/rollback. Nenhuma destrutiva.
|
||||
- **Novas tabelas:** `procedimentos_realizados`, `procedimento_realizado_foto`, `comissao_regras`, `repasses`, `glosas`.
|
||||
- **ALTER `financeiro`:** `paciente_id, profissional_id, procedimento_id, agendamento_id, contrato_id, realizado_id, origem, sem_comissao, pago_em, deleted_at, fornecedor, centro_custo, recorrente, convenio_id, valor_glosa, data_competencia, competencia`.
|
||||
- **ALTER `orcamentos`:** `financeiro_gerado`. **ALTER `contratos`:** `cobranca_automatica, valor_recorrente, periodicidade, dia_cobranca`. **ALTER `gto_builders`:** `financeiro_id`. **ALTER `sala_reservas`:** `financeiro_id`.
|
||||
- **CHECK `usuarios_role_check` / `vinculos_role_check`** ganham `donosala` (DROP+ADD no boot — superset, seguro).
|
||||
- **Índices:** troca `uq_comissao_regra` → `uq_comissao_regra2`; novos `idx_financeiro_contrato_comp`, `idx_financeiro_convenio`, `idx_glosas_*`, `idx_gto_builders_financeiro`.
|
||||
- **Seed (boot):** `cmod_odo_14` "Atendimento a Convênios / Repasse" (ON CONFLICT DO NOTHING).
|
||||
|
||||
## 4. Backend — env / infra (VPS1)
|
||||
- [ ] `DATABASE_URL` → `…@10.99.0.3/scoreodonto`. `JWT_SECRET` estável (login + URLs assinadas de fotos/comprovantes).
|
||||
- [ ] **Volume de mídia** persistente (`MEDIA_PATH`): grava em `…/procedimentos/<id>/` e `…/repasses/<id>/`.
|
||||
- [ ] **Cron** `processarVigenciasContratos` (boot + 6h): Atrasado, vigência, **renovação automática**, **cobrança recorrente** — conferir nos logs.
|
||||
|
||||
## 5. Frontend
|
||||
- [ ] Imagem rebuildada com as views novas (`ComissoesView`, `GlosasView`, `ProcedimentosProntuario`, `SalaHomeView`) e alterações (`FinanceiroView`, `ReportsView`, `OrcamentoModal`, `ContratosView`, `LancarGTO`, `SalasPlugin`, `OnboardingChat`, `Sidebar`, `App.tsx`, `PluginsView`, `pluginRegistry`, `services/backend.ts`).
|
||||
- [ ] Conferir: locação **sempre ativa** (MINHAS/ALUGAR SALAS aparecem) e **seletor de workspace** lista Clínicas × Salas.
|
||||
|
||||
## 6. Verificação pós-deploy (VPS1)
|
||||
```bash
|
||||
# versão no ar
|
||||
curl -s https://scoreodonto.com | grep -o 'V[0-9]*\.[0-9]*\.[0-9]*' | head -1
|
||||
# logs do backend (Docker ROOTLESS na VPS1) — espere "[MIGRATION] all migrations OK"
|
||||
ssh deploy@10.99.0.1 "DOCKER_HOST=unix:///run/user/1000/docker.sock docker logs scoreodontocom-scoreodonto-backend-1 --tail 40"
|
||||
```
|
||||
Smoke tests: criar **lançamento financeiro** (valida `normalizeFinanceiro` — antes nenhum salvava pela UI); **GTO → recebível**; **comissão/repasse/comprovante**; **glosa**; **contrato de convênio**; **orçamento→parcelas**; **fluxo de salas** (Dono de Sala, reserva→recebível, painel); **isolamento cross-tenant 403**.
|
||||
|
||||
## 7. Rollback
|
||||
- Migrações **só adicionam** → reverter o **código** (imagem anterior na VPS1) é seguro; colunas/tabelas extras ficam inertes. Restaurar o dump da §2 só se necessário (`pg_restore -d "<url>" --clean <arquivo>`).
|
||||
|
||||
## 8. Riscos específicos desta entrega
|
||||
- **`normalizeFinanceiro` (crítico):** corrige o bug em que `enforceUppercase` mandava `PAGO`/`PIX` violando o CHECK capitalizado — antes a tabela `financeiro` ficava vazia. Garantir que entrou na imagem.
|
||||
- **Banco único dev/prod:** dado de teste de desenvolvimento pode aparecer; os E2E desta rodada sempre limparam, mas conferir contagens de `financeiro`, `salas`, `repasses`, `glosas`.
|
||||
- **Contratos de convênio:** textos seguem padrão de mercado — recomendar revisão por advogado.
|
||||
- **Locação sempre ativa:** todos os papéis (menos superadmin) passam a ver MINHAS/ALUGAR SALAS.
|
||||
@@ -1,158 +0,0 @@
|
||||
# DEPLOY-PRODUCAO — Runbook operacional (estado de HOJE)
|
||||
|
||||
> 🛑 **LEIA PRIMEIRO.** Procedimento **real e executável hoje**, passo a passo, sem adivinhação.
|
||||
> Reflete o mecanismo atual: **a produção (VPS1) builda a si mesma** via `git pull` + `docker compose build` + `up -d`.
|
||||
> O fluxo-alvo (registry + deploy por tag, prod sem build) está em `deploy.md` e ainda não existe. A realidade da infra está em `arquitetura.md`.
|
||||
>
|
||||
> ⚠️ **Regra de ouro:** antes de qualquer build/deploy/push, confirme com o operador **DEV ou PRODUÇÃO**.
|
||||
> `dev.scoreodonto.com` = VPS4 (descartável, mas **mesmo banco** da prod). `scoreodonto.com` = VPS1 (real).
|
||||
> **Nunca** `git push --force` na `main`. **Dev e prod compartilham o banco** (VPS3) — o backup é a única rede de segurança.
|
||||
|
||||
---
|
||||
|
||||
## Mapa rápido (quem é quem)
|
||||
|
||||
| Onde | Host | Papel | Socket Docker |
|
||||
|------|------|-------|----------------|
|
||||
| VPS4 | `10.99.0.4` | dev + builder (código aqui) | `/var/run/docker.sock` (root) |
|
||||
| VPS1 | `10.99.0.1` | produção | `unix:///run/user/1000/docker.sock` (rootless) |
|
||||
| VPS3 | `10.99.0.3` | Postgres + Gitea | — |
|
||||
|
||||
- Repo: `ssh://git@10.99.0.3/ruicesar/scoreodonto.com.git` (Gitea).
|
||||
- Diretório do repo nas duas máquinas: `/home/deploy/stack/scoreodonto.com`.
|
||||
- Project Compose: `scoreodontocom` (containers `scoreodontocom-*`).
|
||||
|
||||
---
|
||||
|
||||
## FASE 0 — Pré-voo (na VPS4)
|
||||
|
||||
```bash
|
||||
cd /home/deploy/stack/scoreodonto.com
|
||||
|
||||
# 1. Sanidade do backend
|
||||
node --check backend/server.js
|
||||
|
||||
# 2. Validar a mudança em DEV (já no ar em https://dev.scoreodonto.com)
|
||||
# Conferir o bundle servido (deve mudar a cada rebuild):
|
||||
curl -s https://dev.scoreodonto.com/ | grep -oE 'index-[A-Za-z0-9_-]+\.js' | head -1
|
||||
```
|
||||
|
||||
```bash
|
||||
# 3. BACKUP DO BANCO (rede de segurança única — dev/prod compartilham o banco)
|
||||
# Rodar na VPS3. Usar as credenciais do ambiente/segredo, nunca em texto puro.
|
||||
ssh deploy@10.99.0.3 'pg_dump -Fc scoreodonto -f /home/deploy/backups/scoreodonto_$(date +%Y%m%d_%H%M).dump'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FASE 1 — Versão (na VPS4) ⚠️ passo manual
|
||||
|
||||
O bump **não é automático** (`npm run build` é só `vite build`). É preciso incrementar **à mão**:
|
||||
|
||||
```bash
|
||||
cd /home/deploy/stack/scoreodonto.com
|
||||
node update-version.js # incrementa o patch em frontend/constants.ts (ex.: V1.0.12 → V1.0.13)
|
||||
grep APP_VERSION frontend/constants.ts
|
||||
```
|
||||
|
||||
> Hoje a versão **não fica visível no navegador** (está no bundle JS). Até a Etapa 3 (versionamento visível), anote a versão que está promovendo.
|
||||
|
||||
---
|
||||
|
||||
## FASE 2 — Commit & push para o Gitea (na VPS4)
|
||||
|
||||
> Só execute com autorização explícita do operador (é o repo de produção).
|
||||
|
||||
```bash
|
||||
cd /home/deploy/stack/scoreodonto.com
|
||||
git add -A
|
||||
git commit -m "feat: <descrição> (bump vX.Y.Z)"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
> ❗ Push **não** dispara deploy automático hoje (não há listener/webhook ativo). A promoção é a FASE 3.
|
||||
|
||||
---
|
||||
|
||||
## FASE 3 — Promover para produção (na VPS1)
|
||||
|
||||
```bash
|
||||
ssh deploy@10.99.0.1
|
||||
export DOCKER_HOST=unix:///run/user/1000/docker.sock # docker rootless da VPS1
|
||||
cd /home/deploy/stack/scoreodonto.com
|
||||
|
||||
# 1. Trazer o código aprovado
|
||||
git pull origin main
|
||||
|
||||
# 2. Carimbo de versão visível (lido pelos build args do compose) — em PROD use APP_ENV=PROD
|
||||
export APP_VERSION=$(grep -oE 'V[0-9]+\.[0-9]+\.[0-9]+' frontend/constants.ts | head -1)
|
||||
export GIT_COMMIT=$(git rev-parse --short HEAD)
|
||||
export BUILD_TIME=$(date -u +'%Y-%m-%d %H:%M UTC')
|
||||
export APP_ENV=PROD
|
||||
|
||||
# 3. Buildar as imagens (frontend faz vite build; backend npm install)
|
||||
docker compose build scoreodonto-frontend scoreodonto-backend
|
||||
|
||||
# 4. Subir (troca os containers pelas imagens novas)
|
||||
docker compose up -d scoreodonto-frontend scoreodonto-backend nginx
|
||||
```
|
||||
|
||||
> Os 4 `export` carimbam versão/commit/data/ambiente na imagem. Sem eles, o build ainda funciona mas mostra `dev`/`unknown` no rodapé. Em **DEV** (VPS4) use `APP_ENV=DEV`.
|
||||
|
||||
O backend leva ~10–30s no boot (reconecta Postgres/Dragonfly e roda as migrações idempotentes).
|
||||
|
||||
---
|
||||
|
||||
## FASE 4 — Verificação pós-deploy (na VPS1)
|
||||
|
||||
```bash
|
||||
export DOCKER_HOST=unix:///run/user/1000/docker.sock
|
||||
|
||||
# Containers no ar
|
||||
docker ps --format '{{.Names}} | {{.Status}}' | grep score
|
||||
|
||||
# Backend: esperar a linha de migração + carimbo de versão
|
||||
docker logs scoreodontocom-scoreodonto-backend-1 --tail 40 | grep -iE 'migration|VERSION'
|
||||
# alvo: [MIGRATION] all migrations OK
|
||||
# [VERSION] ScoreOdonto API vX.Y.Z · commit ... · build ... · PROD
|
||||
|
||||
# Confirmar a versão no ar SEM acessar container (a forma definitiva):
|
||||
curl -s https://scoreodonto.com/api/version
|
||||
# {"name":"ScoreOdonto API","version":"vX.Y.Z","commit":"...","build":"...","environment":"PROD"}
|
||||
|
||||
# E visualmente: a versão aparece no rodapé do menu lateral (Sidebar).
|
||||
```
|
||||
|
||||
**Smoke tests mínimos:** abrir `https://scoreodonto.com`, logar, e exercitar o que mudou (ex.: lançamento financeiro, agenda, salas). Conferir ausência de erro 500 no `docker logs`.
|
||||
|
||||
---
|
||||
|
||||
## FASE 5 — Rollback (se algo quebrar)
|
||||
|
||||
As migrações são **aditivas** → reverter o **código** é seguro (colunas/tabelas extras ficam inertes).
|
||||
|
||||
```bash
|
||||
ssh deploy@10.99.0.1
|
||||
export DOCKER_HOST=unix:///run/user/1000/docker.sock
|
||||
cd /home/deploy/stack/scoreodonto.com
|
||||
|
||||
git log --oneline -5 # achar o commit bom anterior
|
||||
git checkout <commit-anterior> # ou: git reset --hard <commit-anterior>
|
||||
docker compose build scoreodonto-frontend scoreodonto-backend
|
||||
docker compose up -d scoreodonto-frontend scoreodonto-backend
|
||||
```
|
||||
|
||||
> Hoje o rollback **rebuilda** a versão anterior (as imagens são `:latest`, não há tag por versão). Quando a **Etapa 6** entregar deploy por tag + registry, o rollback vira só `docker compose pull` da tag anterior — sem rebuild. Restaurar o dump do banco (FASE 0) só em caso extremo:
|
||||
> `pg_restore -d "postgresql://scoreodonto_user:<senha>@10.99.0.3:5432/scoreodonto" --clean <arquivo>.dump`
|
||||
|
||||
---
|
||||
|
||||
## Resumo em uma tela
|
||||
|
||||
```
|
||||
VPS4: validar em dev → node --check → backup do banco (VPS3)
|
||||
→ node update-version.js → commit → git push origin main (com OK do operador)
|
||||
VPS1: git pull → docker compose build → docker compose up -d
|
||||
→ verificar logs ([MIGRATION] all migrations OK) + smoke test
|
||||
Rollback: git checkout <commit-bom> → build → up -d
|
||||
```
|
||||
@@ -0,0 +1,485 @@
|
||||
# MAPA FUNCIONAL COMPLETO — ScoreOdonto
|
||||
|
||||
> **Auditoria funcional baseada SOMENTE em evidências do código.** Nada aqui foi assumido.
|
||||
> Fonte: `frontend/` (React 19 + Vite, roteador próprio em `App.tsx`) e `backend/server.js`
|
||||
> (Express monolítico, ~516 KB, **277 rotas**, ~70 tabelas Postgres acessadas por SQL direto).
|
||||
> Estado verificado: produção `v1.0.18` · DEV `dev` (banco isolado `pgdev`). Data: 19/jun/2026.
|
||||
|
||||
---
|
||||
|
||||
## Sumário executivo (o que o sistema É, na prática)
|
||||
|
||||
O ScoreOdonto é um **ERP odontológico multiempresa** (SaaS) cujo núcleo é o ciclo
|
||||
**Lead → Paciente → Agenda → Procedimento → Financeiro → Comissão → Repasse**, com módulos
|
||||
clínicos (Ortodontia, Próteses/GTO, RX), administrativos (Equipe/RBAC, Planos, Procedimentos)
|
||||
e três frentes de marketplace (**Locação de Salas**, **Diretório/Propostas de Profissionais**,
|
||||
**Tutoria de Ortodontia** — esta última a única com cobrança automatizada via MercadoPago).
|
||||
|
||||
A arquitetura é **multi-tenant real**: quase toda rota passa por `tenantGuard` e filtra por
|
||||
`clinica_id`. O "workspace" ativo (clínica **ou** sala) governa menu, permissões e dados visíveis.
|
||||
|
||||
**Evidências estruturais:**
|
||||
- Frontend sem `react-router`: roteamento próprio (`ROUTE_MAP` / `VIEW_TO_ROUTE` em `App.tsx`).
|
||||
- 44 views (`frontend/views/*.tsx`), 16 componentes, 3 plugins.
|
||||
- Backend num único `server.js` (516 KB) — sem camada de serviços/controllers separada.
|
||||
- Prisma quase não usado (4 modelos legados de GTO); o banco real é SQL puro via `pg`.
|
||||
|
||||
---
|
||||
|
||||
# ETAPA 1 — INVENTÁRIO DE ROTAS
|
||||
|
||||
Roteamento definido em `frontend/App.tsx` (`ROUTE_MAP`, `VIEW_TO_ROUTE`, `renderView`, `isViewAllowed`).
|
||||
**Papéis (roles):** `superadmin`, `admin`, `donoclinica`, `donoconsultorio`, `funcionario`,
|
||||
`dentista`, `biomedico`, `protetico`, `paciente`, `donosala`.
|
||||
**Tipos de workspace:** `clínica` (padrão) e `sala` (isola o contexto). **Plugins:** `rx-scoreodonto`,
|
||||
`locacao-salas` (sempre ON), `profissionais-marketplace`.
|
||||
|
||||
| URL | View | Componente / Arquivo | Acesso (papéis) | Origem | Destino |
|
||||
|---|---|---|---|---|---|
|
||||
| `/` | landing | `LandingPage.tsx` | Público | — | login / área |
|
||||
| `/login` | login | `LoginView.tsx` | Público | landing | dashboard por papel |
|
||||
| `/contato` | public | `PublicContactForm.tsx` | Público | landing | cria interesse/lead |
|
||||
| `/dashboard` | dashboard | `Dashboard.tsx` | admin/donos/func/dentista/bio/prot | login, menu | agenda, pacientes |
|
||||
| `/leads` | leads | `LeadsView.tsx` | admin/donos/funcionário | menu | converter→paciente+agenda |
|
||||
| `/pacientes` | pacientes | `PatientsView.tsx` | admin/donos/funcionário | menu, dashboard | agenda, orçamento, procedimento, ortodontia, RX |
|
||||
| `/agenda` | agenda | `AgendaView.tsx` | quase todos os papéis clínicos | menu, dashboard | paciente, avaliação, procedimento |
|
||||
| `/ortodontia` | ortodontia | `OrthoView.tsx` | dentista (odonto) | menu, paciente | evolução, fotos, tutoria |
|
||||
| `/financeiro` | financeiro | `FinanceiroView.tsx` | admin/donos/funcionário | menu | comissão, glosa |
|
||||
| `/comissoes` | comissoes | `ComissoesView.tsx` | admin/donos | **menu apenas** ⚠️ | repasse |
|
||||
| `/glosas` | glosas | `GlosasView.tsx` | admin/donos/funcionário | **menu apenas** ⚠️ | recebíveis convênio |
|
||||
| `/proteses` | proteses | `ProteseView.tsx` | dentista/protético/func | menu | OS de laboratório, GTO |
|
||||
| `/orcamentos` | orcamentos | `OrcamentosView.tsx` | admin/donos/funcionário | menu, paciente | contrato |
|
||||
| `/contratos` | contratos | `ContratosView.tsx` | admin/donos/funcionário | menu, orçamento | cobrança/financeiro |
|
||||
| `/relatorios` | relatorios | `ReportsView.tsx` | admin/donos/funcionário | menu | — |
|
||||
| `/tratamentos` | tratamentos | `MeusTratamentos.tsx` | paciente + clínicos | menu | — |
|
||||
| `/lancar-gto` | lancar-gto | `LancarGTO.tsx` | biomédico/protético/func | menu | guias odontológicas |
|
||||
| `/notificacoes` | notificacoes | `NotificationsView.tsx` | todos | menu | — |
|
||||
| `/clinicas` | clinicas | `ClinicasView.tsx` | membros | menu, seletor de workspace | criar-clinica |
|
||||
| `/gestao-equipe` | gestao-equipe | `GestaoEquipeView.tsx` | admin/donos | menu | membros, cargos (RBAC) |
|
||||
| `/admin/dentistas` | dentistas | `AdminViews.tsx` | admin/donos | menu | horários |
|
||||
| `/admin/especialidades` | especialidades | `AdminViews.tsx` | admin/donos/biomédico | menu | procedimentos |
|
||||
| `/admin/procedimentos` | procedimentos | `AdminViews.tsx` | admin/donos/biomédico | menu | valores por plano |
|
||||
| `/admin/planos` | planos | `AdminViews.tsx` | admin/donos | menu | — |
|
||||
| `/admin/sync` | sync | `AdminViews.tsx` (`SyncView`) | **NINGUÉM (bloqueado)** ⚠️ | — | — |
|
||||
| `/plugins` | plugins | `PluginsView.tsx` | **superadmin apenas** | menu (superadmin) | ativa plugins |
|
||||
| `/rx-radiografias` | rx-radiografias | `plugins/RXPlugin.tsx` | dentista/donos/func (plugin RX) | menu | dispositivos RX |
|
||||
| `/rx-config` | rx-config | `RxConfigView.tsx` | admin/donos/superadmin | menu | dispositivos RX |
|
||||
| `/salas` `/minhas-salas` `/alugar-salas` | salas | `plugins/SalasPlugin.tsx` | todos (plugin `locacao-salas`, sempre ON) | menu | reservas |
|
||||
| `/sala-home` | sala-home | `SalaHomeView.tsx` | workspace tipo `sala` | seletor de workspace | reservas da sala |
|
||||
| `/marketplace-profissionais` | marketplace-profissionais | `plugins/ProfissionaisPlugin.tsx` | clínicos exceto paciente (plugin) | menu | propostas |
|
||||
| `/diretorio-profissionais` | diretorio-profissionais | `DiretorioProfissionaisView.tsx` (absorvido pelo plugin) | clínicos | menu | — |
|
||||
| `/candidatura-tutor` | candidatura-tutor | `TutorCandidaturaView.tsx` | dentista | menu | pagamento MercadoPago |
|
||||
| `/tutoria-painel` | tutoria-painel | `TutorPainelView.tsx` | **tutores aprovados** (`is_tutor`) | menu | solicitações |
|
||||
| `/admin-gestao-tutores` | admin-gestao-tutores | `AdminGestaoTutoresView.tsx` | **superadmin** | menu | confirma pagamentos/repasses |
|
||||
| `/criar-clinica` | criar-clinica | `CreateClinicView.tsx` | admin/donos sem workspace | pós-cadastro | dashboard |
|
||||
| `/cadastro-dentista` | cadastro-dentista | `DentistRegisterView.tsx` | standalone (magic-link) | **menu apenas** ⚠️ | aguardando-convite |
|
||||
| `/aguardando-convite` | aguardando-convite | `WaitingInviteView.tsx` | dentista/bio/prot sem vínculo | pós-cadastro | — |
|
||||
| `/superadmin/*` | superadmin-* | `SuperAdminView.tsx` (tabs) | **superadmin** | menu | usuários, clínicas, logs, financeiro, notificações |
|
||||
| `/update` | update | `UpdateDbView.tsx` | técnica (URL direta, sempre liberada) | **fora do menu** | migração de banco |
|
||||
|
||||
**⚠️ Inconsistências de roteamento encontradas (evidência: `ROUTE_MAP` vs `VIEW_TO_ROUTE` em `App.tsx`):**
|
||||
- `/comissoes`, `/glosas` e `/cadastro-dentista` **existem em `VIEW_TO_ROUTE`** (a navegação por menu empurra a URL),
|
||||
mas **NÃO existem em `ROUTE_MAP`**. Consequência: abrir essas URLs direto (deep-link / refresh) cai em `dashboard`.
|
||||
**Deep-link quebrado** para 3 telas reais.
|
||||
- `/admin/sync` está mapeada, mas `isViewAllowed` retorna `false` **sempre** (substituída por "backup por clínica").
|
||||
A tela e a rota existem, porém são **inacessíveis** (código morto navegável).
|
||||
|
||||
---
|
||||
|
||||
# ETAPA 2 — INVENTÁRIO DE TELAS
|
||||
|
||||
(Resumo por tela — objetivo / usuário-alvo / valor / dependências / impacto. Apenas telas reais.)
|
||||
|
||||
### Dashboard
|
||||
- **Objetivo:** visão consolidada (KPIs via `GET /api/dashboard/stats`). **Usuário:** gestores/clínicos.
|
||||
- **Valor:** ponto de entrada operacional. **Depende de:** agenda, pacientes, financeiro. **Impacta:** navegação.
|
||||
|
||||
### Leads / Gestão de Leads
|
||||
- **Objetivo:** captar e **converter** interessados em pacientes. **Usuário:** funcionário/donos.
|
||||
- **Valor:** topo do funil comercial. **Depende de:** `interesses`/contato público. **Impacta:** Pacientes + Agenda
|
||||
(a conversão cria ambos — `POST /api/leads/:id/converter`).
|
||||
|
||||
### Pacientes
|
||||
- **Objetivo:** prontuário e cadastro (dados, família, score, faltas). **Usuário:** funcionário/donos.
|
||||
- **Valor:** entidade central do negócio. **Depende de:** clínica ativa. **Impacta:** Agenda, Orçamento,
|
||||
Procedimento realizado, Ortodontia, RX, Receita/Exame. É o hub clínico (view de 92 KB).
|
||||
|
||||
### Agenda
|
||||
- **Objetivo:** agendar/confirmar/remarcar; controle de presença e anti-overbooking. **Usuário:** todos clínicos.
|
||||
- **Valor:** coração operacional. **Depende de:** Pacientes, Dentistas+horários. **Impacta:** Avaliação,
|
||||
Procedimento, Financeiro (presença libera lançamento).
|
||||
|
||||
### Ortodontia
|
||||
- **Objetivo:** tratamento orto completo (triagem, diagnóstico, plano, evoluções, fotos, contenção). **Usuário:** dentista.
|
||||
- **Valor:** vertical clínica de alto valor recorrente. **Depende de:** Paciente. **Impacta:** Tutoria, fotos.
|
||||
|
||||
### Próteses / GTO
|
||||
- **Objetivo:** OS de laboratório (`protese_os`), produtos, custo lab, entrega. **Usuário:** protético/dentista.
|
||||
- **Valor:** controle do laboratório e do custo que abate comissão. **Depende de:** Procedimentos, Laboratórios.
|
||||
**Impacta:** Comissão (custo lab entra no cálculo).
|
||||
|
||||
### Financeiro
|
||||
- **Objetivo:** contas a receber/pagar (`financeiro`: RECEITA/DESPESA), formas, centro de custo, fornecedor. **Usuário:** gestores.
|
||||
- **Valor:** caixa da clínica. **Depende de:** Procedimentos realizados, Contratos, Repasses. **Impacta:** Comissão, Glosa.
|
||||
|
||||
### Comissões
|
||||
- **Objetivo:** definir % (padrão da clínica, por procedimento, override por profissional) e ver produção/comissão.
|
||||
- **Usuário:** donos. **Depende de:** Procedimentos realizados + Próteses (custo lab). **Impacta:** Repasses.
|
||||
|
||||
### Glosas
|
||||
- **Objetivo:** registrar glosa sobre recebíveis (convênio), recorrer, recuperar. **Usuário:** gestores.
|
||||
- **Valor:** recupera receita negada por convênio. **Depende de:** Financeiro (recebíveis). **Impacta:** caixa.
|
||||
|
||||
### Orçamentos → Contratos
|
||||
- **Objetivo:** propor tratamento (itens/valores) e formalizar em contrato (modelos com variáveis, assinatura, cobrança).
|
||||
- **Usuário:** funcionário/donos. **Depende de:** Paciente, Procedimentos. **Impacta:** Financeiro (`gerar-cobranca`).
|
||||
|
||||
### Equipe / Usuários (RBAC)
|
||||
- **Objetivo:** membros, cargos (`funcoes`), setores e permissões por workspace. **Usuário:** donos.
|
||||
- **Valor:** governa quem vê o quê. **Impacta:** menu e `isViewAllowed` de todos os membros.
|
||||
|
||||
### Salas (Locação) / Painel da Sala
|
||||
- **Objetivo:** cadastrar salas, publicar no marketplace, receber/aceitar reservas. **Usuário:** qualquer conta.
|
||||
- **Valor:** receita de locação P2P. **Depende de:** conta/sala. **Impacta:** `sala_reservas`.
|
||||
|
||||
### Marketplace de Profissionais / Diretório
|
||||
- **Objetivo:** profissional publica perfil; clínica envia proposta (`propostas_profissional`). **Usuário:** clínicas e profissionais.
|
||||
- **Valor:** conexão oferta/demanda de mão de obra. **Impacta:** mensagens/propostas (sem cobrança hoje).
|
||||
|
||||
### Tutoria de Ortodontia (Candidatura / Painel / Gestão)
|
||||
- **Objetivo:** dentista vira tutor (pagando inscrição via MercadoPago); resolve solicitações orto.
|
||||
- **Usuário:** dentistas + superadmin (gestão). **Valor:** **única receita de plataforma automatizada**.
|
||||
|
||||
### SuperAdmin (overview/users/clínicas/logs/financeiro/notificações)
|
||||
- **Objetivo:** administração global da plataforma (preços/assinaturas, broadcast, auditoria de acesso).
|
||||
- **Usuário:** superadmin. **Valor:** operação do SaaS.
|
||||
|
||||
### Configurações / Notificações / Minhas Clínicas
|
||||
- Conta, integrações (Google), backup por clínica; notificações; seletor/gestão de workspaces.
|
||||
|
||||
---
|
||||
|
||||
# ETAPA 3 — INVENTÁRIO DE BOTÕES (telas transacionais)
|
||||
|
||||
Rótulos extraídos das views; ações mapeadas para as rotas/tabelas do backend.
|
||||
|
||||
### Agenda (`AgendaView.tsx`)
|
||||
- **Novo Agendamento** → `POST /api/agendamentos` → tabela `agendamentos`. Fluxo: Paciente ↓ Procedimento ↓ Financeiro.
|
||||
- **Confirmar / Confirmar Presença** → `POST /api/agendamentos/:id/confirmar` + `/api/agenda/presenca` → `agendamentos`, `agenda_presenca`. **Libera lançamento financeiro.**
|
||||
- **Reagendar / Remarcar** → `POST /api/agendamentos/:id/remarcar` → `agendamentos`.
|
||||
- **Registrar Falta** → `POST /api/agendamentos/:id/falta` → `agendamentos` (impacta score do paciente).
|
||||
- **Solicitar Avaliação** → `POST /api/agendamentos/:id/solicitar-avaliacao` → `avaliacoes`.
|
||||
|
||||
### Pacientes (`PatientsView.tsx`)
|
||||
- **Editar/Cadastrar Paciente** → `POST|PUT /api/pacientes` → `pacientes`.
|
||||
- **Agendar** → abre fluxo da Agenda. **Lançar Procedimento** (`LancarProcedimentoModal`) → `POST /api/procedimentos-realizados` → `procedimentos_realizados` (+ gera `financeiro`).
|
||||
- **Orçamento** (`OrcamentoModal`) → `orcamentos`. **Receita** (`ReceitaModal`) → `modelos_receita`/`items_receita`. **Pedido de Exame** (`PedidoExameModal`) → `pedidos_exame`.
|
||||
|
||||
### Financeiro (`FinanceiroView.tsx`)
|
||||
- **Novo lançamento** (RECEITA/DESPESA, forma, vencimento, centro de custo, fornecedor) → `POST /api/financeiro` → `financeiro`.
|
||||
- **Marcar Pago / Alterar / Excluir** → `PUT|DELETE /api/financeiro/:id` → `financeiro`.
|
||||
|
||||
### Comissões (`ComissoesView.tsx`)
|
||||
- **Salvar % padrão / por procedimento / override por profissional** → `PUT /api/comissoes/regras` → `comissao_regras`.
|
||||
- **Base de cálculo** (produção × recebido) e **Custo lab** abatido. Próximo fluxo: **Fechar repasse**.
|
||||
|
||||
### Repasses (em Financeiro/Comissões)
|
||||
- **Fechar Repasse** → `POST /api/repasses/fechar` → `repasses` (status `fechado`).
|
||||
- **Pagar** → `POST /api/repasses/:id/pagar` → cria DESPESA em `financeiro` (origem `repasse`, centro de custo `COMISSÕES`).
|
||||
- **Anexar Comprovante** → `POST /api/repasses/:id/comprovante`. **Cancelar** → estorna a despesa.
|
||||
|
||||
### Glosas (`GlosasView.tsx`)
|
||||
- **Registrar Glosa** (total/livre, motivo, valor) → `POST /api/glosas` → `glosas`.
|
||||
- **Recorrer** → `POST /api/glosas/:id/recorrer`. **Recuperar** → `POST /api/glosas/:id/recuperar`.
|
||||
|
||||
### Orçamentos / Contratos
|
||||
- **Novo Orçamento** (itens, valor, entrada, parcelas) → `POST /api/orcamentos` → `orcamentos`/`orcamento_itens`.
|
||||
- **Gerar Contrato** (modelo + variáveis + prévia) → `POST /api/contratos/instancias` → `contratos`.
|
||||
- **Enviar / Assinar / Gerar Cobrança / Cancelar** → `POST /api/contratos/instancias/:id/{enviar,assinar,gerar-cobranca,cancelar}` → liga ao `financeiro`.
|
||||
|
||||
### Próteses (`ProteseView.tsx`)
|
||||
- **Nova OS** (produto, dentes, material, custo lab, preço, prazo) → `POST /api/protese/os` → `protese_os`.
|
||||
- **Marcar Entregue / Mudar Status** → `POST /api/protese/os/:id/status` → `protese_os_evento`.
|
||||
|
||||
### Leads (`LeadsView.tsx`)
|
||||
- **Cadastrar Lead** → `POST /api/leads` → `leads`.
|
||||
- **Cadastrar o Paciente / Converter** (com data, hora, dentista) → `POST /api/leads/:id/converter` → cria `pacientes` **+** `agendamentos` (anti-overbooking).
|
||||
|
||||
---
|
||||
|
||||
# ETAPA 4 — FLUXOS DE NEGÓCIO
|
||||
|
||||
## Fluxo principal (núcleo do produto) — COMPLETO
|
||||
|
||||
```
|
||||
Lead/Interesse ──converter──▶ Paciente ──▶ Agenda ──confirmar/presença──▶ Atendimento
|
||||
(leads) (pacientes) (agendamentos) │
|
||||
▼
|
||||
Procedimento Realizado
|
||||
(procedimentos_realizados)
|
||||
│ valor>0
|
||||
▼
|
||||
Financeiro RECEITA (origem='procedimento')
|
||||
(financeiro, flag sem_comissao)
|
||||
│
|
||||
computeComissoes = (base − custo_lab) × %
|
||||
▼
|
||||
Repasse FECHADO (repasses) ──pagar──▶
|
||||
Financeiro DESPESA (origem='repasse',
|
||||
centro_custo='COMISSÕES')
|
||||
```
|
||||
|
||||
- **Onde começa:** Lead/Interesse (ou cadastro direto de Paciente).
|
||||
- **Onde termina:** pagamento do repasse ao profissional (DESPESA conciliada no caixa) — ciclo fechado.
|
||||
- **Onde quebra / desvio:**
|
||||
- **Geração de RECEITA é opcional** (`gerarLancamento !== false` e `valor>0`): um procedimento sem valor
|
||||
**não** entra no financeiro — possível receita não registrada (depende do operador).
|
||||
- **Glosa** é um ramo paralelo sobre recebíveis de convênio (recupera receita), não integrado ao repasse.
|
||||
- **Garantia/Reabertura** marcam `sem_comissao` (regra: garantia sempre; reabertura só se mesmo profissional)
|
||||
— corretamente excluídas do repasse.
|
||||
- **Duplicidade:** não há dupla geração de comissão — o repasse pago entra como DESPESA com `sem_comissao=true`
|
||||
(evita recomissionar). Bom controle.
|
||||
|
||||
## Fluxo comercial paralelo
|
||||
|
||||
```
|
||||
Orçamento (orcamentos) ──▶ Contrato (contratos) ──gerar-cobranca──▶ Financeiro (a receber)
|
||||
```
|
||||
|
||||
## Fluxos de marketplace (ver Etapas 7 e 8)
|
||||
|
||||
```
|
||||
Sala (salas) ──publica──▶ Marketplace ──reserva──▶ sala_reservas (valor, status) [pagamento MANUAL]
|
||||
Profissional ──perfil──▶ Diretório ──proposta──▶ propostas_profissional (mensagem) [sem cobrança]
|
||||
Dentista ──candidatura──▶ MercadoPago (pago) ──webhook──▶ Tutor ativo ──▶ Solicitações de tutoria
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# ETAPA 5 — MATRIZ DE MÓDULOS
|
||||
|
||||
| Módulo | Existe? | Acessível? | Funcional? | Completo? | Em uso? | Classificação |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Autenticação / Workspaces / RBAC | Sim | Sim | Sim | Sim | Sim | **Maduro** |
|
||||
| Pacientes | Sim | Sim | Sim | Sim | Sim | **Maduro** |
|
||||
| Agenda (+presença/anti-overbooking) | Sim | Sim | Sim | Sim | Sim | **Maduro** |
|
||||
| Financeiro (receita/despesa) | Sim | Sim | Sim | Sim | Sim | **Maduro** |
|
||||
| Comissão + Repasse | Sim | Sim | Sim | Sim | Sim | **Operacional** |
|
||||
| Glosas | Sim | menu apenas (deep-link quebrado) | Sim | Parcial | Sim | **Operacional** |
|
||||
| Orçamentos → Contratos | Sim | Sim | Sim | Sim | Sim | **Operacional** |
|
||||
| Ortodontia | Sim | Sim | Sim | Sim | Sim | **Maduro** |
|
||||
| Próteses / GTO | Sim | Sim | Sim | Parcial | Sim | **Operacional** |
|
||||
| Leads | Sim | Sim | Sim | Sim | Sim | **Operacional** |
|
||||
| Plano/Procedimentos/Especialidades | Sim | Sim | Sim | Sim | Sim | **Operacional** |
|
||||
| RX / Radiografias (plugin) | Sim | por plugin/papel | Sim | Parcial | depende clínica | **Parcial** |
|
||||
| Locação de Salas (marketplace) | Sim | Sim (sempre ON) | Sim | Parcial (sem gateway) | baixo | **Parcial** |
|
||||
| Marketplace Profissionais | Sim | por plugin | Sim | Parcial (só mensagem) | baixo | **MVP** |
|
||||
| Tutoria de Ortodontia (MercadoPago) | Sim | Sim | Sim | Sim | baixo | **Operacional** |
|
||||
| SuperAdmin | Sim | superadmin | Sim | Sim | Sim | **Operacional** |
|
||||
| Sync Google/legado (global) | Sim | **bloqueado** | parcial | — | **não** | **Abandonado** |
|
||||
| Relatórios | Sim | Sim | Sim | Parcial | Sim | **Parcial** |
|
||||
|
||||
---
|
||||
|
||||
# ETAPA 6 — MULTIEMPRESA
|
||||
|
||||
**Evidência:** quase todas as 277 rotas usam `tenantGuard` e filtram por `req.clinicaId`/`clinica_id`.
|
||||
O workspace ativo é resolvido por `GET /api/me/workspaces` (tabela `vinculos` = usuário↔clínica) + salas próprias.
|
||||
|
||||
| Entidade | Como é modelada | Multiempresa respeitado? |
|
||||
|---|---|---|
|
||||
| Workspaces | `vinculos` (clínica) + `salas` (tipo `sala`); seletor no Sidebar | ✅ Sim |
|
||||
| Clínicas | `clinicas` + `clinica_id` em todas as tabelas operacionais | ✅ Sim |
|
||||
| Consultórios | papel `donoconsultorio` (variação de dono, mesmo modelo de clínica) | ✅ Sim |
|
||||
| Dentistas / Biomédicos / Protéticos | papéis distintos com menus próprios; vínculo por clínica | ✅ Sim |
|
||||
| Salas | `salas` + workspace `tipo='sala'` **isola** o contexto (menu reduzido) | ✅ Sim |
|
||||
| Laboratórios | `protese_laboratorios` por clínica | ✅ Sim |
|
||||
|
||||
- **Acoplamento indevido:** **não** encontrado acoplamento a "clínica única" — o `clinica_id` é onipresente.
|
||||
- **Ponto de atenção:** o contexto é **cacheado no `localStorage`** (`SCOREODONTO_USER_DATA`, plugins) e
|
||||
reconciliado via `hydrateMyPlugins()`. Plugins ativos vivem em `localStorage` (cliente), não só no servidor —
|
||||
risco de divergência entre dispositivos (mitigado por `me/plugins`, mas é o elo mais frágil do multiempresa).
|
||||
- O **marketplace de salas é cross-tenant** por desenho (uma clínica aluga sala de outra) — acoplamento *intencional*.
|
||||
|
||||
**Veredito:** conceito multiempresa **bem respeitado** no backend; a fragilidade é o estado de UI/plugins no cliente.
|
||||
|
||||
---
|
||||
|
||||
# ETAPA 7 — MARKETPLACE
|
||||
|
||||
```
|
||||
Salas (salas) ─┬─ Locação (sala_reservas: valor, modelo, status pendente/aceita)
|
||||
└─ Profissionais (perfil) ─ Propostas (propostas_profissional: mensagem)
|
||||
└─ Protéticos/Laboratórios (protese_*)
|
||||
```
|
||||
|
||||
| Frente | O que existe hoje | O que falta | Gera receita? | Pode gerar |
|
||||
|---|---|---|---|---|
|
||||
| **Locação de salas** | CRUD de salas, publicação no marketplace, reserva com `valor` e workflow de status (`pendente`→`aceita`), painel da sala | **Gateway de pagamento** e taxa da plataforma (hoje o `valor` é registrado, mas o pagamento é **manual/externo**) | **Não diretamente** | **Sim** — comissão sobre locação |
|
||||
| **Profissionais** | Perfil público + envio de proposta (mensagem) clínica→profissional | Negociação, contrato, e qualquer cobrança | **Não** | Sim — taxa de match/assinatura premium |
|
||||
| **Protéticos / Laboratórios** | OS, produtos, custo lab integrados ao financeiro/comissão | Marketplace aberto de laboratórios (hoje é interno por clínica) | Indireto (operacional) | Sim — rede de laboratórios |
|
||||
| **Tutoria de Ortodontia** | Candidatura **paga via MercadoPago** (`preferences` + `mp-webhook`), `comissao_pct`, solicitações/mensagens | Escala (oferta de tutores), avaliação | ✅ **Sim (a única automatizada)** | Já gera |
|
||||
|
||||
**Conclusão:** a infraestrutura de marketplace existe, mas **só a Tutoria tem captura de pagamento real**.
|
||||
Salas é a frente mais próxima de monetizar (já tem `valor` e reservas) — falta o gateway + a taxa.
|
||||
|
||||
---
|
||||
|
||||
# ETAPA 8 — FINANCEIRO
|
||||
|
||||
```
|
||||
ORIGEM DO DINHEIRO → DESTINO → RESPONSÁVEL
|
||||
Procedimento realizado (RECEITA) → financeiro (a receber) → Clínica
|
||||
Contrato (gerar-cobranca) → financeiro (a receber) → Clínica
|
||||
Convênio (recebíveis) − Glosa → financeiro / recupera → Clínica
|
||||
Comissão (base − custo lab × %) → repasses (fechado) → Profissional
|
||||
Repasse pago → financeiro DESPESA (COMISSÕES) → Clínica paga Profissional
|
||||
Locação de sala (valor reserva) → sala_reservas (status) → Locador [pagamento manual]
|
||||
Tutoria (inscrição) → MercadoPago → ativa tutor → Plataforma (comissao_pct)
|
||||
```
|
||||
|
||||
| Item | Tabela | Estado |
|
||||
|---|---|---|
|
||||
| Orçamentos | `orcamentos`/`orcamento_itens` | ✅ completo |
|
||||
| Contratos | `contratos`/`contrato_*` | ✅ completo (modelos, variáveis, assinatura, cobrança) |
|
||||
| Convênios/Planos | `planos`, `procedimentos_valores_planos` | ✅ |
|
||||
| Produção | `procedimentos_realizados` → `financeiro` | ✅ (RECEITA opcional por valor) |
|
||||
| Comissão | `comissao_regras` + `computeComissoes` | ✅ (3 níveis de regra; abate custo lab) |
|
||||
| Repasse | `repasses` (fechar/pagar/cancelar/comprovante) | ✅ ciclo fechado, com estorno |
|
||||
| Glosas | `glosas` (recorrer/recuperar) | ✅ funcional, isolado do repasse |
|
||||
| Locação | `sala_reservas.valor` | ⚠️ registra valor, **sem captura de pagamento** |
|
||||
|
||||
**Pontos fortes:** rastreabilidade (cada DESPESA de repasse referencia o período/profissional; `audit_log` em tudo),
|
||||
e não-duplicação de comissão (`sem_comissao` propagado). **Lacuna:** receita de procedimento depende de ação manual.
|
||||
|
||||
---
|
||||
|
||||
# ETAPA 9 — MAPA DE CONEXÕES (Mermaid)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
U[Usuário] --> WS{Workspace ativo}
|
||||
WS -->|clínica| CL[Clínica / RBAC]
|
||||
WS -->|sala| SH[Painel da Sala]
|
||||
|
||||
CL --> LEAD[Leads]
|
||||
LEAD -->|converter| PAC[Pacientes]
|
||||
PAC --> AG[Agenda]
|
||||
AG --> AV[Avaliação]
|
||||
AG --> PR[Procedimento Realizado]
|
||||
PAC --> ORTO[Ortodontia]
|
||||
PAC --> RX[RX / Radiografias]
|
||||
PAC --> ORC[Orçamento]
|
||||
ORC --> CON[Contrato]
|
||||
CON -->|gerar-cobranca| FIN[Financeiro]
|
||||
PR -->|RECEITA| FIN
|
||||
PR --> COM[Comissão]
|
||||
PROT[Próteses / Custo Lab] --> COM
|
||||
COM --> REP[Repasse]
|
||||
REP -->|pagar DESPESA| FIN
|
||||
FIN --> GLO[Glosas]
|
||||
FIN --> REL[Relatórios]
|
||||
|
||||
SH --> RES[Reservas de Sala]
|
||||
CL --> SAL[Salas Marketplace]
|
||||
SAL --> RES
|
||||
CL --> MKP[Marketplace Profissionais]
|
||||
MKP --> PROP[Propostas]
|
||||
ORTO --> TUT[Tutoria Orto]
|
||||
TUT -->|MercadoPago| MP[(Pagamento)]
|
||||
|
||||
SUPER[SuperAdmin] --> CL
|
||||
SUPER --> PLUG[Plugins]
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Núcleo financeiro
|
||||
PR[procedimentos_realizados] --> FINr[financeiro RECEITA]
|
||||
FINr --> CR[comissao_regras]
|
||||
CR --> RP[repasses]
|
||||
RP --> FINd[financeiro DESPESA]
|
||||
FINr --> GL[glosas]
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# ETAPA 10 — OPORTUNIDADES
|
||||
|
||||
## Funcionalidades órfãs (existem no código, ninguém usa)
|
||||
- **`views/OrthoReports.tsx`** — não é roteada em `App.tsx` **nem** importada por nenhuma view. Órfã real
|
||||
(importa `OrthoPhotoWorkspace`, mas nada importa `OrthoReports`). Relatórios de ortodontia prontos e desligados.
|
||||
- **`SyncView` (`/admin/sync`)** — tela e rota existem, mas `isViewAllowed` retorna `false` sempre. Código morto navegável.
|
||||
|
||||
## Funcionalidades escondidas (existem mas não aparecem no menu)
|
||||
- **`/update` (`UpdateDbView`)** — migração de banco; acessível por URL direta, fora do menu (ferramenta técnica).
|
||||
- **`/cadastro-dentista`** — só alcançável por fluxo/magic-link; sem entrada no menu e com deep-link quebrado.
|
||||
- **Comissões deep-link** — visível no menu (admin/donos) mas `/comissoes` direto cai em dashboard.
|
||||
|
||||
## Funcionalidades incompletas (começam mas não terminam o fluxo)
|
||||
- **Locação de salas** — reserva com `valor` e status, mas **sem captura de pagamento** (fluxo financeiro não fecha).
|
||||
- **Marketplace de profissionais** — proposta é só mensagem; **não** evolui para contratação/cobrança.
|
||||
- **RECEITA de procedimento opcional** — pode não gerar lançamento (receita potencialmente não registrada).
|
||||
- **Glosas isoladas** — recuperam receita, mas não reconciliam com o repasse correspondente.
|
||||
|
||||
## Funcionalidades de alto potencial (podem gerar receita)
|
||||
1. **Gateway na Locação de Salas** (já existe `valor`+reserva): cobrança + taxa da plataforma → receita recorrente.
|
||||
2. **Assinatura/planos da plataforma** (`superadmin/pricing`, `assinaturas` já existem): monetizar o SaaS por clínica.
|
||||
3. **Marketplace de Profissionais premium**: taxa de match / destaque de perfil.
|
||||
4. **Tutoria de Ortodontia em escala** (único pago hoje): aumentar oferta de tutores e volume.
|
||||
|
||||
---
|
||||
|
||||
# RESULTADO FINAL
|
||||
|
||||
## Notas (0–100) — com base nas evidências
|
||||
|
||||
| Eixo | Nota | Justificativa (evidência) |
|
||||
|---|---:|---|
|
||||
| **Arquitetura funcional** | 72 | Roteamento+RBAC robustos, mas `server.js` monolítico (516 KB), roteador próprio e 3 deep-links quebrados. |
|
||||
| **Integração dos módulos** | 78 | Cadeia Lead→…→Repasse bem costurada, com auditoria e não-duplicação de comissão. |
|
||||
| **Fluxos de negócio** | 70 | Núcleo clínico+financeiro completo; ramos de marketplace incompletos; receita às vezes manual. |
|
||||
| **Multiempresa** | 82 | `tenantGuard`+`clinica_id` onipresentes; isolamento de sala; fragilidade no estado de plugins no cliente. |
|
||||
| **Marketplace** | 45 | Infra existe; só Tutoria captura pagamento; Salas e Profissionais sem fechar fluxo financeiro. |
|
||||
| **Financeiro** | 80 | Receita/despesa, comissão (3 níveis), repasse com estorno, glosas, contratos com cobrança. |
|
||||
| **Experiência do usuário** | 70 | Menus por papel, nav mobile, "visualizar como"; prejudicada por deep-links quebrados e estado em localStorage. |
|
||||
| **Potencial de monetização** | 65 | Tutoria já fatura; Salas e Assinatura SaaS são alavancas prontas e subutilizadas. |
|
||||
|
||||
## 1. O que remover
|
||||
- `views/OrthoReports.tsx` (órfã) — ou religar ao menu de Ortodontia se desejado.
|
||||
- `SyncView` / rota `/admin/sync` e telas de sync global desativadas (já substituídas por backup por clínica).
|
||||
- `server.js.bak` e scripts de transformação legados (`transform.js`, `pg-convert.js`) do diretório de produção.
|
||||
|
||||
## 2. O que corrigir
|
||||
- **Adicionar `/comissoes`, `/glosas`, `/cadastro-dentista` ao `ROUTE_MAP`** (deep-link/refresh quebrado).
|
||||
- Reconciliação de **plugins ativos**: tratar o servidor como fonte da verdade (reduzir dependência de `localStorage`).
|
||||
- Tornar a **geração de RECEITA do procedimento** o padrão claro (evitar receita não registrada).
|
||||
|
||||
## 3. O que finalizar
|
||||
- **Locação de Salas**: captura de pagamento + taxa da plataforma (fecha o fluxo financeiro do marketplace).
|
||||
- **Marketplace de Profissionais**: evoluir proposta → contratação → cobrança.
|
||||
- **Relatórios**: consolidar (há `reports/productivity` + telas parciais).
|
||||
|
||||
## 4. O que priorizar
|
||||
1. Corrigir deep-links e estado de plugins (baixo esforço, destrava UX e confiabilidade).
|
||||
2. Gateway de pagamento na Locação de Salas (alavanca de receita mais próxima).
|
||||
3. Cobrança por assinatura da plataforma (`superadmin/pricing`+`assinaturas` já existem).
|
||||
|
||||
## 5. O que gera receita mais rapidamente
|
||||
- **Locação de Salas com pagamento** — o `valor` e o fluxo de reserva já existem; falta só a captura + taxa.
|
||||
- **Assinatura SaaS por clínica** — a base multiempresa e o `pricing` já estão prontos no superadmin.
|
||||
|
||||
## 6. Próximo módulo mais estratégico
|
||||
- **Monetização da plataforma (Billing/Assinaturas + taxa de marketplace de salas).** É o que converte uma base
|
||||
técnica madura (ERP odontológico multiempresa funcional) em **receita recorrente da plataforma** — hoje só a
|
||||
Tutoria cobra. Tudo o mais já está construído; falta o módulo que **fatura**.
|
||||
|
||||
---
|
||||
|
||||
> **Nota de método:** este mapa reflete exclusivamente o que foi encontrado no código em 19/jun/2026
|
||||
> (`frontend/App.tsx`, `frontend/views/*`, `frontend/components/Sidebar.tsx`, `backend/server.js`).
|
||||
> Onde havia ambiguidade (ex.: receita opcional, pagamento manual de salas), o fato foi descrito como está,
|
||||
> sem inferir intenção. Nenhuma funcionalidade foi inventada.
|
||||
</content>
|
||||
</invoke>
|
||||
@@ -1,344 +0,0 @@
|
||||
# ScoreOdonto — Pendências, Tutoria e Deploy (dev)
|
||||
|
||||
> Documento de handoff. Última atualização: 2026-06-12.
|
||||
> 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)
|
||||
|
||||
- [x] **`notificacoes.lida` é smallint** (confirmado no banco: `int2`), mas havia código
|
||||
usando boolean → corrigido (2026-06-12). Locais: `POST /api/superadmin/notificacoes`
|
||||
inseria `false` (server.js:4385 e 4393) e o marcar-como-lida usava `true/false`
|
||||
(`POST /api/notificacoes/read-all` server.js:1584 e `.../:id/read` server.js:1601).
|
||||
Todos trocados para `1/0`. Os demais INSERT (270, 2188) já usavam `0`.
|
||||
- [x] ~~`POST /api/agendamentos` default `status='Agendado'` viola `agendamentos_status_check`~~
|
||||
**NÃO procede mais.** A constraint atual é case-insensitive e aceita `agendado`
|
||||
(`lower(status) IN ('importado','agendado','confirmado','reagendado','remarcar',
|
||||
'cancelado','falta','atendido','pendente','concluido','faltou')`). O default do código
|
||||
é `rest.status || 'agendado'` → válido. Há registros `agendado` em uso no banco.
|
||||
- [x] ~~`PUT /api/agendamentos/:id` não passa pelo check de overlap~~ **NÃO procede.**
|
||||
O PUT chama `findOverbookingConflict(...)` e retorna 409 em conflito (server.js:~2013).
|
||||
|
||||
---
|
||||
|
||||
## 4c. Integração Plugin RX ↔ servidor dental (2026-06-12) — MODELO GLOBAL
|
||||
|
||||
**Modelo correto (confirmado pelo usuário):** config é **GLOBAL, só do superadmin**
|
||||
(não por clínica, não por usuário admin da conta). Hierarquia desejada no RX:
|
||||
`scoreodonto / conta-mestre (consulttclinic@gmail.com) / clinicaID / paciente`.
|
||||
Mapa: conta-mestre = `api_token` global · clinicaID = `images.clinic_name` ·
|
||||
paciente = `images.patient_name`.
|
||||
|
||||
**Menu — feito antes:** `Sidebar.tsx` voltou a mostrar **PLUGINS** ao superadmin
|
||||
(allowlist linha 67 estava sem `'plugins'`).
|
||||
|
||||
**FASE 0 — scoreodonto realinhado p/ GLOBAL (feito, deploy dev):**
|
||||
- [x] `settings.rx_config_global` = `{ rx_url, api_token }`. **Sem login/senha, sem cache**:
|
||||
o backend usa o `api_token` estático direto como Bearer (`_rxAdminFetch`). `api_tokens`
|
||||
do RX são aceitos pelo `authenticateToken` (viram admin, **não expiram**).
|
||||
- [x] `PUT /api/rx/config` agora é **`superadminGuard`** (só superadmin grava); token preservado
|
||||
se vier vazio. `GET /api/rx/config` (authGuard) devolve `{rx_url, has_token, configured}` (sem token).
|
||||
- [x] `POST /api/rx/devices` cria no RX com **`clinic_name = clinicaId`** (e-mail default único
|
||||
por clínica+PC: `<clinicaId>.<pcSlug>@rx.scoreodonto.com`). `GET /api/rx/devices?clinicaId=`
|
||||
filtra por clinic_name.
|
||||
- [x] **Revertido** o que era do modelo errado: bypass do `tenantGuard` (removido) e seletor de
|
||||
clínica do `RxConfigView` (removido).
|
||||
- [x] `RxConfigView`: card **CONFIGURAÇÃO GERAL DO RX** (URL + token) editável **só superadmin**;
|
||||
clínica vê status read-only + cadastra seus PCs. Superadmin vê PCs de **todas** as clínicas.
|
||||
- [x] `backend.ts`: `getRxConfig()` (0 args, `{rx_url,has_token,configured}`), `putRxConfig({rx_url,api_token})`,
|
||||
`createRxDevice` sem clinicName. `RXPlugin.tsx`: `getRxConfig()` sem arg.
|
||||
|
||||
**FASE 1 — servidor RX (dev.rx, `dental-server/routes/images.js`) (feito, restart dev):**
|
||||
- [x] Filtro **`?clinic=<clinicaId>`** (= `clinic_name`) em `GET /api/images`, `/images/patients`
|
||||
e `/images/by-patient`. Novo `GET /api/images/unique-clinics` (clinic_name + contagens).
|
||||
- [x] Validado: `node --check` ok; `rx_dev_api` reiniciado (porta 3000); endpoints → 401 (auth ok).
|
||||
⚠️ `npm run dev` = `node server.js` (SEM nodemon) → mudanças no RX exigem `docker restart rx_dev_api`.
|
||||
|
||||
**Validado geral:** tsc 0 erros; scoreodonto backend+frontend rebuildados no dev (bundle `govSqkSI`).
|
||||
|
||||
**FASE 2/3 — leitura por proxy (feito, deploy dev bundle `BVGGJsCu`):**
|
||||
- [x] Backend: `GET /api/rx/patients?clinicaId=&search=` e `GET /api/rx/images?clinicaId=&paciente=`
|
||||
→ proxy p/ RX `/images/patients` e `/images/by-patient` com `clinic=<clinicaId>` + api_token.
|
||||
Isolamento `_rxClinicScopeOk`: superadmin vê tudo; demais só clínicas com vínculo (senão 403).
|
||||
- [x] `backend.ts`: `getRxPatients`, `getRxPatientImages`. `RXPlugin` consome esses (lista de
|
||||
pacientes + imagens do paciente). Thumbnails seguem públicos (`/api/images/:id/thumbnail`).
|
||||
|
||||
**PENDENTE:**
|
||||
- [ ] **Falta o `api_token`** colado em CONFIGURAÇÃO GERAL DO RX (superadmin) → sem ele, criar PC
|
||||
dá 400 e a leitura volta vazia. Gerar via `POST /api/auth/tokens` do RX (usuário `is_admin`).
|
||||
- [ ] **Imagem em tamanho cheio** (`/images/:id/file-url`) ainda é autenticada → falta proxy
|
||||
`GET /api/rx/image/:id/file` (galeria mostra thumbnail público; full-res é follow-up).
|
||||
- [ ] **GTOs / upload / trigger-sync** do RXPlugin ainda usam `rxFetch` direto (token vazio) —
|
||||
já estavam quebrados antes; migrar p/ proxy se forem usados.
|
||||
- [ ] **Ativação do plugin** ainda é localStorage + toggle só superadmin (PluginsView) → rever
|
||||
p/ ativação por clínica no backend.
|
||||
- [ ] **Promover FASES 1–3 p/ prod** (rx.scoreodonto.com + scoreodonto prod) após validar no dev.
|
||||
|
||||
---
|
||||
|
||||
## 4d. Plugins: Locação de Salas + Marketplace de Profissionais (2026-06-12) — deploy dev
|
||||
|
||||
Dois plugins novos no padrão do RX (catálogo `PluginsView` + `pluginRegistry` localStorage
|
||||
+ `pluginMenuItems` no Sidebar). Bundle `index-BylSJWKo.js`; `tsc` 0 erros; migrations OK.
|
||||
|
||||
**`locacao-salas`** (teal, view `/salas`, `views/plugins/SalasPlugin.tsx`):
|
||||
- Tabelas `salas` + `sala_reservas`. CRUD do locador, marketplace (tipo/cidade/UF),
|
||||
valores por hora/turno/diária/semanal/mensal.
|
||||
- Reserva em transação com anti-conflito TRIPLO → 409: sala ocupada, reserva do próprio
|
||||
solicitante em outra sala, e **agenda global** (`findOverbookingConflict` via
|
||||
`dentistas.usuario_id`) — sala entra na invariante "1 lugar por vez".
|
||||
- Fluxo: pendente → confirmar/recusar (locador) / cancelar (ambos). Notificações
|
||||
(`notificarUsuario`) + `audit_log` (`entidade='sala'|'sala_reserva'`).
|
||||
|
||||
**`profissionais-marketplace`** (laranja, view `/marketplace-profissionais`,
|
||||
`views/plugins/ProfissionaisPlugin.tsx`) — decisão: **Delivery (nível 8) virou "Profissionais"**:
|
||||
- `usuarios` + `cep`, `raio_atuacao_km`, `bio_profissional` (raio é informativo, sem geodistância).
|
||||
- `GET /api/profissionais/marketplace` inclui **biomédico** (o diretório antigo não incluía)
|
||||
e filtra por CEP-prefixo. `GET/PUT /api/profissionais/perfil` (opt-in `disponivel_diretorio`).
|
||||
- Tabela `propostas_profissional`: clínica→profissional (`POST /api/propostas` com `tenantGuard`,
|
||||
anti-duplicata pendente), aceitar/recusar (profissional) / cancelar (clínica) + notificações.
|
||||
- **Consolidado com o Diretório antigo** (bundle `DZJWcMDu`): com o plugin ativo, o item
|
||||
"DIRETÓRIO PROF." some do menu e a rota `/diretorio-profissionais` renderiza o plugin;
|
||||
com o plugin inativo, o `DiretorioProfissionaisView` antigo continua intacto (sem regressão).
|
||||
`DiretorioProfissionaisView`/`/api/profissionais-disponiveis` podem ser removidos quando
|
||||
o plugin virar padrão.
|
||||
|
||||
**Como testar:** superadmin → PLUGINS → ativar (lembrete: ativação é **localStorage por
|
||||
navegador** — pendência 4c vale aqui também) → itens aparecem no menu dos papéis operacionais.
|
||||
Propostas exigem workspace ativo (clínica/consultório); aba MEU PERFIL só para
|
||||
dentista/biomedico/protetico.
|
||||
|
||||
**⚠️ LIMPEZA pós-teste (banco é de PRODUÇÃO):** dados de teste ficam em `salas`,
|
||||
`sala_reservas`, `propostas_profissional`, colunas novas de `usuarios` e respingos em
|
||||
`notificacoes`/`audit_log`. Limpar com:
|
||||
```sql
|
||||
DELETE FROM sala_reservas; DELETE FROM salas; DELETE FROM propostas_profissional;
|
||||
-- e reverter perfis de teste:
|
||||
-- UPDATE usuarios SET disponivel_diretorio=false, raio_atuacao_km=NULL, bio_profissional=NULL WHERE id='...';
|
||||
```
|
||||
|
||||
**Fluxos institucionais de locação** (2026-06-13, bundle `CvP3Q2X_`):
|
||||
- `sala_reservas` + `clinica_id`/`clinica_nome`/`profissional_usuario_id`/`profissional_nome`.
|
||||
- **Clínica aluga sala**: ReservaModal ganhou "RESERVAR COMO: PESSOAL | <unidade ativa>";
|
||||
como unidade, pode designar o profissional (`GET /api/dentistas?clinicaId=`, só os com
|
||||
`usuario_id`). O **anti-conflito roda sobre o OCUPANTE** (designado ou solicitante) —
|
||||
`COALESCE(profissional_usuario_id, solicitante_id)` nas reservas + agenda global.
|
||||
Designado é notificado. Membros da unidade veem/cancelam as reservas dela
|
||||
(`GET /api/sala-reservas/minhas?clinicaId=`, vínculo validado).
|
||||
- **Consultório/clínica anuncia a própria sala**: checkbox "ANUNCIAR PELA UNIDADE: <nome>"
|
||||
no cadastro/edição (grava `salas.clinica_id`; PUT aceita desvincular). Marketplace e
|
||||
cards mostram "POR <unidade>" (join `clinicas`).
|
||||
- **Dentista aluga sala**: fluxo pessoal original, inalterado.
|
||||
|
||||
**Alinhado com `/superadmin/financeiro`** (bundle `CXf4eiwZ`): a Tabela de Preços ganhou a
|
||||
seção **PLUGINS (ADD-ONS)** — chaves `plugin:locacao-salas` e `plugin:profissionais-marketplace`
|
||||
no mesmo `settings.pricing` (0 = incluído no plano). `GET /api/superadmin/pricing` agora mescla
|
||||
defaults (chaves novas aparecem em configs antigas) e ganhou a linha **biomédico** que faltava.
|
||||
Os cards da Área de Plugins exibem o preço configurado (chip "R$ X/MÊS" / "INCLUÍDO NO PLANO").
|
||||
A COBRANÇA efetiva do add-on (bloquear plugin sem pagamento) segue o modelo manual atual de
|
||||
pagamentos por usuário — não implementada.
|
||||
|
||||
**PENDENTE (roadmap acordado):**
|
||||
- [ ] Cobrança efetiva dos add-ons (ativação por clínica no backend + verificação de pagamento —
|
||||
junto com a pendência 4c de tirar a ativação do localStorage).
|
||||
- [ ] Score Professional (motor de pontuação — alimentado por reservas/propostas/faltas).
|
||||
- [ ] Geodistância real (CEP→lat/lng; raio passa a filtrar de verdade).
|
||||
- [ ] Fotos das salas (upload via `/app/media`, como as fotos de orto).
|
||||
- [ ] Pagamento da locação (MercadoPago já existe na tutoria — reaproveitar).
|
||||
- [ ] Proposta aceita → criar `vinculos` automaticamente (hoje o aceite é só status+notificação).
|
||||
|
||||
---
|
||||
|
||||
## 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.)
|
||||
@@ -0,0 +1,129 @@
|
||||
# Status — Segurança & Produção (ScoreOdonto)
|
||||
|
||||
> Resumo do que foi feito e do que falta. Atualizado em **22/jun/2026**.
|
||||
> Produção no ar: **v1.0.19** (`715b2dc`) — fila **A** concluída (push → CI → tag → deploy). **Commits
|
||||
> publicados (`git push` feito).** A rotação do `JWT_SECRET` segue aplicada e verificada em produção.
|
||||
|
||||
---
|
||||
|
||||
## ✅ O QUE JÁ FOI FEITO
|
||||
|
||||
### 1. Segredos
|
||||
- **`JWT_SECRET` rotacionado em DEV e PRODUÇÃO** (recriado o backend na VPS1 na mesma tag, sem rebuild;
|
||||
`/api/health` e `/api/version` validados). → **vetor de tomada de conta FECHADO** (o segredo vazado não
|
||||
forja mais tokens). **Ação operacional na VPS1 — sem commit** (o segredo não é versionado).
|
||||
- **`backend/.env` fora do tracking do git** + `backend/.dockerignore` (o Dockerfile fazia `COPY . .` sem
|
||||
`.dockerignore` → segredos iam para dentro da imagem) + `.env.example`. Commits `fcf2fb1`.
|
||||
- **Senhas hardcoded removidas do `docker-compose.yml`**: banco via `SCOREO_DB_PASS` (com `:?` fail-fast);
|
||||
`DRAGONFLY_URL` (com senha) movido para `env_file`. Commit `3aa6a44`. Validado em DEV (pgdev/cache ok).
|
||||
|
||||
### 2. Frontend
|
||||
- **`/update` (migração de banco) restrito a superadmin** (era acessível a qualquer um, até deslogado).
|
||||
- **Deep-links corrigidos** (`/comissoes`, `/glosas` faltavam no `ROUTE_MAP` → caíam em dashboard).
|
||||
- Commit `1559387`. (Build compila; validação visual de RBAC precisa de navegador.)
|
||||
|
||||
### 3. RBAC intra-clínica no backend (99 rotas) — **NÚCLEO FECHADO**
|
||||
Antes: qualquer membro autenticado (até paciente) podia chamar ações privilegiadas direto na API
|
||||
(a UI escondia, o backend não barrava). Agora há autorização por **cargo**, espelhando o `isViewAllowed`
|
||||
do frontend, via 3 helpers: `requireCapability`, `requireCapabilityRow`, `requireCapabilityFromOrders`.
|
||||
|
||||
| Grupo | Commit |
|
||||
|---|---|
|
||||
| Financeiro (comissões/repasses/financeiro/glosas) | `f4e7fe3` |
|
||||
| Gestão de equipe/clínica (membros/setores/funções/reset-senha/cor) | `9ad65c3` |
|
||||
| Cadastros administrativos (dentistas/especialidades/procedimentos/planos + reorder) | `116200c` + `0667456` |
|
||||
| Operação clínica (pacientes/agenda/leads) | `5b58a8a` |
|
||||
| Orçamentos/Contratos | `16c13ad` |
|
||||
| Sub-pass clínico (presença/pendências/horários/detalhes de paciente) | `0fc69bb` |
|
||||
|
||||
- **Marketplace (salas/sala-reservas/propostas): auditado — já protegido por POSSE** nos handlers
|
||||
(`owner_usuario_id`, profissional destinatário, dono/solicitante). Sem gap; sem mudança. Commit `0667456`.
|
||||
- Validado em DEV com tokens forjados: dono passa, dentista/paciente 403, isolamento cross-tenant intacto.
|
||||
|
||||
### 4. Documentação & Skills
|
||||
- `documentação/MAPA-FUNCIONAL-COMPLETO.md` (inventário rotas/telas/botões/fluxos/módulos).
|
||||
- `documentação/AUDITORIA-ESTRATEGICA-MODELAGEM.md` (Pessoa→Vínculo→Workspace, débitos, riscos).
|
||||
- `ScoreOdonto_Skills_Enterprise/`: **62/62 skills preenchidas** com conteúdo real. Commits `5b99292`, `e83af04`, `f22e30e`.
|
||||
|
||||
### 5. Pré-requisito de produção
|
||||
- **`SCOREO_DB_PASS` provisionado na VPS1** (`/home/deploy/stack/scoreodonto.com/.env`, 0600), valores
|
||||
extraídos do container em execução, `.gitignore` da VPS1 ajustado, conexão validada (`SELECT 1` = ok).
|
||||
|
||||
### 6. Fila A concluída — **PRODUÇÃO EM `v1.0.19`** (22/jun/2026)
|
||||
- **`git push` do lote** (RBAC 99 rotas + segredos fora do compose/imagem + correções de frontend).
|
||||
- **CI desbloqueada:** o build falhou na 1ª vez (`SCOREO_DB_PASS` com `:?` quebrava a interpolação do
|
||||
compose no `docker compose build` do runner) → corrigido com placeholder runtime-only no `build.yml`
|
||||
(commit `715b2dc`). Build verde → imagens `:715b2dc` (back+front, atômicas) publicadas no registry.
|
||||
- **Tag `v1.0.19` → `715b2dc`:** job `promote` re-tagueou `:715b2dc` → `:v1.0.19` **sem rebuild**
|
||||
(digests idênticos conferidos) e rodou `deploy-prod.sh v1.0.19` na VPS1.
|
||||
- **Backup do banco antes do deploy:** `~/backups/scoreodonto_20260622_0005.dump` na VPS1 (PG18).
|
||||
- **Verificado em produção:** `/api/version` = `v1.0.19` (`715b2dc`, PROD); `/api/health` = ok
|
||||
(DB ok, cache ok). DEV (8020) realinhado ao mesmo commit via `./dev-build.sh` (mostra `v1.0.19`).
|
||||
- **Rollback trivial:** `./deploy-prod.sh v1.0.18` (+ dump do banco, se necessário).
|
||||
|
||||
---
|
||||
|
||||
## ⏳ O QUE FALTA
|
||||
|
||||
### A) Levar o trabalho a produção (fila) — ✅ CONCLUÍDA
|
||||
1. ~~**Imagem limpa `v1.0.19`**~~ → **FEITO** (ver "Fila A concluída" acima). Produção em `v1.0.19`/`715b2dc`.
|
||||
2. **Compose novo na VPS1 (opcional, passo 2b):** `deploy-prod.sh` **não faz `git pull`** → o
|
||||
`docker-compose.yml` da VPS1 segue o antigo. Para tirar a senha hardcoded do compose local, copiar o
|
||||
novo `docker-compose.yml` (scp) + recreate. O root `.env` já provisionado cobre isso. **Ainda pendente.**
|
||||
|
||||
### B) Rotação dos demais segredos (ainda vivos no histórico/artefatos antigos)
|
||||
3. **Senha do banco** (`scoreodonto_user`) — contida ao scoreodonto; exige refactor já feito no compose +
|
||||
`ALTER USER` + atualizar `.env`/`backend/.env` + recreate. Risco de outage → com cuidado/janela.
|
||||
4. **Senha do Dragonfly (cache)** — ⚠️ **compartilhada** entre apps (rx, newwhats, subdominio, mercado);
|
||||
trocar derruba os outros → exige **janela coordenada**.
|
||||
5. **`OPENAI_API_KEY` / `GEMINI_API_KEY`** — **só você** rotaciona (painéis OpenAI/Google); depois colo os
|
||||
novos valores no `.env` e recrio.
|
||||
6. **Histórico do git** — `backend/.env` (e um `backend/.env.bak`) estão no histórico. Após rotacionar tudo,
|
||||
o *scrub* de histórico (force-push, destrutivo) vira **cosmético** — não recomendado às pressas.
|
||||
|
||||
### C) Residual de qualidade (não-bloqueante; do MAPA / AUDITORIA)
|
||||
- RECEITA de procedimento é **opcional** (`gerarLancamento`/`valor>0`) → decidir se vira padrão (risco de caixa).
|
||||
- Estado de plugins divergente cliente/servidor (`localStorage` vs `usuario_plugins`).
|
||||
- Órfã `OrthoReports.tsx`; `SyncView` desativada; `server.js.bak`/scripts legados → limpeza.
|
||||
- Marketplace: **gateway de pagamento** (salas têm `valor`, sem captura) e **assinaturas SaaS** (manual).
|
||||
|
||||
### D) Débitos de modelagem (estratégico; exige aprovação + janela)
|
||||
- `dentistas` como **projeção** da pessoa (hoje duplica a pessoa por clínica).
|
||||
- `pacientes` com **identidade de pessoa** (hoje preso à clínica, não-portável) — antes, decisão LGPD/jurídica.
|
||||
- Unificar workspace (`clinicas` + `salas`); entidade **"rede/grupo"** acima da clínica.
|
||||
> Recomendação estratégica única: assumir a **Pessoa como âncora** e parar de manter o legado clínica-cêntrico
|
||||
> em paralelo. Detalhe em `AUDITORIA-ESTRATEGICA-MODELAGEM.md`.
|
||||
|
||||
---
|
||||
|
||||
## Commits da sessão — **PUBLICADOS** (`git push` feito; produção em `v1.0.19`)
|
||||
> Todos empurrados para `origin/main`. Promovidos a produção via tag `v1.0.19` (commit `715b2dc`).
|
||||
> A rotação do `JWT_SECRET` **não** está nesta lista (foi ação operacional na VPS, sem commit).
|
||||
```
|
||||
715b2dc ci(build): desbloqueia build — SCOREO_DB_PASS placeholder na interpolação
|
||||
8a27f3b docs+infra: corrige status (JWT/contagem) + versão em DEV (dev-build.sh)
|
||||
6adf353 docs: status geral de segurança & produção (este documento)
|
||||
f22e30e docs(skills): completa as 62 skills
|
||||
e83af04 docs(skills): biblioteca operacional
|
||||
5b99292 docs: auditoria funcional + estratégica
|
||||
0667456 security(rbac): reorder + auditoria do marketplace
|
||||
0fc69bb security(rbac): sub-pass clínico
|
||||
16c13ad security(rbac): orçamentos e contratos
|
||||
5b58a8a security(rbac): operação clínica
|
||||
116200c security(rbac): cadastros administrativos
|
||||
9ad65c3 security(rbac): gestão de equipe/clínica
|
||||
f4e7fe3 security(rbac): rotas financeiras
|
||||
1559387 security/fix(front): /update + deep-links
|
||||
3aa6a44 security(infra): senhas fora do compose
|
||||
fcf2fb1 security(infra): .env fora do git + .dockerignore
|
||||
```
|
||||
|
||||
## Como retomar
|
||||
- **Fila A:** ✅ concluída — produção em **`v1.0.19`** (`715b2dc`). Próximo release: `git push` → CI
|
||||
builda `:<sha>` → `git tag vX.Y.Z` → job `promote` deploya na VPS1.
|
||||
- **Antes de abrir a usuários reais:** concluir **B** (rotações) — sobretudo as API keys (você) e a senha do banco.
|
||||
- **Pendência pontual:** passo **A.2** (compose novo na VPS1 via scp + recreate) para tirar a senha
|
||||
hardcoded do compose local.
|
||||
- Código rodando em **DEV** (`dev.scoreodonto.com`) e **PROD** no mesmo commit `715b2dc`; rollback de
|
||||
prod: `./deploy-prod.sh v1.0.18` (+ dump `~/backups/scoreodonto_20260622_0005.dump` se necessário).
|
||||
</content>
|
||||
@@ -1,46 +0,0 @@
|
||||
# Qual é a verdade hoje? — Auditoria de infra ScoreOdonto
|
||||
|
||||
> Resposta direta às 6 perguntas essenciais, **verificada em 16/jun/2026**. Sem teoria, sem fantasma.
|
||||
> Detalhes em `arquitetura.md` (real), `deploy.md` (alvo) e `DEPLOY-PRODUCAO.md` (runbook).
|
||||
|
||||
---
|
||||
|
||||
### 1. Onde está o código?
|
||||
- **Fonte da verdade:** Gitea na **VPS3**, repo `ruicesar/scoreodonto.com` (`ssh://git@10.99.0.3/ruicesar/scoreodonto.com.git`).
|
||||
- **Cópia de trabalho (dev):** VPS4 em `/home/deploy/stack/scoreodonto.com` (onde se programa).
|
||||
- **Cópia de produção:** VPS1 em `/home/deploy/stack/scoreodonto.com` (HEAD verificado: `19dbbae`).
|
||||
- **Documentação:** centralizada em `scoreodonto.com/documentação/` (este diretório).
|
||||
|
||||
### 2. Onde está o banco?
|
||||
- **PostgreSQL** no host da **VPS3** (`10.99.0.3:5432`), banco `scoreodonto`, usuário `scoreodonto_user`.
|
||||
- ⚠️ **DEV e PROD usam o MESMO banco.** Não há isolamento. Backup (`pg_dump -Fc`) é a única rede de segurança.
|
||||
- Cache **DragonflyDB** (`10.99.0.3:6379`, índice `/1`). Mensageria **NATS** (`:4222`) e **Temporal** (`:7233`).
|
||||
|
||||
### 3. Onde estão os containers?
|
||||
- **Produção (VPS1, docker rootless `unix:///run/user/1000/docker.sock`):**
|
||||
- `scoreodontocom-scoreodonto-frontend-1` — Up ~5 dias (imagem `:latest` buildada na própria VPS1).
|
||||
- `scoreodontocom-scoreodonto-backend-1` — Up ~5 dias (`:8018`).
|
||||
- `traefik-traefik-1` — borda TLS (Up ~13 dias). Também: rx, newwhats, subdominio, mercado.
|
||||
- **Dev (VPS4, docker root `/var/run/docker.sock`):**
|
||||
- `scoreodontocom-nginx-1` (`0.0.0.0:8020->80`, alvo do Traefik para `dev.`), `scoreodonto-frontend-1` (`:3013`), `scoreodonto-backend-1` (`:8018`).
|
||||
- **Roteamento:** Traefik na VPS1 → `scoreodonto.com` (containers locais) e `dev.scoreodonto.com` → `http://10.99.0.4:8020`.
|
||||
|
||||
### 4. Quem faz deploy?
|
||||
- **Hoje: ninguém automático.** Não há listener nem webhook ativo (o antigo `listener-scoreodonto.js` porta 9002 foi deletado; serviço desligado). **`git push` NÃO dispara deploy.**
|
||||
- O deploy é **manual**: na VPS1, `git pull` + `docker compose build` + `docker compose up -d` (passo a passo em `DEPLOY-PRODUCAO.md`).
|
||||
- Ou seja: **a produção builda a si mesma** hoje (o oposto do alvo "prod não builda", que depende do registry — Etapas 4–6).
|
||||
|
||||
### 5. Como versiona?
|
||||
- `APP_VERSION` em `frontend/constants.ts` (dev hoje **`V1.0.13`**), incrementado por `update-version.js` (patch++).
|
||||
- ⚠️ **Bump é manual** — nada chama `update-version.js` no build (`npm run build` = só `vite build`). Por isso a versão fica parada se ninguém rodar o script.
|
||||
- ✅ **Versão agora é visível** (Etapa 3, 16/jun): rodapé do Sidebar + endpoint `GET /api/version` + log de boot `[VERSION] …`. Carimbo (commit/data/ambiente) injetado no build via build args. **No ar em DEV; produção passa a exibir após o próximo deploy.**
|
||||
|
||||
### 6. Como volta uma versão (rollback)?
|
||||
- **Hoje:** na VPS1, `git checkout <commit-bom>` + `docker compose build` + `up -d` (rebuilda a versão anterior; imagens são `:latest`, sem tag por versão).
|
||||
- Seguro porque as migrações são **aditivas** (voltar código não quebra o schema). Restaurar dump só em caso extremo.
|
||||
- **Alvo (Etapa 6):** rollback = `docker compose pull` da tag anterior no registry, **sem rebuild**.
|
||||
|
||||
---
|
||||
|
||||
## Veredito de uma linha
|
||||
> O código está salvo no Gitea (VPS3) e roda em containers na VPS1 (prod) e VPS4 (dev), ambos no **mesmo banco** da VPS3. **Não há CI/CD automático**: deploy e rollback são manuais via `git pull` + `docker compose build/up` na VPS1, e a **versão é invisível e bumpada à mão** — as duas dores que as Etapas 3–6 vão eliminar.
|
||||
@@ -1,131 +0,0 @@
|
||||
# Arquitetura ScoreOdonto — Estado REAL
|
||||
|
||||
> Documento de **estado atual verificado** (16/jun/2026). Descreve apenas o que existe e está rodando hoje.
|
||||
> Sem referências históricas, sem staging, sem listeners desativados. Para o fluxo-alvo de deploy ver `deploy.md`; para o passo a passo operacional ver `DEPLOY-PRODUCAO.md`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Visão geral
|
||||
|
||||
O ScoreOdonto roda sobre **3 VPS** ligadas por uma rede privada **WireGuard** (`10.99.0.0/24`):
|
||||
|
||||
| VPS | IP interno | Papel |
|
||||
|------|-------------|-------------------------------|
|
||||
| VPS1 | 10.99.0.1 | **Produção** (borda + apps) |
|
||||
| VPS3 | 10.99.0.3 | **Dados & Controle** (Postgres, Gitea) |
|
||||
| VPS4 | 10.99.0.4 | **Desenvolvimento & Builder** |
|
||||
|
||||
```
|
||||
Internet (HTTPS)
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────── VPS1 (10.99.0.1) ─────────────────────────────┐
|
||||
│ Traefik (TLS Let's Encrypt, entrypoint websecure) │
|
||||
│ • scoreodonto.com / www ─────► containers de PRODUÇÃO (local) │
|
||||
│ • dev.scoreodonto.com ─────► http://10.99.0.4:8020 (via WireGuard) │
|
||||
│ │
|
||||
│ Produção (imagens buildadas): │
|
||||
│ scoreodontocom-scoreodonto-frontend-1 (SPA em nginx) │
|
||||
│ scoreodontocom-scoreodonto-backend-1 (Node :8018) │
|
||||
│ (também hospeda: rx, newwhats, subdominio, mercado) │
|
||||
└────────────────────────────────┬──────────────────────────────────────────┘
|
||||
│ WireGuard
|
||||
┌───────────────────────┼───────────────────────┐
|
||||
▼ ▼
|
||||
┌──── VPS3 (10.99.0.3) ────┐ ┌──── VPS4 (10.99.0.4) ──────────┐
|
||||
│ PostgreSQL :5432 │◄────────────│ Desenvolvimento + Builder │
|
||||
│ db: scoreodonto │ (dev E │ nginx (borda dev) :8020→80 │
|
||||
│ Dragonfly :6379 (/1) │ prod │ └─► frontend :3013 │
|
||||
│ NATS :4222 │ usam o │ └─► backend (/api):8018 │
|
||||
│ Temporal :7233 │ MESMO │ Código-fonte em: │
|
||||
│ Gitea :3000 │ banco) │ /home/deploy/stack/ │
|
||||
│ repo: ruicesar/ │ │ scoreodonto.com/ │
|
||||
│ scoreodonto.com │ │ Docker: socket ROOT │
|
||||
│ Registry: NÃO existe │ │ (/var/run/docker.sock) │
|
||||
│ (porta 5000 fechada) │ │ │
|
||||
└───────────────────────────┘ └─────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. VPS1 — Produção (10.99.0.1)
|
||||
|
||||
- **Traefik** (container `traefik-traefik-1`) é a borda: termina TLS (Let's Encrypt, resolver `le`, entrypoint `websecure`) e roteia por `Host`.
|
||||
- `scoreodonto.com` e `www.scoreodonto.com` → containers de produção locais.
|
||||
- `dev.scoreodonto.com` → `http://10.99.0.4:8020` (encaminha para a VPS4 pela WireGuard). Config dinâmica em `traefik/dynamic/dev.scoreodonto.yml`.
|
||||
- **Containers de produção** (imagens já buildadas, project `scoreodontocom`):
|
||||
- `scoreodontocom-scoreodonto-frontend-1` — SPA React servido por nginx.
|
||||
- `scoreodontocom-scoreodonto-backend-1` — API Node/Express na porta `8018`.
|
||||
- **Docker rootless**: o daemon roda no socket do usuário `deploy` (`unix:///run/user/1000/docker.sock`). Comandos `docker` na VPS1 precisam dessa variável `DOCKER_HOST`.
|
||||
- Produção **não builda** o ScoreOdonto: ela sobe imagens prontas. (O `docker-compose.yml` tem `build:`, mas o fluxo correto é trazer a imagem pronta da VPS4 — ver `deploy.md`.)
|
||||
|
||||
## 3. VPS3 — Dados & Controle (10.99.0.3)
|
||||
|
||||
Serviços instalados **direto no host** (não em container):
|
||||
|
||||
- **PostgreSQL** `:5432` — banco `scoreodonto`, usuário `scoreodonto_user`. **Fonte de dados única.**
|
||||
- **DragonflyDB** `:6379` — cache (índice `/1` reservado ao ScoreOdonto).
|
||||
- **NATS** `:4222` e **Temporal** `:7233` — mensageria / workflows.
|
||||
- **Gitea** `:3000` — **fonte oficial da verdade do código**. Repo `ruicesar/scoreodonto.com`, acesso por `ssh://git@10.99.0.3/ruicesar/scoreodonto.com.git`. É o motor de CI/CD (ver `deploy.md`).
|
||||
- **Registry de imagens:** o **Container Registry embutido do Gitea** está ativo e em uso, acessível via HTTPS em `git.clube67.com` (namespace `ruicesar/`). Não há `registry:2` separado. Ver `REGISTRY.md`.
|
||||
|
||||
## 4. VPS4 — Desenvolvimento & Builder (10.99.0.4)
|
||||
|
||||
- **Esta é a máquina de trabalho.** Código-fonte em `/home/deploy/stack/scoreodonto.com/`.
|
||||
- **Docker é ROOT** aqui (socket `/var/run/docker.sock`) — diferente da VPS1.
|
||||
- Containers em execução (project `scoreodontocom`):
|
||||
- `scoreodontocom-nginx-1` — borda do dev, publica `0.0.0.0:8020->80`. É o alvo do Traefik para `dev.scoreodonto.com`.
|
||||
- `scoreodontocom-scoreodonto-frontend-1` — SPA (porta interna `3013`).
|
||||
- `scoreodontocom-scoreodonto-backend-1` — API Node (porta interna `8018`).
|
||||
- **O dev NÃO é Vite HMR.** O container de frontend roda uma **imagem buildada** (`vite build` → `nginx:alpine`); editar `frontend/` ou `backend/` exige **rebuild + recreate** do container para refletir em `dev.scoreodonto.com`. (Existe um override `docker-compose.dev.yml` com Vite/HMR, mas não é o que está no ar.)
|
||||
- O `nginx/default.conf` da VPS4 faz: `/api/` e `/socket.io/` → `scoreodonto-backend:8018`; `/` → `scoreodonto-frontend:3013`.
|
||||
|
||||
### Caminho de uma requisição
|
||||
|
||||
- **Produção:** navegador → `https://scoreodonto.com` → Traefik (VPS1, TLS) → frontend/backend de produção (VPS1) → Postgres (VPS3).
|
||||
- **Dev:** navegador → `https://dev.scoreodonto.com` → Traefik (VPS1, TLS) → `10.99.0.4:8020` (nginx VPS4) → frontend/backend de dev (VPS4) → **mesmo Postgres** (VPS3).
|
||||
|
||||
---
|
||||
|
||||
## 5. ⚠️ Banco compartilhado entre DEV e PROD
|
||||
|
||||
**Dev e produção apontam para o MESMO banco `scoreodonto` na VPS3.** O backend de dev usa `DATABASE_URL=postgresql://scoreodonto_user:<senha>@10.99.0.3:5432/scoreodonto`.
|
||||
|
||||
Consequências práticas:
|
||||
- Migrações rodadas no boot do backend de dev **já alteram o banco real**. As migrações do projeto são **idempotentes e aditivas** (só criam tabelas/colunas), o que torna isso tolerável — mas **não há isolamento**.
|
||||
- A **única rede de segurança** é o backup (`pg_dump scoreodonto -Fc`) antes de mudanças sensíveis.
|
||||
- Isolar um banco `scoreodonto_dev` é o conserto estrutural recomendado para o futuro (fora do escopo deste documento).
|
||||
|
||||
---
|
||||
|
||||
## 6. Stack técnica e portas
|
||||
|
||||
| Componente | Tecnologia | Porta interna |
|
||||
|------------|-----------|----------------|
|
||||
| Frontend | React + Vite + TypeScript + Tailwind (build estático servido por nginx) | 3013 |
|
||||
| Backend | Node.js + Express + driver `pg` nativo (**sem ORM**) | 8018 |
|
||||
| Borda dev (VPS4) | nginx:alpine | 8020 → 80 |
|
||||
| Banco | PostgreSQL 18 | 5432 |
|
||||
| Cache | DragonflyDB (índice `/1`) | 6379 |
|
||||
| Mensageria | NATS / Temporal | 4222 / 7233 |
|
||||
| Git/CI | Gitea | 3000 |
|
||||
| Borda prod | Traefik (TLS Let's Encrypt) | 443 (websecure) |
|
||||
|
||||
**Imagens (Dockerfiles):**
|
||||
- Frontend: multi-stage — `node:20-alpine` (build `npm run build`) → `nginx:alpine` (serve `dist/`).
|
||||
- Backend: `node:20-slim`, `npm install --omit=dev`, `CMD node server.js`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Versionamento (estado atual e limitação)
|
||||
|
||||
- A versão vive em `frontend/constants.ts` (`APP_VERSION`, hoje `V1.0.12`) e é incrementada (patch) por `update-version.js` antes do build.
|
||||
- **Limitação crítica:** a versão **não aparece** no HTML servido — fica embutida no bundle `index-<hash>.js`. Hoje só dá para saber a versão no ar inspecionando o container. É por isso que não se consegue confirmar "subiu mesmo?" pelo navegador. **Tornar a versão visível** (rodapé + endpoint + log de boot) é uma melhoria planejada (ver lista de afazeres / `deploy.md`).
|
||||
|
||||
---
|
||||
|
||||
## 8. O que NÃO existe (para encerrar fantasmas)
|
||||
|
||||
- ❌ **Não existe staging.** Não há `staging.scoreodonto.com`, nem stack `*-staging`, nem banco `scoreodonto_staging`. O único ambiente de teste é `dev.scoreodonto.com` (VPS4), com o banco de produção.
|
||||
- ❌ **Não existe listener de deploy ativo** para o scoreodonto.com. O antigo `listener-scoreodonto.js` (porta 9002) foi deletado e o serviço está desligado; não há webhook do scoreodonto.com registrado no Gitea. Hoje, `git push` **não dispara** deploy automático. (O fluxo-alvo para reativar isso está em `deploy.md`.)
|
||||
- ✅ **Registry:** existe e está em uso — é o Container Registry embutido do Gitea (`git.clube67.com`), não um serviço separado. Ver `REGISTRY.md`.
|
||||
@@ -1,103 +0,0 @@
|
||||
# Deploy ScoreOdonto — Como um commit vira produção (estado-ALVO)
|
||||
|
||||
> Este documento responde **uma** pergunta: **"como um commit vira produção?"**
|
||||
> Descreve o **fluxo-alvo** com **Gitea como motor de CI/CD**. O que já existe hoje está marcado como ✅; o que ainda será construído está marcado como 🔜.
|
||||
> Para a realidade da infra ver `arquitetura.md`. Para o procedimento manual executável **hoje** ver `DEPLOY-PRODUCAO.md`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Princípios (inegociáveis)
|
||||
|
||||
1. **Gitea (VPS3) é a fonte única da verdade** e o motor de CI/CD.
|
||||
2. **Produção (VPS1) nunca builda e nunca recebe código-fonte** — só consome **imagens versionadas** prontas.
|
||||
3. **Build acontece fora da produção** (na CI / na VPS4).
|
||||
4. **Deploy é um ato deliberado por TAG**, nunca automático em todo push (a `main` é usada para desenvolvimento e o banco é compartilhado — promover sem querer é perigoso).
|
||||
5. **Rollback é trocar a imagem** por uma tag anterior — sem rebuild, sem recompilar, sem copiar arquivo.
|
||||
6. **Toda imagem é rastreável**: carrega versão semântica + commit + data de build.
|
||||
|
||||
---
|
||||
|
||||
## 2. O fluxo-alvo, em uma frase
|
||||
|
||||
> **Push** abastece o desenvolvimento e (🔜) **produz uma imagem versionada no registry**;
|
||||
> **criar uma tag `vX.Y.Z`** é o que **promove** essa imagem para produção.
|
||||
|
||||
```
|
||||
git push ──► Gitea (VPS3) ──► CI: build da imagem ──► Registry privado (VPS3)
|
||||
(na main) (versão+commit+data) imagem :vX.Y.Z
|
||||
│
|
||||
criar tag vX.Y.Z ────┘
|
||||
▼
|
||||
VPS1: docker compose pull + up -d
|
||||
(troca o container pela imagem da tag)
|
||||
▼
|
||||
Produção rodando vX.Y.Z
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. As duas metades do fluxo
|
||||
|
||||
### Metade A — Integração contínua (push → imagem) ✅ FEITO
|
||||
1. Desenvolvedor trabalha na VPS4 e valida em `https://dev.scoreodonto.com`.
|
||||
2. `git push origin main` (ou tag `v*`) para o Gitea (VPS3).
|
||||
3. A **CI do Gitea** (Gitea Actions) builda backend+frontend via `docker compose build`, injetando **versão, commit curto e data de build** (build args → `VITE_*`/`process.env`).
|
||||
4. As imagens são enviadas ao **Container Registry do Gitea** com tags `:<versão>`, `:<commit-curto>` e `:latest`.
|
||||
5. **Nada vai para produção aqui.** Esta metade só produz e guarda o artefato.
|
||||
|
||||
> **Implementado em 16/jun/2026.** Workflow: `.gitea/workflows/build.yml`. Runner: `vps4-builder` (act_runner 0.6.1, **modo host** com Docker, serviço systemd `act_runner` na VPS4, label `self-hosted`). Login no registry usa o secret `REGISTRY_TOKEN` se presente, senão a credencial do host. Validado: push na main → build → `scoreodonto-{backend,frontend}:v1.0.13`/`:<commit>`/`:latest` publicados.
|
||||
|
||||
### Metade B — Entrega por tag (tag → produção) ✅ FEITO
|
||||
1. Quando uma versão está aprovada, cria-se uma **tag semântica**: `git tag v1.0.15 && git push origin v1.0.15`.
|
||||
2. A CI builda e publica a imagem `:v1.0.15` no registry **e** o job `deploy` (só em tags `v*`) promove para a VPS1.
|
||||
3. Na VPS1, o `deploy-prod.sh <tag>` faz `docker compose pull` + `up -d --no-build` apontando para a imagem da tag.
|
||||
4. A versão visível (rodapé/endpoint `/api/version`) confirma a tag no ar com `environment: PROD`.
|
||||
|
||||
> **Implementado em 16/jun/2026.** `docker-compose.prod.yml` (imagens do registry, sem build) + `deploy-prod.sh <tag>` (na VPS1) + job `deploy` no workflow (SSH VPS4→VPS1, só em tags). **Push na main NÃO deploya — só tags.** Rollback: `deploy-prod.sh <tag-anterior>`. Validado: v1.0.13 promovida e confirmada (`V1.0.13 · fc0d65f · PROD`).
|
||||
|
||||
---
|
||||
|
||||
## 4. Versionamento visível (pré-requisito da confiança) ✅ FEITO
|
||||
|
||||
> Implementado em 16/jun/2026 (Etapa 3). O build carimba versão/commit/data/ambiente via build args (`GIT_COMMIT`, `BUILD_TIME`, `APP_ENV`, `APP_VERSION`) → `import.meta.env.VITE_*` no frontend e `process.env` no backend. Visível no **rodapé do Sidebar**, no endpoint **`GET /api/version`** e na **linha de boot** `[VERSION] …` dos logs.
|
||||
|
||||
Cada build carimba e **exibe**:
|
||||
|
||||
```
|
||||
ScoreOdonto
|
||||
v1.0.15
|
||||
commit: 7a9b3c1
|
||||
build: 2026-06-16 14:32 UTC
|
||||
ambiente: PRODUÇÃO
|
||||
```
|
||||
|
||||
Visível no **rodapé** da aplicação, em um **endpoint de diagnóstico** (ex.: `GET /api/version`) e na **linha de boot** dos logs. Hoje a versão fica escondida no bundle `index-<hash>.js` (ver `arquitetura.md` §7) — esta é a primeira melhoria de maior valor.
|
||||
|
||||
---
|
||||
|
||||
## 5. Rollback 🔜
|
||||
|
||||
- Listar as tags/imagens no registry da VPS3.
|
||||
- Na VPS1, apontar o compose para a tag anterior (`:v1.0.14`) e `docker compose up -d`.
|
||||
- Recuperação em minutos, **sem rebuild**. O banco é aditivo (ver `arquitetura.md` §5), então voltar o código é seguro; restaurar dump só em caso extremo.
|
||||
|
||||
---
|
||||
|
||||
## 6. Estado de hoje (honesto) ✅/❌
|
||||
|
||||
- ✅ `git push` para o Gitea **funciona** (guarda o código).
|
||||
- ❌ **Não há build automático**, **não há registry**, **não há deploy automático**. O antigo listener (porta 9002) foi deletado e não há webhook do scoreodonto.com no Gitea.
|
||||
- ➡️ Enquanto a CI acima não existe, a promoção para produção é **manual** e está documentada passo a passo em **`DEPLOY-PRODUCAO.md`** (build na VPS4 → enviar imagem para a VPS1 → `up -d`).
|
||||
|
||||
---
|
||||
|
||||
## 7. Roadmap para sair do "hoje" e chegar no "alvo"
|
||||
|
||||
| Etapa | Entrega | Status |
|
||||
|-------|---------|--------|
|
||||
| 3 | Versionamento visível (rodapé + endpoint + log) | ✅ |
|
||||
| 4 | Registry Docker privado na VPS3 | ✅ (Gitea Container Registry — ver `REGISTRY.md`) |
|
||||
| 5 | Pipeline de build no Gitea (push → imagem → registry), **sem deploy** | ✅ (`.gitea/workflows/build.yml` + runner `vps4-builder`) |
|
||||
| 6 | Deploy por TAG (`vX.Y.Z` → produção na VPS1) + rollback por imagem | ✅ (`deploy-prod.sh` + job `deploy` em tags) |
|
||||
|
||||
Quando as Etapas 3–6 estiverem prontas, este documento deixa de ter marcações 🔜 e passa a descrever o fluxo corrente.
|
||||
+61
-4
@@ -8,6 +8,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
|
||||
import { ToastContainer } from './components/Toast.tsx';
|
||||
import { useAppVersion } from './buildInfo.ts';
|
||||
|
||||
import { Dashboard } from './views/Dashboard.tsx';
|
||||
import { LeadsView } from './views/LeadsView.tsx';
|
||||
@@ -30,6 +31,12 @@ import { ReportsView } from './views/ReportsView.tsx';
|
||||
import { ComissoesView } from './views/ComissoesView.tsx';
|
||||
import { GlosasView } from './views/GlosasView.tsx';
|
||||
import { SalaHomeView } from './views/SalaHomeView.tsx';
|
||||
import { LabHomeView } from './views/LabHomeView.tsx';
|
||||
import { LabAgendaView } from './views/LabAgendaView.tsx';
|
||||
import { LabClientesView } from './views/LabClientesView.tsx';
|
||||
import { LabCotacoesView } from './views/LabCotacoesView.tsx';
|
||||
import { LabEquipeView } from './views/LabEquipeView.tsx';
|
||||
import { LabIndicadoresView } from './views/LabIndicadoresView.tsx';
|
||||
import { ConfiguracoesView } from './views/ConfiguracoesView.tsx';
|
||||
import { OrcamentosView } from './views/OrcamentosView.tsx';
|
||||
import { ContratosView } from './views/ContratosView.tsx';
|
||||
@@ -47,6 +54,7 @@ import { AdminGestaoTutoresView } from './views/AdminGestaoTutoresView.tsx';
|
||||
import { WaitingInviteView } from './views/WaitingInviteView.tsx';
|
||||
import { SuperAdminView } from './views/SuperAdminView.tsx';
|
||||
import { DiretorioProfissionaisView } from './views/DiretorioProfissionaisView.tsx';
|
||||
import { ProteseView } from './views/ProteseView.tsx';
|
||||
|
||||
export type ViewKey =
|
||||
| 'landing'
|
||||
@@ -59,6 +67,7 @@ export type ViewKey =
|
||||
| 'financeiro'
|
||||
| 'comissoes'
|
||||
| 'glosas'
|
||||
| 'proteses'
|
||||
| 'login'
|
||||
| 'update'
|
||||
| 'dentistas'
|
||||
@@ -81,6 +90,14 @@ export type ViewKey =
|
||||
| 'minhas-salas'
|
||||
| 'alugar-salas'
|
||||
| 'sala-home'
|
||||
| 'lab-home'
|
||||
| 'lab-bancada'
|
||||
| 'lab-agenda'
|
||||
| 'lab-clientes'
|
||||
| 'lab-financeiro'
|
||||
| 'lab-cotacoes'
|
||||
| 'lab-equipe'
|
||||
| 'lab-indicadores'
|
||||
| 'marketplace-profissionais'
|
||||
| 'criar-clinica'
|
||||
| 'rx-config'
|
||||
@@ -131,6 +148,14 @@ const ROUTE_MAP: Record<string, ViewKey> = {
|
||||
'/minhas-salas': 'minhas-salas',
|
||||
'/alugar-salas': 'alugar-salas',
|
||||
'/sala-home': 'sala-home',
|
||||
'/lab-home': 'lab-home',
|
||||
'/lab-bancada': 'lab-bancada',
|
||||
'/lab-agenda': 'lab-agenda',
|
||||
'/lab-clientes': 'lab-clientes',
|
||||
'/lab-financeiro': 'lab-financeiro',
|
||||
'/lab-cotacoes': 'lab-cotacoes',
|
||||
'/lab-equipe': 'lab-equipe',
|
||||
'/lab-indicadores': 'lab-indicadores',
|
||||
'/marketplace-profissionais': 'marketplace-profissionais',
|
||||
'/criar-clinica': 'criar-clinica',
|
||||
'/rx-config': 'rx-config',
|
||||
@@ -147,6 +172,10 @@ const ROUTE_MAP: Record<string, ViewKey> = {
|
||||
'/superadmin/notificacoes': 'superadmin-notificacoes',
|
||||
'/aguardando-convite': 'aguardando-convite',
|
||||
'/diretorio-profissionais': 'diretorio-profissionais',
|
||||
'/proteses': 'proteses',
|
||||
// Corrige deep-link/refresh: existiam em VIEW_TO_ROUTE mas faltavam aqui (caíam em dashboard).
|
||||
'/comissoes': 'comissoes',
|
||||
'/glosas': 'glosas',
|
||||
};
|
||||
|
||||
const VIEW_TO_ROUTE: Record<ViewKey, string> = {
|
||||
@@ -182,6 +211,14 @@ const VIEW_TO_ROUTE: Record<ViewKey, string> = {
|
||||
'minhas-salas': '/minhas-salas',
|
||||
'alugar-salas': '/alugar-salas',
|
||||
'sala-home': '/sala-home',
|
||||
'lab-home': '/lab-home',
|
||||
'lab-bancada': '/lab-bancada',
|
||||
'lab-agenda': '/lab-agenda',
|
||||
'lab-clientes': '/lab-clientes',
|
||||
'lab-financeiro': '/lab-financeiro',
|
||||
'lab-cotacoes': '/lab-cotacoes',
|
||||
'lab-equipe': '/lab-equipe',
|
||||
'lab-indicadores': '/lab-indicadores',
|
||||
'marketplace-profissionais': '/marketplace-profissionais',
|
||||
'criar-clinica': '/criar-clinica',
|
||||
'rx-config': '/rx-config',
|
||||
@@ -198,6 +235,7 @@ const VIEW_TO_ROUTE: Record<ViewKey, string> = {
|
||||
'superadmin-financeiro': '/superadmin/financeiro',
|
||||
'superadmin-notificacoes': '/superadmin/notificacoes',
|
||||
'diretorio-profissionais': '/diretorio-profissionais',
|
||||
proteses: '/proteses',
|
||||
};
|
||||
|
||||
/** Resolve current URL to a ViewKey */
|
||||
@@ -261,6 +299,7 @@ const MobileBottomNav: React.FC<{
|
||||
|
||||
// ------- App -------
|
||||
const App: React.FC = () => {
|
||||
const ver = useAppVersion();
|
||||
const [isSidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [, forcePwRerender] = useState(0); // re-avalia o modal de troca obrigatória de senha
|
||||
const currentRole = HybridBackend.getCurrentRole();
|
||||
@@ -268,7 +307,8 @@ const App: React.FC = () => {
|
||||
const clinColor = activeWorkspace?.cor || '#2563eb';
|
||||
|
||||
const isViewAllowed = (view: ViewKey, role: string): boolean => {
|
||||
if (['landing', 'login', 'public', 'update'].includes(view)) return true;
|
||||
if (['landing', 'login', 'public'].includes(view)) return true;
|
||||
// /update (migração de banco) é EXCLUSIVA do superadmin (liberado na linha seguinte).
|
||||
if (role === 'superadmin') return true;
|
||||
// Sync GLOBAL removido do app (substituído pelo backup por clínica em Configurações).
|
||||
if (view === 'sync') return false;
|
||||
@@ -277,6 +317,8 @@ const App: React.FC = () => {
|
||||
if (view === 'salas' || view === 'minhas-salas' || view === 'alugar-salas') return isPluginActive('locacao-salas');
|
||||
// Contexto de SALA: isola da clínica — só painel da sala/salas/conta/notificações.
|
||||
if ((HybridBackend.getActiveWorkspace() as any)?.tipo === 'sala') return ['sala-home', 'configuracoes', 'notificacoes', 'clinicas'].includes(view);
|
||||
// Contexto de LABORATÓRIO: área do protético — painel do lab + bancada/conta/notificações.
|
||||
if ((HybridBackend.getActiveWorkspace() as any)?.tipo === 'laboratorio') return ['lab-home', 'lab-bancada', 'lab-agenda', 'lab-clientes', 'lab-cotacoes', 'lab-financeiro', 'lab-equipe', 'lab-indicadores', 'configuracoes', 'notificacoes', 'clinicas'].includes(view);
|
||||
// Catálogo de PLUGINS: EXCLUSIVO do superadmin (já retorna true acima); ninguém mais.
|
||||
if (view === 'plugins') return false;
|
||||
// Admin/donos têm acesso amplo, MAS as telas de superadmin são exclusivas do superadmin.
|
||||
@@ -293,10 +335,10 @@ const App: React.FC = () => {
|
||||
|
||||
const permissions: Record<string, ViewKey[]> = {
|
||||
paciente: ['tratamentos', 'notificacoes', 'configuracoes'],
|
||||
dentista: ['dashboard', 'agenda', 'ortodontia', 'tratamentos', 'lancar-gto', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'rx-radiografias', 'salas', 'marketplace-profissionais', 'candidatura-tutor', 'tutoria-painel', 'diretorio-profissionais'],
|
||||
dentista: ['dashboard', 'agenda', 'ortodontia', 'tratamentos', 'lancar-gto', 'proteses', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'rx-radiografias', 'salas', 'marketplace-profissionais', 'candidatura-tutor', 'tutoria-painel', 'diretorio-profissionais'],
|
||||
biomedico: ['dashboard', 'agenda', 'tratamentos', 'lancar-gto', 'especialidades', 'procedimentos', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'salas', 'marketplace-profissionais', 'diretorio-profissionais'],
|
||||
protetico: ['dashboard', 'agenda', 'tratamentos', 'lancar-gto', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'salas', 'marketplace-profissionais', 'diretorio-profissionais'],
|
||||
funcionario: ['dashboard', 'leads', 'pacientes', 'agenda', 'financeiro', 'tratamentos', 'lancar-gto', 'relatorios', 'notificacoes', 'clinicas', 'configuracoes', 'orcamentos', 'contratos', 'plugins', 'rx-radiografias', 'salas', 'marketplace-profissionais', 'candidatura-tutor', 'diretorio-profissionais'],
|
||||
protetico: ['lab-home', 'lab-bancada', 'lab-agenda', 'lab-clientes', 'lab-cotacoes', 'lab-financeiro', 'lab-equipe', 'lab-indicadores', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'salas', 'marketplace-profissionais', 'diretorio-profissionais'],
|
||||
funcionario: ['dashboard', 'leads', 'pacientes', 'agenda', 'financeiro', 'tratamentos', 'lancar-gto', 'proteses', 'relatorios', 'notificacoes', 'clinicas', 'configuracoes', 'orcamentos', 'contratos', 'plugins', 'rx-radiografias', 'salas', 'marketplace-profissionais', 'candidatura-tutor', 'diretorio-profissionais'],
|
||||
};
|
||||
|
||||
return permissions[role]?.includes(view) || false;
|
||||
@@ -306,6 +348,8 @@ const App: React.FC = () => {
|
||||
if (role === 'superadmin') return 'superadmin-overview';
|
||||
// Contexto de SALA (workspace tipo='sala') sempre cai no painel da sala.
|
||||
if ((HybridBackend.getActiveWorkspace() as any)?.tipo === 'sala') return 'sala-home';
|
||||
// Contexto de LABORATÓRIO (workspace tipo='laboratorio') cai no painel do lab.
|
||||
if ((HybridBackend.getActiveWorkspace() as any)?.tipo === 'laboratorio') return 'lab-home';
|
||||
if (role === 'paciente') return 'tratamentos';
|
||||
if (role === 'donosala') return 'minhas-salas';
|
||||
return 'dashboard';
|
||||
@@ -455,6 +499,7 @@ const App: React.FC = () => {
|
||||
case 'relatorios': return <ReportsView />;
|
||||
case 'comissoes': return <ComissoesView />;
|
||||
case 'glosas': return <GlosasView />;
|
||||
case 'proteses': return <ProteseView />;
|
||||
case 'cadastro-dentista': return <DentistRegisterView />;
|
||||
case 'configuracoes': return <ConfiguracoesView />;
|
||||
case 'orcamentos': return <OrcamentosView />;
|
||||
@@ -465,6 +510,14 @@ const App: React.FC = () => {
|
||||
case 'minhas-salas': return <SalasPlugin view="minhas" />;
|
||||
case 'alugar-salas': return <SalasPlugin view="alugar" />;
|
||||
case 'sala-home': return <SalaHomeView />;
|
||||
case 'lab-home': return <LabHomeView />;
|
||||
case 'lab-bancada': return <ProteseView />;
|
||||
case 'lab-agenda': return <LabAgendaView />;
|
||||
case 'lab-clientes': return <LabClientesView tab="clientes" />;
|
||||
case 'lab-financeiro': return <LabClientesView tab="financeiro" />;
|
||||
case 'lab-cotacoes': return <LabCotacoesView />;
|
||||
case 'lab-equipe': return <LabEquipeView />;
|
||||
case 'lab-indicadores': return <LabIndicadoresView />;
|
||||
case 'marketplace-profissionais': return <ProfissionaisPlugin />;
|
||||
case 'criar-clinica': return <CreateClinicView onCreated={() => { window.location.reload(); }} />;
|
||||
case 'rx-config': return <RxConfigView />;
|
||||
@@ -572,6 +625,10 @@ const App: React.FC = () => {
|
||||
</main>
|
||||
</div>
|
||||
<ToastContainer />
|
||||
{/* Versão sempre visível no canto inferior direito (lê /api/version; não bloqueia cliques) */}
|
||||
<div className="fixed bottom-1.5 right-2 z-40 pointer-events-none select-none text-[10px] font-mono text-gray-400/70">
|
||||
{ver.version}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+24
-6
@@ -1,17 +1,35 @@
|
||||
import { APP_VERSION } from './constants.ts';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
// Metadados de build. commit/build/ambiente são carimbados no build via
|
||||
// variáveis VITE_* (ARGs do Dockerfile do frontend). Em dev sem build args,
|
||||
// caem nos defaults abaixo.
|
||||
// Carimbo do BUILD (bundle). É só FALLBACK enquanto o /api/version não responde.
|
||||
// A FONTE OFICIAL da versão é o backend, em runtime, via useAppVersion().
|
||||
// Não depende mais de APP_VERSION manual: version cai em VITE_APP_VERSION (se o
|
||||
// pipeline injetar) ou 'dev'. commit/build/ambiente seguem carimbados no build (VITE_*).
|
||||
const meta = (import.meta as any).env || {};
|
||||
|
||||
export const BUILD_INFO = {
|
||||
name: 'ScoreOdonto',
|
||||
version: APP_VERSION,
|
||||
version: meta.VITE_APP_VERSION || 'dev',
|
||||
commit: meta.VITE_GIT_COMMIT || 'dev',
|
||||
build: meta.VITE_BUILD_TIME || '',
|
||||
environment: String(meta.VITE_APP_ENV || 'DEV').toUpperCase(),
|
||||
};
|
||||
|
||||
// Rótulo curto: "V1.0.13 · 7a9b3c1 · DEV"
|
||||
// Rótulo curto: "v1.0.15 · 7a9b3c1 · DEV"
|
||||
export const versionLabel = `${BUILD_INFO.version} · ${BUILD_INFO.commit} · ${BUILD_INFO.environment}`;
|
||||
|
||||
// Fonte ÚNICA da versão exibida: o backend em runtime (GET /api/version, público).
|
||||
// Cache em módulo p/ não refazer o fetch a cada tela. Fallback = BUILD_INFO (bundle).
|
||||
let _verCache: any = null;
|
||||
export function useAppVersion() {
|
||||
const [info, setInfo] = useState<any>(_verCache || BUILD_INFO);
|
||||
useEffect(() => {
|
||||
if (_verCache) { setInfo(_verCache); return; }
|
||||
let alive = true;
|
||||
fetch('/api/version')
|
||||
.then(r => (r.ok ? r.json() : null))
|
||||
.then(v => { if (alive && v && v.version) { _verCache = { ...BUILD_INFO, ...v }; setInfo(_verCache); } })
|
||||
.catch(() => {});
|
||||
return () => { alive = false; };
|
||||
}, []);
|
||||
return info;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user