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 = ``;
-
- const pieceDetails = groupPieces(bar.pieces);
- const table = `
-
- | TAG | mm | Qtd |
- ${pieceDetails.map(g => `| ${g.tag} | ${g.length} | ${g.count} |
`).join('')}
- | SOBRA | ${waste} | mm |
-
- `;
-
- const effBar = ``;
-
- const repetitionText = bar.count > 1 ? ' Ć ' + bar.count + '' : '';
-
- return `
-
-
- ${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 `
-
-
-
-
- ${pageBody}
-
-
-
-
`;
- }).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 `
-
-
-
-
- | PeƧa | Comp. (mm) | Qtd no Corte |
-
- ${groups.map(g => `| ${g.tag} | ${g.length} | ${g.count} |
`).join('')}
- ${waste > 0 ? `| 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 = ``;
+
+ const pieceDetails = groupPieces(bar.pieces);
+ const table = `
+
+ | TAG | mm | Qtd |
+ ${pieceDetails.map(g => `| ${g.tag} | ${g.length} | ${g.count} |
`).join('')}
+ | SOBRA | ${waste} | mm |
+
+ `;
+
+ const effBar = ``;
+
+ const repetitionText = bar.count > 1 ? ' Ć ' + bar.count + '' : '';
+
+ return `
+
+
+ ${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 `
+
+
+
+
+ ${pageBody}
+
+
+
+
`;
+ }).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 `
+
+
+
+
+ | PeƧa | Comp. (mm) | Qtd no Corte |
+
+ ${groups.map(g => `| ${g.tag} | ${g.length} | ${g.count} |
`).join('')}
+ ${waste > 0 ? `| 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