feat: Implantação do módulo PCP com Mesa Tática Multi-OF e simulação what-if
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
import React, { useState } from 'react';
|
||||
import { StandardPageLayout } from '@/components/layout/StandardPageLayout';
|
||||
import { usePcpAnalysis, RecomendacaoPcp } from '@/hooks/usePcpAnalysis';
|
||||
import { PcpOfThumbnail } from '@/components/pcp/PcpOfThumbnail';
|
||||
import { PcpMesaTatica } from '@/components/pcp/PcpMesaTatica';
|
||||
import { PcpHistogramaCarga } from '@/components/pcp/PcpHistogramaCarga';
|
||||
import { PcpMultiGantt } from '@/components/pcp/PcpMultiGantt';
|
||||
import { PcpRecomendacoesPainel } from '@/components/pcp/PcpRecomendacoesPainel';
|
||||
import { PcpWhatIfSimulator } from '@/components/pcp/PcpWhatIfSimulator';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Layers,
|
||||
BarChart3,
|
||||
Calendar,
|
||||
Sparkles,
|
||||
Sliders,
|
||||
Search,
|
||||
RefreshCw,
|
||||
PlusCircle,
|
||||
HelpCircle,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const PCP = () => {
|
||||
const {
|
||||
loading,
|
||||
allOfs,
|
||||
selectedOfNumbers,
|
||||
ofsNaMesa,
|
||||
totaisMesa,
|
||||
cargaPorPeriodo,
|
||||
recomendacoes,
|
||||
simulador,
|
||||
alternarSelecaoOf,
|
||||
adicionarTodasOfs,
|
||||
limparMesa,
|
||||
setHorasExtras,
|
||||
setEfetivoFator,
|
||||
setDeslocamentoOf,
|
||||
resetarSimulador,
|
||||
recarregarDados,
|
||||
} = usePcpAnalysis();
|
||||
|
||||
const [busca, setBusca] = useState('');
|
||||
const [activeTab, setActiveTab] = useState<'histograma' | 'gantt' | 'recomendacoes' | 'whatif'>('histograma');
|
||||
|
||||
// Filtragem de OFs na gaveta lateral
|
||||
const ofsFiltradas = allOfs.filter((of) => {
|
||||
const termo = busca.toLowerCase();
|
||||
return (
|
||||
of.num_of.toLowerCase().includes(termo) ||
|
||||
of.descritivo.toLowerCase().includes(termo)
|
||||
);
|
||||
});
|
||||
|
||||
// Ação ao clicar em "Simular Impacto" de uma recomendação
|
||||
const handleAplicarRecomendacao = (rec: RecomendacaoPcp) => {
|
||||
if (rec.tipo === 'nivelamento_prazo' && rec.ofAlvo && rec.sugestaoDiasAjuste) {
|
||||
setDeslocamentoOf(rec.ofAlvo, rec.sugestaoDiasAjuste);
|
||||
setActiveTab('whatif');
|
||||
toast.success(`Simulação aplicada: OF ${rec.ofAlvo} dilatada em +${rec.sugestaoDiasAjuste} dias no Simulador!`);
|
||||
} else if (rec.tipo === 'hora_extra' && rec.processoAlvo) {
|
||||
setHorasExtras(rec.processoAlvo, 2);
|
||||
setActiveTab('whatif');
|
||||
toast.success(`Simulação aplicada: +2h extras/dia adicionadas em ${rec.processoAlvo}!`);
|
||||
} else if (rec.tipo === 'rebalanceamento_equipe' && rec.processoAlvo) {
|
||||
setEfetivoFator(rec.processoAlvo, 1.25);
|
||||
setActiveTab('whatif');
|
||||
toast.success(`Simulação aplicada: +25% de capacidade transferida para ${rec.processoAlvo}!`);
|
||||
} else {
|
||||
setActiveTab('whatif');
|
||||
toast.info('Abra os controles do simulador para testar este cenário.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 md:space-y-6 p-2 md:p-0">
|
||||
<StandardPageLayout
|
||||
title="PCP (Planejamento e Controle da Produção)"
|
||||
subtitle="Mesa Tática & Torre de Controle de Capacidade Fabril e Prazos de Obra"
|
||||
badge={{
|
||||
text: `${allOfs.length} OFs Ativas`,
|
||||
variant: 'secondary',
|
||||
}}
|
||||
actions={
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={recarregarDados}
|
||||
disabled={loading}
|
||||
className="text-xs h-9"
|
||||
>
|
||||
<RefreshCw className={`h-3.5 w-3.5 mr-1.5 ${loading ? 'animate-spin' : ''}`} />
|
||||
Atualizar
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
|
||||
{/* COLUNA ESQUERDA: GAVETA DE FIGURINHAS / THUMBNAILS DAS OFS (4 colunas) */}
|
||||
<div className="lg:col-span-4 space-y-4 bg-card/40 border border-border/60 rounded-2xl p-4 backdrop-blur-sm">
|
||||
<div className="flex items-center justify-between pb-3 border-b border-border/40">
|
||||
<div className="flex items-center gap-2">
|
||||
<Layers className="h-4 w-4 text-primary" />
|
||||
<h3 className="font-bold text-sm text-foreground">
|
||||
Catálogo de OFs
|
||||
</h3>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs font-mono">
|
||||
{selectedOfNumbers.length}/{allOfs.length} na mesa
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Barra de Busca de OFs */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Buscar OF ou obra..."
|
||||
value={busca}
|
||||
onChange={(e) => setBusca(e.target.value)}
|
||||
className="pl-8 text-xs h-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-muted-foreground italic">
|
||||
* Dica: Arraste a figurinha para a mesa ou clique nela para incluir/remover do cálculo de capacidade.
|
||||
</p>
|
||||
|
||||
{/* Lista com scroll das Figurinhas */}
|
||||
<div className="space-y-3 max-h-[700px] overflow-y-auto pr-1">
|
||||
{loading ? (
|
||||
<div className="py-12 text-center text-xs text-muted-foreground">
|
||||
Carregando ordens de fabricação e cronogramas...
|
||||
</div>
|
||||
) : ofsFiltradas.length === 0 ? (
|
||||
<div className="py-8 text-center text-xs text-muted-foreground">
|
||||
Nenhuma OF encontrada para o filtro.
|
||||
</div>
|
||||
) : (
|
||||
ofsFiltradas.map((of) => (
|
||||
<PcpOfThumbnail
|
||||
key={of.id}
|
||||
of={of}
|
||||
isSelected={selectedOfNumbers.includes(of.num_of)}
|
||||
onToggle={alternarSelecaoOf}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* COLUNA DIREITA: MESA TÁTICA E VISUALIZADORES ACUMULADOS (8 colunas) */}
|
||||
<div className="lg:col-span-8 space-y-6">
|
||||
{/* 1. MESA TÁTICA DE DROP E AGRUPAMENTO */}
|
||||
<PcpMesaTatica
|
||||
ofsNaMesa={ofsNaMesa}
|
||||
totais={totaisMesa}
|
||||
onRemoveOf={alternarSelecaoOf}
|
||||
onDropOf={(numOf) => {
|
||||
if (!selectedOfNumbers.includes(numOf)) {
|
||||
alternarSelecaoOf(numOf);
|
||||
toast.success(`OF ${numOf} adicionada à mesa de análise acumulada!`);
|
||||
}
|
||||
}}
|
||||
onClear={limparMesa}
|
||||
onSelectAll={adicionarTodasOfs}
|
||||
onAnalisar={() => {
|
||||
setActiveTab('histograma');
|
||||
toast.success('Cenário consolidado processado!');
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 2. ABAS DE VISUALIZAÇÃO DO CENÁRIO ACUMULADO */}
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(val: any) => setActiveTab(val)}
|
||||
className="w-full space-y-4"
|
||||
>
|
||||
<TabsList className="grid grid-cols-2 md:grid-cols-4 w-full h-auto p-1 bg-muted/60">
|
||||
<TabsTrigger
|
||||
value="histograma"
|
||||
className="flex items-center gap-1.5 text-xs py-2"
|
||||
>
|
||||
<BarChart3 className="h-3.5 w-3.5 text-sky-400" />
|
||||
Carga vs Capacidade
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger
|
||||
value="gantt"
|
||||
className="flex items-center gap-1.5 text-xs py-2"
|
||||
>
|
||||
<Calendar className="h-3.5 w-3.5 text-indigo-400" />
|
||||
Cronograma Multi-OF
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger
|
||||
value="recomendacoes"
|
||||
className="flex items-center gap-1.5 text-xs py-2 relative"
|
||||
>
|
||||
<Sparkles className="h-3.5 w-3.5 text-amber-400" />
|
||||
Plano & Ações
|
||||
{recomendacoes.length > 0 && (
|
||||
<span className="w-2 h-2 rounded-full bg-rose-500 inline-block ml-0.5" />
|
||||
)}
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger
|
||||
value="whatif"
|
||||
className="flex items-center gap-1.5 text-xs py-2"
|
||||
>
|
||||
<Sliders className="h-3.5 w-3.5 text-emerald-400" />
|
||||
Simulador What-If
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* ABA 1: HISTOGRAMA DE CARGA VS CAPACIDADE */}
|
||||
<TabsContent value="histograma" className="m-0 focus-visible:outline-none">
|
||||
<PcpHistogramaCarga dadosCarga={cargaPorPeriodo} />
|
||||
</TabsContent>
|
||||
|
||||
{/* ABA 2: CRONOGRAMA GANTT INTEGRADO */}
|
||||
<TabsContent value="gantt" className="m-0 focus-visible:outline-none">
|
||||
<PcpMultiGantt ofs={ofsNaMesa} />
|
||||
</TabsContent>
|
||||
|
||||
{/* ABA 3: RECOMENDAÇÕES E BALANCEAMENTO */}
|
||||
<TabsContent value="recomendacoes" className="m-0 focus-visible:outline-none">
|
||||
<PcpRecomendacoesPainel
|
||||
recomendacoes={recomendacoes}
|
||||
onAplicarSimulacao={handleAplicarRecomendacao}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* ABA 4: SIMULADOR WHAT-IF */}
|
||||
<TabsContent value="whatif" className="m-0 focus-visible:outline-none">
|
||||
<PcpWhatIfSimulator
|
||||
ofs={ofsNaMesa}
|
||||
simulador={simulador}
|
||||
onSetHorasExtras={setHorasExtras}
|
||||
onSetEfetivoFator={setEfetivoFator}
|
||||
onSetDeslocamentoOf={setDeslocamentoOf}
|
||||
onReset={resetarSimulador}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</StandardPageLayout>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PCP;
|
||||
@@ -15,6 +15,7 @@ export { default as Producao } from './Producao';
|
||||
export { default as DiarioProducao } from './DiarioProducao';
|
||||
export { default as ApontamentoProducao } from './ApontamentoProducao';
|
||||
export { default as DashboardProducao } from './DashboardProducao';
|
||||
export { default as PCP } from './PCP';
|
||||
export { default as Expedicao } from './Expedicao';
|
||||
export { default as Obra } from './Obra';
|
||||
export { default as ObraConfiguracoes } from './ObraConfiguracoes';
|
||||
|
||||
Reference in New Issue
Block a user