// --- LOGTO AUTH GUARD --- import LogtoClient from '@logto/browser'; // Escondemos o app imediatamente const style = document.createElement('style'); style.innerHTML = 'body { display: none !important; }'; document.head.appendChild(style); const logtoClient = new LogtoClient({ endpoint: import.meta.env.VITE_LOGTO_ENDPOINT, appId: import.meta.env.VITE_LOGTO_APP_ID, resource: 'https://default.logto.app/api', scopes: ['openid', 'offline_access', 'profile', 'email', 'organizations'], }); async function protectPage() { const isCallback = window.location.pathname.includes('callback'); if (isCallback) { try { await logtoClient.handleSignInCallback(window.location.href); window.location.assign('/'); } catch (error) { console.error('Falha no callback do Logto:', error); } return; } const isAuthenticated = await logtoClient.isAuthenticated(); if (!isAuthenticated) { await logtoClient.signIn(window.location.origin); return; } // Se chegou aqui, está logado. Mostramos o app. style.remove(); console.log('TSCUT: Acesso autorizado.'); } // Execução imediata protectPage(); // ------------------------- import * as XLSX from 'xlsx'; import Papa from 'papaparse'; import './style.css'; let availableBars = []; let editingBarId = null; let demandPieces = []; let lastResults = null; let editingPieceId = null; let selectedProcess = 'plasma'; // Default let kerfSize = 3; // Default for plasma const PROCESS_KERFS = { 'plasma': 3, 'saw': 2, 'oxy': 5 }; function updateProcess() { const radios = document.getElementsByName('cuttingProcess'); for (const radio of radios) { if (radio.checked) { selectedProcess = radio.value; kerfSize = PROCESS_KERFS[selectedProcess]; break; } } console.log(`Process updated: ${selectedProcess}, Kerf: ${kerfSize}mm`); } const colors = ['#3498db', '#2ecc71', '#9b59b6', '#f39c12', '#1abc9c', '#e74c3c', '#34495e', '#16a085', '#d68910', '#2980b9']; const PROCESS_NAMES = { 'plasma': 'Plasma', 'saw': 'Serra/Disco', 'oxy': 'Oxicorte' }; // ===== BARRAS ===== function addBar() { const desc = document.getElementById('barDesc').value.trim(); const qty = parseInt(document.getElementById('barQty').value); const length = parseInt(document.getElementById('barLength').value); const weight = parseFloat(document.getElementById('barWeight').value); if (!desc || !qty || !length || !weight) { showToast('Preencha todos os campos da barra', 'warning'); return; } if (editingBarId) { const index = availableBars.findIndex(b => b.id === editingBarId); if (index !== -1) { availableBars[index] = { ...availableBars[index], desc, qty, length, weight, remainingQty: qty }; } cancelBarEdit(); // Reset form and state } else { availableBars.push({ id: Date.now(), desc, qty, length, weight, remainingQty: qty }); document.getElementById('barDesc').value = ''; document.getElementById('barQty').value = '1'; document.getElementById('barLength').value = '6000'; document.getElementById('barWeight').value = '45'; } renderBars(); } function removeBar(id) { if (editingBarId === id) cancelBarEdit(); availableBars = availableBars.filter(b => b.id !== id); renderBars(); } function editBar(id) { const bar = availableBars.find(b => b.id === id); if (!bar) return; document.getElementById('barDesc').value = bar.desc; document.getElementById('barQty').value = bar.qty; document.getElementById('barLength').value = bar.length; document.getElementById('barWeight').value = bar.weight; editingBarId = id; const btnAdd = document.getElementById('btnAddBar'); btnAdd.textContent = '💾 Salvar Alteração'; btnAdd.classList.remove('btn-primary'); btnAdd.classList.add('btn-success'); document.getElementById('btnCancelBarEdit').style.display = 'block'; } function cancelBarEdit() { editingBarId = null; document.getElementById('barDesc').value = ''; document.getElementById('barQty').value = '1'; document.getElementById('barLength').value = '6000'; document.getElementById('barWeight').value = '45'; const btnAdd = document.getElementById('btnAddBar'); btnAdd.textContent = '➕ Adicionar Barra'; btnAdd.classList.remove('btn-success'); btnAdd.classList.add('btn-primary'); document.getElementById('btnCancelBarEdit').style.display = 'none'; } function renderBars() { const container = document.getElementById('barsList'); if (availableBars.length === 0) { container.innerHTML = '
Nenhuma barra adicionada
'; return; } container.innerHTML = availableBars.map(b => `
${b.desc} ${b.qty}x ${b.length}mm | ${b.weight}kg
`).join(''); } // ===== PEÇAS ===== function addPiece() { const tag = document.getElementById('pieceTag').value.trim(); const length = parseInt(document.getElementById('pieceLength').value); const qty = parseInt(document.getElementById('pieceQty').value); if (!tag || !length || !qty) { showToast('Preencha TAG, comprimento e quantidade', 'warning'); return; } if (editingPieceId) { const index = demandPieces.findIndex(p => p.id === editingPieceId); if (index !== -1) { demandPieces[index] = { ...demandPieces[index], tag, length, qty }; } cancelEdit(); // Reset form and state } else { demandPieces.push({ id: Date.now(), tag, length, qty }); document.getElementById('pieceTag').value = ''; document.getElementById('pieceLength').value = ''; document.getElementById('pieceQty').value = '1'; } renderPieces(); } function removePiece(id) { if (editingPieceId === id) cancelEdit(); demandPieces = demandPieces.filter(p => p.id !== id); renderPieces(); } function editPiece(id) { const piece = demandPieces.find(p => p.id === id); if (!piece) return; document.getElementById('pieceTag').value = piece.tag; document.getElementById('pieceLength').value = piece.length; document.getElementById('pieceQty').value = piece.qty; editingPieceId = id; const btnAdd = document.getElementById('btnAddPiece'); btnAdd.textContent = '💾 Salvar Alteração'; btnAdd.classList.remove('btn-primary'); btnAdd.classList.add('btn-success'); document.getElementById('btnCancelEdit').style.display = 'block'; } function cancelEdit() { editingPieceId = null; document.getElementById('pieceTag').value = ''; document.getElementById('pieceLength').value = ''; document.getElementById('pieceQty').value = '1'; const btnAdd = document.getElementById('btnAddPiece'); btnAdd.textContent = '➕ Adicionar Peça'; btnAdd.classList.remove('btn-success'); btnAdd.classList.add('btn-primary'); document.getElementById('btnCancelEdit').style.display = 'none'; } function renderPieces() { const container = document.getElementById('piecesList'); if (demandPieces.length === 0) { container.innerHTML = '
Nenhuma peça adicionada
'; renderBalloons([]); return; } container.innerHTML = demandPieces.map(p => `
${p.tag} ${p.qty}x ${p.length}mm
`).join(''); renderBalloons(demandPieces); } function renderBalloons(pieces) { const container = document.getElementById('piecesBalloons'); if (pieces.length === 0) { container.innerHTML = '
Nenhuma peça adicionada
'; return; } container.innerHTML = pieces.map(p => `
${p.tag}
${p.length}mm × ${p.qty}
`).join(''); } // ===== IMPORTAÇÃO DE ARQUIVO ===== function importFile() { const fileInput = document.getElementById('fileImport'); const file = fileInput.files[0]; if (!file) return; const ext = file.name.split('.').pop().toLowerCase(); if (ext === 'csv') { Papa.parse(file, { header: false, complete: (results) => processImportedData(results.data), error: (error) => showFeedback('Erro ao ler arquivo: ' + error.message, 'error') }); } else if (['xlsx', 'xls'].includes(ext)) { const reader = new FileReader(); reader.onload = (e) => { try { const data = new Uint8Array(e.target.result); const workbook = XLSX.read(data, { type: 'array' }); const sheet = workbook.Sheets[workbook.SheetNames[0]]; const rows = XLSX.utils.sheet_to_json(sheet, { header: 1 }); processImportedData(rows); } catch (error) { showFeedback('Erro ao ler arquivo Excel: ' + error.message, 'error'); } }; reader.readAsArrayBuffer(file); } else { showFeedback('Formato não suportado. Use CSV, XLSX ou XLS', 'error'); } fileInput.value = ''; } function processImportedData(rows) { if (!rows || rows.length === 0) { showFeedback('Arquivo vazio', 'error'); return; } // Detectar cabeçalho let startRow = 0; if (isHeaderRow(rows[0])) { startRow = 1; } // Processar dados let importedCount = 0; let errors = []; const duplicates = []; const newPieces = []; for (let i = startRow; i < rows.length; i++) { const row = rows[i]; if (!row || row.length < 3) continue; const tag = String(row[0]).trim(); const qtyStr = String(row[1]).trim(); const lengthStr = String(row[2]).trim(); if (!tag || !qtyStr || !lengthStr) continue; const qty = parseInt(qtyStr); const length = parseInt(lengthStr); if (isNaN(qty) || isNaN(length) || qty <= 0 || length <= 0) { errors.push(`Linha ${i + 1}: dados inválidos`); continue; } if (demandPieces.some(p => p.tag === tag)) { duplicates.push(tag); } newPieces.push({ id: Date.now() + i, tag, length, qty }); importedCount++; } if (newPieces.length === 0) { showToast('Nenhuma peça válida encontrada no arquivo', 'error'); return; } const processImport = () => { demandPieces.push(...newPieces); renderPieces(); showToast(`✓ ${newPieces.length} peça(s) importada(s)`, 'success'); }; if (duplicates.length > 0) { showConfirmModal( "Peças Duplicadas", `Foram encontradas ${duplicates.length} peças com TAGs que já existem na lista (ex: ${duplicates.slice(0, 3).join(', ')}...).\n\nDeseja importá-las mesmo assim?`, processImport ); } else { processImport(); } if (errors.length > 0) { console.warn('Erros na importação:', errors); } } function isHeaderRow(row) { if (!row || row.length < 3) return false; const col1 = String(row[0]).toLowerCase().trim(); const col2 = String(row[1]).toLowerCase().trim(); const col3 = String(row[2]).toLowerCase().trim(); // Palavras-chave comuns em cabeçalhos const headerKeywords = ['tag', 'id', 'identificacao', 'nome', 'qtd', 'quantidade', 'comp', 'comprimento', 'mm', 'length']; return headerKeywords.some(kw => col1.includes(kw) || col2.includes(kw) || col3.includes(kw) ); } // ===== UI HELPERS ===== function showToast(message, type = 'info', title = '') { const container = document.getElementById('toastContainer'); const toast = document.createElement('div'); toast.className = `toast ${type}`; let icon = 'ℹ️'; if (type === 'success') icon = '✅'; if (type === 'error') icon = '❌'; if (type === 'warning') icon = '⚠️'; if (!title) { if (type === 'success') title = 'Sucesso'; if (type === 'error') title = 'Erro'; if (type === 'warning') title = 'Atenção'; if (type === 'info') title = 'Informação'; } toast.innerHTML = `
${icon}
${title}
${message}
`; container.appendChild(toast); // Trigger animation requestAnimationFrame(() => { toast.classList.add('show'); }); // Auto remove setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, 5000); } let currentConfirmCallback = null; function showConfirmModal(title, message, onConfirm) { document.getElementById('modalTitle').textContent = title; document.getElementById('modalMessage').innerHTML = message.replace(/\n/g, '
'); const modal = document.getElementById('confirmModal'); modal.classList.add('show'); currentConfirmCallback = onConfirm; } function closeModal() { document.getElementById('confirmModal').classList.remove('show'); currentConfirmCallback = null; } // Setup modal confirm button listener once document.getElementById('modalConfirmBtn').addEventListener('click', () => { if (currentConfirmCallback) currentConfirmCallback(); closeModal(); }); function showFeedback(message, type) { // Deprecated in favor of showToast, but keeping for compatibility if needed // or redirecting to showToast showToast(message, type); } // ===== ALGORITMO FFD AVANÇADO ===== function optimizeCutting() { if (availableBars.length === 0 || demandPieces.length === 0) { showToast('Adicione barras e peças', 'warning'); return; } // Expandir peças com ID único global const expandedPieces = []; let uniqueIdCounter = 0; demandPieces.forEach(p => { for (let i = 0; i < p.qty; i++) { expandedPieces.push({ ...p, uniqueId: ++uniqueIdCounter }); } }); // FFD em cada barra disponível const usedBars = []; const unusedPieces = [...expandedPieces]; // Ordenar barras por comprimento (maior para menor) para tentar usar as maiores primeiro? // O código original não ordenava, seguia a ordem de inserção. Manteremos assim por enquanto. for (let barType of availableBars) { for (let barCopy = 0; barCopy < barType.qty; barCopy++) { if (unusedPieces.length === 0) break; const bar = { barType: barType.desc, length: barType.length, weight: barType.weight, pieces: [], remaining: barType.length, isSimulated: barType.isSimulated || false }; // Tentar encaixar peças (FFD) // Ordenar peças restantes por tamanho decrescente const sorted = [...unusedPieces].sort((a, b) => b.length - a.length); const toRemoveIds = []; for (let piece of sorted) { // Check if piece fits considering kerf loss for this cut // We assume each piece consumes its length + kerf // Exception: The very last piece in a bar might not strictly need a kerf if it's the end, // but usually in cutting processes, you cut the piece out, so kerf is consumed. // User requirement: "consumo adicional de cada corte... sera de mais 2mm para cada corte" const requiredSpace = piece.length + kerfSize; // However, we need to be careful. If remaining is EXACTLY piece.length, can we cut it? // If we cut it, we lose the kerf. So we need remaining >= piece.length + kerf? // Or does the kerf come from the "waste"? // Usually: Bar 6000. Piece 1000. Kerf 3. // Cut 1: Consumes 1003. Remaining 4997. // So yes, we treat piece length as (length + kerf). if (bar.remaining >= requiredSpace) { bar.pieces.push(piece); bar.remaining -= requiredSpace; toRemoveIds.push(piece.uniqueId); } else if (bar.remaining >= piece.length && bar.remaining < requiredSpace) { // Edge case: Fits exactly or with less than kerf remaining? // If I have 1000mm remaining and need 1000mm piece. // If I cut, I destroy 3mm. So I need 1003mm to get a 1000mm piece? // Yes, usually. Unless it's the raw end of the bar, but we can't assume that. // Let's stick to the rule: consumption = length + kerf. // If bar.remaining < length + kerf, we can't cut it. } } // Remover peças colocadas usando ID único for (let uid of toRemoveIds) { const idx = unusedPieces.findIndex(p => p.uniqueId === uid); if (idx !== -1) unusedPieces.splice(idx, 1); } if (bar.pieces.length > 0) { usedBars.push(bar); } } } return { usedBars, unusedPieces, availableBars }; } function checkStockSufficiency() { const totalDemandLength = demandPieces.reduce((sum, p) => sum + (p.length * p.qty), 0); const totalStockLength = availableBars.reduce((sum, b) => sum + (b.length * b.qty), 0); return totalStockLength >= totalDemandLength; } // ===== CÁLCULO ===== function calculateOptimization() { if (availableBars.length === 0 || demandPieces.length === 0) { showToast('Adicione barras e peças para calcular', 'warning'); return; } let currentAvailableBars = JSON.parse(JSON.stringify(availableBars)); if (!checkStockSufficiency()) { showConfirmModal( "Estoque Insuficiente", "⚠️ O estoque atual de barras NÃO é suficiente para atender toda a demanda.\n\nDeseja continuar simulando a quantidade necessária de barras?", () => { runOptimizationWithSimulation(currentAvailableBars, true); } ); return; } runOptimizationWithSimulation(currentAvailableBars, false); } function runOptimizationWithSimulation(currentAvailableBars, simulateMode) { if (simulateMode) { // Find the longest bar to use as standard for simulation const standardBar = availableBars.reduce((prev, current) => (prev.length > current.length) ? prev : current); // Add a virtually infinite amount of the standard bar // We add enough to cover the deficit + buffer const totalDemandLength = demandPieces.reduce((sum, p) => sum + (p.length * p.qty), 0); const totalStockLength = availableBars.reduce((sum, b) => sum + (b.length * b.qty), 0); const deficit = totalDemandLength - totalStockLength; const extraBarsNeeded = Math.ceil(deficit / standardBar.length) + 10; // +10 buffer currentAvailableBars.push({ ...standardBar, qty: extraBarsNeeded, remainingQty: extraBarsNeeded, desc: standardBar.desc + " (Simulado)", isSimulated: true }); } // Temporarily swap availableBars with the simulated list for the optimize function const originalBars = availableBars; availableBars = currentAvailableBars; const result = optimizeCutting(); // Restore original bars availableBars = originalBars; if (!result) return; const { usedBars, unusedPieces } = result; // Calcular totais const totalPieces = demandPieces.reduce((s, p) => s + p.qty, 0); const totalLength = demandPieces.reduce((s, p) => s + (p.length * p.qty), 0); const totalWaste = usedBars.reduce((s, b) => s + b.remaining, 0); const totalBarLength = usedBars.reduce((s, b) => s + b.length, 0); const efficiency = totalBarLength > 0 ? ((1 - totalWaste / totalBarLength) * 100).toFixed(2) : 0; const totalWeight = usedBars.reduce((s, b) => s + b.weight, 0); const unusedPiecesCount = unusedPieces.reduce((s, p) => s + 1, 0); // Calculate total kerf loss const totalKerfLoss = usedBars.reduce((sum, bar) => sum + (bar.pieces.length * kerfSize), 0); // Generate Scraps List const scraps = usedBars.map(b => b.remaining).filter(r => r > 0).sort((a, b) => b - a); const scrapsListHtml = scraps.length > 0 ? `
${Object.entries(scraps.reduce((acc, val) => { acc[val] = (acc[val] || 0) + 1; return acc; }, {})) .map(([size, count]) => `
${count}x ${size}mm
`).join('')}
` : '
Sem sobras!
'; // Resumo Detalhado const simulationWarning = simulateMode ? `
Aviso de Estoque
Uso de barras Adicionais (sem estoque)
` : ''; document.getElementById('summaryResults').innerHTML = `
Eficiência Global
${efficiency}%
Aproveitamento do Material
Barras Usadas
${usedBars.length}
Total: ${totalWeight.toFixed(1)}kg
Peças Faltando
${unusedPiecesCount}
De ${totalPieces} totais
Resumo de Sobras (Retalhos)
Total: ${totalWaste}mm
${scrapsListHtml}
Peso Sucata
${(totalWeight * (1 - (efficiency / 100))).toFixed(1)}kg
Processo de Corte
${PROCESS_NAMES[selectedProcess]}
Consumo adicional de material: ${totalKerfLoss} mm
${simulationWarning} `; // Barras - Mostrar todas as barras individualmente document.getElementById('barsContainer').innerHTML = usedBars.map((bar, idx) => { const used = bar.pieces.reduce((s, p) => s + p.length, 0); const eff = ((used / bar.length) * 100).toFixed(2); return renderBar(bar, idx + 1, used, eff); }).join(''); lastResults = { usedBars, unusedPieces, totalWeight, totalLength, totalPieces }; if (simulateMode) { showToast("O cálculo foi realizado em MODO SIMULAÇÃO.", "warning", "Atenção"); } } function renderBar(bar, barNum, used, efficiency) { const scale = 6200 / bar.length; const noStockLabel = bar.isSimulated ? 'SEM ESTOQUE' : ''; // Reconstruindo o SVG corretamente let svgContent = ``; let position = 0; bar.pieces.forEach((piece, idx) => { const scaledWidth = piece.length * scale; const colorIdx = idx % colors.length; svgContent += ` ${piece.tag} ${piece.length}`; position += scaledWidth; }); const waste = bar.remaining; if (waste > 0) { const wasteWidth = waste * scale; svgContent += ` sobra ${waste}`; } const svg = `${svgContent}`; const pieceDetails = groupPieces(bar.pieces); const table = ` ${pieceDetails.map(g => ``).join('')}
TAGmmQtd
${g.tag}${g.length}${g.count}
SOBRA${waste}mm
`; const effBar = `
${efficiency}%
`; const repetitionText = bar.count > 1 ? ' × ' + bar.count + '' : ''; return `
BARRA ${barNum}${repetitionText}${noStockLabel}
${bar.barType}
Usado: ${used}mm
Peso: ${bar.weight}kg
${svg} ${table} ${effBar}
`; } function groupPieces(pieces) { const groups = {}; pieces.forEach(p => { if (!groups[p.tag]) groups[p.tag] = { length: p.length, count: 0 }; groups[p.tag].count++; }); return Object.entries(groups).map(([tag, data]) => ({ tag, ...data })); } function clearAll() { availableBars = []; demandPieces = []; lastResults = null; renderBars(); renderPieces(); document.getElementById('summaryResults').innerHTML = '
Calcule a otimização para ver os resultados
'; document.getElementById('barsContainer').innerHTML = '
Resultados aparecerão após o cálculo
'; } function generateReportHTML() { if (!lastResults) return null; const { usedBars, totalWeight } = lastResults; const dateStr = new Date().toLocaleDateString('pt-BR'); const timeStr = new Date().toLocaleTimeString('pt-BR'); const totalPieces = demandPieces.reduce((s, p) => s + p.qty, 0); // Paginação simples: 3 barras por página const barsPerPage = 3; const pages = []; for (let i = 0; i < usedBars.length; i += barsPerPage) { pages.push({ items: usedBars.slice(i, i + barsPerPage).map((bar, idx) => ({ type: 'bar', data: bar, index: i + idx + 1 })), type: 'bars' }); } // Adicionar resumo na última página ou nova página pages.push({ items: [{ type: 'summary', data: lastResults }], type: 'summary' }); const totalPages = pages.length; const jobTitle = document.getElementById('jobTitle').value.trim() || 'Relatório de Corte'; let htmlContent = pages.map((page, pageIdx) => { const pageNum = pageIdx + 1; let pageBody = page.items.map(item => { if (item.type === 'bar') return renderReportBar(item.data, item.index); if (item.type === 'summary') return renderReportSummary(item.data); return ''; }).join(''); return `

📊 ${jobTitle}

Gerado em: ${dateStr} às ${timeStr}

Página ${pageNum} de ${totalPages}
${pageBody}
Otimizador de Corte | Total de Peças: ${totalPieces} | Aproveitamento Global: ${((1 - (lastResults.usedBars.reduce((s, b) => s + b.remaining, 0) / lastResults.usedBars.reduce((s, b) => s + b.length, 0))) * 100).toFixed(2)}%
`; }).join(''); return ` Relatório de Corte - ${dateStr} ${htmlContent} `; } function renderReportBar(bar, index) { const used = bar.pieces.reduce((s, p) => s + p.length, 0); const efficiency = ((used / bar.length) * 100).toFixed(2); const waste = bar.remaining; const groups = groupPieces(bar.pieces); const scale = 6200 / bar.length; let svgContent = ``; let pos = 0; bar.pieces.forEach((p, i) => { const w = p.length * scale; svgContent += ``; svgContent += `${p.tag}`; pos += w; }); if (waste > 0) { svgContent += ``; svgContent += `sobra`; } const repetition = bar.count > 1 ? `${bar.count} cópias idênticas` : ''; return `
BARRA ${index} - ${bar.barType} ${repetition}
Eficiência: ${efficiency}% | Sobra: ${waste}mm
${svgContent} ${groups.map(g => ``).join('')} ${waste > 0 ? `` : ''}
PeçaComp. (mm)Qtd no Corte
${g.tag}${g.length}${g.count}
SOBRA${waste}-
`; } function renderReportSummary(data) { const totalWaste = data.usedBars.reduce((s, b) => s + b.remaining, 0); const totalLength = data.usedBars.reduce((s, b) => s + b.length, 0); const efficiency = ((1 - totalWaste / totalLength) * 100).toFixed(2); const totalKerfLoss = data.usedBars.reduce((sum, bar) => sum + (bar.pieces.length * kerfSize), 0); return `

📌 Resumo Final

Total de Barras
${data.usedBars.length}
Peso Total
${data.totalWeight.toFixed(1)} kg
Sobra Total
${totalWaste} mm
Eficiência Global
${efficiency}%
Processo: ${PROCESS_NAMES[selectedProcess]}
Perda estimada por corte (Kerf): ${totalKerfLoss} mm
`; } function exportHTML() { const html = generateReportHTML(); if (!html) { showToast('Calcule a otimização primeiro', 'warning'); return; } const blob = new Blob([html], { type: 'text/html' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'relatorio-corte-' + new Date().toISOString().split('T')[0] + '.html'; a.click(); } function printReport() { const html = generateReportHTML(); if (!html) { showToast('Calcule a otimização primeiro', 'warning'); return; } const win = window.open('', '_blank'); win.document.write(html); win.document.close(); setTimeout(() => { win.print(); }, 500); } // Expose functions to window for HTML onclick events window.addBar = addBar; window.removeBar = removeBar; window.addPiece = addPiece; window.removePiece = removePiece; window.editPiece = editPiece; window.cancelEdit = cancelEdit; window.importFile = importFile; window.calculateOptimization = calculateOptimization; window.clearAll = clearAll; window.exportHTML = exportHTML; // ===== GERENCIAMENTO DE TRABALHO ===== // ===== GERENCIAMENTO DE TRABALHO ===== function saveJob() { const title = document.getElementById('jobTitle').value.trim() || 'Trabalho Sem Titulo'; // Formato CSV Customizado: TYPE, P1, P2, P3, P4 let csvContent = "TYPE,PARAM1,PARAM2,PARAM3,PARAM4\n"; // 1. Metadata csvContent += `JOB,${title},,,\n`; csvContent += `METADATA,PROCESS,${selectedProcess},,\n`; // 2. Barras availableBars.forEach(b => { csvContent += `BAR,${b.desc},${b.qty},${b.length},${b.weight}\n`; }); // 3. Peças demandPieces.forEach(p => { csvContent += `PIECE,${p.tag},${p.length},${p.qty},\n`; }); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.setAttribute("href", url); link.setAttribute("download", `${title}.csv`); document.body.appendChild(link); link.click(); document.body.removeChild(link); } function loadJob() { const fileInput = document.getElementById('jobImport'); const file = fileInput.files[0]; if (!file) return; Papa.parse(file, { header: true, skipEmptyLines: true, complete: function (results) { try { let jobTitleFound = ""; const newBars = []; const newPieces = []; const duplicateBars = []; results.data.forEach(row => { const type = row.TYPE; if (type === 'JOB') { jobTitleFound = row.PARAM1; } else if (type === 'METADATA' && row.PARAM1 === 'PROCESS') { selectedProcess = row.PARAM2; // Update UI const radios = document.getElementsByName('cuttingProcess'); for (const radio of radios) { if (radio.value === selectedProcess) { radio.checked = true; } } updateProcess(); } else if (type === 'BAR') { const desc = row.PARAM1; if (availableBars.some(b => b.desc === desc)) { duplicateBars.push(desc); } newBars.push({ id: Date.now() + Math.random(), desc: desc, qty: parseInt(row.PARAM2), length: parseInt(row.PARAM3), weight: parseFloat(row.PARAM4), remainingQty: parseInt(row.PARAM2) }); } else if (type === 'PIECE') { newPieces.push({ id: Date.now() + Math.random(), tag: row.PARAM1, length: parseInt(row.PARAM2), qty: parseInt(row.PARAM3) }); } }); const finishLoad = () => { // Limpar estado atual clearAll(); if (jobTitleFound) document.getElementById('jobTitle').value = jobTitleFound; newBars.forEach(b => availableBars.push(b)); newPieces.forEach(p => demandPieces.push(p)); renderBars(); renderPieces(); showToast('Trabalho carregado com sucesso!', 'success'); }; // If the list is not empty, warn user that current data will be lost if (availableBars.length > 0 || demandPieces.length > 0) { showConfirmModal( "Substituir Trabalho Atual?", "Carregar um novo trabalho irá limpar todas as barras e peças atuais.\n\nDeseja continuar?", finishLoad ); } else { finishLoad(); } } catch (e) { showToast('Erro ao carregar arquivo: ' + e.message, 'error'); } } }); fileInput.value = ''; } // Expose functions to window for HTML onclick events window.addBar = addBar; window.removeBar = removeBar; window.editBar = editBar; window.cancelBarEdit = cancelBarEdit; window.addPiece = addPiece; window.removePiece = removePiece; window.importFile = importFile; window.calculateOptimization = calculateOptimization; window.clearAll = clearAll; window.exportHTML = exportHTML; window.printReport = printReport; window.saveJob = saveJob; window.loadJob = loadJob; window.closeModal = closeModal; window.updateProcess = updateProcess; // THEME HANDLING function toggleTheme() { const body = document.body; body.classList.toggle('dark-theme'); const isDark = body.classList.contains('dark-theme'); localStorage.setItem('theme', isDark ? 'dark' : 'light'); updateThemeIcon(isDark); } function updateThemeIcon(isDark) { const btn = document.getElementById('themeToggle'); if (btn) { btn.textContent = isDark ? '☀️' : '🌙'; btn.title = isDark ? 'Mudar para Modo Claro' : 'Mudar para Modo Escuro'; } } function initTheme() { const savedTheme = localStorage.getItem('theme'); const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; if (savedTheme === 'dark' || (!savedTheme && prefersDark)) { document.body.classList.add('dark-theme'); updateThemeIcon(true); } else { document.body.classList.remove('dark-theme'); updateThemeIcon(false); } } window.toggleTheme = toggleTheme; initTheme(); renderBars(); renderPieces(); updateProcess(); // Initialize kerf size based on default checked radio