Ajustes Modo Smart: tema claro/escuro, processos sincronizados com desktop e gerar lista de selecao
This commit is contained in:
@@ -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<string, number>;
|
||||
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<SmartModelViewer3DProps> = ({
|
||||
@@ -62,10 +73,16 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
|
||||
const modelGroupRef = useRef<THREE.Group | null>(null);
|
||||
const raycasterRef = useRef<THREE.Raycaster>(new THREE.Raycaster());
|
||||
|
||||
const { theme, setTheme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
// Filtros principais solicitados: Fase e Processo
|
||||
const [selectedPhase, setSelectedPhase] = useState<string>('all');
|
||||
const [selectedProcess, setSelectedProcess] = useState<string>('all');
|
||||
|
||||
// Modal de Lista da Seleção
|
||||
const [isSelectionModalOpen, setIsSelectionModalOpen] = useState<boolean>(false);
|
||||
|
||||
// Seleção e Toast Discreto
|
||||
interface SelectedElement {
|
||||
expressID: number;
|
||||
@@ -97,6 +114,43 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
|
||||
})
|
||||
);
|
||||
|
||||
const gridRef = useRef<THREE.GridHelper | null>(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<SmartModelViewer3DProps> = ({
|
||||
|
||||
// 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<SmartModelViewer3DProps> = ({
|
||||
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<SmartModelViewer3DProps> = ({
|
||||
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<SmartModelViewer3DProps> = ({
|
||||
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<SmartModelViewer3DProps> = ({
|
||||
}, 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<string, number> = (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<string, string> = {
|
||||
'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<SmartModelViewer3DProps> = ({
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [selectedPhase, selectedProcess, productionData]);
|
||||
}, [selectedPhase, selectedProcess, productionData, selectedOF]);
|
||||
|
||||
// 5. Interação Touch / Clique na Peça (Single Tap)
|
||||
const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
@@ -406,6 +551,7 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
|
||||
});
|
||||
|
||||
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<SmartModelViewer3DProps> = ({
|
||||
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<SmartModelViewer3DProps> = ({
|
||||
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<SmartModelViewer3DProps> = ({
|
||||
|
||||
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<SmartModelViewer3DProps> = ({
|
||||
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<SmartModelViewer3DProps> = ({
|
||||
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 */}
|
||||
<div ref={canvasContainerRef} className="absolute inset-0 w-full h-full z-0 cursor-grab active:cursor-grabbing" />
|
||||
|
||||
{/* 1. Header Minimalista Flutuante (Touch-First) */}
|
||||
<div className="absolute top-2.5 left-2.5 right-2.5 z-30 flex items-center justify-between gap-2 pointer-events-none">
|
||||
{/* Lado Esquerdo: Botão Voltar + Info da Obra */}
|
||||
{/* Lado Esquerdo: Botão Voltar + Info da Obra + Botão Modo Claro/Escuro (Print 1) */}
|
||||
<div className="flex items-center gap-2 pointer-events-auto">
|
||||
<button
|
||||
type="button"
|
||||
@@ -509,21 +701,39 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
|
||||
smartAudio.playClick();
|
||||
onVoltar();
|
||||
}}
|
||||
className="h-10 px-3 rounded-xl bg-slate-900/90 hover:bg-slate-800 text-slate-100 border border-slate-700/80 shadow-md backdrop-blur-md flex items-center gap-1.5 text-xs font-bold transition-all active:scale-95"
|
||||
className="h-10 px-3 rounded-xl bg-white/90 dark:bg-slate-900/90 hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-800 dark:text-slate-100 border border-slate-200 dark:border-slate-700/80 shadow-md backdrop-blur-md flex items-center gap-1.5 text-xs font-bold transition-all active:scale-95"
|
||||
title="Voltar ao Menu Smart"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 text-cyan-400" />
|
||||
<ArrowLeft className="w-4 h-4 text-cyan-600 dark:text-cyan-400" />
|
||||
<span className="hidden sm:inline">Voltar</span>
|
||||
</button>
|
||||
|
||||
<div className="px-3 py-1.5 rounded-xl bg-slate-900/85 backdrop-blur-md border border-slate-800 shadow-md flex flex-col justify-center">
|
||||
<span className="text-[9px] font-black uppercase tracking-wider text-cyan-400">
|
||||
<div className="px-3 py-1.5 rounded-xl bg-white/85 dark:bg-slate-900/85 backdrop-blur-md border border-slate-200 dark:border-slate-800 shadow-md flex flex-col justify-center">
|
||||
<span className="text-[9px] font-black uppercase tracking-wider text-cyan-600 dark:text-cyan-400">
|
||||
OF {selectedOF}
|
||||
</span>
|
||||
<span className="text-xs font-bold text-slate-200 truncate max-w-[120px] sm:max-w-[200px]">
|
||||
<span className="text-xs font-bold text-slate-800 dark:text-slate-200 truncate max-w-[120px] sm:max-w-[200px]">
|
||||
{cliente || 'Obra Industrial'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Botão Modo Claro / Modo Escuro (Print 1) */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
smartAudio.playClick();
|
||||
setTheme(isDark ? 'light' : 'dark');
|
||||
}}
|
||||
className="h-10 px-3 rounded-xl bg-white/90 dark:bg-slate-900/90 hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-slate-700/80 shadow-md backdrop-blur-md flex items-center gap-1.5 text-xs font-bold transition-all active:scale-95 cursor-pointer"
|
||||
title={isDark ? "Alternar para Modo Claro" : "Alternar para Modo Escuro"}
|
||||
>
|
||||
{isDark ? (
|
||||
<Sun className="w-4 h-4 text-amber-400" />
|
||||
) : (
|
||||
<Moon className="w-4 h-4 text-slate-700" />
|
||||
)}
|
||||
<span className="hidden md:inline">{isDark ? 'Claro' : 'Escuro'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Lado Direito: Filtros Discretos (Fase & Processo) + Enquadrar */}
|
||||
@@ -533,7 +743,7 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
|
||||
<select
|
||||
value={selectedPhase}
|
||||
onChange={(e) => setSelectedPhase(e.target.value)}
|
||||
className="h-10 pl-7 pr-3 rounded-xl bg-slate-900/90 text-slate-200 border border-slate-700/80 text-xs font-bold backdrop-blur-md shadow-md appearance-none cursor-pointer focus:outline-none focus:ring-1 focus:ring-cyan-500"
|
||||
className="h-10 pl-7 pr-3 rounded-xl bg-white/90 dark:bg-slate-900/90 text-slate-800 dark:text-slate-200 border border-slate-200 dark:border-slate-700/80 text-xs font-bold backdrop-blur-md shadow-md appearance-none cursor-pointer focus:outline-none focus:ring-1 focus:ring-cyan-500"
|
||||
title="Filtrar por Fase da Obra"
|
||||
>
|
||||
<option value="all">Todas as Fases</option>
|
||||
@@ -543,15 +753,15 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Layers className="w-3.5 h-3.5 text-cyan-400 absolute left-2.5 top-3.5 pointer-events-none" />
|
||||
<Layers className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400 absolute left-2.5 top-3.5 pointer-events-none" />
|
||||
</div>
|
||||
|
||||
{/* Filtro por Processo */}
|
||||
{/* Filtro por Processo (Print 2: ordem dos botões de processos do modo completo) */}
|
||||
<div className="relative">
|
||||
<select
|
||||
value={selectedProcess}
|
||||
onChange={(e) => setSelectedProcess(e.target.value)}
|
||||
className="h-10 pl-7 pr-3 rounded-xl bg-slate-900/90 text-slate-200 border border-slate-700/80 text-xs font-bold backdrop-blur-md shadow-md appearance-none cursor-pointer focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
className="h-10 pl-7 pr-3 rounded-xl bg-white/90 dark:bg-slate-900/90 text-slate-800 dark:text-slate-200 border border-slate-200 dark:border-slate-700/80 text-xs font-bold backdrop-blur-md shadow-md appearance-none cursor-pointer focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
title="Filtrar por Etapa Fabril"
|
||||
>
|
||||
{PROCESS_LIST.map((proc) => (
|
||||
@@ -560,22 +770,22 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Activity className="w-3.5 h-3.5 text-emerald-400 absolute left-2.5 top-3.5 pointer-events-none" />
|
||||
<Activity className="w-3.5 h-3.5 text-emerald-600 dark:text-emerald-400 absolute left-2.5 top-3.5 pointer-events-none" />
|
||||
</div>
|
||||
|
||||
{/* Botão Enquadrar (Fit View) */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={fitModelToView}
|
||||
className="h-10 w-10 rounded-xl bg-slate-900/90 hover:bg-slate-800 text-slate-200 border border-slate-700/80 shadow-md backdrop-blur-md flex items-center justify-center transition-all active:scale-95"
|
||||
className="h-10 w-10 rounded-xl bg-white/90 dark:bg-slate-900/90 hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-slate-700/80 shadow-md backdrop-blur-md flex items-center justify-center transition-all active:scale-95"
|
||||
title="Enquadrar Modelo Centralizado"
|
||||
>
|
||||
<Maximize2 className="w-4 h-4 text-cyan-400" />
|
||||
<Maximize2 className="w-4 h-4 text-cyan-600 dark:text-cyan-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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) */}
|
||||
<div className="absolute top-16 right-3 z-20 flex flex-col items-end gap-2 pointer-events-auto">
|
||||
{/* Cubo Compacto Discreto */}
|
||||
<ViewCube
|
||||
@@ -585,19 +795,45 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
|
||||
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 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearSelection}
|
||||
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-xl bg-slate-900/95 hover:bg-rose-950/80 text-rose-300 border border-rose-500/40 shadow-lg backdrop-blur-md text-[11px] font-bold animate-in fade-in zoom-in-95 duration-150 active:scale-95 transition-all"
|
||||
title="Desmarcar todas as peças"
|
||||
<div
|
||||
className="flex flex-col items-stretch gap-1.5 p-2 rounded-2xl bg-white/95 dark:bg-slate-900/95 border border-slate-300 dark:border-slate-700 shadow-xl dark:shadow-slate-950/80 backdrop-blur-md animate-in fade-in zoom-in-95 duration-200"
|
||||
style={{ minWidth: '135px' }}
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-fuchsia-400 animate-pulse" />
|
||||
<span>{selectedCount} {selectedCount === 1 ? 'marcada' : 'marcadas'}</span>
|
||||
<span className="text-slate-500">|</span>
|
||||
<span className="text-rose-400 hover:text-white">Limpar ✕</span>
|
||||
</button>
|
||||
{/* Badge Indicador de Seleção Ativa */}
|
||||
<div className="flex items-center justify-center gap-1.5 px-2 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-slate-800 dark:text-slate-200 text-[10px] font-mono font-bold">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-fuchsia-500 shadow-sm" />
|
||||
<span>
|
||||
{selectedCount} {selectedCount === 1 ? 'marcada' : 'marcadas'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Botão Gerar Lista da Seleção (Print 3 - idêntico ao modo completo) */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
smartAudio.playClick();
|
||||
setIsSelectionModalOpen(true);
|
||||
}}
|
||||
className="flex items-center justify-center gap-1.5 px-2.5 py-1.5 rounded-xl text-[11px] font-bold bg-blue-950 hover:bg-blue-900 text-white shadow-sm dark:bg-blue-600 dark:hover:bg-blue-500 scale-[1.02] active:scale-[0.98] transition-all cursor-pointer"
|
||||
title="Abrir lista detalhada e exportar planilha das peças selecionadas"
|
||||
>
|
||||
<FileSpreadsheet className="w-3.5 h-3.5 text-white" />
|
||||
<span className="whitespace-nowrap">Gerar lista</span>
|
||||
</button>
|
||||
|
||||
{/* Botão Limpar Seleção */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearSelection}
|
||||
className="flex items-center justify-center gap-1.5 px-2.5 py-1 rounded-xl text-[10px] font-semibold text-slate-600 hover:text-rose-600 hover:bg-rose-50 border border-slate-200 dark:border-slate-700 dark:text-slate-400 dark:hover:text-rose-300 dark:hover:bg-rose-950/30 active:scale-[0.98] transition-all cursor-pointer"
|
||||
title="Desmarcar todas as peças"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
<span className="whitespace-nowrap">Limpar Seleção</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -609,14 +845,14 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
|
||||
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 */}
|
||||
<div className="flex items-center justify-between gap-1.5 pb-1.5 mb-1.5 border-b border-slate-800">
|
||||
<div className="flex items-center justify-between gap-1.5 pb-1.5 mb-1.5 border-b border-slate-200 dark:border-slate-800">
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<div className="w-2 h-2 rounded-full bg-fuchsia-500 shadow-sm" />
|
||||
<span className="text-xs font-black text-fuchsia-300 font-mono tracking-tight truncate">
|
||||
<span className="text-xs font-black text-fuchsia-600 dark:text-fuchsia-300 font-mono tracking-tight truncate">
|
||||
{toastInfo.piece.pieceMark || 'Sem Marca'}
|
||||
</span>
|
||||
</div>
|
||||
@@ -626,7 +862,7 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
|
||||
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"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@@ -635,21 +871,21 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
|
||||
{/* Dados Rápidos da Peça */}
|
||||
<div className="space-y-1 text-[11px]">
|
||||
{toastInfo.piece.phase && (
|
||||
<div className="flex items-center justify-between text-slate-300">
|
||||
<div className="flex items-center justify-between text-slate-600 dark:text-slate-300">
|
||||
<span className="text-slate-400">Fase:</span>
|
||||
<span className="font-bold text-slate-200">Fase {toastInfo.piece.phase}</span>
|
||||
<span className="font-bold text-slate-800 dark:text-slate-200">Fase {toastInfo.piece.phase}</span>
|
||||
</div>
|
||||
)}
|
||||
{toastInfo.piece.section && (
|
||||
<div className="flex items-center justify-between text-slate-300">
|
||||
<div className="flex items-center justify-between text-slate-600 dark:text-slate-300">
|
||||
<span className="text-slate-400">Perfil:</span>
|
||||
<span className="font-mono font-semibold text-slate-100 truncate max-w-[120px]">
|
||||
<span className="font-mono font-semibold text-slate-800 dark:text-slate-100 truncate max-w-[120px]">
|
||||
{toastInfo.piece.section}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{toastInfo.prod && (
|
||||
<div className="pt-1 mt-1 border-t border-slate-800/80 flex items-center justify-between">
|
||||
<div className="pt-1 mt-1 border-t border-slate-200 dark:border-slate-800/80 flex items-center justify-between">
|
||||
<span className="text-slate-400">Status:</span>
|
||||
<span
|
||||
className="font-bold px-1.5 py-0.5 rounded text-[10px] uppercase tracking-wider"
|
||||
@@ -665,11 +901,20 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Dica para Fechar */}
|
||||
<div className="mt-2 pt-1 border-t border-slate-800 text-[9px] text-center text-slate-400 font-medium">
|
||||
<div className="mt-2 pt-1 border-t border-slate-200 dark:border-slate-800 text-[9px] text-center text-slate-400 font-medium">
|
||||
Toque no card para fechar ✕
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal de Lista e Exportação da Seleção 3D (Print 3) */}
|
||||
<SelectionListModal
|
||||
isOpen={isSelectionModalOpen}
|
||||
onClose={() => setIsSelectionModalOpen(false)}
|
||||
selectedPieces={selectedPiecesList}
|
||||
selectedOF={selectedOF}
|
||||
onClearSelection={handleClearSelection}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -78,43 +78,94 @@ export const SmartViewer3DFlow: React.FC<SmartViewer3DFlowProps> = ({
|
||||
|
||||
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<string, string> = {
|
||||
'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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user