# 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 ; // já renderiza modo 'bancada' p/ role protetico case 'lab-home': return ; // 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).