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}
`;
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