feat(protese): módulo de OS de prótese (clínica ↔ laboratório/protético)
build-and-push / build (push) Successful in 54s
build-and-push / deploy (push) Successful in 3m38s

- backend: tabelas protese_os/_evento/_foto/_produtos + endpoints /api/protese/*
  (laboratorios, os, bancada, status, foto/raw assinado, produtos); catálogo do protético;
  custo de lab → DESPESA na entrega (idempotente, estorno no delete) + garantia com foto.
- Acesso: helper proteticoAcessivelPelaClinica (interno por vínculo OU diretório) valida o POST
  e o catálogo; catálogos passam por tenantGuard; correção do label da notificação (tipo resolvido).
- Frontend: ProteseView (Bancada do protético / Minhas OS da clínica) + rota /proteses
  + item de menu PRÓTESES no Sidebar (dentista/protético/funcionário).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Builder
2026-06-17 05:20:44 +02:00
parent a991f1fe2c
commit f2eb90ccc8
5 changed files with 961 additions and 6 deletions
+73
View File
@@ -742,6 +742,79 @@ export const HybridBackend = {
await apiFetch(`${API_URL}/glosas/${id}?clinicaId=${encodeURIComponent(clinicaId)}`, { method: 'DELETE' });
},
// ── Próteses · Ordem de Serviço (clínica ↔ laboratório/protético) ───────────
getProteseLaboratorios: async () => {
const clinicaId = HybridBackend.getActiveWorkspace()?.id || '';
const res = await apiFetch(`${API_URL}/protese/laboratorios?clinicaId=${encodeURIComponent(clinicaId)}`);
return await res.json();
},
getProteseOS: async (opts: { status?: string; paciente_id?: string } = {}) => {
const clinicaId = HybridBackend.getActiveWorkspace()?.id || '';
const qs = new URLSearchParams({ clinicaId });
if (opts.status) qs.set('status', opts.status);
if (opts.paciente_id) qs.set('paciente_id', opts.paciente_id);
const res = await apiFetch(`${API_URL}/protese/os?${qs.toString()}`);
return await res.json();
},
getProteseBancada: async (status?: string) => {
const qs = status ? `?status=${encodeURIComponent(status)}` : '';
const res = await apiFetch(`${API_URL}/protese/bancada${qs}`);
return await res.json();
},
getProteseOSDetalhe: async (id: string) => {
const res = await apiFetch(`${API_URL}/protese/os/${id}`);
return await res.json();
},
createProteseOS: async (payload: any) => {
const clinicaId = HybridBackend.getActiveWorkspace()?.id || '';
const res = await apiFetch(`${API_URL}/protese/os?clinicaId=${encodeURIComponent(clinicaId)}`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao criar OS');
return await res.json();
},
setProteseOSStatus: async (id: string, status: string, observacao?: string) => {
const res = await apiFetch(`${API_URL}/protese/os/${id}/status`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status, observacao }) });
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao mudar status');
return await res.json();
},
uploadProteseFoto: async (id: string, file: File) => {
const fd = new FormData();
fd.append('foto', file);
const res = await apiFetch(`${API_URL}/protese/os/${id}/foto`, { method: 'POST', body: fd });
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao anexar foto');
return await res.json();
},
deleteProteseOS: async (id: string) => {
const clinicaId = HybridBackend.getActiveWorkspace()?.id || '';
await apiFetch(`${API_URL}/protese/os/${id}?clinicaId=${encodeURIComponent(clinicaId)}`, { method: 'DELETE' });
},
// Catálogo do protético (preço fixo + garantia)
getProteseProdutos: async () => {
const res = await apiFetch(`${API_URL}/protese/produtos`);
return await res.json();
},
getLaboratorioProdutos: async (proteticoId: string) => {
const clinicaId = HybridBackend.getActiveWorkspace()?.id || '';
const res = await apiFetch(`${API_URL}/protese/laboratorios/${proteticoId}/produtos?clinicaId=${encodeURIComponent(clinicaId)}`);
return await res.json();
},
createProteseProduto: async (payload: { nome: string; preco?: number; garantia_meses?: number }) => {
const res = await apiFetch(`${API_URL}/protese/produtos`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao criar produto');
return await res.json();
},
updateProteseProduto: async (id: string, payload: any) => {
const res = await apiFetch(`${API_URL}/protese/produtos/${id}`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao salvar produto');
return await res.json();
},
deleteProteseProduto: async (id: string) => {
await apiFetch(`${API_URL}/protese/produtos/${id}`, { method: 'DELETE' });
},
// ── GTO (builder → finaliza → lança no financeiro) ──────────────────────────
createGtoBuilder: async (payload: { pacienteId: string; dentistaId: string; credenciadaId: string }) => {
const res = await apiFetch(`${API_URL}/gto-builder`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });