docs(protese): modelo Fase 2 (área do protético) + análise área recepção/TC
- FASE2-PROTETICO-AREA: Laboratório como workspace de 1ª classe (tipo='laboratorio'), roteamento por laboratorio_id, área de gestão própria, financeiro espelhado. - FASE2-IMPL-2.0-2.1: detalhamento de implementação (migrações, backfill, bancada, receita espelhada, menu da área). - ANALISE-AREA-RECEPCAO-TC: gap de permissões da recepção fundamentado em modelos validados (Treatment Coordinator / RBAC / Lab Case Manager). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,206 @@
|
||||
# Fase 2 — Detalhamento de implementação: passos 2.0 e 2.1
|
||||
|
||||
> 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.
|
||||
Reference in New Issue
Block a user