From fcb3fc08fd7c14070b9c3aa3197d585b803828ad Mon Sep 17 00:00:00 2001 From: admbrainsteel Date: Tue, 20 Oct 2026 15:28:21 -0300 Subject: [PATCH] feat: mapa termico de kits de corte e liberacao balanceada para solda --- .../apontamento/kits/MapaTermicoKitsModal.tsx | 53 ++ .../apontamento/kits/MapaTermicoKitsView.tsx | 574 ++++++++++++++++++ src/hooks/useKitsProducao.ts | 461 ++++++++++++++ src/pages/ApontamentoProducao.tsx | 31 +- .../components/SmartComponentesCorteModal.tsx | 411 +++++++++++++ .../smart/components/SmartProducaoFlow.tsx | 329 ++++++++-- 6 files changed, 1816 insertions(+), 43 deletions(-) create mode 100644 src/components/apontamento/kits/MapaTermicoKitsModal.tsx create mode 100644 src/components/apontamento/kits/MapaTermicoKitsView.tsx create mode 100644 src/hooks/useKitsProducao.ts create mode 100644 src/pages/smart/components/SmartComponentesCorteModal.tsx diff --git a/src/components/apontamento/kits/MapaTermicoKitsModal.tsx b/src/components/apontamento/kits/MapaTermicoKitsModal.tsx new file mode 100644 index 0000000..9391f79 --- /dev/null +++ b/src/components/apontamento/kits/MapaTermicoKitsModal.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { MapaTermicoKitsView } from './MapaTermicoKitsView'; +import { PecaKitInfo } from '@/hooks/useKitsProducao'; +import { Flame } from 'lucide-react'; + +interface MapaTermicoKitsModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + ofNumber?: string; + fase?: string; + onSelectPecaParaApontar?: (peca: PecaKitInfo) => void; +} + +export const MapaTermicoKitsModal: React.FC = ({ + open, + onOpenChange, + ofNumber, + fase, + onSelectPecaParaApontar, +}) => { + return ( + + + + + + Mapa Térmico de Kits de Montagem + + + +
+ { + if (onSelectPecaParaApontar) { + onSelectPecaParaApontar(peca); + onOpenChange(false); + } + }} + /> +
+
+
+ ); +}; diff --git a/src/components/apontamento/kits/MapaTermicoKitsView.tsx b/src/components/apontamento/kits/MapaTermicoKitsView.tsx new file mode 100644 index 0000000..339b752 --- /dev/null +++ b/src/components/apontamento/kits/MapaTermicoKitsView.tsx @@ -0,0 +1,574 @@ +import React, { useState, useMemo } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { + Flame, + CheckCircle2, + AlertTriangle, + Clock, + Layers, + Search, + RefreshCw, + ChevronDown, + ChevronUp, + PackageCheck, + ShieldCheck, + Wrench, + Scissors +} from 'lucide-react'; +import { useKitsProducao, PecaKitInfo } from '@/hooks/useKitsProducao'; +import { useOFs } from '@/hooks/useOFs'; + +interface MapaTermicoKitsViewProps { + initialOF?: string; + initialFase?: string; + readOnlyOF?: boolean; + onSelectPecaParaApontar?: (peca: PecaKitInfo) => void; +} + +export const MapaTermicoKitsView: React.FC = ({ + initialOF = '', + initialFase = 'all', + readOnlyOF = false, + onSelectPecaParaApontar, +}) => { + const { ofs, loading: loadingOFs } = useOFs(); + const [selectedOF, setSelectedOF] = useState(initialOF || (ofs[0]?.of_number || '')); + const [selectedFase, setSelectedFase] = useState(initialFase); + const [busca, setBusca] = useState(''); + const [filtroStatus, setFiltroStatus] = useState<'todos' | 'liberados' | 'parciais' | 'travados'>('todos'); + const [pecasExpandidas, setPecasExpandidas] = useState>(new Set()); + + // Atualizar OF padrão se carregar depois + React.useEffect(() => { + if (!selectedOF && ofs.length > 0) { + setSelectedOF(ofs[0].of_number); + } + }, [ofs, selectedOF]); + + const { listaKits, resumoKits, loading, refetch } = useKitsProducao(selectedOF, selectedFase); + + // Extrair fases únicas da lista de kits carregada + const fasesDisponiveis = useMemo(() => { + const s = new Set(); + listaKits.forEach((k) => { + if (k.etapa_fase) s.add(k.etapa_fase); + }); + return Array.from(s).sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' })); + }, [listaKits]); + + // Alternar expansão de componentes de uma peça + const toggleExpand = (pecaId: string) => { + setPecasExpandidas((prev) => { + const next = new Set(prev); + if (next.has(pecaId)) { + next.delete(pecaId); + } else { + next.add(pecaId); + } + return next; + }); + }; + + const expandirTodas = () => { + setPecasExpandidas(new Set(listaKits.map((k) => k.peca_id))); + }; + + const recolherTodas = () => { + setPecasExpandidas(new Set()); + }; + + // Filtragem + const pecasFiltradas = useMemo(() => { + return listaKits.filter((item) => { + // Filtro de status + if (filtroStatus === 'liberados') { + if (item.kits_completos_cortados <= 0) return false; + } else if (filtroStatus === 'parciais') { + if ( + item.kits_completos_cortados >= item.quantidade_total || + item.status_termico === 'vermelho' + ) + return false; + } else if (filtroStatus === 'travados') { + if (item.status_termico !== 'vermelho') return false; + } + + // Filtro de busca textual + if (busca.trim()) { + const t = busca.toLowerCase().trim(); + const bateMarca = item.marca.toLowerCase().includes(t); + const bateDesc = (item.descricao || '').toLowerCase().includes(t); + const batePerfil = (item.perfil_principal || '').toLowerCase().includes(t); + const bateComp = item.componentes.some((c) => + c.marca_componente.toLowerCase().includes(t) || (c.perfil || '').toLowerCase().includes(t) + ); + if (!bateMarca && !bateDesc && !batePerfil && !bateComp) return false; + } + + return true; + }); + }, [listaKits, filtroStatus, busca]); + + return ( +
+ {/* ──────────────── Barra de Controles e Seletores ──────────────── */} + + +
+
+
+ +
+
+

+ Mapa Térmico de Kits de Produção + + Corte ➔ Solda + +

+

+ Prontidão física de montagem: kits cortados disponíveis para liberar a Solda +

+
+
+ + +
+ +
+ {/* Seletor de OF */} +
+ + {readOnlyOF ? ( +
+ OF {selectedOF} +
+ ) : ( + + )} +
+ + {/* Seletor de Fase */} +
+ + +
+ + {/* Busca Rápida */} +
+ +
+ + setBusca(e.target.value)} + className="h-9 pl-9 text-xs" + /> +
+
+
+
+
+ + {/* ──────────────── Cards de KPIs do Mapa Térmico ──────────────── */} + {resumoKits && ( +
+ {/* KPI 1: 100% Liberadas */} +
+
+
+ 100% Liberadas +
+
+ {resumoKits.totalKits100Porcento} +
+
+ peças com todos os kits cortados +
+
+
+ +
+
+ + {/* KPI 2: Kits Montáveis Liberados */} +
+
+
+ Unidades Liberadas +
+
+ {resumoKits.totalUnidadesLiberadasSolda} / {resumoKits.totalUnidadesTotal} un +
+
+ kits prontos na bancada de solda +
+
+
+ +
+
+ + {/* KPI 3: Em Andamento / Quase Prontos */} +
+
+
+ Corte em Andamento +
+
+ {resumoKits.totalKitsParciais} +
+
+ kits parciais (prioridade de corte) +
+
+
+ +
+
+ + {/* KPI 4: Travados / 0% */} +
+
+
+ Não Iniciados +
+
+ {resumoKits.totalKitsNaoIniciados} +
+
+ sem nenhum componente cortado +
+
+
+ +
+
+
+ )} + + {/* ──────────────── Filtros por Status Térmico & Ações em Massa ──────────────── */} +
+
+ + + + +
+ +
+ + +
+
+ + {/* ──────────────── Lista de Peças com Mapa Térmico ──────────────── */} + {loading ? ( +
+
+ Calculando matriz de kits e completude... +
+ ) : pecasFiltradas.length === 0 ? ( + + +

Nenhuma peça encontrada

+

Tente ajustar a busca ou os filtros de status

+
+ ) : ( +
+ {pecasFiltradas.map((peca) => { + const isExpanded = pecasExpandidas.has(peca.peca_id); + const totalKits = peca.quantidade_total; + const kitsProntos = peca.kits_completos_cortados; + const pct = peca.percentual_termico; + const eh100 = kitsProntos >= totalKits; + const temParcial = kitsProntos > 0 && kitsProntos < totalKits; + const gargalo = peca.componente_gargalo; + + // Cores térmicas + let barColor = 'bg-rose-500'; + let badgeBg = 'bg-rose-100 text-rose-800 border-rose-300 dark:bg-rose-950/60 dark:text-rose-300 dark:border-rose-800'; + if (eh100) { + barColor = 'bg-emerald-500'; + badgeBg = 'bg-emerald-100 text-emerald-800 border-emerald-300 dark:bg-emerald-950/60 dark:text-emerald-300 dark:border-emerald-800'; + } else if (temParcial) { + barColor = 'bg-emerald-500'; + badgeBg = 'bg-blue-100 text-blue-800 border-blue-300 dark:bg-blue-950/60 dark:text-blue-300 dark:border-blue-800'; + } else if (pct >= 40) { + barColor = 'bg-amber-500'; + badgeBg = 'bg-amber-100 text-amber-800 border-amber-300 dark:bg-amber-950/60 dark:text-amber-300 dark:border-amber-800'; + } + + return ( +
+ {/* Linha Principal do Card */} +
toggleExpand(peca.peca_id)} + className="p-4 cursor-pointer flex flex-col sm:flex-row sm:items-center justify-between gap-3 hover:bg-slate-50/50 dark:hover:bg-slate-800/40 transition-colors select-none" + > + {/* Lado Esquerdo: Identificação da Peça */} +
+
= 40 + ? 'bg-amber-500 text-slate-950' + : 'bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400' + }`} + > + {pct}% +
+ +
+
+ + {peca.marca} + + + {peca.etapa_fase.toLowerCase().startsWith('fase') ? peca.etapa_fase : `Fase ${peca.etapa_fase}`} + + {peca.perfil_principal && ( + + {peca.perfil_principal} + + )} + {peca.tem_componentes === false && ( + + S/M (Sem Montagem) + + )} +
+ +
+ {peca.descricao || 'Peça Estrutural'} +
+
+
+ + {/* Lado Direito: Status dos Kits e Métricas */} +
+
+
+ + {eh100 + ? `✅ 100% Liberado (${kitsProntos}/${totalKits} un)` + : temParcial + ? `🟢 ${kitsProntos}/${totalKits} kits liberados na Solda` + : pct >= 40 + ? `🟡 0/${totalKits} kits (Corte ${pct}%)` + : `🔴 0/${totalKits} kits (Não iniciado)`} + +
+ + {/* Gargalo em Destaque */} + {gargalo && kitsProntos < totalKits && ( +
+ + + Gargalo: {gargalo.marca_componente} (faltam {gargalo.falta_para_prox_kit} un para próx. kit) + +
+ )} +
+ +
+ {isExpanded ? : } +
+
+
+ + {/* Barra Térmica de Progresso */} +
+
+
+
+
+ + {/* ──────────────── Detalhamento dos Componentes do Kit (Expansível) ──────────────── */} + {isExpanded && ( +
+
+ + + Componentes Requeridos para {totalKits}x {peca.marca} ({peca.componentes.length} itens no kit): + + {onSelectPecaParaApontar && ( + + )} +
+ + {peca.componentes.length === 0 ? ( +

+ Esta peça não possui subcomponentes cadastrados (é fabricada como peça inteira). +

+ ) : ( +
+ {peca.componentes.map((comp) => { + const isGargalo = gargalo?.id === comp.id && kitsProntos < totalKits; + const comp100 = comp.concluido; + + return ( +
+
+
+ + {comp.marca_componente} + + {comp.perfil && ( + + {comp.perfil} + + )} + {isGargalo && ( + + Gargalo + + )} +
+
+ {comp.quantidade_por_peca} un/peça • Total da OF: {comp.quantidade_total_necessaria} un +
+
+ +
+
+ {comp.quantidade_cortada} / {comp.quantidade_total_necessaria} un +
+
+ Dá para {comp.kits_possiveis} {comp.kits_possiveis === 1 ? 'kit' : 'kits'} ({comp.percentual_cortado}%) +
+
+
+ ); + })} +
+ )} +
+ )} +
+ ); + })} +
+ )} +
+ ); +}; diff --git a/src/hooks/useKitsProducao.ts b/src/hooks/useKitsProducao.ts new file mode 100644 index 0000000..e235f65 --- /dev/null +++ b/src/hooks/useKitsProducao.ts @@ -0,0 +1,461 @@ +import { useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { supabase } from '@/integrations/supabase/client'; + +export interface ComponenteKitInfo { + id: string; + peca_id: string; + marca_componente: string; + descricao?: string | null; + perfil?: string | null; + peso_unitario: number; + quantidade_por_peca: number; + quantidade_total_necessaria: number; + quantidade_cortada: number; + kits_possiveis: number; + percentual_cortado: number; + falta_para_prox_kit: number; + concluido: boolean; +} + +export interface PecaKitInfo { + peca_id: string; + marca: string; + descricao?: string | null; + perfil_principal?: string | null; + quantidade_total: number; + etapa_fase: string; + tem_componentes: boolean; + componentes: ComponenteKitInfo[]; + kits_completos_cortados: number; // Quantidade de kits 100% montáveis liberados + percentual_termico: number; // 0 a 100% + status_termico: 'verde' | 'verde_parcial' | 'amarelo' | 'vermelho'; + componente_gargalo: ComponenteKitInfo | null; + peso_total_componentes: number; + peso_cortado_componentes: number; + ja_apontado_solda: number; + saldo_disponivel_solda: number; + bloqueado_para_solda: boolean; + motivo_bloqueio_solda: string | null; +} + +export interface ResumoKitsOF { + totalPecas: number; + totalKits100Porcento: number; + totalKitsParciais: number; + totalKitsNaoIniciados: number; + pesoTotalOFKg: number; + pesoCortadoTotalKg: number; + percentualGeralOF: number; + totalUnidadesTotal: number; + totalUnidadesLiberadasSolda: number; +} + +/** + * Função pura para cálculo determinístico de Kits de Produção. + * Recebe peças, componentes e apontamentos e retorna o mapa térmico de kits. + */ +export const calcularKitsOF = ( + pecas: Array<{ + id: string; + marca: string; + descricao?: string | null; + perfil_principal?: string | null; + quantidade: number; + etapa_fase?: string | null; + tem_componentes?: boolean | null; + }>, + componentes: Array<{ + id: string; + peca_id: string; + marca_componente: string; + descricao?: string | null; + perfil?: string | null; + peso_unitario?: number | null; + quantidade_por_peca?: number | null; + }>, + apontamentos: Array<{ + id?: string; + peca_id?: string | null; + componente_id?: string | null; + processo_id: string; + quantidade_produzida: number; + tipo_apontamento?: string | null; + }>, + processoCorteId?: string, + processoSoldaId?: string +): Map => { + const mapaKits = new Map(); + + // 1. Mapear componentes por peca_id + const componentesPorPeca = new Map(); + componentes.forEach((c) => { + if (!componentesPorPeca.has(c.peca_id)) { + componentesPorPeca.set(c.peca_id, []); + } + componentesPorPeca.get(c.peca_id)!.push(c); + }); + + // 2. Mapear apontamentos de corte por componente_id + const cortadoPorComponente = new Map(); + // Apontamentos de corte por peca_id (caso seja peça sem componentes ou apontamento direto) + const cortadoPecaNoCorte = new Map(); + // Apontamentos de solda por peca_id + const apontadoNaSoldaPorPeca = new Map(); + + apontamentos.forEach((ap) => { + const qtd = Number(ap.quantidade_produzida || 0); + + // Se for apontamento de componente no corte (ou se não especificou processoCorteId, computa todos de componente) + if (ap.tipo_apontamento === 'componente' && ap.componente_id) { + if (!processoCorteId || ap.processo_id === processoCorteId) { + cortadoPorComponente.set( + ap.componente_id, + (cortadoPorComponente.get(ap.componente_id) || 0) + qtd + ); + } + } + + // Se for apontamento de peça mestre no corte + if ( + (!ap.tipo_apontamento || ap.tipo_apontamento === 'peca') && + !ap.componente_id && + ap.peca_id + ) { + if (!processoCorteId || ap.processo_id === processoCorteId) { + cortadoPecaNoCorte.set( + ap.peca_id, + (cortadoPecaNoCorte.get(ap.peca_id) || 0) + qtd + ); + } + } + + // Se for apontamento de solda + if ( + (!ap.tipo_apontamento || ap.tipo_apontamento === 'peca') && + !ap.componente_id && + ap.peca_id + ) { + if (processoSoldaId && ap.processo_id === processoSoldaId) { + apontadoNaSoldaPorPeca.set( + ap.peca_id, + (apontadoNaSoldaPorPeca.get(ap.peca_id) || 0) + qtd + ); + } + } + }); + + // 3. Processar cada peça + pecas.forEach((peca) => { + const compsRaw = componentesPorPeca.get(peca.id) || []; + const qtdTotalPeca = Number(peca.quantidade) || 0; + const fase = peca.etapa_fase || 'Geral'; + const temComps = peca.tem_componentes !== false && compsRaw.length > 0; + const jaApontadoSolda = apontadoNaSoldaPorPeca.get(peca.id) || 0; + + // Cenário A: Peça sem componentes (monobloco / S/M) + if (!temComps) { + const qtdCortada = cortadoPecaNoCorte.get(peca.id) || 0; + const kitsCompletos = Math.min(qtdTotalPeca, qtdCortada); + const pct = qtdTotalPeca > 0 ? Math.min(100, Math.round((kitsCompletos / qtdTotalPeca) * 100)) : 0; + const saldoSolda = Math.max(0, kitsCompletos - jaApontadoSolda); + + mapaKits.set(peca.id, { + peca_id: peca.id, + marca: peca.marca, + descricao: peca.descricao, + perfil_principal: peca.perfil_principal, + quantidade_total: qtdTotalPeca, + etapa_fase: fase, + tem_componentes: false, + componentes: [], + kits_completos_cortados: kitsCompletos, + percentual_termico: pct, + status_termico: + kitsCompletos >= qtdTotalPeca + ? 'verde' + : kitsCompletos > 0 + ? 'verde_parcial' + : pct >= 40 + ? 'amarelo' + : 'vermelho', + componente_gargalo: null, + peso_total_componentes: 0, + peso_cortado_componentes: 0, + ja_apontado_solda: jaApontadoSolda, + saldo_disponivel_solda: peca.tem_componentes === false ? 0 : saldoSolda, + bloqueado_para_solda: peca.tem_componentes === false || saldoSolda <= 0, + motivo_bloqueio_solda: + peca.tem_componentes === false + ? 'Peça S/M (Pula Solda)' + : saldoSolda <= 0 && kitsCompletos === 0 + ? 'Aguardando corte da peça' + : saldoSolda <= 0 + ? `Todos os ${kitsCompletos} kits cortados já foram soldados` + : null, + }); + return; + } + + // Cenário B: Peça com componentes + let pesoTotalComps = 0; + let pesoCortadoComps = 0; + let menorKits = qtdTotalPeca; + let gargaloComp: ComponenteKitInfo | null = null; + let menorRazao = 999999; + + const listaCompsInfo: ComponenteKitInfo[] = []; + + compsRaw.forEach((comp) => { + const porPeca = Math.max(1, Number(comp.quantidade_por_peca) || 1); + const totalNecessario = qtdTotalPeca * porPeca; + const cortado = cortadoPorComponente.get(comp.id) || 0; + const pesoUnit = Number(comp.peso_unitario) || 0; + + const kitsDesteComp = Math.floor(cortado / porPeca); + if (kitsDesteComp < menorKits) { + menorKits = kitsDesteComp; + } + + const pctComp = totalNecessario > 0 ? Math.min(100, Math.round((cortado / totalNecessario) * 100)) : 0; + const pesoTotalComp = totalNecessario * pesoUnit; + const pesoCortadoComp = Math.min(cortado, totalNecessario) * pesoUnit; + + pesoTotalComps += pesoTotalComp; + pesoCortadoComps += pesoCortadoComp; + + const infoComp: ComponenteKitInfo = { + id: comp.id, + peca_id: comp.peca_id, + marca_componente: comp.marca_componente, + descricao: comp.descricao, + perfil: comp.perfil, + peso_unitario: pesoUnit, + quantidade_por_peca: porPeca, + quantidade_total_necessaria: totalNecessario, + quantidade_cortada: cortado, + kits_possiveis: kitsDesteComp, + percentual_cortado: pctComp, + falta_para_prox_kit: 0, // calculado logo abaixo + concluido: cortado >= totalNecessario, + }; + + listaCompsInfo.push(infoComp); + }); + + const kitsCompletos = Math.min(qtdTotalPeca, Math.max(0, menorKits)); + + // Determinar o componente gargalo para o próximo kit + listaCompsInfo.forEach((ci) => { + const necessarioParaProxKit = Math.min(qtdTotalPeca, kitsCompletos + 1) * ci.quantidade_por_peca; + ci.falta_para_prox_kit = Math.max(0, necessarioParaProxKit - ci.quantidade_cortada); + + // Critério de gargalo: quem mais segura a formação de novos kits + const razao = ci.kits_possiveis; + if (razao < menorRazao && kitsCompletos < qtdTotalPeca) { + menorRazao = razao; + gargaloComp = ci; + } + }); + + // Percentual térmico de prontidão ponderado por peso (ou por qtd se peso=0) + let percentualTermico = 0; + if (pesoTotalComps > 0) { + percentualTermico = Math.min(100, Math.round((pesoCortadoComps / pesoTotalComps) * 100)); + } else { + const qtdTotalNecessaria = listaCompsInfo.reduce((s, c) => s + c.quantidade_total_necessaria, 0); + const qtdTotalCortada = listaCompsInfo.reduce((s, c) => s + Math.min(c.quantidade_cortada, c.quantidade_total_necessaria), 0); + percentualTermico = qtdTotalNecessaria > 0 ? Math.min(100, Math.round((qtdTotalCortada / qtdTotalNecessaria) * 100)) : 0; + } + + // Status térmico + let statusTermico: 'verde' | 'verde_parcial' | 'amarelo' | 'vermelho' = 'vermelho'; + if (kitsCompletos >= qtdTotalPeca) { + statusTermico = 'verde'; + } else if (kitsCompletos > 0) { + statusTermico = 'verde_parcial'; + } else if (percentualTermico >= 40) { + statusTermico = 'amarelo'; + } else { + statusTermico = 'vermelho'; + } + + const saldoSolda = Math.max(0, kitsCompletos - jaApontadoSolda); + const bloqueadoSolda = saldoSolda <= 0; + + let motivoBloqueioSolda: string | null = null; + if (bloqueadoSolda) { + if (kitsCompletos === 0) { + const detGargalo = gargaloComp ? ` • Gargalo: ${gargaloComp.marca_componente} (faltam ${gargaloComp.falta_para_prox_kit} un)` : ''; + motivoBloqueioSolda = `Aguardando kits completos de corte (0 un liberadas)${detGargalo}`; + } else if (jaApontadoSolda >= kitsCompletos) { + motivoBloqueioSolda = `Todos os ${kitsCompletos} kits cortados já foram soldados (${jaApontadoSolda}/${qtdTotalPeca} un) • Aguardando novos kits cortados`; + } + } + + mapaKits.set(peca.id, { + peca_id: peca.id, + marca: peca.marca, + descricao: peca.descricao, + perfil_principal: peca.perfil_principal, + quantidade_total: qtdTotalPeca, + etapa_fase: fase, + tem_componentes: true, + componentes: listaCompsInfo, + kits_completos_cortados: kitsCompletos, + percentual_termico: percentualTermico, + status_termico: statusTermico, + componente_gargalo: gargaloComp, + peso_total_componentes: pesoTotalComps, + peso_cortado_componentes: pesoCortadoComps, + ja_apontado_solda: jaApontadoSolda, + saldo_disponivel_solda: saldoSolda, + bloqueado_para_solda: bloqueadoSolda, + motivo_bloqueio_solda: motivoBloqueioSolda, + }); + }); + + return mapaKits; +}; + +/** + * Hook completo para carregar e gerenciar os Kits de uma OF e Fase. + */ +export const useKitsProducao = (ofNumber?: string, fase?: string) => { + const { data, isLoading, refetch, error } = useQuery({ + queryKey: ['kits-producao', ofNumber, fase], + enabled: Boolean(ofNumber), + staleTime: 10000, // 10 segundos + queryFn: async () => { + if (!ofNumber) return { mapaKits: new Map(), resumo: null, lista: [] }; + + // 1. Buscar processos para identificar IDs de Corte e Solda + const { data: procs } = await supabase + .from('processos_fabricacao') + .select('id, nome, ordem') + .order('ordem'); + + const procCorte = (procs || []).find( + (p) => p.nome.toLowerCase().includes('corte') || p.nome.toLowerCase().includes('prepar') || p.ordem === 1 + ); + const procSolda = (procs || []).find( + (p) => p.nome.toLowerCase().includes('solda') || p.nome.toLowerCase().includes('montag') || p.ordem === 3 + ); + + // 2. Buscar peças da OF + let pecasQuery = supabase + .from('pecas') + .select('id, marca, descricao, perfil_principal, quantidade, etapa_fase, tem_componentes') + .eq('of_number', ofNumber); + + if (fase && fase !== 'all' && fase !== 'Todas as fases' && fase !== 'todas') { + pecasQuery = pecasQuery.eq('etapa_fase', fase); + } + + const { data: pecasData, error: pecasErr } = await pecasQuery; + if (pecasErr) throw pecasErr; + + const pecas = pecasData || []; + const pecaIds = pecas.map((p) => p.id); + + // 3. Buscar componentes das peças + let componentes: Array<{ + id: string; + peca_id: string; + marca_componente: string; + descricao?: string | null; + perfil?: string | null; + peso_unitario?: number | null; + quantidade_por_peca?: number | null; + }> = []; + + if (pecaIds.length > 0) { + for (let i = 0; i < pecaIds.length; i += 300) { + const chunk = pecaIds.slice(i, i + 300); + const { data: compsChunk, error: compsErr } = await supabase + .from('componentes_peca') + .select('id, peca_id, marca_componente, descricao, perfil, peso_unitario, quantidade_por_peca') + .in('peca_id', chunk); + + if (compsErr) throw compsErr; + if (compsChunk) { + componentes = [...componentes, ...compsChunk]; + } + } + } + + // 4. Buscar apontamentos da OF + const { data: apontamentosData, error: aptsErr } = await supabase + .from('apontamentos_producao') + .select('id, peca_id, componente_id, processo_id, quantidade_produzida, tipo_apontamento') + .eq('of_number', ofNumber); + + if (aptsErr) throw aptsErr; + + // 5. Calcular kits + const mapaKits = calcularKitsOF( + pecas, + componentes, + apontamentosData || [], + procCorte?.id, + procSolda?.id + ); + + const lista = Array.from(mapaKits.values()).sort((a, b) => + a.marca.localeCompare(b.marca, undefined, { numeric: true, sensitivity: 'base' }) + ); + + // 6. Resumo geral + let total100 = 0; + let totalParciais = 0; + let totalTravados = 0; + let pesoTot = 0; + let pesoCort = 0; + let unTot = 0; + let unLib = 0; + + lista.forEach((item) => { + unTot += item.quantidade_total; + unLib += item.kits_completos_cortados; + pesoTot += item.peso_total_componentes; + pesoCort += item.peso_cortado_componentes; + + if (item.status_termico === 'verde') total100++; + else if (item.status_termico === 'verde_parcial' || item.status_termico === 'amarelo') totalParciais++; + else totalTravados++; + }); + + const resumo: ResumoKitsOF = { + totalPecas: lista.length, + totalKits100Porcento: total100, + totalKitsParciais: totalParciais, + totalKitsNaoIniciados: totalTravados, + pesoTotalOFKg: pesoTot, + pesoCortadoTotalKg: pesoCort, + percentualGeralOF: pesoTot > 0 ? Math.round((pesoCortadoTotalKg(pesoTot, pesoCort)) * 100) : 0, + totalUnidadesTotal: unTot, + totalUnidadesLiberadasSolda: unLib, + }; + + return { + mapaKits, + resumo, + lista, + }; + }, + }); + + return { + kitsPorPeca: data?.mapaKits || new Map(), + listaKits: data?.lista || [], + resumoKits: data?.resumo, + loading: isLoading, + refetch, + error, + }; +}; + +function pesoCortadoTotalKg(total: number, cortado: number): number { + if (total <= 0) return 0; + return Math.min(1, cortado / total); +} diff --git a/src/pages/ApontamentoProducao.tsx b/src/pages/ApontamentoProducao.tsx index 6390283..9875eb3 100644 --- a/src/pages/ApontamentoProducao.tsx +++ b/src/pages/ApontamentoProducao.tsx @@ -3,10 +3,11 @@ import React, { useState } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { ClipboardList, BarChart3, Settings } from 'lucide-react'; +import { ClipboardList, BarChart3, Settings, Flame } from 'lucide-react'; import { ApontamentoForm } from '@/components/apontamento/ApontamentoForm'; import { ApontamentosListOtimizado } from '@/components/apontamento/ApontamentosListOtimizado'; import { ProcessosList } from '@/components/apontamento/ProcessosList'; +import { MapaTermicoKitsView } from '@/components/apontamento/kits/MapaTermicoKitsView'; import { useApontamentosProducao } from '@/hooks/useApontamentosProducao'; import { useIsMobile } from '@/hooks/use-mobile'; import { usePermissionControl } from '@/hooks/usePermissionControl'; @@ -34,7 +35,7 @@ const ApontamentoProducao = () => { { Novo Apontamento + + + Mapa de Kits + { )} + + + + + + Mapa Térmico de Kits de Montagem + +

+ Visão integrada do balanceamento do corte de componentes e liberação de kits completos para montagem/solda +

+
+ + + +
+
+ diff --git a/src/pages/smart/components/SmartComponentesCorteModal.tsx b/src/pages/smart/components/SmartComponentesCorteModal.tsx new file mode 100644 index 0000000..fe4ed14 --- /dev/null +++ b/src/pages/smart/components/SmartComponentesCorteModal.tsx @@ -0,0 +1,411 @@ +import React, { useState } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Input } from '@/components/ui/input'; +import { + Scissors, + CheckCircle2, + AlertTriangle, + Zap, + Plus, + RefreshCw, + Layers, + Wrench, + Flame +} from 'lucide-react'; +import { supabase } from '@/integrations/supabase/client'; +import { useAuth } from '@/hooks/useAuth'; +import { smartAudio } from '@/utils/smartAudio'; +import { toast } from 'sonner'; +import { PecaKitInfo, ComponenteKitInfo } from '@/hooks/useKitsProducao'; + +interface SmartComponentesCorteModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + pecaKit: PecaKitInfo | null; + ofNumber: string; + processoCorteId: string; + onApontamentoSalvo: () => void; +} + +export const SmartComponentesCorteModal: React.FC = ({ + open, + onOpenChange, + pecaKit, + ofNumber, + processoCorteId, + onApontamentoSalvo, +}) => { + const { user } = useAuth(); + const [salvando, setSalvando] = useState(false); + const [quantidadesManual, setQuantidadesManual] = useState>({}); + + if (!pecaKit) return null; + + const hoje = new Date().toISOString().split('T')[0]; + const nomeUsuario = user?.user_metadata?.nome || user?.email?.split('@')[0] || 'Operador Corte'; + + // Atualizar valor digitado para um componente específico + const setQtdComp = (compId: string, valor: number) => { + setQuantidadesManual((prev) => ({ + ...prev, + [compId]: Math.max(1, valor), + })); + }; + + // ───────────────────────────────────────────────────────────── + // AÇÃO 1: CORTAR 1 KIT COMPLETO (ATALHO RÁPIDO) + // ───────────────────────────────────────────────────────────── + const handleCortarKitCompleto = async (multiplicadorKits: number = 1) => { + try { + setSalvando(true); + smartAudio.playClick(); + + if (!pecaKit.componentes || pecaKit.componentes.length === 0) { + toast.error('Esta peça não possui subcomponentes cadastrados.'); + return; + } + + // Preparar lote de inserções para cada componente da peça + const batchPayloads: Array<{ + of_number: string; + peca_id: string; + componente_id: string; + tipo_apontamento: 'componente'; + processo_id: string; + quantidade_produzida: number; + data_apontamento: string; + created_by?: string; + usuario_nome: string; + observacoes: string; + }> = []; + + let pesoTotalKg = 0; + + for (const comp of pecaKit.componentes) { + const porKit = comp.quantidade_por_peca || 1; + const qtdParaApontar = porKit * multiplicadorKits; + const restante = Math.max(0, comp.quantidade_total_necessaria - comp.quantidade_cortada); + + // Não apontar além do total se já estiver concluído, exceto se solicitado + const qtdFinal = Math.min(qtdParaApontar, Math.max(qtdParaApontar, restante)); + + if (qtdFinal > 0) { + batchPayloads.push({ + of_number: ofNumber, + peca_id: pecaKit.peca_id, + componente_id: comp.id, + tipo_apontamento: 'componente', + processo_id: processoCorteId, + quantidade_produzida: qtdFinal, + data_apontamento: hoje, + created_by: user?.id, + usuario_nome: nomeUsuario, + observacoes: `Corte de Kit Completo (${multiplicadorKits}x ${pecaKit.marca}) via Modo Smart`, + }); + + pesoTotalKg += qtdFinal * (comp.peso_unitario || 0); + } + } + + if (batchPayloads.length === 0) { + toast.info('Todos os componentes desta peça já estão 100% cortados!'); + return; + } + + const { error } = await supabase.from('apontamentos_producao').insert(batchPayloads); + if (error) throw error; + + smartAudio.playSuccess(); + toast.success( + `✅ ${multiplicadorKits} Kit Completo da peça ${pecaKit.marca} cortado com sucesso! (+${pesoTotalKg.toFixed(1)} kg)` + ); + + onApontamentoSalvo(); + onOpenChange(false); + } catch (err: any) { + smartAudio.playAlert(); + console.error('Erro ao cortar kit completo:', err); + toast.error('Erro ao registrar kit completo: ' + (err?.message || 'Falha de rede')); + } finally { + setSalvando(false); + } + }; + + // ───────────────────────────────────────────────────────────── + // AÇÃO 2: APONTAR COMPONENTE INDIVIDUAL (DESBALANCEADO / NESTING) + // ───────────────────────────────────────────────────────────── + const handleApontarComponenteIndividual = async (comp: ComponenteKitInfo) => { + try { + const qtd = quantidadesManual[comp.id] || 1; + if (qtd <= 0) return; + + setSalvando(true); + smartAudio.playClick(); + + const payload = { + of_number: ofNumber, + peca_id: pecaKit.peca_id, + componente_id: comp.id, + tipo_apontamento: 'componente' as const, + processo_id: processoCorteId, + quantidade_produzida: qtd, + data_apontamento: hoje, + created_by: user?.id, + usuario_nome: nomeUsuario, + observacoes: `Corte de subcomponente ${comp.marca_componente} (${qtd} un) via Modo Smart`, + }; + + const { error } = await supabase.from('apontamentos_producao').insert(payload); + if (error) throw error; + + const pesoKg = qtd * (comp.peso_unitario || 0); + smartAudio.playSuccess(); + toast.success( + `✂️ Apontado no Corte: ${qtd}x ${comp.marca_componente} (+${pesoKg.toFixed(1)} kg)` + ); + + // Limpar valor digitado + setQuantidadesManual((prev) => { + const next = { ...prev }; + delete next[comp.id]; + return next; + }); + + onApontamentoSalvo(); + } catch (err: any) { + smartAudio.playAlert(); + console.error('Erro ao apontar componente:', err); + toast.error('Erro ao apontar componente: ' + (err?.message || 'Falha de rede')); + } finally { + setSalvando(false); + } + }; + + const kitsProntos = pecaKit.kits_completos_cortados; + const totalKits = pecaKit.quantidade_total; + const pct = pecaKit.percentual_termico; + const gargalo = pecaKit.componente_gargalo; + + return ( + + + + +
+
+ +
+
+
+ + {pecaKit.marca} + + + {pecaKit.etapa_fase.toLowerCase().startsWith('fase') + ? pecaKit.etapa_fase + : `Fase ${pecaKit.etapa_fase}`} + + {pecaKit.perfil_principal && ( + + {pecaKit.perfil_principal} + + )} +
+
+ {pecaKit.descricao || 'Peça Estrutural'} • Total: {totalKits} un +
+
+
+ + = totalKits + ? 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300' + : kitsProntos > 0 + ? 'bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300' + : 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300' + }`} + > + + {kitsProntos}/{totalKits} Kits Montáveis ({pct}%) + +
+
+ +
+ {/* ──────────────── Barra de Prontidão e Gargalo ──────────────── */} +
+
+ Prontidão Térmica do Kit de Montagem + {pct}% dos componentes cortados +
+ +
+
= totalKits + ? 'bg-emerald-500' + : kitsProntos > 0 + ? 'bg-blue-500' + : 'bg-amber-500' + }`} + style={{ width: `${pct}%` }} + /> +
+ + {gargalo && kitsProntos < totalKits && ( +
+ + + Gargalo do Próximo Kit: Falta cortar{' '} + + {gargalo.falta_para_prox_kit} un do componente {gargalo.marca_componente} + {' '} + para liberar mais 1 kit completo na Solda! + +
+ )} +
+ + {/* ──────────────── ATALHO MESTRE: CORTAR 1 KIT COMPLETO ──────────────── */} +
+
+
+ + Atalho Rápido de Corte por Conjunto +
+

+ Corta todos os componentes necessários para fechar mais 1 kit montável de uma só vez. +

+
+ + +
+ + {/* ──────────────── LISTA DE SUBCOMPONENTES PARA CORTE AVULSO ──────────────── */} +
+
+ + + Corte Individual de Subcomponentes (Nesting / Chapas avulsas): + + {pecaKit.componentes.length} subcomponentes +
+ +
+ {pecaKit.componentes.map((comp) => { + const isGargalo = gargalo?.id === comp.id && kitsProntos < totalKits; + const comp100 = comp.concluido; + const valorAtual = quantidadesManual[comp.id] || comp.quantidade_por_peca || 1; + + return ( +
+
+ {/* Info do componente */} +
+
+ + {comp.marca_componente} + + {comp.perfil && ( + + {comp.perfil} + + )} + {isGargalo && ( + + Gargalo do Kit + + )} + {comp100 && ( + + ✅ 100% Cortado + + )} +
+ +
+ {comp.quantidade_por_peca} un/peça • Total da OF:{' '} + {comp.quantidade_total_necessaria} un • Já cortado:{' '} + + {comp.quantidade_cortada} un + {' '} + ({comp.percentual_cortado}%) • {comp.peso_unitario > 0 ? `${comp.peso_unitario.toFixed(2)} kg/un` : ''} +
+
+ + {/* Teclado de corte rápido deste componente */} +
+
+ + + + setQtdComp(comp.id, parseInt(e.target.value) || 1)} + className="w-14 h-7 text-xs text-center font-bold px-1" + /> +
+ + +
+
+
+ ); + })} +
+
+
+ +
+ ); +}; diff --git a/src/pages/smart/components/SmartProducaoFlow.tsx b/src/pages/smart/components/SmartProducaoFlow.tsx index d9f4633..aac9e01 100644 --- a/src/pages/smart/components/SmartProducaoFlow.tsx +++ b/src/pages/smart/components/SmartProducaoFlow.tsx @@ -15,7 +15,8 @@ import { Filter, Delete, Pencil, - CheckCheck + CheckCheck, + Flame } from 'lucide-react'; import { OFAtiva } from '@/hooks/useOFsAtivas'; import { useProcessosFabricacao, ProcessoFabricacao } from '@/hooks/useProcessosFabricacao'; @@ -25,6 +26,9 @@ import { smartAudio } from '@/utils/smartAudio'; import { Button } from '@/components/ui/button'; import { toast } from 'sonner'; import { SmartForcarApontamentoModal } from './SmartForcarApontamentoModal'; +import { calcularKitsOF, PecaKitInfo } from '@/hooks/useKitsProducao'; +import { MapaTermicoKitsModal } from '@/components/apontamento/kits/MapaTermicoKitsModal'; +import { SmartComponentesCorteModal } from './SmartComponentesCorteModal'; interface SmartProducaoFlowProps { obra: OFAtiva; @@ -58,7 +62,8 @@ export const calcularSaldoProcessoPeca = ( peca: PecaData, processoAtual: ProcessoFabricacao, todosProcessos: ProcessoFabricacao[], - mapaProd: Map + mapaProd: Map, + mapaKits?: Map ): CalculoSaldoPeca => { const ehSemMontagem = peca.tem_componentes === false; const keyAtual = `${peca.id}_${processoAtual.id}`; @@ -68,7 +73,7 @@ export const calcularSaldoProcessoPeca = ( const indexAtual = procsOrdenados.findIndex((p) => p.id === processoAtual.id); const nomeProcAtual = (processoAtual.nome || '').toLowerCase(); - const ehSolda = nomeProcAtual.includes('solda'); + const ehSolda = nomeProcAtual.includes('solda') || nomeProcAtual.includes('montag'); let qtdDisponivelParaEntrar = Number(peca.quantidade) || 0; let motivoBloqueio: string | null = null; @@ -77,6 +82,18 @@ export const calcularSaldoProcessoPeca = ( if (ehSemMontagem && ehSolda) { qtdDisponivelParaEntrar = 0; motivoBloqueio = 'Peça S/M (Pula Solda - vai direto para Pintura)'; + } else if (ehSolda && !ehSemMontagem && mapaKits?.has(peca.id)) { + // REGRA 1.1: REGRA DO KIT COMPLETO DE CORTE PARA A SOLDA! + // Apenas peças com componentes 100% cortados (em lotes de kits inteiros) são liberadas para Solda! + const kitInfo = mapaKits.get(peca.id)!; + if (kitInfo.tem_componentes) { + qtdDisponivelParaEntrar = kitInfo.kits_completos_cortados; + if (kitInfo.kits_completos_cortados === 0) { + motivoBloqueio = kitInfo.motivo_bloqueio_solda || 'Aguardando corte completo dos kits'; + } else if (kitInfo.kits_completos_cortados <= jaProduzidoAtual) { + motivoBloqueio = `Todos os ${kitInfo.kits_completos_cortados} kits cortados já foram soldados (${jaProduzidoAtual}/${peca.quantidade})`; + } + } } else if (indexAtual > 0) { // REGRA 2: Encontrar processo anterior válido let procAnteriorValido: ProcessoFabricacao | null = null; @@ -176,12 +193,17 @@ export const SmartProducaoFlow: React.FC = ({ // Modal de Forçar Apontamento const [isForcarModalOpen, setIsForcarModalOpen] = useState(false); + // Estados do Mapa Térmico de Kits e Corte de Componentes + const [isMapaKitsOpen, setIsMapaKitsOpen] = useState(false); + const [pecaParaCorteModal, setPecaParaCorteModal] = useState(null); + const [componentesOF, setComponentesOF] = useState([]); + // Dados carregados da OF const [pecas, setPecas] = useState([]); const [apontamentosExistentes, setApontamentosExistentes] = useState([]); const [carregandoDados, setCarregandoDados] = useState(true); - // Carregar peças e apontamentos da OF + // Carregar peças, componentes e apontamentos da OF const carregarDadosOF = React.useCallback(async () => { try { setCarregandoDados(true); @@ -199,6 +221,22 @@ export const SmartProducaoFlow: React.FC = ({ if (resPecas.data) { setPecas(resPecas.data); + + // Buscar componentes das peças da OF em lotes + const pecaIds = resPecas.data.map((p) => p.id); + if (pecaIds.length > 0) { + let todosComps: any[] = []; + for (let i = 0; i < pecaIds.length; i += 300) { + const chunk = pecaIds.slice(i, i + 300); + const { data: compsData } = await supabase + .from('componentes_peca') + .select('id, peca_id, marca_componente, descricao, perfil, peso_unitario, quantidade_por_peca') + .in('peca_id', chunk); + + if (compsData) todosComps = [...todosComps, ...compsData]; + } + setComponentesOF(todosComps); + } } if (resApontamentos.data) { setApontamentosExistentes(resApontamentos.data as any); @@ -292,6 +330,32 @@ export const SmartProducaoFlow: React.FC = ({ return map; }, [apontamentosPendentes, processoSelecionado]); + // Identificar processos de Corte e Solda + const procCorteId = useMemo(() => { + return processos.find((p) => { + const n = (p.nome || '').toLowerCase(); + return n.includes('corte') || n.includes('prepar') || p.ordem === 1; + })?.id; + }, [processos]); + + const procSoldaId = useMemo(() => { + return processos.find((p) => { + const n = (p.nome || '').toLowerCase(); + return n.includes('solda') || n.includes('montag') || p.ordem === 3; + })?.id; + }, [processos]); + + // Mapa Térmico e de Kits completo para toda a OF + const mapaKits = useMemo(() => { + return calcularKitsOF( + pecas, + componentesOF, + apontamentosExistentes, + procCorteId, + procSoldaId + ); + }, [pecas, componentesOF, apontamentosExistentes, procCorteId, procSoldaId]); + // Peças filtradas pela fase selecionada com cálculo estrito de precedência de processos e regra S/M const pecasComSaldo = useMemo(() => { if (!processoSelecionado) return []; @@ -302,7 +366,7 @@ export const SmartProducaoFlow: React.FC = ({ return (p.etapa_fase || 'Geral').trim() === faseSelecionada; }) .map((p) => { - const info = calcularSaldoProcessoPeca(p, processoSelecionado, processos, mapaProducao); + const info = calcularSaldoProcessoPeca(p, processoSelecionado, processos, mapaProducao, mapaKits); return { ...p, ...info, @@ -399,7 +463,7 @@ export const SmartProducaoFlow: React.FC = ({ return; } - const info = calcularSaldoProcessoPeca(pecaSelecionada, processoSelecionado, processos, mapaProducao); + const info = calcularSaldoProcessoPeca(pecaSelecionada, processoSelecionado, processos, mapaProducao, mapaKits); if (modoEdicao && qtdApontar < info.minPermitido) { smartAudio.playAlert(); toast.error(`Não é possível reduzir para ${qtdApontar}. O processo seguinte (${info.procSeguinteNome}) já consumiu ${info.minPermitido} peças. Ajuste o processo seguinte primeiro.`); @@ -760,6 +824,19 @@ export const SmartProducaoFlow: React.FC = ({ > 🏢 {faseSelecionada} + +
@@ -805,6 +882,10 @@ export const SmartProducaoFlow: React.FC = ({ const temPendencia = pendenciasItem.length > 0; const concluida = item.jaProduzido >= item.quantidade; const aguardandoAnterior = !concluida && item.saldo <= 0; + const kitInfo = mapaKits.get(item.id); + const ehCorte = processoSelecionado?.id === procCorteId; + const ehSolda = processoSelecionado?.id === procSoldaId; + const temComponentes = item.tem_componentes !== false && (kitInfo?.componentes.length || 0) > 0; return (
= ({ ) : ( - {item.motivoBloqueio ? item.motivoBloqueio.slice(0, 22) : 'Aguardando anterior'} + {item.motivoBloqueio ? item.motivoBloqueio.slice(0, 26) : 'Aguardando anterior'} )}
@@ -904,6 +985,68 @@ export const SmartProducaoFlow: React.FC = ({ />
+ {/* MAPA TÉRMICO DE KITS NO CARD (QUANDO PEÇA POSSUI COMPONENTES) */} + {kitInfo && temComponentes && ( +
+
+
+ 0 + ? 'text-orange-500 fill-orange-500 animate-pulse' + : 'text-slate-400' + }`} + /> + + Kits Montáveis no Corte: + + = item.quantidade + ? 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300' + : kitInfo.kits_completos_cortados > 0 + ? 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300' + : 'bg-slate-200 text-slate-700 dark:bg-slate-800 dark:text-slate-400' + }`} + > + {kitInfo.kits_completos_cortados} de {item.quantidade} un + +
+ + {kitInfo.percentual_termico}% peso + +
+ + {/* Mini barra térmica do kit com gradiente */} +
+
= 100 + ? 'bg-emerald-500' + : kitInfo.percentual_termico >= 50 + ? 'bg-amber-500' + : kitInfo.percentual_termico > 0 + ? 'bg-orange-500' + : 'bg-slate-300 dark:bg-slate-700' + }`} + style={{ width: `${kitInfo.percentual_termico}%` }} + /> +
+ + {/* Gargalo se houver */} + {kitInfo.componente_gargalo && kitInfo.kits_completos_cortados < item.quantidade && ( +
+ + ⚠️ Gargalo: {kitInfo.componente_gargalo.marca_componente || kitInfo.componente_gargalo.descricao} (falta {kitInfo.componente_gargalo.falta_para_prox_kit} un p/ próx kit) + + + {kitInfo.componentes.length} itens + +
+ )} +
+ )} + {/* ALERTA VISUAL: BAIXA FORÇADA PENDENTE DE CONFIRMAÇÃO */} {temPendencia && (
@@ -927,40 +1070,107 @@ export const SmartProducaoFlow: React.FC = ({ {/* Botões de Ação no Card */}
- + {ehCorte && temComponentes && kitInfo ? ( + <> + - {!concluida && item.saldo > 0 && ( - + + + ) : ehSolda && temComponentes && kitInfo ? ( + <> + + + {!concluida && item.saldo > 0 && ( + + )} + + ) : ( + <> + + + {!concluida && item.saldo > 0 && ( + + )} + )}
@@ -998,6 +1208,42 @@ export const SmartProducaoFlow: React.FC = ({ }} /> )} + + {/* Modal de Mapa Térmico de Kits */} + { + const ehProcCorte = processoSelecionado?.id === procCorteId; + if (ehProcCorte && pecaKit.tem_componentes) { + setPecaParaCorteModal(pecaKit); + } else { + const pItem = pecasComSaldo.find((p) => p.id === pecaKit.peca_id); + if (pItem && pItem.saldo > 0) { + handleIniciarApontamentoPeca(pItem, pItem.saldo); + } + } + }} + /> + + {/* Modal de Apontamento de Componentes de Corte */} + {pecaParaCorteModal && procCorteId && ( + { + if (!aberto) setPecaParaCorteModal(null); + }} + pecaKit={pecaParaCorteModal} + ofNumber={obra.of_number} + processoCorteId={procCorteId} + onApontamentoSalvo={() => { + carregarDadosOF(); + onApontamentoRealizado(1, 0); + }} + /> + )}
); } @@ -1010,7 +1256,8 @@ export const SmartProducaoFlow: React.FC = ({ pecaSelecionada, processoSelecionado, processos, - mapaProducao + mapaProducao, + mapaKits ); const saldoPendente = infoSaldoAtual.saldo; const maxPermitido = modoEdicao ? infoSaldoAtual.jaProduzido + infoSaldoAtual.saldo : infoSaldoAtual.saldo;