feat(smart-3d): replica card de engenharia e adiciona ferramenta girar 90 com trava

This commit is contained in:
2026-10-15 16:29:17 -03:00
parent 16414ca4ba
commit ac8bacc9e1
2 changed files with 354 additions and 98 deletions
+345 -96
View File
@@ -22,8 +22,12 @@ import {
Moon,
FileSpreadsheet,
Grid as GridIcon,
Bell
Bell,
RotateCw,
Lock,
Unlock
} from 'lucide-react';
import { supabase } from '@/integrations/supabase/client';
export interface ProductionPieceStatus {
pecaId?: string;
@@ -38,6 +42,91 @@ export interface ProductionPieceStatus {
processesCompleted?: string[];
processPointedQtds?: Record<string, number>;
hasDetalhamento?: boolean;
perfilPrincipal?: string;
descricao?: string;
}
/**
* Busca de apontamento e status fabril com máxima tolerância e amostragem de chaves para IFC e WRL.
*/
function findProductionDataForPiece(
productionData?: Map<string, ProductionPieceStatus>,
piece?: {
pieceMark?: string;
cleanMark?: string;
assemblyMark?: string;
cleanAssemblyMark?: string;
phase?: string;
} | null,
selectedOF?: string
): ProductionPieceStatus | undefined {
if (!productionData || !piece) return undefined;
const pmark = String(piece.pieceMark || '').trim().toUpperCase();
const cleanMark = String(piece.cleanMark || pmark).trim().toUpperCase();
const assMark = String(piece.assemblyMark || '').trim().toUpperCase();
const cleanAssMark = String(piece.cleanAssemblyMark || cleanMark).trim().toUpperCase();
const elementPhase = piece.phase ? String(piece.phase).trim() : '';
const searchKeys = [
elementPhase && selectedOF ? `${selectedOF}-${elementPhase}-${cleanAssMark}` : '',
elementPhase ? `${elementPhase}-${cleanAssMark}` : '',
elementPhase && selectedOF ? `${selectedOF}-${elementPhase}-${cleanMark}` : '',
elementPhase ? `${elementPhase}-${cleanMark}` : '',
selectedOF ? `${selectedOF}-${cleanAssMark}` : '',
selectedOF ? `${selectedOF}-${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)) {
return match;
}
}
}
return undefined;
}
/**
* Helper para formatar a Marca Principal da Peça no padrão de Engenharia (OF-Fase-Marca, ex: B138-1-59 / B138-2-10)
* e obter o Perfil/Bitola oficial a partir do banco de dados ou do modelo 3D.
*/
function resolvePieceEngineeringDetails(
rawPieceMark: string,
rawSection: string | undefined,
phase: string | undefined,
singlePartMark: string | undefined,
prod: ProductionPieceStatus | undefined,
selectedOF: string | undefined
): {
displayMark: string;
displaySection: string;
} {
const baseMark = prod?.marca || rawPieceMark || 'Peça';
let displayMark = baseMark;
// Se tivermos OF e Fase e a marca for pura (ex: "1" ou "8" ou "59"), formata como "B138-1-59"
if (selectedOF && phase && !baseMark.toUpperCase().startsWith(selectedOF.toUpperCase())) {
displayMark = `${selectedOF}-${phase}-${baseMark}`;
}
// Perfil / Bitola oficial: prioriza banco de produção (TS_ERP.pecas.perfil_principal), seguido de section limpo do modelo
let displaySection = prod?.perfilPrincipal || '';
if (!displaySection || displaySection === '1001' || displaySection === 'Perfil') {
if (rawSection && rawSection !== '1001' && rawSection !== 'Perfil') {
displaySection = rawSection;
} else {
displaySection = prod?.descricao || 'Perfil';
}
}
return { displayMark, displaySection };
}
interface SmartModelViewer3DProps {
@@ -46,6 +135,7 @@ interface SmartModelViewer3DProps {
selectedOF: string;
cliente?: string;
phasesList: string[];
initialRotation?: { x: number; y: number; z: number } | null;
onVoltar: () => void;
}
@@ -65,6 +155,7 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
selectedOF,
cliente,
phasesList,
initialRotation,
onVoltar,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
@@ -97,6 +188,11 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
// Modal de Lista da Seleção
const [isSelectionModalOpen, setIsSelectionModalOpen] = useState<boolean>(false);
// Controles de Rotação 90° e Trava com Cadeado (Memória de Posição)
const [rotationX, setRotationX] = useState<number>(0);
const [rotationY, setRotationY] = useState<number>(0);
const [isRotationLocked, setIsRotationLocked] = useState<boolean>(false);
// Seleção e Toast Discreto
interface SelectedElement {
expressID: number;
@@ -372,7 +468,7 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
controlsRef.current.update();
}, []);
// 3. Montar malhas do modelo IFC na cena Three.js
// 3. Montar malhas do modelo IFC/WRL na cena Three.js & Aplicar Rotação Memorizada
useEffect(() => {
if (!modelData || !sceneRef.current) return;
@@ -384,11 +480,87 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
modelGroupRef.current = group;
sceneRef.current.add(group);
// Recupera rotação memorizada para esta OF (localStorage ou banco de dados)
const storageKey = selectedOF ? `tracksteel_model_rot_${selectedOF}` : null;
let savedX = 0;
let savedY = 0;
let isLocked = false;
if (storageKey) {
const stored = localStorage.getItem(storageKey);
if (stored) {
try {
const parsed = JSON.parse(stored);
if (typeof parsed.x === 'number') savedX = parsed.x;
if (typeof parsed.y === 'number') savedY = parsed.y;
isLocked = true;
} catch (e) {}
}
}
if (!isLocked && initialRotation) {
if (typeof initialRotation.x === 'number') savedX = initialRotation.x;
if (typeof initialRotation.y === 'number') savedY = initialRotation.y;
isLocked = true;
}
setRotationX(savedX);
setRotationY(savedY);
setIsRotationLocked(isLocked);
// Aplica rotação no grupo do modelo Three.js (corrige modelos tombados)
group.rotation.set(
THREE.MathUtils.degToRad(savedX),
THREE.MathUtils.degToRad(savedY),
0
);
group.updateMatrixWorld(true);
// Enquadra a câmera automaticamente após carregar o modelo
setTimeout(() => {
fitModelToView();
}, 150);
}, [modelData, fitModelToView]);
}, [modelData, selectedOF, initialRotation, fitModelToView]);
// Gira o modelo em 90 graus no eixo X (tombamento vertical para alinhar Z-Up do CAD com Y-Up)
const handleRotate90X = useCallback(() => {
if (!modelGroupRef.current) return;
const nextX = (rotationX + 90) % 360;
setRotationX(nextX);
setIsRotationLocked(false);
modelGroupRef.current.rotation.x = THREE.MathUtils.degToRad(nextX);
modelGroupRef.current.rotation.y = THREE.MathUtils.degToRad(rotationY);
modelGroupRef.current.updateMatrixWorld(true);
fitModelToView();
smartAudio.playClick();
}, [rotationX, rotationY, fitModelToView]);
// Alterna o bloqueio/gravação da posição ideal de visualização no banco e no navegador
const handleToggleLockRotation = useCallback(async () => {
if (!selectedOF) return;
if (!isRotationLocked) {
setIsRotationLocked(true);
const rotData = { x: rotationX, y: rotationY, z: 0 };
localStorage.setItem(`tracksteel_model_rot_${selectedOF}`, JSON.stringify(rotData));
try {
await supabase
.from('ordens_fabricacao' as any)
.update({ model_3d_rotation: rotData })
.eq('num_of', selectedOF);
} catch (err) {
console.error('Erro ao salvar rotação no banco:', err);
}
smartAudio.playSuccess();
} else {
setIsRotationLocked(false);
smartAudio.playClick();
}
}, [selectedOF, isRotationLocked, rotationX, rotationY]);
// 4. Aplicação de Filtros (Fase e Processo) e Cores de Produção idêntico ao modo desktop
useEffect(() => {
@@ -655,16 +827,40 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
}
});
const pmark = hitMesh.userData.pieceMark;
const phase = hitMesh.userData.phase;
const piece: PieceInfo = (modelData && modelData.pieceByExpressID.get(targetIfcId)) || {
const rawPiece = modelData?.pieceByExpressID.get(targetIfcId);
const rawMark = hitMesh.userData.pieceMark || rawPiece?.pieceMark || '';
const phase = hitMesh.userData.phase || rawPiece?.phase;
const singlePartMark = hitMesh.userData.singlePartMark || rawPiece?.singlePartMark;
const prodInfo = findProductionDataForPiece(productionData, {
pieceMark: rawMark,
cleanMark: hitMesh.userData.cleanMark || rawPiece?.cleanMark,
assemblyMark: hitMesh.userData.assemblyMark || rawPiece?.assemblyMark,
cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark,
phase,
}, selectedOF);
const { displayMark, displaySection } = resolvePieceEngineeringDetails(
rawMark,
hitMesh.userData.section || rawPiece?.section,
phase,
singlePartMark,
prodInfo,
selectedOF
);
const piece: PieceInfo = {
expressID: targetIfcId,
guid: '',
name: hitMesh.userData.section || 'Peça',
type: 'PIECE',
pieceMark: pmark,
section: hitMesh.userData.section,
phase: phase,
guid: hitMesh.userData.guid || rawPiece?.guid || '',
name: hitMesh.userData.name || rawPiece?.name || 'Peça',
type: hitMesh.userData.type || rawPiece?.type || 'PIECE',
pieceMark: displayMark,
cleanMark: hitMesh.userData.cleanMark || rawPiece?.cleanMark,
assemblyMark: hitMesh.userData.assemblyMark || rawPiece?.assemblyMark,
cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark,
section: displaySection,
phase,
singlePartMark,
};
nextMap.set(targetIfcId, {
@@ -675,22 +871,7 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
setSelectedElementsMap(nextMap);
smartAudio.playSuccess();
// 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; }
}
}
// Abre o Toast com dados de engenharia enriquecidos
setToastInfo({
piece,
position: { x: e.clientX, y: e.clientY },
@@ -718,26 +899,10 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
// 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; }
}
}
const prod = findProductionDataForPiece(productionData, piece, selectedOF);
return {
id: String(expressID),
pieceMark: pmark,
pieceMark: piece.pieceMark || 'Sem Marca',
phase: piece.phase,
section: piece.section,
name: piece.name,
@@ -775,13 +940,14 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
let left = toastInfo.position.x - rect.left + 14;
let top = toastInfo.position.y - rect.top - 20;
if (left + 230 > rect.width) {
left = Math.max(12, left - 245);
if (left + 310 > rect.width) {
left = Math.max(12, left - 325);
}
if (top + 160 > rect.height) {
top = Math.max(12, rect.height - 170);
if (top + 230 > rect.height) {
top = Math.max(12, rect.height - 240);
}
if (top < 12) top = 12;
if (left < 12) left = 12;
return { left: `${left}px`, top: `${top}px` };
}, [toastInfo]);
@@ -943,7 +1109,7 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
</div>
</div>
{/* 2. Top Right: Cubo 3D Compacto + Card de Ações da Seleção (Print 3) */}
{/* 2. Top Right: Cubo 3D Compacto + Card de Giro 90° + Card de Ações da Seleção */}
<div className="absolute top-16 right-3 z-20 flex flex-col items-end gap-2 pointer-events-auto">
{/* Cubo Compacto Discreto */}
<ViewCube
@@ -953,11 +1119,57 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
compact={true}
/>
{/* Card de Ajuste e Travamento de Rotação 3D (Tombamento 90° + Cadeado Memorizável) */}
<div className="w-[140px] flex items-center gap-1.5 p-1.5 rounded-2xl bg-white/95 dark:bg-slate-900/95 backdrop-blur-md border border-slate-300 dark:border-slate-700 shadow-lg shadow-slate-300/30 dark:shadow-slate-950/50 pointer-events-auto">
{/* Botão Rotacionar / Tombar 90° */}
<button
type="button"
onClick={handleRotate90X}
disabled={!modelData}
className={`flex-1 min-w-0 flex items-center justify-center gap-1 px-1.5 py-1.5 rounded-xl text-[10px] font-bold transition-all duration-150 active:scale-95 ${
modelData
? 'bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-800 dark:text-slate-100 cursor-pointer shadow-sm'
: 'bg-slate-100/50 dark:bg-slate-800/30 text-slate-400 dark:text-slate-600 cursor-not-allowed opacity-50'
}`}
title="Girar o modelo 90° no eixo de tombamento (adequar orientação Z/Y)"
>
<RotateCw className="w-3 h-3 text-blue-600 dark:text-blue-400 shrink-0" />
<span className="whitespace-nowrap">Girar 90°</span>
<span className="text-[9px] font-mono font-normal text-slate-600 dark:text-slate-300 bg-white dark:bg-slate-950 px-1 py-0.2 rounded border border-slate-200 dark:border-slate-800 shrink-0">
{rotationX}°
</span>
</button>
{/* Botão Cadeado: Travar / Gravar ou Destravar */}
<button
type="button"
onClick={handleToggleLockRotation}
disabled={!modelData}
className={`flex items-center justify-center p-1.5 rounded-xl transition-all duration-200 active:scale-95 border shrink-0 ${
!modelData
? 'bg-slate-100/50 dark:bg-slate-800/30 text-slate-400 border-transparent cursor-not-allowed opacity-40'
: isRotationLocked
? 'bg-emerald-50 hover:bg-emerald-100 text-emerald-700 border-emerald-300 dark:bg-emerald-950/60 dark:hover:bg-emerald-900/80 dark:text-emerald-300 dark:border-emerald-700/80 shadow-sm cursor-pointer'
: 'bg-amber-50 hover:bg-amber-100 text-amber-700 border-amber-300 dark:bg-amber-950/50 dark:hover:bg-amber-900/70 dark:text-amber-300 dark:border-amber-700/70 shadow-sm cursor-pointer animate-pulse'
}`}
title={
isRotationLocked
? 'Posição TRAVADA e salva para esta OF. Clique para destravar e ajustar.'
: 'Posição DESTRAVADA. Clique aqui no cadeado para travar e salvar a posição ideal.'
}
>
{isRotationLocked ? (
<Lock className="w-3.5 h-3.5 text-emerald-600 dark:text-emerald-400" />
) : (
<Unlock className="w-3.5 h-3.5 text-amber-600 dark:text-amber-400" />
)}
</button>
</div>
{/* Card Flutuante de Seleção com "Gerar lista da seleção" (Print 3) */}
{selectedCount > 0 && (
<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' }}
className="w-[140px] 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"
>
{/* 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">
@@ -995,7 +1207,7 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
)}
</div>
{/* 3. Toast Discreto ao Lado da Peça Clicada */}
{/* 3. Toast Discreto ao Lado da Peça Clicada (Réplica Idêntica do Modo Completo - Print 2) */}
{toastInfo && (
<div
onClick={() => {
@@ -1003,63 +1215,100 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
setToastInfo(null);
}}
style={toastStyle}
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)"
className="absolute z-40 p-4 rounded-2xl bg-slate-900/95 backdrop-blur-xl border border-cyan-500/40 shadow-2xl shadow-cyan-950/60 text-slate-100 min-w-[270px] max-w-[330px] animate-in fade-in zoom-in-95 duration-150 cursor-pointer pointer-events-auto select-none transition-all group"
title="Toque no card 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-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-600 dark:text-fuchsia-300 font-mono tracking-tight truncate">
{toastInfo.piece.pieceMark || 'Sem Marca'}
</span>
{/* Cabeçalho do Toast com Ícone Box e Marca Principal de Engenharia */}
<div className="flex items-center justify-between gap-2 pb-2.5 mb-2.5 border-b border-slate-800">
<div className="flex items-center gap-2 min-w-0">
<div className="p-1.5 rounded-lg bg-cyan-500/20 text-cyan-400 border border-cyan-500/30 shrink-0">
<Box className="w-4 h-4" />
</div>
<div className="min-w-0">
<span className="text-[10px] uppercase font-mono tracking-widest text-slate-400 block leading-tight">
Marca Principal
</span>
<span className="text-base font-extrabold text-cyan-300 font-mono tracking-tight truncate block">
{toastInfo.piece.pieceMark || 'Sem Marca'}
</span>
</div>
</div>
<div className="flex items-center gap-1.5 shrink-0">
{toastInfo.piece.phase && (
<span className="px-2 py-0.5 rounded-full text-[10px] font-bold font-mono bg-slate-800 text-slate-300 border border-slate-700">
Fase {toastInfo.piece.phase}
</span>
)}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setToastInfo(null);
}}
className="p-1 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition-colors"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setToastInfo(null);
}}
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>
</div>
{/* 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-600 dark:text-slate-300">
<span className="text-slate-400">Fase:</span>
<span className="font-bold text-slate-800 dark:text-slate-200">Fase {toastInfo.piece.phase}</span>
</div>
)}
{/* Detalhes Técnicos da Peça */}
<div className="space-y-1.5 text-xs">
{toastInfo.piece.section && (
<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-800 dark:text-slate-100 truncate max-w-[120px]">
<div className="flex items-center justify-between text-slate-300">
<span className="text-slate-400">Perfil / Bitola:</span>
<span className="font-semibold text-white font-mono bg-slate-950/60 px-2 py-0.5 rounded border border-slate-800">
{toastInfo.piece.section}
</span>
</div>
)}
{toastInfo.prod && (
<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"
style={{
backgroundColor: `${toastInfo.prod.processColor || '#10b981'}25`,
color: toastInfo.prod.processColor || '#10b981',
}}
>
{toastInfo.prod.currentProcessName || 'Produção'} ({toastInfo.prod.pointedQtd}/{toastInfo.prod.totalQtd})
{toastInfo.piece.singlePartMark && toastInfo.piece.singlePartMark !== toastInfo.piece.pieceMark && (
<div className="flex items-center justify-between text-slate-300">
<span className="text-slate-400">Sub-peça / Corte:</span>
<span className="font-semibold text-slate-200 font-mono bg-slate-950/60 px-2 py-0.5 rounded border border-slate-800">
Pos. {toastInfo.piece.singlePartMark}
</span>
</div>
)}
{/* Apontamentos e Etapa Atual */}
{toastInfo.prod && (
<div className="mt-3 pt-2.5 border-t border-slate-800/80 space-y-1.5">
<div className="flex items-center justify-between">
<span className="text-slate-400 flex items-center gap-1.5">
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-400" />
Apontamento:
</span>
<span className="font-bold text-emerald-400 font-mono">
{toastInfo.prod.pointedQtd ?? 0} de {toastInfo.prod.totalQtd ?? 1} peças
</span>
</div>
{toastInfo.prod.currentProcessName && (
<div className="flex items-center justify-between text-[11px]">
<span className="text-slate-400 flex items-center gap-1">
<Clock className="w-3 h-3 text-amber-400" />
Etapa Atual:
</span>
<span
className="px-2 py-0.5 rounded-full font-bold uppercase tracking-wider text-[10px]"
style={{
backgroundColor: `${toastInfo.prod.processColor || '#10b981'}25`,
color: toastInfo.prod.processColor || '#10b981',
border: `1px solid ${toastInfo.prod.processColor || '#10b981'}40`,
}}
>
{toastInfo.prod.currentProcessName}
</span>
</div>
)}
</div>
)}
</div>
{/* Dica para Fechar */}
<div className="mt-2 pt-1 border-t border-slate-200 dark:border-slate-800 text-[9px] text-center text-slate-400 font-medium">
<div className="mt-2.5 pt-1.5 border-t border-slate-800 text-[9px] text-center text-slate-400 font-medium">
Toque no card para fechar ✕
</div>
</div>
@@ -20,6 +20,7 @@ export const SmartViewer3DFlow: React.FC<SmartViewer3DFlowProps> = ({
const [modelData, setModelData] = useState<LoadedModel3DResult | null>(null);
const [phasesList, setPhasesList] = useState<string[]>([]);
const [productionMap, setProductionMap] = useState<Map<string, ProductionPieceStatus>>(new Map());
const [initialRotation, setInitialRotation] = useState<{ x: number; y: number; z: number } | null>(null);
// Loading States
const [isLoading, setIsLoading] = useState<boolean>(true);
@@ -35,10 +36,10 @@ export const SmartViewer3DFlow: React.FC<SmartViewer3DFlowProps> = ({
setLoadingPercent(15);
try {
// 1.1. Buscar registro da OF para obter URL do IFC
// 1.1. Buscar registro da OF para obter URL do IFC e rotação memorizada
const { data: ofData, error: ofError } = await supabase
.from('ordens_fabricacao' as any)
.select('id, num_of, descritivo, ifc_url, ifc_filename')
.select('id, num_of, descritivo, ifc_url, ifc_filename, model_3d_rotation')
.eq('num_of', obra.of_number)
.maybeSingle();
@@ -52,6 +53,9 @@ export const SmartViewer3DFlow: React.FC<SmartViewer3DFlowProps> = ({
const ifcUrl = (ofData as any).ifc_url;
const ifcFilename = (ofData as any).ifc_filename;
if ((ofData as any).model_3d_rotation) {
setInitialRotation((ofData as any).model_3d_rotation);
}
// 1.2. Buscar Peças e Apontamentos em paralelo
setLoadingStep('Carregando dados das peças e apontamentos...');
@@ -87,6 +91,8 @@ export const SmartViewer3DFlow: React.FC<SmartViewer3DFlowProps> = ({
marca: marcaStr,
fase,
etapa_fase: fase,
descricao: p.descricao || '',
perfilPrincipal: p.perfil_principal || p.descricao || '',
totalQtd: Number(p.quantidade || 1),
pointedQtd: 0,
currentProcessName: 'Pendente',
@@ -277,6 +283,7 @@ export const SmartViewer3DFlow: React.FC<SmartViewer3DFlowProps> = ({
selectedOF={obra.of_number}
cliente={obra.cliente}
phasesList={phasesList}
initialRotation={initialRotation}
onVoltar={onVoltarHub}
/>
</div>