37cc818f4c
- reúne todos os .md de ScoreOdonto numa pasta única (fonte única, dentro do repo versionado pelo Gitea) - arquitetura.md: reescrito p/ o estado REAL (3 VPS, banco compartilhado dev/prod, sem staging/listener/registry fantasmas) - deploy.md: reescrito p/ o estado-ALVO (Gitea CI/CD: push->build->registry->deploy por tag) - DEPLOY-PRODUCAO.md: runbook operacional executável (fluxo manual real de hoje) - VERDADE-HOJE.md: auditoria (codigo/banco/containers/deploy/versao/rollback) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
345 lines
22 KiB
Markdown
345 lines
22 KiB
Markdown
# 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.)
|