import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; import { supabase } from '@/integrations/supabase/client'; import { loadAndAuditIFC, LoadedIFCResult, IFCQualityAudit } from '@/lib/ifc/ifcLoaderService'; import { uploadIFCToCloud, fetchSavedIFCModel, removeSavedIFCModel } from '@/lib/ifc/ifcStorageService'; import { ModelViewer3D } from '@/components/viewer3d/ModelViewer3D'; import { IFCQualityModal } from '@/components/viewer3d/IFCQualityModal'; import { useToast } from '@/hooks/use-toast'; import { Upload, Box, Layers, ShieldCheck, CheckCircle2, Loader2, TrendingUp, Weight, Cloud, Trash2 } from 'lucide-react'; interface OFOption { id: string; of_number: string; descritivo?: string; cliente?: string; peso_total?: number; ifc_url?: string | null; ifc_filename?: string | null; ifc_file_size?: number | null; ifc_updated_at?: string | null; } const PROCESS_COLORS: Record = { 'Detalhamento': '#3b82f6', 'Corte': '#f59e0b', 'Montagem': '#6366f1', 'Solda': '#f97316', 'Pintura': '#10b981', 'Pintura/Galv': '#10b981', 'Expedicao': '#94a3b8', 'Aceite/DB': '#06b6d4', 'Concluido': '#22c55e', }; export default function Visualizador3D() { const { toast } = useToast(); const [ofs, setOfs] = useState([]); const [selectedOF, setSelectedOF] = useState(''); const [selectedPhase, setSelectedPhase] = useState('all'); const [phasesList, setPhasesList] = useState([]); // Production pointing data from Supabase const [productionMap, setProductionMap] = useState>(new Map()); const [uniqueItemsList, setUniqueItemsList] = useState([]); const [totalPecasBD, setTotalPecasBD] = useState(0); const [totalApontadasBD, setTotalApontadasBD] = useState(0); const [pesoTotalBD, setPesoTotalBD] = useState(0); const [progressoOF, setProgressoOF] = useState(0); // 3D Model and Quality State const [modelData, setModelData] = useState(null); const [auditData, setAuditData] = useState(null); const [isAuditModalOpen, setIsAuditModalOpen] = useState(false); const [loadingStep, setLoadingStep] = useState(''); const [loadingPercent, setLoadingPercent] = useState(0); const [isLoadingModel, setIsLoadingModel] = useState(false); const fileInputRef = useRef(null); // 1. Fetch OFs (com colunas de IFC para persistência na nuvem) useEffect(() => { async function fetchOFs() { try { const { data, error } = await supabase .from('ordens_fabricacao' as any) .select('id, num_of, descritivo, peso_total, ifc_url, ifc_filename, ifc_file_size, ifc_updated_at') .order('num_of', { ascending: false }); if (!error && data) { const list: OFOption[] = (data as any[]).map((item) => ({ id: item.id, of_number: item.num_of || '', descritivo: item.descritivo || '', peso_total: Number(item.peso_total || 0), ifc_url: item.ifc_url || null, ifc_filename: item.ifc_filename || null, ifc_file_size: item.ifc_file_size ? Number(item.ifc_file_size) : null, ifc_updated_at: item.ifc_updated_at || null, })); setOfs(list); } } catch (err) { console.error('Erro ao carregar OFs:', err); } } fetchOFs(); }, []); // 2. Fetch Pieces & Production Apontamentos for Selected OF useEffect(() => { if (!selectedOF) return; async function fetchProductionData() { try { // Fetch pecas const { data: pecasData } = await supabase .from('pecas' as any) .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 const { data: apontamentosData, error: errorApontamentos } = await supabase .from('apontamentos_producao' as any) .select(` id, of_number, peca_id, quantidade_produzida, peca:pecas!apontamentos_producao_peca_id_fkey(marca, etapa_fase), processo:processos_fabricacao!apontamentos_producao_processo_id_fkey(nome, cor, ordem) `) .eq('of_number', selectedOF); if (errorApontamentos) { console.error('Erro ao buscar apontamentos no visualizador:', errorApontamentos); } const phases = new Set(); const prodMap = new Map(); const uniqueItems: any[] = []; let totalPecas = 0; let totalPeso = 0; if (pecasData) { pecasData.forEach((p: any) => { const fase = String(p.etapa_fase || '').trim(); if (fase) phases.add(fase); totalPecas += Number(p.quantidade || 0); totalPeso += Number(p.peso_total || 0); const marcaStr = String(p.marca || '').trim(); const item = { pecaId: p.id, marca: marcaStr, fase, ofNumber: selectedOF, totalQtd: Number(p.quantidade || 1), pesoTotal: Number(p.peso_total || 0), pesoUnitario: Number(p.peso_unitario || 0), pointedQtd: 0, currentProcessName: 'Pendente', processColor: '#64748b', processOrdem: 0, }; uniqueItems.push(item); // Indexações para amarração gráfica flexível prodMap.set(marcaStr, item); prodMap.set(marcaStr.toUpperCase(), item); prodMap.set(`${fase}-${marcaStr}`, item); prodMap.set(`${fase}-${marcaStr}`.toUpperCase(), item); prodMap.set(`${selectedOF}-${fase}-${marcaStr}`, item); prodMap.set(`${selectedOF}-${fase}-${marcaStr}`.toUpperCase(), item); prodMap.set(`${selectedOF}-${marcaStr}`, item); prodMap.set(`${selectedOF}-${marcaStr}`.toUpperCase(), item); if (p.descricao) { const d = String(p.descricao).trim().toUpperCase(); prodMap.set(`DESC:${d}`, item); } if (p.perfil_principal) { const perf = String(p.perfil_principal).trim().toUpperCase(); prodMap.set(`PERFIL:${perf}`, item); } }); } if (apontamentosData) { apontamentosData.sort((a: any, b: any) => (a.processo?.ordem || 0) - (b.processo?.ordem || 0)); apontamentosData.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(`${selectedOF}-${fase}-${marca}`) || prodMap.get(`${fase}-${marca}`) || prodMap.get(marca); if (existing) { const qty = Number(ap.quantidade_produzida || 0); if (qty > 0) { existing.pointedQtd = Math.max(existing.pointedQtd, qty); existing.currentProcessName = ap.processo?.nome || existing.currentProcessName; existing.processColor = ap.processo?.cor || PROCESS_COLORS[ap.processo?.nome || ''] || '#10b981'; existing.processOrdem = ap.processo?.ordem || existing.processOrdem; } } } }); } let pointedTotal = 0; let sumProgress = 0; uniqueItems.forEach((val) => { pointedTotal += val.pointedQtd; const pecaProgress = val.pointedQtd > 0 ? Math.min(val.processOrdem / 5, 1) * (val.pointedQtd / val.totalQtd) : 0; sumProgress += pecaProgress * val.totalQtd; }); const progressoGeralPercent = totalPecas > 0 ? Math.min(Math.round((sumProgress / totalPecas) * 100), 100) : 0; setPhasesList(Array.from(phases).sort((a, b) => { const na = Number(a); const nb = Number(b); return !isNaN(na) && !isNaN(nb) ? na - nb : a.localeCompare(b); })); setProductionMap(prodMap); setUniqueItemsList(uniqueItems); setTotalPecasBD(totalPecas); setTotalApontadasBD(pointedTotal); setPesoTotalBD(totalPeso); setProgressoOF(progressoGeralPercent); } catch (err) { console.error('Erro ao carregar dados de produção da OF:', err); } } fetchProductionData(); }, [selectedOF]); // Função auxiliar de carregamento de modelo salvo const loadSavedModel = useCallback(async (ofNumber: string, url: string, filename?: string | null) => { setIsLoadingModel(true); setLoadingStep('Buscando modelo 3D vinculado...'); setLoadingPercent(15); try { const buffer = await fetchSavedIFCModel(ofNumber, url, (percent, step) => { setLoadingPercent(percent); setLoadingStep(step); }); setLoadingStep('Decodificando entidades e montagens IFC...'); setLoadingPercent(55); const result = await loadAndAuditIFC(buffer, (percent, step) => { setLoadingPercent(55 + Math.round(percent * 0.45)); setLoadingStep(step); }); setModelData(result); setAuditData(result.audit); // Mescla fases detectadas no IFC às fases da OF if (result.detectedPhases && result.detectedPhases.length > 0) { setPhasesList((prev) => { const combined = new Set([...prev, ...result.detectedPhases]); return Array.from(combined).sort((a, b) => { const na = Number(a); const nb = Number(b); return !isNaN(na) && !isNaN(nb) ? na - nb : a.localeCompare(b); }); }); } toast({ title: 'Modelo 3D Carregado', description: `Modelo da OF ${ofNumber} carregado automaticamente da nuvem.`, }); } catch (err: any) { console.error('Erro ao carregar modelo salvo:', err); toast({ variant: 'destructive', title: 'Falha no Carregamento Automático', description: `Não foi possível carregar o modelo salvo: ${err.message || 'Erro de rede'}`, }); } finally { setIsLoadingModel(false); } }, [toast]); // 3. Efeito para verificar e carregar automaticamente o modelo IFC salvo ao selecionar uma obra useEffect(() => { if (!selectedOF) return; // Reseta visualizador para a nova OF setModelData(null); setAuditData(null); setSelectedPhase('all'); const currentOF = ofs.find((o) => o.of_number === selectedOF); if (currentOF && currentOF.ifc_url) { loadSavedModel(selectedOF, currentOF.ifc_url, currentOF.ifc_filename); } else if (ofs.length === 0) { // Caso a lista de ofs ainda esteja sendo buscada, tenta consulta direta rápida async function checkDirect() { try { const { data } = await supabase .from('ordens_fabricacao' as any) .select('ifc_url, ifc_filename') .eq('num_of', selectedOF) .maybeSingle(); if (data && (data as any).ifc_url) { loadSavedModel(selectedOF, (data as any).ifc_url, (data as any).ifc_filename); } } catch (err) { console.debug('Checagem direta de IFC:', err); } } checkDirect(); } }, [selectedOF, ofs, loadSavedModel]); // Handle File Upload: Processa, audita e salva automaticamente na nuvem const handleFileChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setIsLoadingModel(true); setLoadingStep('Iniciando processamento e auditoria...'); setLoadingPercent(10); try { // 1. Processa e audita localmente para renderização imediata const result = await loadAndAuditIFC(file, (percent, step) => { setLoadingPercent(Math.min(percent, 70)); setLoadingStep(step); }); setModelData(result); setAuditData(result.audit); setIsAuditModalOpen(true); // Abre modal de qualidade automaticamente // Mescla fases detectadas no IFC if (result.detectedPhases && result.detectedPhases.length > 0) { setPhasesList((prev) => { const combined = new Set([...prev, ...result.detectedPhases]); return Array.from(combined).sort((a, b) => { const na = Number(a); const nb = Number(b); return !isNaN(na) && !isNaN(nb) ? na - nb : a.localeCompare(b); }); }); } // 2. Upload e armazenamento permanente no banco de dados e nuvem setLoadingStep(`Salvando modelo no banco de dados da OF ${selectedOF}...`); setLoadingPercent(75); const { publicUrl } = await uploadIFCToCloud( file, selectedOF, result.audit, (step, percent) => { setLoadingStep(step); setLoadingPercent(75 + Math.round(percent * 0.25)); } ); // Atualiza o estado local das OFs com o novo arquivo vinculado setOfs((prev) => prev.map((o) => o.of_number === selectedOF ? { ...o, ifc_url: publicUrl, ifc_filename: file.name, ifc_file_size: file.size, ifc_updated_at: new Date().toISOString(), } : o ) ); toast({ title: 'Modelo IFC Gravado no Banco!', description: `O arquivo "${file.name}" foi salvo com sucesso e carregará automaticamente ao entrar na OF ${selectedOF}.`, }); } catch (err: any) { console.error('Erro ao carregar e salvar IFC:', err); toast({ variant: 'destructive', title: 'Erro no Carregamento', description: `Falha: ${err.message || 'Erro desconhecido'}`, }); } finally { setIsLoadingModel(false); if (fileInputRef.current) fileInputRef.current.value = ''; } }; // Remover modelo salvo const handleRemoveSavedModel = async () => { if (!confirm(`Deseja remover o modelo IFC vinculado à OF ${selectedOF}?`)) return; try { await removeSavedIFCModel(selectedOF); setOfs((prev) => prev.map((o) => o.of_number === selectedOF ? { ...o, ifc_url: null, ifc_filename: null, ifc_file_size: null, ifc_updated_at: null } : o ) ); setModelData(null); setAuditData(null); toast({ title: 'Modelo Removido', description: `O modelo vinculado à OF ${selectedOF} foi desvinculado com sucesso.`, }); } catch (err: any) { console.error('Erro ao remover modelo:', err); toast({ variant: 'destructive', title: 'Erro ao desvincular', description: err.message, }); } }; // Métricas dinâmicas refletindo a fase selecionada const activeMetrics = useMemo(() => { if (selectedPhase === 'all' || !selectedPhase) { return { totalPecas: totalPecasBD, apontadas: totalApontadasBD, peso: pesoTotalBD, progresso: progressoOF, phaseLabel: null, }; } const filtered = uniqueItemsList.filter((item) => String(item.fase) === String(selectedPhase)); let pTotal = 0; let pApontadas = 0; let pPeso = 0; let sumProg = 0; filtered.forEach((item) => { pTotal += item.totalQtd; pApontadas += item.pointedQtd; pPeso += (item.pesoTotal || 0); const pecaProgress = item.pointedQtd > 0 ? Math.min(item.processOrdem / 5, 1) * (item.pointedQtd / item.totalQtd) : 0; sumProg += pecaProgress * item.totalQtd; }); const progPercent = pTotal > 0 ? Math.min(Math.round((sumProg / pTotal) * 100), 100) : 0; return { totalPecas: pTotal, apontadas: pApontadas, peso: pPeso, progresso: progPercent, phaseLabel: `Fase ${selectedPhase}`, }; }, [selectedPhase, totalPecasBD, totalApontadasBD, pesoTotalBD, progressoOF, uniqueItemsList]); const currentOFInfo = ofs.find((o) => o.of_number === selectedOF); const hasSavedModel = Boolean(currentOFInfo?.ifc_url); return (
{/* Top Header Bar */}
{/* Left: Title, OF Selector & Phase Filter */}

Visualizador 3D Industrial BIM + Produção

Acompanhamento visual de peças, montagens e apontamentos industriais

{/* OF Selector Dropdown */}
OF:
{/* Cloud Stored Model Badge */} {hasSavedModel && (
{currentOFInfo?.ifc_filename || 'Modelo Salvo'}
)} {/* Phase Filter Buttons */} {phasesList.length > 0 && (
{phasesList.map((f) => ( ))}
)}
{/* Right: Actions & Audit Badge */}
{auditData && ( )}
{/* Production KPIs Bar */}
Total de Peças {activeMetrics.phaseLabel ? `(${activeMetrics.phaseLabel})` : ''} {activeMetrics.totalPecas} peças
Apontamentos Realizados {activeMetrics.phaseLabel ? `(${activeMetrics.phaseLabel})` : ''} {activeMetrics.apontadas} apontadas
Evolução {activeMetrics.phaseLabel ? `(${activeMetrics.phaseLabel})` : 'da OF'} {activeMetrics.progresso}% concluído
Peso {activeMetrics.phaseLabel ? `(${activeMetrics.phaseLabel})` : 'da Estrutura'} {activeMetrics.peso > 0 ? `${(activeMetrics.peso / 1000).toFixed(1)} ton` : '0.0 ton'}
{/* Main 3D Viewport */}
{isLoadingModel && (

{loadingStep}

{loadingPercent}%
)} {!modelData && !isLoadingModel && (

Nenhum modelo IFC carregado para {selectedOF}

Clique no botão "Carregar Modelo IFC" acima para importar o arquivo de fabricação desta obra. Ele será gravado no banco de dados e recarregado automaticamente nas próximas visitas.

)}
{/* Quality Audit Modal */} setIsAuditModalOpen(false)} />
); }