diff --git a/frontend/App.tsx b/frontend/App.tsx index 6c4d7fe..5e72a89 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -31,6 +31,7 @@ import { ReportsView } from './views/ReportsView.tsx'; import { ComissoesView } from './views/ComissoesView.tsx'; import { GlosasView } from './views/GlosasView.tsx'; import { SalaHomeView } from './views/SalaHomeView.tsx'; +import { LabHomeView } from './views/LabHomeView.tsx'; import { ConfiguracoesView } from './views/ConfiguracoesView.tsx'; import { OrcamentosView } from './views/OrcamentosView.tsx'; import { ContratosView } from './views/ContratosView.tsx'; @@ -84,6 +85,8 @@ export type ViewKey = | 'minhas-salas' | 'alugar-salas' | 'sala-home' + | 'lab-home' + | 'lab-bancada' | 'marketplace-profissionais' | 'criar-clinica' | 'rx-config' @@ -134,6 +137,8 @@ const ROUTE_MAP: Record = { '/minhas-salas': 'minhas-salas', '/alugar-salas': 'alugar-salas', '/sala-home': 'sala-home', + '/lab-home': 'lab-home', + '/lab-bancada': 'lab-bancada', '/marketplace-profissionais': 'marketplace-profissionais', '/criar-clinica': 'criar-clinica', '/rx-config': 'rx-config', @@ -189,6 +194,8 @@ const VIEW_TO_ROUTE: Record = { 'minhas-salas': '/minhas-salas', 'alugar-salas': '/alugar-salas', 'sala-home': '/sala-home', + 'lab-home': '/lab-home', + 'lab-bancada': '/lab-bancada', 'marketplace-profissionais': '/marketplace-profissionais', 'criar-clinica': '/criar-clinica', 'rx-config': '/rx-config', @@ -287,6 +294,8 @@ const App: React.FC = () => { if (view === 'salas' || view === 'minhas-salas' || view === 'alugar-salas') return isPluginActive('locacao-salas'); // Contexto de SALA: isola da clínica — só painel da sala/salas/conta/notificações. if ((HybridBackend.getActiveWorkspace() as any)?.tipo === 'sala') return ['sala-home', 'configuracoes', 'notificacoes', 'clinicas'].includes(view); + // Contexto de LABORATÓRIO: área do protético — painel do lab + bancada/conta/notificações. + if ((HybridBackend.getActiveWorkspace() as any)?.tipo === 'laboratorio') return ['lab-home', 'lab-bancada', 'configuracoes', 'notificacoes', 'clinicas'].includes(view); // Catálogo de PLUGINS: EXCLUSIVO do superadmin (já retorna true acima); ninguém mais. if (view === 'plugins') return false; // Admin/donos têm acesso amplo, MAS as telas de superadmin são exclusivas do superadmin. @@ -305,7 +314,7 @@ const App: React.FC = () => { paciente: ['tratamentos', 'notificacoes', 'configuracoes'], dentista: ['dashboard', 'agenda', 'ortodontia', 'tratamentos', 'lancar-gto', 'proteses', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'rx-radiografias', 'salas', 'marketplace-profissionais', 'candidatura-tutor', 'tutoria-painel', 'diretorio-profissionais'], biomedico: ['dashboard', 'agenda', 'tratamentos', 'lancar-gto', 'especialidades', 'procedimentos', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'salas', 'marketplace-profissionais', 'diretorio-profissionais'], - protetico: ['dashboard', 'agenda', 'tratamentos', 'lancar-gto', 'proteses', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'salas', 'marketplace-profissionais', 'diretorio-profissionais'], + protetico: ['dashboard', 'agenda', 'tratamentos', 'lancar-gto', 'proteses', 'lab-home', 'lab-bancada', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'salas', 'marketplace-profissionais', 'diretorio-profissionais'], funcionario: ['dashboard', 'leads', 'pacientes', 'agenda', 'financeiro', 'tratamentos', 'lancar-gto', 'proteses', 'relatorios', 'notificacoes', 'clinicas', 'configuracoes', 'orcamentos', 'contratos', 'plugins', 'rx-radiografias', 'salas', 'marketplace-profissionais', 'candidatura-tutor', 'diretorio-profissionais'], }; @@ -316,6 +325,8 @@ const App: React.FC = () => { if (role === 'superadmin') return 'superadmin-overview'; // Contexto de SALA (workspace tipo='sala') sempre cai no painel da sala. if ((HybridBackend.getActiveWorkspace() as any)?.tipo === 'sala') return 'sala-home'; + // Contexto de LABORATÓRIO (workspace tipo='laboratorio') cai no painel do lab. + if ((HybridBackend.getActiveWorkspace() as any)?.tipo === 'laboratorio') return 'lab-home'; if (role === 'paciente') return 'tratamentos'; if (role === 'donosala') return 'minhas-salas'; return 'dashboard'; @@ -476,6 +487,8 @@ const App: React.FC = () => { case 'minhas-salas': return ; case 'alugar-salas': return ; case 'sala-home': return ; + case 'lab-home': return ; + case 'lab-bancada': return ; case 'marketplace-profissionais': return ; case 'criar-clinica': return { window.location.reload(); }} />; case 'rx-config': return ; diff --git a/frontend/components/Sidebar.tsx b/frontend/components/Sidebar.tsx index f83dd84..0c23fa3 100644 --- a/frontend/components/Sidebar.tsx +++ b/frontend/components/Sidebar.tsx @@ -61,6 +61,8 @@ export const Sidebar: React.FC = ({ activeTab, setActiveTab }) => { id: 'superadmin-financeiro', label: 'FINANCEIRO', icon: DollarSign }, { id: 'superadmin-notificacoes', label: 'NOTIFICAÇÕES', icon: Bell }, { id: 'sala-home', label: 'PAINEL DA SALA', icon: DoorOpen }, + { id: 'lab-home', label: 'PAINEL DO LAB', icon: LayoutDashboard }, + { id: 'lab-bancada', label: 'BANCADA', icon: FlaskConical }, { id: 'dashboard', label: 'VISÃO GERAL', icon: LayoutDashboard }, { id: 'agenda', label: 'AGENDA', icon: CalendarIcon }, { id: 'pacientes', label: 'PACIENTES', icon: Users }, @@ -97,11 +99,17 @@ export const Sidebar: React.FC = ({ activeTab, setActiveTab }) => // PAINEL DA SALA só existe quando o workspace ativo é uma sala. if (item.id === 'sala-home') return (activeWorkspace as any)?.tipo === 'sala'; + // PAINEL DO LAB / BANCADA só existem no workspace de laboratório. + if (item.id === 'lab-home' || item.id === 'lab-bancada') return (activeWorkspace as any)?.tipo === 'laboratorio'; // Contexto de SALA (workspace tipo='sala'): isola da clínica — menu base só conta/notificações. // (MINHAS SALAS / ALUGAR SALAS são itens de plugin, mostrados à parte.) if ((activeWorkspace as any)?.tipo === 'sala') return ['sala-home', 'notificacoes', 'configuracoes'].includes(item.id); + // Contexto de LABORATÓRIO (workspace tipo='laboratorio'): área própria do protético — + // painel do lab + bancada de produção, sem as telas clínicas da clínica. + if ((activeWorkspace as any)?.tipo === 'laboratorio') return ['lab-home', 'lab-bancada', 'notificacoes', 'configuracoes'].includes(item.id); + // plugins e gestão de tutores: exclusivo superadmin if (item.id === 'plugins' || item.id === 'admin-gestao-tutores') return false; diff --git a/frontend/views/LabHomeView.tsx b/frontend/views/LabHomeView.tsx new file mode 100644 index 0000000..f2e8223 --- /dev/null +++ b/frontend/views/LabHomeView.tsx @@ -0,0 +1,124 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { FlaskConical, Wallet, Clock, AlertTriangle, CheckCircle2, RefreshCw, Inbox } from 'lucide-react'; +import { HybridBackend } from '../services/backend.ts'; +import { PageHeader } from '../components/PageHeader.tsx'; + +const fmtBRL = (v: number) => 'R$ ' + Number(v || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 }); + +// Painel do LABORATÓRIO (protético): aparece quando o workspace ativo é tipo='laboratorio'. +// Resumo a partir do que já existe — a Bancada (OS) + o financeiro do workspace do lab. +export const LabHomeView: React.FC = () => { + const ws: any = HybridBackend.getActiveWorkspace(); + const [os, setOs] = useState([]); + const [fin, setFin] = useState([]); + const [loading, setLoading] = useState(false); + + const carregar = useCallback(async () => { + setLoading(true); + try { + const [b, f] = await Promise.all([HybridBackend.getProteseBancada(), HybridBackend.getFinanceiro()]); + setOs(Array.isArray(b) ? b : []); + setFin(Array.isArray(f) ? f : []); + } catch { /* silent */ } finally { setLoading(false); } + }, []); + useEffect(() => { carregar(); }, [carregar]); + + const ativas = os.filter(o => o.status !== 'entregue' && o.status !== 'cancelado'); + const atrasadas = os.filter(o => o.atrasada).length; + const ymMes = new Date().toISOString().slice(0, 7); + const entreguesMes = os.filter(o => o.status === 'entregue' && (o.updated_at || '').slice(0, 7) === ymMes).length; + // Faturamento do mês: RECEITA de prótese lançada no workspace do lab (espelho da entrega). + const fatMes = fin + .filter(l => l.origem === 'protese' && (l.data_competencia || l.datavencimento || '').slice(0, 7) === ymMes) + .reduce((s, l) => s + Number(l.valor || 0), 0); + + const KPI: React.FC<{ titulo: string; valor: string; cor: string; Icon: any; sub?: string }> = ({ titulo, valor, cor, Icon, sub }) => ( +
+
+

{titulo}

+
{valor}
+ {sub &&

{sub}

} +
+ ); + + // Distribuição por status (fila de produção). + const STATUS_ORD = ['solicitado', 'recebido', 'producao', 'prova', 'ajuste', 'finalizado']; + const STATUS_LABEL: Record = { solicitado: 'Solicitado', recebido: 'Recebido', producao: 'Produção', prova: 'Prova', ajuste: 'Ajuste', finalizado: 'Finalizado' }; + const porStatus = STATUS_ORD.map(s => ({ s, n: ativas.filter(o => o.status === s).length })).filter(x => x.n > 0); + + return ( +
+ + + + +
+
+
+

{ws?.nome || 'Laboratório'}

+

Área de gestão do protético

+
+
+ + {loading ?
: ( + <> +
+ + + + +
+ + {/* Fila por status */} +
+
+ +

Fila de produção

+
+ {porStatus.length === 0 ? ( +
Nenhuma OS em produção.
+ ) : ( +
+ {porStatus.map(({ s, n }) => ( +
+

{STATUS_LABEL[s]}

+

{n}

+
+ ))} +
+ )} +
+ + {/* Próximas entregas */} +
+
+

Próximas entregas

+
+ {ativas.filter(o => o.prazo_entrega).length === 0 ? ( +
Sem prazos agendados.
+ ) : ( +
+ + {['Paciente', 'Tipo', 'Prazo', 'Status', 'Valor'].map(h => )} + + {ativas.filter(o => o.prazo_entrega).sort((a, b) => (a.prazo_entrega || '').localeCompare(b.prazo_entrega || '')).slice(0, 12).map(o => ( + + + + + + + + ))} + +
{h}
{o.paciente_nome || '—'}{o.tipo || '—'}{(o.prazo_entrega || '').slice(0, 10).split('-').reverse().join('/')}{STATUS_LABEL[o.status] || o.status}{fmtBRL(o.custo)}
+
+ )} +
+ + )} +
+ ); +};