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>
221 lines
11 KiB
Markdown
221 lines
11 KiB
Markdown
# 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.
|