- 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>
11 KiB
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 |
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)
-- 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_idcontinua populado em todas as tabelas (compatibilidade da Fase 1 e do roteamento por pessoa).laboratorio_idpassa 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:
bancadafiltraWHERE protetico_id = me. - Fase 2:
bancadafiltraWHERE laboratorio_id = activeWorkspace.id(com fallbackOR (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):
-- 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;bancadalê 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:
ProdutosModalviralab-catalogo; telalab-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(modoclinica) e o envio de OS. - A Fase 1: GTO finalizada continua abrindo OS (
gto_item_id), agora já comlaboratorio_idpreenchido na criação. protetico_idpermanece 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)
- Um laboratório por protético. A UI assume 1 lab. O modelo de dados já suporta N
(
laboratorio_idpor OS/produto), então multi-lab é expansão futura sem migração. - Laboratório terceirizado (empresa separada que cobra da clínica). A flag
internodegetProteseLaboratoriosfica como gancho futuro, mas não é tratada agora — todo lab gera receita/despesa cruzada entre tenants. - 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 > 0etipo_os ≠ 'garantia'(OS de garantia refaz sem cobrar). Sai no mesmoif (novo === 'entregue'). - Idempotente, no padrão da Fase 1: novo
protese_os.financeiro_lab_idguarda o id do lançamento de receita; se já existe (COALESCE), não duplica. - Isolado em
try/catchpara 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_idprecisa 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.