`).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
? `