fix(3d-viewer): improve ifc model loading resiliency and fallback topology
This commit is contained in:
@@ -46,7 +46,7 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
const [isOrthographic, setIsOrthographic] = useState<boolean>(false);
|
||||
const [isWireframe, setIsWireframe] = useState<boolean>(false);
|
||||
const [navMode, setNavMode] = useState<'orbit' | 'walk'>('orbit');
|
||||
const [colorMode, setColorMode] = useState<'description' | 'production'>('description');
|
||||
const [colorMode, setColorMode] = useState<'description' | 'production'>('production');
|
||||
const [hasSectionPlanes, setHasSectionPlanes] = useState<boolean>(false);
|
||||
const [isMeasuring, setIsMeasuring] = useState<boolean>(false);
|
||||
|
||||
@@ -55,16 +55,39 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
const [hoveredPiece, setHoveredPiece] = useState<PieceInfo | null>(null);
|
||||
const [hoverPosition, setHoverPosition] = useState<{ x: number; y: number } | null>(null);
|
||||
|
||||
// Highlight Material (Light Green #4ade80)
|
||||
const highlightMaterialRef = useRef<THREE.MeshStandardMaterial>(
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: 0x4ade80,
|
||||
emissive: 0x22c55e,
|
||||
emissiveIntensity: 0.35,
|
||||
metalness: 0.3,
|
||||
roughness: 0.4,
|
||||
side: THREE.DoubleSide,
|
||||
})
|
||||
// Smart Matcher: Matches any mark format (e.g. "B135-3-21", "3-21", "21", "B135-21") against production data
|
||||
const getPieceProductionStatus = useCallback(
|
||||
(rawMark: string): ProductionPieceStatus | undefined => {
|
||||
if (!productionData || !rawMark) return undefined;
|
||||
|
||||
const clean = rawMark.trim();
|
||||
// 1. Direct map lookup
|
||||
if (productionData.has(clean)) return productionData.get(clean);
|
||||
|
||||
// 2. Tokenize by -, /, ., _, space
|
||||
const tokens = clean.split(/[-/._\s]+/).filter(Boolean);
|
||||
|
||||
// Suffix combinations e.g. "3-21"
|
||||
if (tokens.length >= 2) {
|
||||
const lastTwo = `${tokens[tokens.length - 2]}-${tokens[tokens.length - 1]}`;
|
||||
if (productionData.has(lastTwo)) return productionData.get(lastTwo);
|
||||
}
|
||||
|
||||
// Last token is the specific piece mark (e.g. "21" in "B135-3-21")
|
||||
if (tokens.length > 0) {
|
||||
const lastTok = tokens[tokens.length - 1];
|
||||
if (productionData.has(lastTok)) return productionData.get(lastTok);
|
||||
}
|
||||
|
||||
// 3. Fallback scan across map values
|
||||
for (const [key, val] of productionData.entries()) {
|
||||
if (val.marca === clean) return val;
|
||||
if (tokens.length > 0 && val.marca === tokens[tokens.length - 1]) return val;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
[productionData]
|
||||
);
|
||||
|
||||
// Initialize Scene
|
||||
@@ -119,7 +142,7 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
controlsRef.current = controls;
|
||||
|
||||
// 5. Lights
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.75);
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.85);
|
||||
scene.add(ambientLight);
|
||||
|
||||
const dirLight1 = new THREE.DirectionalLight(0xffffff, 1.2);
|
||||
@@ -127,11 +150,11 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
dirLight1.castShadow = true;
|
||||
scene.add(dirLight1);
|
||||
|
||||
const dirLight2 = new THREE.DirectionalLight(0x94a3b8, 0.6);
|
||||
const dirLight2 = new THREE.DirectionalLight(0x94a3b8, 0.7);
|
||||
dirLight2.position.set(-100, -50, -100);
|
||||
scene.add(dirLight2);
|
||||
|
||||
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x1e293b, 0.5);
|
||||
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x1e293b, 0.6);
|
||||
scene.add(hemiLight);
|
||||
|
||||
// 6. Floor Grid
|
||||
@@ -229,59 +252,116 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
if (!modelData || !modelGroupRef.current) return;
|
||||
|
||||
const group = modelGroupRef.current;
|
||||
const allMeshes: THREE.Mesh[] = [];
|
||||
|
||||
group.traverse((child) => {
|
||||
if ((child as THREE.Mesh).isMesh) {
|
||||
const mesh = child as THREE.Mesh;
|
||||
const pmark = mesh.userData.pieceMark || '';
|
||||
|
||||
if (colorMode === 'production' && productionData) {
|
||||
// Look up production pointing status
|
||||
const prod = productionData.get(pmark);
|
||||
if (prod && prod.pointedQtd > 0) {
|
||||
// Apply stage color (e.g., Light Green #4ade80 for pointed)
|
||||
const stageHex = prod.processColor || '#4ade80';
|
||||
mesh.material = new THREE.MeshStandardMaterial({
|
||||
color: new THREE.Color(stageHex),
|
||||
metalness: 0.35,
|
||||
roughness: 0.45,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
} else {
|
||||
// Unpointed / Pending (Default Gray)
|
||||
mesh.material = new THREE.MeshStandardMaterial({
|
||||
color: 0x64748b,
|
||||
metalness: 0.3,
|
||||
roughness: 0.6,
|
||||
transparent: opacity < 100,
|
||||
opacity: opacity / 100,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Color Mode "Descrição" (Palette by Profile/Section)
|
||||
const colorKey = mesh.userData.colorKey;
|
||||
const cachedMat = modelData.materialsByColor.get(colorKey);
|
||||
if (cachedMat) {
|
||||
mesh.material = cachedMat;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply Opacity & Wireframe
|
||||
if (Array.isArray(mesh.material)) {
|
||||
mesh.material.forEach((m) => {
|
||||
m.transparent = opacity < 100;
|
||||
m.opacity = opacity / 100;
|
||||
if ('wireframe' in m) (m as any).wireframe = isWireframe;
|
||||
});
|
||||
} else if (mesh.material) {
|
||||
mesh.material.transparent = opacity < 100;
|
||||
mesh.material.opacity = opacity / 100;
|
||||
if ('wireframe' in mesh.material) (mesh.material as any).wireframe = isWireframe;
|
||||
}
|
||||
allMeshes.push(child as THREE.Mesh);
|
||||
}
|
||||
});
|
||||
}, [modelData, colorMode, productionData, opacity, isWireframe]);
|
||||
|
||||
if (colorMode === 'production') {
|
||||
// Group meshes by their resolved piece/assembly mark
|
||||
const meshesByMark = new Map<string, THREE.Mesh[]>();
|
||||
|
||||
allMeshes.forEach((mesh) => {
|
||||
let pmark = mesh.userData.pieceMark || '';
|
||||
if (!pmark && mesh.userData.expressID && modelData.pieceByExpressID) {
|
||||
const pInfo = modelData.pieceByExpressID.get(mesh.userData.expressID);
|
||||
pmark = pInfo?.pieceMark || '';
|
||||
}
|
||||
|
||||
const prod = getPieceProductionStatus(pmark);
|
||||
const groupKey = prod ? `${prod.fase}-${prod.marca}` : (pmark || `mesh-${mesh.id}`);
|
||||
|
||||
if (!meshesByMark.has(groupKey)) {
|
||||
meshesByMark.set(groupKey, []);
|
||||
}
|
||||
meshesByMark.get(groupKey)!.push(mesh);
|
||||
});
|
||||
|
||||
// Apply proportional coloring for each mark
|
||||
meshesByMark.forEach((meshes, groupKey) => {
|
||||
const sampleMark = meshes[0]?.userData.pieceMark || groupKey;
|
||||
const prod = getPieceProductionStatus(sampleMark);
|
||||
|
||||
if (prod && prod.pointedQtd > 0) {
|
||||
// If pointed, calculate how many mesh instances to highlight green
|
||||
// Example: 2 of 2 un. -> all 2 green. 2 of 4 un. -> 2 green, 2 gray.
|
||||
const totalQtd = Math.max(prod.totalQtd || 1, 1);
|
||||
const pointedQtd = Math.max(prod.pointedQtd, 0);
|
||||
|
||||
// If there are multiple meshes, highlight proportional count
|
||||
const ratio = Math.min(pointedQtd / totalQtd, 1.0);
|
||||
const countToHighlight = Math.max(1, Math.min(meshes.length, Math.round(meshes.length * ratio)));
|
||||
|
||||
meshes.forEach((mesh, idx) => {
|
||||
const isPointed = idx < countToHighlight;
|
||||
if (isPointed) {
|
||||
// Vibrant Light Green (#4ade80) for pointed pieces
|
||||
mesh.material = new THREE.MeshStandardMaterial({
|
||||
color: 0x4ade80,
|
||||
emissive: 0x16a34a,
|
||||
emissiveIntensity: 0.28,
|
||||
metalness: 0.35,
|
||||
roughness: 0.4,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
} else {
|
||||
// Sleek Metallic Light Gray for unpointed
|
||||
mesh.material = new THREE.MeshStandardMaterial({
|
||||
color: 0xb0bec5,
|
||||
metalness: 0.58,
|
||||
roughness: 0.38,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Unpointed / Pending -> Sleek Metallic Light Gray (Steel)
|
||||
meshes.forEach((mesh) => {
|
||||
mesh.material = new THREE.MeshStandardMaterial({
|
||||
color: 0xb0bec5,
|
||||
metalness: 0.58,
|
||||
roughness: 0.38,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Color Mode "Descrição" (Palette by Profile/Section)
|
||||
allMeshes.forEach((mesh) => {
|
||||
const colorKey = mesh.userData.colorKey;
|
||||
const cachedMat = modelData.materialsByColor.get(colorKey);
|
||||
if (cachedMat) {
|
||||
mesh.material = cachedMat;
|
||||
} else {
|
||||
mesh.material = new THREE.MeshStandardMaterial({
|
||||
color: 0xb0bec5,
|
||||
metalness: 0.58,
|
||||
roughness: 0.38,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Apply Opacity & Wireframe to all meshes
|
||||
allMeshes.forEach((mesh) => {
|
||||
if (Array.isArray(mesh.material)) {
|
||||
mesh.material.forEach((m) => {
|
||||
m.transparent = opacity < 100;
|
||||
m.opacity = opacity / 100;
|
||||
if ('wireframe' in m) (m as any).wireframe = isWireframe;
|
||||
});
|
||||
} else if (mesh.material) {
|
||||
mesh.material.transparent = opacity < 100;
|
||||
mesh.material.opacity = opacity / 100;
|
||||
if ('wireframe' in mesh.material) (mesh.material as any).wireframe = isWireframe;
|
||||
}
|
||||
});
|
||||
}, [modelData, colorMode, productionData, opacity, isWireframe, getPieceProductionStatus]);
|
||||
|
||||
// Camera Toggle (Ortogonal / Perspectiva)
|
||||
const handleToggleCamera = useCallback(() => {
|
||||
@@ -353,6 +433,39 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Click Selection
|
||||
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!containerRef.current || !activeCameraRef.current || !modelGroupRef.current) return;
|
||||
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const mouseX = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
const mouseY = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
const clickPos = new THREE.Vector2(mouseX, mouseY);
|
||||
|
||||
raycasterRef.current.setFromCamera(clickPos, activeCameraRef.current);
|
||||
const intersects = raycasterRef.current.intersectObjects(modelGroupRef.current.children, true);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const hitMesh = intersects[0].object as THREE.Mesh;
|
||||
const expressID = hitMesh.userData.expressID;
|
||||
const pmark = hitMesh.userData.pieceMark;
|
||||
|
||||
if (modelData && expressID) {
|
||||
const piece = modelData.pieceByExpressID.get(expressID) || {
|
||||
expressID,
|
||||
guid: '',
|
||||
name: hitMesh.userData.section || 'Peça',
|
||||
type: 'PIECE',
|
||||
pieceMark: pmark,
|
||||
section: hitMesh.userData.section,
|
||||
};
|
||||
onSelectPiece?.(piece);
|
||||
}
|
||||
} else {
|
||||
onSelectPiece?.(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 1.5-Second Hover Detection
|
||||
const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (!containerRef.current || !activeCameraRef.current || !modelGroupRef.current) return;
|
||||
@@ -411,6 +524,7 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
onClick={handleClick}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerLeave={handlePointerLeave}
|
||||
className="relative w-full h-full min-h-[600px] bg-slate-950 rounded-2xl overflow-hidden border border-slate-800 shadow-2xl select-none"
|
||||
@@ -426,12 +540,17 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
position={hoverPosition}
|
||||
productionInfo={
|
||||
hoveredPiece?.pieceMark && productionData
|
||||
? {
|
||||
pointedQty: productionData.get(hoveredPiece.pieceMark)?.pointedQtd,
|
||||
totalQty: productionData.get(hoveredPiece.pieceMark)?.totalQtd,
|
||||
currentStage: productionData.get(hoveredPiece.pieceMark)?.currentProcessName,
|
||||
stageColor: productionData.get(hoveredPiece.pieceMark)?.processColor,
|
||||
}
|
||||
? (() => {
|
||||
const prod = getPieceProductionStatus(hoveredPiece.pieceMark);
|
||||
return prod
|
||||
? {
|
||||
pointedQty: prod.pointedQtd,
|
||||
totalQty: prod.totalQtd,
|
||||
currentStage: prod.currentProcessName,
|
||||
stageColor: prod.processColor,
|
||||
}
|
||||
: undefined;
|
||||
})()
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -160,14 +160,23 @@ export async function loadAndAuditIFC(
|
||||
// 4.1 Native Aggregates mapping (IFCRELAGGREGATES)
|
||||
const aggregates = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCRELAGGREGATES);
|
||||
const assemblyToChildren = new Map<number, number[]>();
|
||||
const childToAssemblyMap = new Map<number, number>();
|
||||
|
||||
for (let i = 0; i < aggregates.size(); i++) {
|
||||
const relID = aggregates.get(i);
|
||||
try {
|
||||
const rel = ifcApi.GetLine(modelID, relID);
|
||||
if (rel && rel.RelatingObject && rel.RelatedObjects) {
|
||||
const parentID = rel.RelatingObject.value;
|
||||
const childIDs = (rel.RelatedObjects as any[]).map((r: any) => r.value);
|
||||
const parentID = typeof rel.RelatingObject === 'number' ? rel.RelatingObject : rel.RelatingObject.value;
|
||||
const childObjs = Array.isArray(rel.RelatedObjects) ? rel.RelatedObjects : [rel.RelatedObjects];
|
||||
const childIDs: number[] = [];
|
||||
for (const c of childObjs) {
|
||||
const cId = typeof c === 'number' ? c : c?.value;
|
||||
if (typeof cId === 'number' && cId > 0) {
|
||||
childIDs.push(cId);
|
||||
childToAssemblyMap.set(cId, parentID);
|
||||
}
|
||||
}
|
||||
assemblyToChildren.set(parentID, childIDs);
|
||||
}
|
||||
} catch {
|
||||
@@ -240,31 +249,35 @@ export async function loadAndAuditIFC(
|
||||
if (relId && relId > 0) {
|
||||
const rel = ifcApi.GetLine(modelID, relId);
|
||||
if (rel && rel.RelatedObjects && rel.RelatingPropertyDefinition) {
|
||||
const propDefId = rel.RelatingPropertyDefinition.value;
|
||||
const propDefId = typeof rel.RelatingPropertyDefinition === 'number'
|
||||
? rel.RelatingPropertyDefinition
|
||||
: rel.RelatingPropertyDefinition.value;
|
||||
|
||||
if (propDefId && typeof propDefId === 'number' && propDefId > 0) {
|
||||
const propDef = ifcApi.GetLine(modelID, propDefId);
|
||||
if (propDef && propDef.HasProperties) {
|
||||
const props: Record<string, string> = {};
|
||||
for (const pRef of propDef.HasProperties) {
|
||||
const pId = pRef?.value;
|
||||
const pId = typeof pRef === 'number' ? pRef : pRef?.value;
|
||||
if (pId && typeof pId === 'number' && pId > 0) {
|
||||
const pLine = ifcApi.GetLine(modelID, pId);
|
||||
if (pLine && pLine.Name) {
|
||||
const name = pLine.Name.value;
|
||||
const name = pLine.Name.value || pLine.Name;
|
||||
let value = '';
|
||||
if (pLine.NominalValue) {
|
||||
value = String(pLine.NominalValue.value);
|
||||
value = String(pLine.NominalValue.value !== undefined ? pLine.NominalValue.value : pLine.NominalValue);
|
||||
}
|
||||
if (name && value) {
|
||||
props[name] = value;
|
||||
props[String(name)] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const relatedObjs = Array.isArray(rel.RelatedObjects) ? rel.RelatedObjects : [rel.RelatedObjects];
|
||||
for (const objRef of relatedObjs) {
|
||||
const objId = objRef?.value || objRef;
|
||||
if (objId && typeof objId === 'number') {
|
||||
const objId = typeof objRef === 'number' ? objRef : objRef?.value;
|
||||
if (objId && typeof objId === 'number' && objId > 0) {
|
||||
const existing = elementPropertiesMap.get(objId) || {};
|
||||
elementPropertiesMap.set(objId, { ...existing, ...props });
|
||||
}
|
||||
@@ -292,7 +305,8 @@ export async function loadAndAuditIFC(
|
||||
const assObj = ifcApi.GetLine(modelID, assID);
|
||||
const props = elementPropertiesMap.get(assID) || {};
|
||||
const rawProps = getSyncProperties(ifcApi, modelID, assID);
|
||||
const pmark = props.PieceMark || props.Reference || props.Mark || props.Tag || rawProps.Name || 'indefinido';
|
||||
let pmark = props.PieceMark || props.Reference || props.Mark || props.Tag || rawProps.Name || 'indefinido';
|
||||
if (pmark === 'indefinido') pmark = '';
|
||||
|
||||
const children = assemblyToChildren.get(assID) || [];
|
||||
const piece: PieceInfo = {
|
||||
@@ -308,7 +322,7 @@ export async function loadAndAuditIFC(
|
||||
ofNumber: extractOFFromMark(pmark)
|
||||
};
|
||||
|
||||
if (pmark !== 'indefinido') {
|
||||
if (pmark) {
|
||||
uniqueMarksSet.add(pmark);
|
||||
if (!piecesByMark.has(pmark)) piecesByMark.set(pmark, []);
|
||||
piecesByMark.get(pmark)!.push(piece);
|
||||
@@ -324,10 +338,11 @@ export async function loadAndAuditIFC(
|
||||
const sceneGroup = new THREE.Group();
|
||||
sceneGroup.name = 'IFC_ROOT_MODEL';
|
||||
|
||||
const defaultGrayMat = new THREE.MeshStandardMaterial({
|
||||
color: 0x94a3b8,
|
||||
metalness: 0.35,
|
||||
roughness: 0.55,
|
||||
// Base metallic light gray material
|
||||
const defaultSteelMat = new THREE.MeshStandardMaterial({
|
||||
color: 0xb0bec5, // Metallic Light Gray
|
||||
metalness: 0.58,
|
||||
roughness: 0.38,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
@@ -342,7 +357,34 @@ export async function loadAndAuditIFC(
|
||||
// Check properties for this element
|
||||
const props = elementPropertiesMap.get(expressID) || {};
|
||||
const rawProps = getSyncProperties(ifcApi, modelID, expressID);
|
||||
const pmark = props.PieceMark || props.Reference || props.Mark || props.Tag || rawProps.Name || '';
|
||||
|
||||
// Look up mark from element properties OR parent assembly OR weld group peers
|
||||
let pmark = props.PieceMark || props.Reference || props.Mark || props.Tag || rawProps.Name || '';
|
||||
if (pmark === 'indefinido') pmark = '';
|
||||
|
||||
if (!pmark) {
|
||||
const parentID = childToAssemblyMap.get(expressID);
|
||||
if (parentID) {
|
||||
const parentProps = elementPropertiesMap.get(parentID) || {};
|
||||
const parentRaw = getSyncProperties(ifcApi, modelID, parentID);
|
||||
pmark = parentProps.PieceMark || parentProps.Reference || parentProps.Mark || parentProps.Tag || parentRaw.Name || '';
|
||||
if (pmark === 'indefinido') pmark = '';
|
||||
}
|
||||
}
|
||||
|
||||
const connectedGroup = weldedGroupMap.get(expressID) || [expressID];
|
||||
if (!pmark) {
|
||||
for (const peerID of connectedGroup) {
|
||||
const peerProps = elementPropertiesMap.get(peerID) || {};
|
||||
const peerRaw = getSyncProperties(ifcApi, modelID, peerID);
|
||||
const peerMark = peerProps.PieceMark || peerProps.Reference || peerProps.Mark || peerProps.Tag || peerRaw.Name || '';
|
||||
if (peerMark && peerMark !== 'indefinido') {
|
||||
pmark = peerMark;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const section = props.Section || props.Profile || props.Description || rawProps.Name || '';
|
||||
|
||||
// Determine section color
|
||||
@@ -364,10 +406,7 @@ export async function loadAndAuditIFC(
|
||||
})
|
||||
);
|
||||
}
|
||||
const mat = materialCache.get(hexColor) || defaultGrayMat;
|
||||
|
||||
// Get welded connected peers (Fallback topology)
|
||||
const connectedGroup = weldedGroupMap.get(expressID) || [expressID];
|
||||
const mat = defaultSteelMat;
|
||||
|
||||
for (let g = 0; g < placedGeometries.size(); g++) {
|
||||
const placedGeo = placedGeometries.get(g);
|
||||
|
||||
@@ -99,17 +99,25 @@ export default function Visualizador3D() {
|
||||
.select('id, of_number, etapa_fase, marca, descricao, quantidade, peso_unitario, peso_total, perfil_principal, material')
|
||||
.eq('of_number', selectedOF);
|
||||
|
||||
// Fetch apontamentos com join de peca e processo
|
||||
// Fetch apontamentos
|
||||
const { data: apontamentosData } = await supabase
|
||||
.from('apontamentos_producao' as any)
|
||||
.select(`
|
||||
id, of_number, peca_id, quantidade_produzida,
|
||||
peca:pecas(marca, etapa_fase),
|
||||
processo:processos_fabricacao(nome, cor, ordem)
|
||||
`)
|
||||
.select('id, of_number, peca_id, processo_id, quantidade_produzida')
|
||||
.eq('of_number', selectedOF);
|
||||
|
||||
// Build phases list
|
||||
// Fetch processos de fabricação
|
||||
const { data: processosData } = await supabase
|
||||
.from('processos_fabricacao' as any)
|
||||
.select('id, nome, cor, ordem');
|
||||
|
||||
const processoById = new Map<string, any>();
|
||||
if (processosData) {
|
||||
processosData.forEach((pr: any) => {
|
||||
processoById.set(pr.id, pr);
|
||||
});
|
||||
}
|
||||
|
||||
const pecaById = new Map<string, any>();
|
||||
const phases = new Set<string>();
|
||||
const prodMap = new Map<string, any>();
|
||||
let totalPecas = 0;
|
||||
@@ -117,40 +125,57 @@ export default function Visualizador3D() {
|
||||
|
||||
if (pecasData) {
|
||||
pecasData.forEach((p: any) => {
|
||||
pecaById.set(p.id, p);
|
||||
const fase = String(p.etapa_fase || '1');
|
||||
const marca = String(p.marca || '').trim();
|
||||
phases.add(fase);
|
||||
totalPecas += Number(p.quantidade || 0);
|
||||
totalPeso += Number(p.peso_total || 0);
|
||||
|
||||
// Composite Key matching IFC (ex: B135-2-1)
|
||||
const key = `${selectedOF}-${fase}-${p.marca}`;
|
||||
prodMap.set(key, {
|
||||
const pieceStatus = {
|
||||
pecaId: p.id,
|
||||
marca: p.marca,
|
||||
marca,
|
||||
fase,
|
||||
of_number: selectedOF,
|
||||
totalQtd: Number(p.quantidade || 1),
|
||||
pointedQtd: 0,
|
||||
currentProcessName: 'Pendente',
|
||||
processColor: '#64748b'
|
||||
});
|
||||
};
|
||||
|
||||
// Register multiple lookup keys for resilient matching:
|
||||
// 1. OF-Fase-Marca: e.g. "B135-3-21"
|
||||
prodMap.set(`${selectedOF}-${fase}-${marca}`, pieceStatus);
|
||||
// 2. Fase-Marca: e.g. "3-21"
|
||||
prodMap.set(`${fase}-${marca}`, pieceStatus);
|
||||
// 3. Marca only: e.g. "21"
|
||||
prodMap.set(marca, pieceStatus);
|
||||
// 4. OF-Marca: e.g. "B135-21"
|
||||
prodMap.set(`${selectedOF}-${marca}`, pieceStatus);
|
||||
});
|
||||
}
|
||||
|
||||
let pointedTotal = 0;
|
||||
if (apontamentosData) {
|
||||
apontamentosData.forEach((ap: any) => {
|
||||
const marca = ap.peca?.marca;
|
||||
const fase = String(ap.peca?.etapa_fase || '1');
|
||||
if (marca) {
|
||||
const peca = pecaById.get(ap.peca_id);
|
||||
if (peca) {
|
||||
const marca = String(peca.marca || '').trim();
|
||||
const fase = String(peca.etapa_fase || '1');
|
||||
const key = `${selectedOF}-${fase}-${marca}`;
|
||||
const existing = prodMap.get(key);
|
||||
if (existing) {
|
||||
const qty = Number(ap.quantidade_produzida || 0);
|
||||
existing.pointedQtd += qty;
|
||||
pointedTotal += qty;
|
||||
if (ap.processo?.nome) {
|
||||
existing.currentProcessName = ap.processo.nome;
|
||||
existing.processColor = ap.processo.cor || PROCESS_COLORS[ap.processo.nome] || '#10b981';
|
||||
|
||||
const proc = processoById.get(ap.processo_id);
|
||||
if (proc) {
|
||||
existing.currentProcessName = proc.nome;
|
||||
existing.processColor = proc.cor || PROCESS_COLORS[proc.nome] || '#22c55e';
|
||||
} else {
|
||||
existing.currentProcessName = 'Apontado';
|
||||
existing.processColor = '#22c55e';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user