Files
scoreodonto.com/doc/FASE2-PROTETICO-AREA.md
VPS 4 Builder 718a7886e1 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>
2026-06-23 17:49:36 +02:00

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_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):

-- 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.