718a7886e1
- 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>
207 lines
10 KiB
Markdown
207 lines
10 KiB
Markdown
# 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).
|