diff --git a/src/main.js b/src/main.js index d26e39a..c973a0c 100644 --- a/src/main.js +++ b/src/main.js @@ -71,1190 +71,1186 @@ async function protectPage() { 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 +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 OTIMIZADO AVANƇADO (Programação DinĆ¢mica / Knapsack) ===== +function optimizeCutting() { + if (availableBars.length === 0 || demandPieces.length === 0) { + showToast("Adicione barras e peƧas", "warning"); + return; + } + + const expandedPieces = []; + let uniqueIdCounter = 0; + demandPieces.forEach(p => { + for (let i = 0; i < p.qty; i++) { + expandedPieces.push({ ...p, uniqueId: ++uniqueIdCounter }); + } + }); + + const usedBars = []; + let unusedPieces = [...expandedPieces]; + + const sortedBarTypes = [...availableBars].sort((a, b) => b.length - a.length); + + for (let barType of sortedBarTypes) { + 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 + }; + + unusedPieces.sort((a, b) => b.length - a.length); + + const N = unusedPieces.length; + const capacity = bar.remaining; + + const dp = new Int32Array(capacity + 1); + const keep = new Array(N); + + for (let i = 0; i < N; i++) { + keep[i] = new Uint8Array(capacity + 1); + const w = unusedPieces[i].length + kerfSize; + + for (let j = capacity; j >= 0; j--) { + if (j >= w && dp[j - w] + w > dp[j]) { + dp[j] = dp[j - w] + w; + keep[i][j] = 1; + } + } + } + + let bestW = 0; + for(let j = 0; j <= capacity; j++) { + if(dp[j] > dp[bestW]) bestW = j; + } + let currW = bestW; + + const toRemoveIds = []; + for (let i = N - 1; i >= 0; i--) { + if (keep[i][currW] === 1) { + const selectedPiece = unusedPieces[i]; + bar.pieces.push(selectedPiece); + const w = selectedPiece.length + kerfSize; + bar.remaining -= w; + currW -= w; + toRemoveIds.push(selectedPiece.uniqueId); + } + } + + if (toRemoveIds.length > 0) { + const removeSet = new Set(toRemoveIds); + unusedPieces = unusedPieces.filter(p => !removeSet.has(p.uniqueId)); + 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