From 9981d5c1dd175745c78b63cb01d0da607a6c7ef5 Mon Sep 17 00:00:00 2001 From: Marcos Reifonas Date: Wed, 23 Sep 2026 14:54:33 +0000 Subject: [PATCH] Ajustes Modo Smart: tema claro/escuro, processos sincronizados com desktop e gerar lista de selecao --- .../viewer3d/SmartModelViewer3D.tsx | 461 ++++++++++++++---- .../smart/components/SmartViewer3DFlow.tsx | 109 +++-- 2 files changed, 433 insertions(+), 137 deletions(-) diff --git a/src/components/viewer3d/SmartModelViewer3D.tsx b/src/components/viewer3d/SmartModelViewer3D.tsx index bdc4b1e..8bf45a9 100644 --- a/src/components/viewer3d/SmartModelViewer3D.tsx +++ b/src/components/viewer3d/SmartModelViewer3D.tsx @@ -3,6 +3,8 @@ import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import { LoadedIFCResult, PieceInfo } from '@/lib/ifc/ifcLoaderService'; import { ViewCube } from './ViewCube'; +import { SelectionListModal, SelectedPieceItem } from './SelectionListModal'; +import { useTheme } from '@/hooks/useTheme'; import { smartAudio } from '@/utils/smartAudio'; import { ArrowLeft, @@ -14,17 +16,25 @@ import { CheckCircle2, Clock, Box, - Check + Check, + Sun, + Moon, + FileSpreadsheet } from 'lucide-react'; export interface ProductionPieceStatus { + pecaId?: string; marca: string; - etapa_fase: string; + fase?: string; + etapa_fase?: string; totalQtd: number; pointedQtd: number; currentProcessName?: string; processColor?: string; processOrdem?: number; + processesCompleted?: string[]; + processPointedQtds?: Record; + hasDetalhamento?: boolean; } interface SmartModelViewer3DProps { @@ -36,13 +46,14 @@ interface SmartModelViewer3DProps { onVoltar: () => void; } +// Ordem e cores oficiais sincronizadas com o menu de processos do modo completo (Desktop) const PROCESS_LIST = [ - { id: 'all', nome: 'Todos os Processos', cor: '#06b6d4' }, + { id: 'all', nome: 'Todos os Processos', cor: '#475569' }, { id: 'corte', nome: 'Corte', cor: '#3b82f6' }, - { id: 'montagem', nome: 'Montagem', cor: '#6366f1' }, { id: 'solda', nome: 'Solda', cor: '#f97316' }, { id: 'pintura', nome: 'Pintura', cor: '#10b981' }, - { id: 'concluido', nome: 'Concluído', cor: '#22c55e' }, + { id: 'expedicao', nome: 'Expedição', cor: '#06b6d4' }, + { id: 'montagem', nome: 'Montagem', cor: '#8b5cf6' }, ]; export const SmartModelViewer3D: React.FC = ({ @@ -62,10 +73,16 @@ export const SmartModelViewer3D: React.FC = ({ const modelGroupRef = useRef(null); const raycasterRef = useRef(new THREE.Raycaster()); + const { theme, setTheme } = useTheme(); + const isDark = theme === 'dark'; + // Filtros principais solicitados: Fase e Processo const [selectedPhase, setSelectedPhase] = useState('all'); const [selectedProcess, setSelectedProcess] = useState('all'); + // Modal de Lista da Seleção + const [isSelectionModalOpen, setIsSelectionModalOpen] = useState(false); + // Seleção e Toast Discreto interface SelectedElement { expressID: number; @@ -97,6 +114,43 @@ export const SmartModelViewer3D: React.FC = ({ }) ); + const gridRef = useRef(null); + + // Função para atualizar cores do piso de acordo com o tema claro / escuro + const updateGridTheme = useCallback((gridHelper: THREE.GridHelper) => { + const isDarkMode = document.documentElement.classList.contains('dark') || theme === 'dark'; + const centerColor = isDarkMode ? new THREE.Color(0x475569) : new THREE.Color(0x94a3b8); + const gridColor = isDarkMode ? new THREE.Color(0x1e293b) : new THREE.Color(0xe2e8f0); + + const colors = gridHelper.geometry.attributes.color as THREE.BufferAttribute; + if (colors) { + const colorArray = colors.array as Float32Array; + for (let i = 0; i < colorArray.length; i += 6) { + const isCenter = i === Math.floor(colorArray.length / 2); + const c = isCenter ? centerColor : gridColor; + colorArray[i] = c.r; + colorArray[i + 1] = c.g; + colorArray[i + 2] = c.b; + colorArray[i + 3] = c.r; + colorArray[i + 4] = c.g; + colorArray[i + 5] = c.b; + } + colors.needsUpdate = true; + } + + if (gridHelper.material && (gridHelper.material as THREE.Material)) { + (gridHelper.material as THREE.Material).transparent = true; + (gridHelper.material as THREE.Material).opacity = isDarkMode ? 0.35 : 0.45; + } + }, [theme]); + + // Atualiza cores do grid quando o tema mudar + useEffect(() => { + if (gridRef.current) { + updateGridTheme(gridRef.current); + } + }, [theme, updateGridTheme]); + // Rastreamento para diferenciar gesto touch/drag de toque simples (Tap) const pointerDownRef = useRef<{ x: number; y: number; time: number }>({ x: 0, y: 0, time: 0 }); @@ -109,7 +163,7 @@ export const SmartModelViewer3D: React.FC = ({ // Cena const scene = new THREE.Scene(); - scene.background = null; // Fundo transparente controlado por Tailwind + scene.background = null; // Fundo transparente controlado por Tailwind (modo claro / escuro) sceneRef.current = scene; // Câmera Perspectiva otimizada para campo de visão amplo em telas touch @@ -134,7 +188,6 @@ export const SmartModelViewer3D: React.FC = ({ controls.rotateSpeed = 0.8; controls.zoomSpeed = 1.0; controls.panSpeed = 0.8; - // 1 dedo: Rotação suave | 2 dedos: Pinça para zoom + arrastar para pan controls.touches = { ONE: THREE.TOUCH.ROTATE, TWO: THREE.TOUCH.DOLLY_PAN, @@ -153,12 +206,12 @@ export const SmartModelViewer3D: React.FC = ({ dirLight2.position.set(-40, -20, -40); scene.add(dirLight2); - // Grid suave de chão + // Grid suave de chão sincronizado com o tema claro/escuro const grid = new THREE.GridHelper(60, 60, 0x06b6d4, 0x94a3b8); - (grid.material as THREE.Material).opacity = 0.15; - (grid.material as THREE.Material).transparent = true; grid.position.y = -0.05; + updateGridTheme(grid); scene.add(grid); + gridRef.current = grid; // Grupo de Modelo const group = new THREE.Group(); @@ -190,7 +243,7 @@ export const SmartModelViewer3D: React.FC = ({ window.removeEventListener('resize', handleResize); renderer.dispose(); }; - }, []); + }, [updateGridTheme]); // 2. Centralizar Modelo na Visão (Fit View) const fitModelToView = useCallback(() => { @@ -235,78 +288,170 @@ export const SmartModelViewer3D: React.FC = ({ }, 150); }, [modelData, fitModelToView]); - // 4. Aplicação de Filtros (Fase e Processo) e Cores de Produção + // 4. Aplicação de Filtros (Fase e Processo) e Cores de Produção idêntico ao modo desktop useEffect(() => { if (!modelGroupRef.current) return; - const targetPhaseClean = selectedPhase !== 'all' ? String(selectedPhase).trim().toLowerCase() : null; - const targetProcessNorm = selectedProcess !== 'all' - ? selectedProcess.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase() - : null; - modelGroupRef.current.traverse((child) => { if ((child as THREE.Mesh).isMesh) { const mesh = child as THREE.Mesh; - const meshPhase = mesh.userData.phase ? String(mesh.userData.phase).trim().toLowerCase() : ''; - const pieceMark = mesh.userData.pieceMark ? String(mesh.userData.pieceMark).trim().toUpperCase() : ''; - const prod = productionData?.get(pieceMark); + const pmark = String(mesh.userData.pieceMark || '').trim().toUpperCase(); + const cleanMark = String(mesh.userData.cleanMark || pmark).trim().toUpperCase(); + const assMark = String(mesh.userData.assemblyMark || '').trim().toUpperCase(); + const cleanAssMark = String(mesh.userData.cleanAssemblyMark || cleanMark).trim().toUpperCase(); - // 4.1. Filtro de Fase: visibilidade da malha - let isPhaseVisible = true; - if (targetPhaseClean) { - isPhaseVisible = meshPhase === targetPhaseClean; + // 1. Extração da fase deste elemento + let elementPhase = ''; + if (mesh.userData.phase) { + elementPhase = String(mesh.userData.phase).trim(); + } + if (!elementPhase) { + const raw = assMark || pmark; + const match = raw.match(/^(?:[A-Za-z0-9]+-)?(\d+)-/i); + if (match) { + elementPhase = match[1]; + } } - mesh.visible = isPhaseVisible; + // 2. Determinação do vínculo de produção (com matching rigoroso de fase) + let prod: ProductionPieceStatus | undefined = undefined; + if (productionData && productionData.size > 0) { + const searchKeys = [ + elementPhase && selectedOF ? `${selectedOF}-${elementPhase}-${cleanAssMark}` : '', + elementPhase ? `${elementPhase}-${cleanAssMark}` : '', + elementPhase && selectedOF ? `${selectedOF}-${elementPhase}-${cleanMark}` : '', + elementPhase ? `${elementPhase}-${cleanMark}` : '', + assMark, + pmark, + cleanAssMark, + cleanMark + ].filter(Boolean).map(k => k.toUpperCase()); + + for (const key of searchKeys) { + const match = productionData.get(key); + if (match) { + if (!elementPhase || !match.fase || String(match.fase) === String(elementPhase)) { + prod = match; + break; + } + } + } + } + + // 3. Filtro de Fase: visibilidade da malha + const isPhaseVisible = + !selectedPhase || + selectedPhase === 'all' || + elementPhase === selectedPhase || + String(elementPhase).toLowerCase() === String(selectedPhase).toLowerCase(); + + mesh.visible = isPhaseVisible; if (!isPhaseVisible) return; - // 4.2. Aplicação de Cor por Processo / Produção - let pieceColorHex = '#a1a1aa'; // Padrão cinza neutro - let isDimmed = false; + // 4. Mapeamento de processos industriais (Corte, Solda, Pintura, Expedição, Montagem) + const procPointedMap: Record = (prod && prod.processPointedQtds) || {}; + const procsDone: string[] = Array.isArray(prod?.processesCompleted) ? prod.processesCompleted : []; - if (prod && prod.pointedQtd > 0) { - const currNorm = (prod.currentProcessName || '') - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, '') - .toLowerCase(); + const hasMontagem = Boolean(procPointedMap['montagem'] > 0 || procsDone.some((p) => p.includes('montag'))); + const hasExpedicao = Boolean(procPointedMap['expedicao'] > 0 || procsDone.some((p) => p.includes('exped'))); + const hasPintura = Boolean( + procPointedMap['pintura'] > 0 || + procPointedMap['pintura/galv'] > 0 || + procPointedMap['pintura / galvanizacao'] > 0 || + procPointedMap['galvanizacao'] > 0 || + procsDone.some((p) => p.includes('pint') || p.includes('galv')) + ); + const hasSolda = Boolean(procPointedMap['solda'] > 0 || procsDone.some((p) => p.includes('sold'))); + const hasCorte = Boolean(procPointedMap['corte'] > 0 || procsDone.some((p) => p.includes('corte'))); + const hasDetalhamento = Boolean( + (procPointedMap['detalhamento'] && procPointedMap['detalhamento'] > 0) || + (procPointedMap['detalh'] && procPointedMap['detalh'] > 0) || + procsDone.some((p) => p.includes('detalh')) || + prod?.hasDetalhamento + ); - // Determina a cor base do processo - if (currNorm.includes('corte')) pieceColorHex = '#3b82f6'; - else if (currNorm.includes('montag')) pieceColorHex = '#6366f1'; - else if (currNorm.includes('solda')) pieceColorHex = '#f97316'; - else if (currNorm.includes('pintu')) pieceColorHex = '#10b981'; - else if (currNorm.includes('conclu')) pieceColorHex = '#22c55e'; - else pieceColorHex = prod.processColor || '#10b981'; + const hasAnyApontamento = hasMontagem || hasExpedicao || hasPintura || hasSolda || hasCorte || hasDetalhamento; - // Se houver filtro de processo ativo, atenua peças de outros processos - if (targetProcessNorm && !currNorm.includes(targetProcessNorm)) { - isDimmed = true; + const normalizeStr = (s: string) => s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(); + const targetProcKey = normalizeStr(String(selectedProcess || 'all')); + + const PROCESS_COLOR_MAP: Record = { + 'corte': '#3b82f6', + 'solda': '#f97316', + 'pintura': '#10b981', + 'expedicao': '#06b6d4', + 'montagem': '#8b5cf6', + }; + + if (selectedProcess !== 'all') { + // --- FILTRO POR PROCESSO ESPECÍFICO --- + let isPointedInThisProcess = false; + if (targetProcKey === 'corte') isPointedInThisProcess = hasCorte; + else if (targetProcKey === 'solda') isPointedInThisProcess = hasSolda; + else if (targetProcKey === 'pintura') isPointedInThisProcess = hasPintura; + else if (targetProcKey === 'expedicao') isPointedInThisProcess = hasExpedicao; + else if (targetProcKey === 'montagem') isPointedInThisProcess = hasMontagem; + else isPointedInThisProcess = Boolean(procPointedMap[targetProcKey] > 0 || procsDone.includes(targetProcKey)); + + if (isPointedInThisProcess) { + // SÓLIDO COM A COR DO PROCESSO SELECIONADO (OPACO 100%) + mesh.material = new THREE.MeshStandardMaterial({ + color: new THREE.Color(PROCESS_COLOR_MAP[targetProcKey] || '#22c55e'), + metalness: 0.35, + roughness: 0.45, + depthWrite: true, + side: THREE.DoubleSide, + }); + } else { + // SÓLIDO TRANSLÚCIDO ATENUADO (Ghost Solid a 8% - sem linhas) + mesh.material = new THREE.MeshStandardMaterial({ + color: new THREE.Color('#94a3b8'), + metalness: 0.1, + roughness: 0.8, + transparent: true, + opacity: 0.08, + depthWrite: false, + side: THREE.DoubleSide, + }); } } else { - // Sem apontamento na produção: cinza atenuado se houver filtro de processo - if (targetProcessNorm) { - isDimmed = true; - } - } + // --- MODO "TODOS OS PROCESSOS" --- + if (hasAnyApontamento) { + // RENDERIZA EM SÓLIDO COM A COR DA ETAPA MAIS AVANÇADA + let pieceColorHex = '#a1a1aa'; + if (hasMontagem) { + pieceColorHex = '#8b5cf6'; + } else if (hasExpedicao) { + pieceColorHex = '#06b6d4'; + } else if (hasPintura) { + pieceColorHex = '#10b981'; + } else if (hasSolda) { + pieceColorHex = '#f97316'; + } else if (hasCorte) { + pieceColorHex = '#3b82f6'; + } else if (hasDetalhamento) { + pieceColorHex = '#a1a1aa'; + } - // Se está atenuado pelo filtro de processo, aplica opacidade fantasma - if (isDimmed) { - mesh.material = new THREE.MeshStandardMaterial({ - color: new THREE.Color('#94a3b8'), - metalness: 0.1, - roughness: 0.8, - transparent: true, - opacity: 0.12, - depthWrite: false, - side: THREE.DoubleSide, - }); - } else { - mesh.material = new THREE.MeshStandardMaterial({ - color: new THREE.Color(pieceColorHex), - metalness: 0.3, - roughness: 0.45, - side: THREE.DoubleSide, - }); + mesh.material = new THREE.MeshStandardMaterial({ + color: new THREE.Color(pieceColorHex), + metalness: 0.35, + roughness: 0.45, + depthWrite: true, + side: THREE.DoubleSide, + }); + } else { + // NÃO TEM NENHUM APONTAMENTO: SÓLIDO TRANSLÚCIDO ATENUADO (Ghost Solid a 8%) + mesh.material = new THREE.MeshStandardMaterial({ + color: new THREE.Color('#94a3b8'), + metalness: 0.1, + roughness: 0.8, + transparent: true, + opacity: 0.08, + depthWrite: false, + side: THREE.DoubleSide, + }); + } } // 4.3. Preserva o destaque magenta se a peça estiver selecionada pelo operador @@ -319,7 +464,7 @@ export const SmartModelViewer3D: React.FC = ({ } } }); - }, [selectedPhase, selectedProcess, productionData]); + }, [selectedPhase, selectedProcess, productionData, selectedOF]); // 5. Interação Touch / Clique na Peça (Single Tap) const handlePointerDown = (e: React.PointerEvent) => { @@ -406,6 +551,7 @@ export const SmartModelViewer3D: React.FC = ({ }); const pmark = hitMesh.userData.pieceMark; + const phase = hitMesh.userData.phase; const piece: PieceInfo = (modelData && modelData.pieceByExpressID.get(targetIfcId)) || { expressID: targetIfcId, guid: '', @@ -413,7 +559,7 @@ export const SmartModelViewer3D: React.FC = ({ type: 'PIECE', pieceMark: pmark, section: hitMesh.userData.section, - phase: hitMesh.userData.phase, + phase: phase, }; nextMap.set(targetIfcId, { @@ -424,8 +570,22 @@ export const SmartModelViewer3D: React.FC = ({ setSelectedElementsMap(nextMap); smartAudio.playSuccess(); - // Abre o Toast discreto ao lado do toque - const prodInfo = pmark ? productionData?.get(pmark) : undefined; + // Abre o Toast discreto ao lado do toque com matching enriquecido + let prodInfo: ProductionPieceStatus | undefined = undefined; + if (productionData && productionData.size > 0 && pmark) { + const searchKeys = [ + phase && selectedOF ? `${selectedOF}-${phase}-${pmark}` : '', + phase ? `${phase}-${pmark}` : '', + selectedOF ? `${selectedOF}-${pmark}` : '', + pmark, + pmark.toUpperCase(), + ].filter(Boolean); + for (const k of searchKeys) { + const m = productionData.get(k); + if (m) { prodInfo = m; break; } + } + } + setToastInfo({ piece, position: { x: e.clientX, y: e.clientY }, @@ -450,6 +610,40 @@ export const SmartModelViewer3D: React.FC = ({ const selectedCount = selectedElementsMap.size; + // Lista consolidada de peças físicas para o modal de seleção (Print 3) + const selectedPiecesList: SelectedPieceItem[] = useMemo(() => { + return Array.from(selectedElementsMap.values()).map(({ expressID, piece }) => { + const pmark = piece.pieceMark || 'Sem Marca'; + const phase = piece.phase; + let prod: ProductionPieceStatus | undefined = undefined; + if (productionData && productionData.size > 0 && pmark) { + const searchKeys = [ + phase && selectedOF ? `${selectedOF}-${phase}-${pmark}` : '', + phase ? `${phase}-${pmark}` : '', + selectedOF ? `${selectedOF}-${pmark}` : '', + pmark, + pmark.toUpperCase(), + ].filter(Boolean); + for (const k of searchKeys) { + const m = productionData.get(k); + if (m) { prod = m; break; } + } + } + + return { + id: String(expressID), + pieceMark: pmark, + phase: piece.phase, + section: piece.section, + name: piece.name, + pointedQty: prod?.pointedQtd, + totalQty: prod?.totalQtd, + currentStage: prod?.currentProcessName, + stageColor: prod?.processColor, + }; + }); + }, [selectedElementsMap, productionData, selectedOF]); + // Orientação da Câmera pelo ViewCube const handleViewCubeSelect = (view: 'top' | 'bottom' | 'front' | 'back' | 'left' | 'right' | 'iso') => { if (!cameraRef.current || !controlsRef.current) return; @@ -476,11 +670,9 @@ export const SmartModelViewer3D: React.FC = ({ let left = toastInfo.position.x - rect.left + 14; let top = toastInfo.position.y - rect.top - 20; - // Se estiver muito à direita, joga para a esquerda do cursor if (left + 230 > rect.width) { left = Math.max(12, left - 245); } - // Se estiver muito abaixo, sobe if (top + 160 > rect.height) { top = Math.max(12, rect.height - 170); } @@ -494,14 +686,14 @@ export const SmartModelViewer3D: React.FC = ({ ref={containerRef} onPointerDown={handlePointerDown} onPointerUp={handlePointerUp} - className="relative w-full h-full flex-1 bg-slate-950 text-slate-100 select-none overflow-hidden touch-none" + className="relative w-full h-full flex-1 bg-slate-50 dark:bg-slate-950 text-slate-800 dark:text-slate-100 select-none overflow-hidden touch-none transition-colors duration-200" > {/* Canvas Three.js */}
{/* 1. Header Minimalista Flutuante (Touch-First) */}
- {/* Lado Esquerdo: Botão Voltar + Info da Obra */} + {/* Lado Esquerdo: Botão Voltar + Info da Obra + Botão Modo Claro/Escuro (Print 1) */}
-
- +
+ OF {selectedOF} - + {cliente || 'Obra Industrial'}
+ + {/* Botão Modo Claro / Modo Escuro (Print 1) */} +
{/* Lado Direito: Filtros Discretos (Fase & Processo) + Enquadrar */} @@ -533,7 +743,7 @@ export const SmartModelViewer3D: React.FC = ({ - +
- {/* Filtro por Processo */} + {/* Filtro por Processo (Print 2: ordem dos botões de processos do modo completo) */}
- +
{/* Botão Enquadrar (Fit View) */}
- {/* 2. Top Right: Cubo 3D Compacto + Botão Limpar Seleção */} + {/* 2. Top Right: Cubo 3D Compacto + Card de Ações da Seleção (Print 3) */}
{/* Cubo Compacto Discreto */} = ({ compact={true} /> - {/* Botão Limpar Seleção (Apenas quando houver peças marcadas) */} + {/* Card Flutuante de Seleção com "Gerar lista da seleção" (Print 3) */} {selectedCount > 0 && ( - + {/* Badge Indicador de Seleção Ativa */} +
+ + + {selectedCount} {selectedCount === 1 ? 'marcada' : 'marcadas'} + +
+ + {/* Botão Gerar Lista da Seleção (Print 3 - idêntico ao modo completo) */} + + + {/* Botão Limpar Seleção */} + +
)} @@ -609,14 +845,14 @@ export const SmartModelViewer3D: React.FC = ({ setToastInfo(null); }} style={toastStyle} - className="absolute z-40 p-3 rounded-2xl bg-slate-900/95 backdrop-blur-xl border border-fuchsia-500/50 shadow-2xl shadow-fuchsia-950/50 text-slate-100 min-w-[210px] max-w-[260px] animate-in fade-in zoom-in-95 duration-150 cursor-pointer pointer-events-auto select-none transition-all group" + className="absolute z-40 p-3 rounded-2xl bg-white/95 dark:bg-slate-900/95 backdrop-blur-xl border border-fuchsia-500/50 shadow-2xl shadow-fuchsia-950/20 text-slate-800 dark:text-slate-100 min-w-[210px] max-w-[260px] animate-in fade-in zoom-in-95 duration-150 cursor-pointer pointer-events-auto select-none transition-all group" title="Toque no toast para fechar (a peça continua marcada)" > {/* Cabeçalho do Toast */} -
+
- + {toastInfo.piece.pieceMark || 'Sem Marca'}
@@ -626,7 +862,7 @@ export const SmartModelViewer3D: React.FC = ({ e.stopPropagation(); setToastInfo(null); }} - className="p-1 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition-colors" + className="p-1 rounded-lg text-slate-400 hover:text-slate-700 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors" > @@ -635,21 +871,21 @@ export const SmartModelViewer3D: React.FC = ({ {/* Dados Rápidos da Peça */}
{toastInfo.piece.phase && ( -
+
Fase: - Fase {toastInfo.piece.phase} + Fase {toastInfo.piece.phase}
)} {toastInfo.piece.section && ( -
+
Perfil: - + {toastInfo.piece.section}
)} {toastInfo.prod && ( -
+
Status: = ({
{/* Dica para Fechar */} -
+
Toque no card para fechar ✕
)} + + {/* Modal de Lista e Exportação da Seleção 3D (Print 3) */} + setIsSelectionModalOpen(false)} + selectedPieces={selectedPiecesList} + selectedOF={selectedOF} + onClearSelection={handleClearSelection} + />
); }; diff --git a/src/pages/smart/components/SmartViewer3DFlow.tsx b/src/pages/smart/components/SmartViewer3DFlow.tsx index d9f7ab6..6f4d3f9 100644 --- a/src/pages/smart/components/SmartViewer3DFlow.tsx +++ b/src/pages/smart/components/SmartViewer3DFlow.tsx @@ -78,43 +78,94 @@ export const SmartViewer3DFlow: React.FC = ({ if (pecasRes.data) { pecasRes.data.forEach((p: any) => { - const marca = p.marca ? String(p.marca).trim().toUpperCase() : ''; - const fase = p.etapa_fase ? String(p.etapa_fase).trim() : ''; + const marcaStr = String(p.marca || '').trim(); + const fase = String(p.etapa_fase || '').trim(); if (fase) phasesSet.add(fase); - if (marca) { - const existing = prodMap.get(marca); - if (!existing) { - prodMap.set(marca, { - marca, - etapa_fase: fase, - totalQtd: Number(p.quantidade || 1), - pointedQtd: 0, - currentProcessName: 'Pendente', - processColor: '#94a3b8', - processOrdem: 0, - }); - } else { - existing.totalQtd += Number(p.quantidade || 0); - } + const item: ProductionPieceStatus = { + pecaId: p.id, + marca: marcaStr, + fase, + etapa_fase: fase, + totalQtd: Number(p.quantidade || 1), + pointedQtd: 0, + currentProcessName: 'Pendente', + processColor: '#64748b', + processOrdem: 0, + processesCompleted: [], + processPointedQtds: {}, + }; + + // Indexações para amarração gráfica flexível idêntica ao modo completo + prodMap.set(marcaStr, item); + prodMap.set(marcaStr.toUpperCase(), item); + prodMap.set(`${fase}-${marcaStr}`, item); + prodMap.set(`${fase}-${marcaStr}`.toUpperCase(), item); + prodMap.set(`${obra.of_number}-${fase}-${marcaStr}`, item); + prodMap.set(`${obra.of_number}-${fase}-${marcaStr}`.toUpperCase(), item); + prodMap.set(`${obra.of_number}-${marcaStr}`, item); + prodMap.set(`${obra.of_number}-${marcaStr}`.toUpperCase(), item); + + if (p.descricao) { + prodMap.set(`DESC:${String(p.descricao).trim().toUpperCase()}`, item); + } + if (p.perfil_principal) { + prodMap.set(`PERFIL:${String(p.perfil_principal).trim().toUpperCase()}`, item); } }); } if (apontamentosRes.data) { - apontamentosRes.data.forEach((ap: any) => { - const marca = ap.peca?.marca ? String(ap.peca.marca).trim().toUpperCase() : ''; - if (marca && prodMap.has(marca)) { - const item = prodMap.get(marca)!; - const q = Number(ap.quantidade_produzida || 0); - const procNome = ap.processo?.nome || 'Produção'; - const procOrdem = Number(ap.processo?.ordem || 0); + const PROCESS_COLORS: Record = { + 'Detalhamento': '#3b82f6', + 'Corte': '#3b82f6', + 'Solda': '#f97316', + 'Pintura': '#10b981', + 'Pintura/Galv': '#10b981', + 'Expedicao': '#06b6d4', + 'Expedição': '#06b6d4', + 'Montagem': '#8b5cf6', + 'Concluido': '#22c55e', + }; - if (q > 0 && procOrdem >= (item.processOrdem || 0)) { - item.pointedQtd = Math.max(item.pointedQtd, q); - item.currentProcessName = procNome; - item.processColor = ap.processo?.cor || '#10b981'; - item.processOrdem = procOrdem; + const sortedApontamentos = [...apontamentosRes.data].sort( + (a: any, b: any) => (a.processo?.ordem || 0) - (b.processo?.ordem || 0) + ); + + sortedApontamentos.forEach((ap: any) => { + const marca = ap.peca?.marca ? String(ap.peca.marca).trim() : ''; + const fase = ap.peca?.etapa_fase ? String(ap.peca.etapa_fase).trim() : ''; + if (marca) { + const existing = + prodMap.get(`${obra.of_number}-${fase}-${marca}`) || + prodMap.get(`${fase}-${marca}`) || + prodMap.get(marca); + + if (existing) { + const qty = Number(ap.quantidade_produzida || 0); + const procRaw = String(ap.processo?.nome || '').trim(); + const procNorm = procRaw.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(); + + const isDetalhamento = procNorm.includes('detalh'); + + if (qty > 0) { + if (!existing.processPointedQtds) existing.processPointedQtds = {}; + if (!existing.processesCompleted) existing.processesCompleted = []; + + existing.processPointedQtds[procNorm] = (existing.processPointedQtds[procNorm] || 0) + qty; + if (!existing.processesCompleted.includes(procNorm)) { + existing.processesCompleted.push(procNorm); + } + + if (!isDetalhamento) { + existing.pointedQtd = Math.max(existing.pointedQtd, qty); + existing.currentProcessName = procRaw || existing.currentProcessName; + existing.processColor = ap.processo?.cor || PROCESS_COLORS[procRaw] || '#10b981'; + existing.processOrdem = Math.max(existing.processOrdem || 0, Number(ap.processo?.ordem || 0)); + } else { + existing.hasDetalhamento = true; + } + } } } });