import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; import { supabase } from '@/integrations/supabase/client'; import { loadAndAuditModel3D, LoadedModel3DResult } from '@/lib/3d/modelLoaderService'; import { fetchSavedIFCModel } from '@/lib/ifc/ifcStorageService'; import { AnalyzerViewer3D } from '@/components/viewer3d/AnalyzerViewer3D'; import { PieceAuditDetailModal } from '@/components/viewer3d/PieceAuditDetailModal'; import { AuditEngine } from '@/lib/3d/analyzer/auditEngine'; import { AuditReportService } from '@/lib/3d/analyzer/auditReportService'; import { DefectItem, DefectType, DefectSeverity, ColorizationMode, ComparisonDiffSummary } from '@/types/analisador3d'; import { useToast } from '@/hooks/use-toast'; import { usePermissions } from '@/hooks/usePermissions'; import { ScanSearch, Upload, FileSpreadsheet, FileText, Layers, Filter, AlertTriangle, AlertCircle, CheckCircle2, RefreshCw, Search, Crosshair, Factory, Maximize2, Minimize2, Boxes, ShieldAlert, SlidersHorizontal, ChevronDown, Info, Wrench, Sparkles, Weight } from 'lucide-react'; interface OFOption { id: string; of_number: string; descritivo?: string; peso_total?: number; ifc_url?: string | null; ifc_filename?: string | null; model_3d_rotation?: { x: number; y: number; z: number } | null; } export default function Analisador3D() { const { toast } = useToast(); const { checkResourceAccess } = usePermissions(); // Estados principais de dados const [ofs, setOfs] = useState([]); const [selectedOF, setSelectedOF] = useState(''); const [selectedPhase, setSelectedPhase] = useState('all'); const [phasesList, setPhasesList] = useState([]); // Modos de Operação const [analysisMode, setAnalysisMode] = useState<'auditoria' | 'comparacao'>('auditoria'); const [colorizationMode, setColorizationMode] = useState('status_defeito'); const [isGhostMode, setIsGhostMode] = useState(true); const [isFullscreen, setIsFullscreen] = useState(false); const [isDarkTheme, setIsDarkTheme] = useState(() => typeof document !== 'undefined' ? document.documentElement.classList.contains('dark') : false ); useEffect(() => { const observer = new MutationObserver(() => { setIsDarkTheme(document.documentElement.classList.contains('dark')); }); observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); return () => observer.disconnect(); }, []); // Modelos 3D Carregados const [modelBase, setModelBase] = useState(null); const [modelRevisao, setModelRevisao] = useState(null); const [isLoadingModel, setIsLoadingModel] = useState(false); const [loadingStep, setLoadingStep] = useState(''); const [loadingPercent, setLoadingPercent] = useState(0); // Dados de Produção e Auditoria const [productionMap, setProductionMap] = useState>(new Map()); const [defects, setDefects] = useState([]); const [summary, setSummary] = useState(null); // Filtros e Seleção const [filterSeverity, setFilterSeverity] = useState('all'); const [filterType, setFilterType] = useState('all'); const [filterProcess, setFilterProcess] = useState('all'); const [filterAssembly, setFilterAssembly] = useState('all'); const [searchPiece, setSearchPiece] = useState(''); const [selectedDefect, setSelectedDefect] = useState(null); const [selectedPieceMark, setSelectedPieceMark] = useState(null); const [detailModalDefect, setDetailModalDefect] = useState(null); const fileInputBaseRef = useRef(null); const fileInputRevRef = useRef(null); // 1. Carrega OFs do Supabase 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, model_3d_rotation') .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, model_3d_rotation: item.model_3d_rotation || null, })); setOfs(list); if (list.length > 0 && !selectedOF) { setSelectedOF(list[0].of_number); } } } catch (err) { console.error('Erro ao buscar OFs:', err); } } fetchOFs(); }, []); // 2. Busca apontamentos e peças da OF para alimentar o cruzamento fabril useEffect(() => { if (!selectedOF) return; async function fetchProductionData() { try { 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); const { data: apontamentosData } = 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); const phases = new Set(); const prodMap = new Map(); if (pecasData) { pecasData.forEach((p: any) => { const fase = String(p.etapa_fase || '').trim(); if (fase) phases.add(fase); const marcaStr = String(p.marca || '').trim(); const item = { marca: marcaStr, fase, 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, }; prodMap.set(marcaStr, item); prodMap.set(marcaStr.toUpperCase(), item); }); } if (apontamentosData) { apontamentosData.forEach((ap: any) => { const marca = ap.peca?.marca ? String(ap.peca.marca).trim() : ''; if (marca && prodMap.has(marca)) { const item = prodMap.get(marca); const qty = Number(ap.quantidade_produzida || 0); const procName = String(ap.processo?.nome || '').trim(); if (qty > 0 && !procName.toLowerCase().includes('detalh')) { item.pointedQtd = Math.max(item.pointedQtd, qty); item.currentProcessName = procName; item.processColor = ap.processo?.cor || '#10b981'; item.processOrdem = Number(ap.processo?.ordem || 0); } } }); } setPhasesList(Array.from(phases).sort()); setProductionMap(prodMap); } catch (err) { console.error('Erro ao buscar dados de produção:', err); } } fetchProductionData(); }, [selectedOF]); // 3. Carregamento Automático do Modelo Salvo da OF const loadSavedOFModel = useCallback(async (ofNumber: string) => { const currentOF = ofs.find((o) => o.of_number === ofNumber); if (!currentOF || !currentOF.ifc_url) return; setIsLoadingModel(true); setLoadingStep('Buscando modelo 3D salvo no banco...'); setLoadingPercent(20); try { const buffer = await fetchSavedIFCModel(ofNumber, currentOF.ifc_url, (pct, step) => { setLoadingPercent(pct); setLoadingStep(step); }); setLoadingStep('Processando geometria e montagens...'); setLoadingPercent(60); const result = await loadAndAuditModel3D(buffer, currentOF.ifc_filename || undefined, (pct, step) => { setLoadingPercent(60 + Math.round(pct * 0.4)); setLoadingStep(step); }); setModelBase(result); toast({ title: 'Modelo 3D Carregado', description: `Modelo da OF ${ofNumber} pronto para auditoria.`, }); } catch (err: any) { console.error('Falha ao carregar modelo da OF:', err); toast({ variant: 'destructive', title: 'Falha no Carregamento', description: 'Não foi possível carregar o modelo salvo no banco.', }); } finally { setIsLoadingModel(false); } }, [ofs, toast]); useEffect(() => { if (selectedOF) { loadSavedOFModel(selectedOF); } }, [selectedOF, loadSavedOFModel]); // 4. Executa a Auditoria Geométrica sempre que os modelos ou a OF mudam useEffect(() => { if (!modelBase) { setDefects([]); setSummary(null); return; } const result = AuditEngine.runAudit({ modelBase, modelRevisao: analysisMode === 'comparacao' ? modelRevisao : null, productionMap, }); setDefects(result.defects); setSummary(result.summary); }, [modelBase, modelRevisao, analysisMode, productionMap]); // 5. Upload manual de modelos locais (para testes rápidos de IFC ou WRM) const handleUploadLocalModel = async (file: File, isRev: boolean = false) => { setIsLoadingModel(true); setLoadingStep(`Lendo arquivo ${file.name}...`); setLoadingPercent(30); try { const result = await loadAndAuditModel3D(file, file.name, (pct, step) => { setLoadingPercent(30 + Math.round(pct * 0.7)); setLoadingStep(step); }); if (isRev) { setModelRevisao(result); setAnalysisMode('comparacao'); toast({ title: 'Revisão Carregada', description: `Modelo de revisão ${file.name} integrado para comparação.`, }); } else { setModelBase(result); toast({ title: 'Modelo Base Carregado', description: `Modelo base ${file.name} carregado com sucesso.`, }); } } catch (err: any) { toast({ variant: 'destructive', title: 'Erro no Arquivo', description: err.message || 'Falha ao processar o arquivo 3D.', }); } finally { setIsLoadingModel(false); } }; // 6. Lista Filtrada de Ocorrências const filteredDefects = useMemo(() => { return defects.filter((d) => { if (filterSeverity !== 'all' && d.gravidade !== filterSeverity) return false; if (filterType !== 'all' && d.tipo !== filterType) return false; if (filterProcess !== 'all' && d.processoAtual?.toLowerCase() !== filterProcess.toLowerCase()) return false; if (filterAssembly !== 'all' && d.conjuntoMarca !== filterAssembly) return false; if (searchPiece) { const query = searchPiece.toLowerCase().trim(); const matchMarca = d.pecaMarca.toLowerCase().includes(query); const matchConjunto = d.conjuntoMarca?.toLowerCase().includes(query); const matchConectada = d.pecaConectadaMarca?.toLowerCase().includes(query); if (!matchMarca && !matchConjunto && !matchConectada) return false; } return true; }); }, [defects, filterSeverity, filterType, filterProcess, filterAssembly, searchPiece]); // 7. Lista de Conjuntos disponíveis para filtro const assembliesList = useMemo(() => { const set = new Set(); defects.forEach((d) => { if (d.conjuntoMarca) set.add(d.conjuntoMarca); }); return Array.from(set).sort(); }, [defects]); // 8. Disparo de Relatórios const handleExportPDF = () => { if (!summary) return; const currentOF = ofs.find((o) => o.of_number === selectedOF); AuditReportService.exportToPDF({ ofNumber: selectedOF || 'GERAL', ofDescritivo: currentOF?.descritivo, dataAnalise: new Date().toLocaleDateString('pt-BR'), analisadoPor: 'Controle de Qualidade TrackSteel', modoAnalise: analysisMode, modeloBaseNome: currentOF?.ifc_filename || 'Modelo Base', modeloRevisaoNome: modelRevisao ? 'Revisão Projeto' : undefined, summary, defects: filteredDefects.length > 0 ? filteredDefects : defects, }); }; const handleExportExcel = () => { if (!summary) return; const currentOF = ofs.find((o) => o.of_number === selectedOF); AuditReportService.exportToExcel({ ofNumber: selectedOF || 'GERAL', ofDescritivo: currentOF?.descritivo, dataAnalise: new Date().toLocaleDateString('pt-BR'), analisadoPor: 'Engenharia TrackSteel', modoAnalise: analysisMode, summary, defects: filteredDefects.length > 0 ? filteredDefects : defects, }); }; const currentOFObj = ofs.find((o) => o.of_number === selectedOF); return (
{/* ── BARRA SUPERIOR: SELEÇÕES E AÇÕES ── */}

Analisador 3D BIM & QA/QC TrackSteel Engine

Auditoria de conexões, furações coaxiais e comparador de revisões IFC / WRM

{/* Seletor de OF */}
{/* Alternância de Modo */}
{/* Botões de Ações e Exportações */}
{/* Uploads Locais de Teste */} { if (e.target.files?.[0]) handleUploadLocalModel(e.target.files[0], false); }} /> { if (e.target.files?.[0]) handleUploadLocalModel(e.target.files[0], true); }} /> {analysisMode === 'comparacao' && ( )}
{/* Exportações */}
{/* ── CARDS DE KPIS / INDICADORES ── */} {summary && (
Total Peças
{summary.totalPecasRevisao}
auditadas no 3D
Críticos
{summary.inconformidadesCriticas}
furo cego / colisão
Alertas Furo
{summary.alertasFuros}
desalinhados / excêntricos
Modificadas / Novas
{summary.modificadas} / +{summary.novas}
variação de projeto
Peso Afetado
{summary.pesoAfetadoKg.toFixed(0)} kg
em peças com ocorrência
Risco Fábrica
{summary.riscoGeral}
retrabalho potencial
)} {/* ── ÁREA PRINCIPAL DIVIDIDA (SPLIT RESPONSIVO) ── */}
{/* PAINEL LATERAL ESQUERDO: FILTROS & LISTA DE OCORRÊNCIAS */}
{/* Barra de Filtros */}
{/* Campo de Busca por Peça */}
setSearchPiece(e.target.value)} className="w-full pl-8 pr-3 py-1.5 text-xs bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-700 rounded-lg text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500" /> {searchPiece && ( )}
{/* Filtros em Grade */}
{/* Seletor de Modo de Cores 3D */}
Colorização 3D:
{/* Lista de Ocorrências (Scrollável) */}
{isLoadingModel ? (
{loadingStep} {loadingPercent}% processado
) : filteredDefects.length === 0 ? (

Nenhuma inconformidade encontrada

Todas as furações e geometrias conferem com os parâmetros e filtros ativos.

) : ( filteredDefects.map((defect) => { const isSelected = selectedDefect?.id === defect.id || selectedPieceMark === defect.pecaMarca; return (
{ setSelectedDefect(defect); setSelectedPieceMark(defect.pecaMarca); }} className={`p-3 rounded-lg border transition-all cursor-pointer ${ isSelected ? 'bg-indigo-50 dark:bg-indigo-950/40 border-indigo-500 shadow-md ring-1 ring-indigo-500' : 'bg-white dark:bg-slate-800/80 border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600' }`} >
{defect.pecaMarca} {defect.conjuntoMarca && ( (Conj: {defect.conjuntoMarca}) )}
{defect.gravidade === 'critico' && ( CRÍTICO )} {defect.gravidade === 'alerta' && ( ALERTA )} {defect.gravidade === 'informativo' && ( NOVO )}

{defect.titulo}

{defect.descricao}

{/* Rodapé do Card com Status Fabril e Ação */}
{defect.processoAtual || 'Pendente'} Risco: {defect.riscoFabril}
); }) )}
{/* VISUALIZADOR 3D CENTRAL */}
setSelectedDefect(def)} onSelectPiece={(mark) => setSelectedPieceMark(mark)} isGhostMode={isGhostMode} onToggleGhostMode={() => setIsGhostMode(!isGhostMode)} isFullscreen={isFullscreen} onToggleFullscreen={() => setIsFullscreen(!isFullscreen)} theme={isDarkTheme ? 'dark' : 'light'} selectedOF={selectedOF} initialRotation={currentOFObj?.model_3d_rotation || null} />
{/* MODAL DETALHADO DA PEÇA AUDITADA */} setDetailModalDefect(null)} defect={detailModalDefect} onFocus3D={(coords) => { if (detailModalDefect) { setSelectedDefect(detailModalDefect); setSelectedPieceMark(detailModalDefect.pecaMarca); } }} />
); }