1232 lines
45 KiB
JavaScript
1232 lines
45 KiB
JavaScript
|
||
// --- LOGTO AUTH GUARD ---
|
||
import LogtoClient from '@logto/browser';
|
||
|
||
// Escondemos o app imediatamente
|
||
const style = document.createElement('style');
|
||
style.innerHTML = 'body { display: none !important; }';
|
||
document.head.appendChild(style);
|
||
|
||
const logtoClient = new LogtoClient({
|
||
endpoint: import.meta.env.VITE_LOGTO_ENDPOINT,
|
||
appId: import.meta.env.VITE_LOGTO_APP_ID,
|
||
resource: 'https://default.logto.app/api',
|
||
scopes: ['openid', 'offline_access', 'profile', 'email', 'organizations'],
|
||
});
|
||
|
||
async function protectPage() {
|
||
const isCallback = window.location.pathname.includes('callback');
|
||
|
||
if (isCallback) {
|
||
try {
|
||
await logtoClient.handleSignInCallback(window.location.href);
|
||
window.location.assign('/');
|
||
} catch (error) {
|
||
console.error('Falha no callback do Logto:', error);
|
||
}
|
||
return;
|
||
}
|
||
|
||
const isAuthenticated = await logtoClient.isAuthenticated();
|
||
if (!isAuthenticated) {
|
||
await logtoClient.signIn(window.location.origin);
|
||
return;
|
||
}
|
||
|
||
// Se chegou aqui, está logado. Mostramos o app.
|
||
style.remove();
|
||
console.log('TSCUT: Acesso autorizado.');
|
||
}
|
||
|
||
// Execução imediata
|
||
protectPage();
|
||
// -------------------------
|
||
|
||
import * as XLSX from 'xlsx';
|
||
import Papa from 'papaparse';
|
||
import './style.css';
|
||
|
||
let availableBars = [];
|
||
let editingBarId = null;
|
||
let demandPieces = [];
|
||
let lastResults = null;
|
||
let editingPieceId = null;
|
||
let selectedProcess = 'plasma'; // Default
|
||
let kerfSize = 3; // Default for plasma
|
||
|
||
const PROCESS_KERFS = {
|
||
'plasma': 3,
|
||
'saw': 2,
|
||
'oxy': 5
|
||
};
|
||
|
||
function updateProcess() {
|
||
const radios = document.getElementsByName('cuttingProcess');
|
||
for (const radio of radios) {
|
||
if (radio.checked) {
|
||
selectedProcess = radio.value;
|
||
kerfSize = PROCESS_KERFS[selectedProcess];
|
||
break;
|
||
}
|
||
}
|
||
console.log(`Process updated: ${selectedProcess}, Kerf: ${kerfSize}mm`);
|
||
}
|
||
|
||
const colors = ['#3498db', '#2ecc71', '#9b59b6', '#f39c12', '#1abc9c', '#e74c3c', '#34495e', '#16a085', '#d68910', '#2980b9'];
|
||
|
||
const PROCESS_NAMES = {
|
||
'plasma': 'Plasma',
|
||
'saw': 'Serra/Disco',
|
||
'oxy': 'Oxicorte'
|
||
};
|
||
|
||
// ===== BARRAS =====
|
||
function addBar() {
|
||
const desc = document.getElementById('barDesc').value.trim();
|
||
const qty = parseInt(document.getElementById('barQty').value);
|
||
const length = parseInt(document.getElementById('barLength').value);
|
||
const weight = parseFloat(document.getElementById('barWeight').value);
|
||
|
||
if (!desc || !qty || !length || !weight) {
|
||
showToast('Preencha todos os campos da barra', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (editingBarId) {
|
||
const index = availableBars.findIndex(b => b.id === editingBarId);
|
||
if (index !== -1) {
|
||
availableBars[index] = { ...availableBars[index], desc, qty, length, weight, remainingQty: qty };
|
||
}
|
||
cancelBarEdit(); // Reset form and state
|
||
} else {
|
||
availableBars.push({ id: Date.now(), desc, qty, length, weight, remainingQty: qty });
|
||
document.getElementById('barDesc').value = '';
|
||
document.getElementById('barQty').value = '1';
|
||
document.getElementById('barLength').value = '6000';
|
||
document.getElementById('barWeight').value = '45';
|
||
}
|
||
|
||
renderBars();
|
||
}
|
||
|
||
function removeBar(id) {
|
||
if (editingBarId === id) cancelBarEdit();
|
||
availableBars = availableBars.filter(b => b.id !== id);
|
||
renderBars();
|
||
}
|
||
|
||
function editBar(id) {
|
||
const bar = availableBars.find(b => b.id === id);
|
||
if (!bar) return;
|
||
|
||
document.getElementById('barDesc').value = bar.desc;
|
||
document.getElementById('barQty').value = bar.qty;
|
||
document.getElementById('barLength').value = bar.length;
|
||
document.getElementById('barWeight').value = bar.weight;
|
||
|
||
editingBarId = id;
|
||
|
||
const btnAdd = document.getElementById('btnAddBar');
|
||
btnAdd.textContent = '💾 Salvar Alteração';
|
||
btnAdd.classList.remove('btn-primary');
|
||
btnAdd.classList.add('btn-success');
|
||
|
||
document.getElementById('btnCancelBarEdit').style.display = 'block';
|
||
}
|
||
|
||
function cancelBarEdit() {
|
||
editingBarId = null;
|
||
document.getElementById('barDesc').value = '';
|
||
document.getElementById('barQty').value = '1';
|
||
document.getElementById('barLength').value = '6000';
|
||
document.getElementById('barWeight').value = '45';
|
||
|
||
const btnAdd = document.getElementById('btnAddBar');
|
||
btnAdd.textContent = '➕ Adicionar Barra';
|
||
btnAdd.classList.remove('btn-success');
|
||
btnAdd.classList.add('btn-primary');
|
||
|
||
document.getElementById('btnCancelBarEdit').style.display = 'none';
|
||
}
|
||
|
||
function renderBars() {
|
||
const container = document.getElementById('barsList');
|
||
if (availableBars.length === 0) {
|
||
container.innerHTML = '<div class="no-data">Nenhuma barra adicionada</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = availableBars.map(b => `
|
||
<div class="item-card dynamic-flex-row">
|
||
<div class="dynamic-item-info">
|
||
<strong>${b.desc}</strong> <span class="dynamic-item-subinfo">${b.qty}x ${b.length}mm | ${b.weight}kg</span>
|
||
</div>
|
||
<div class="dynamic-item-actions">
|
||
<button onclick="editBar(${b.id})" class="dynamic-action-btn edit" title="Editar">✎</button>
|
||
<button onclick="removeBar(${b.id})" class="dynamic-action-btn remove" title="Remover">✕</button>
|
||
</div>
|
||
</div>
|
||
`).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 = '<div class="no-data">Nenhuma peça adicionada</div>';
|
||
renderBalloons([]);
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = demandPieces.map(p => `
|
||
<div class="item-card dynamic-flex-row">
|
||
<div class="dynamic-item-info">
|
||
<strong>${p.tag}</strong> <span class="dynamic-item-subinfo">${p.qty}x ${p.length}mm</span>
|
||
</div>
|
||
<div class="dynamic-item-actions">
|
||
<button onclick="editPiece(${p.id})" class="dynamic-action-btn edit" title="Editar">✎</button>
|
||
<button onclick="removePiece(${p.id})" class="dynamic-action-btn remove" title="Remover">✕</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
renderBalloons(demandPieces);
|
||
}
|
||
|
||
function renderBalloons(pieces) {
|
||
const container = document.getElementById('piecesBalloons');
|
||
if (pieces.length === 0) {
|
||
container.innerHTML = '<div class="no-data full-width">Nenhuma peça adicionada</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = pieces.map(p => `
|
||
<div class="piece-balloon">
|
||
<span class="piece-balloon-tag">${p.tag}</span>
|
||
<div class="piece-balloon-info">
|
||
<span>${p.length}mm × ${p.qty}</span>
|
||
</div>
|
||
</div>
|
||
`).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 = `
|
||
<div class="toast-icon">${icon}</div>
|
||
<div class="toast-content">
|
||
<div class="toast-title">${title}</div>
|
||
<div class="toast-message">${message}</div>
|
||
</div>
|
||
<button class="toast-close" onclick="this.parentElement.remove()">✕</button>
|
||
`;
|
||
|
||
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, '<br>');
|
||
|
||
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
|
||
? `<div class="scraps-list">
|
||
${Object.entries(scraps.reduce((acc, val) => { acc[val] = (acc[val] || 0) + 1; return acc; }, {}))
|
||
.map(([size, count]) => `<div>${count}x ${size}mm</div>`).join('')}
|
||
</div>`
|
||
: '<div class="no-scraps">Sem sobras!</div>';
|
||
|
||
// Resumo Detalhado
|
||
const simulationWarning = simulateMode
|
||
? `<div class="result-card warning simulation-warning">
|
||
<div class="result-label danger">Aviso de Estoque</div>
|
||
<div class="result-value small-danger">Uso de barras Adicionais (sem estoque)</div>
|
||
</div>`
|
||
: '';
|
||
|
||
document.getElementById('summaryResults').innerHTML = `
|
||
<div class="result-card success grid-full-col">
|
||
<div class="result-label">Eficiência Global</div>
|
||
<div class="result-value">${efficiency}%</div>
|
||
<div class="result-subtext">Aproveitamento do Material</div>
|
||
</div>
|
||
<div class="result-card">
|
||
<div class="result-label">Barras Usadas</div>
|
||
<div class="result-value">${usedBars.length}</div>
|
||
<div class="result-subtext">Total: ${totalWeight.toFixed(1)}kg</div>
|
||
</div>
|
||
<div class="result-card ${unusedPiecesCount > 0 ? 'warning' : 'success'}">
|
||
<div class="result-label">Peças Faltando</div>
|
||
<div class="result-value">${unusedPiecesCount}</div>
|
||
<div class="result-subtext">De ${totalPieces} totais</div>
|
||
</div>
|
||
<div class="result-card warning grid-full-col">
|
||
<div class="result-label">Resumo de Sobras (Retalhos)</div>
|
||
<div class="result-value medium-size">Total: ${totalWaste}mm</div>
|
||
${scrapsListHtml}
|
||
</div>
|
||
<div class="result-card">
|
||
<div class="result-label">Peso Sucata</div>
|
||
<div class="result-value">${(totalWeight * (1 - (efficiency / 100))).toFixed(1)}kg</div>
|
||
</div>
|
||
<div class="result-card process-info grid-full-col">
|
||
<div class="result-label primary">Processo de Corte</div>
|
||
<div class="result-value medium-small">${PROCESS_NAMES[selectedProcess]}</div>
|
||
<div class="kerf-info">Consumo adicional de material: <strong>${totalKerfLoss} mm</strong></div>
|
||
</div>
|
||
${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 ? '<span class="no-stock-badge">SEM ESTOQUE</span>' : '';
|
||
|
||
// Reconstruindo o SVG corretamente
|
||
let svgContent = `<rect x="0" y="150" width="${bar.length * scale}" height="100" fill="#ecf0f1" stroke="#34495e" stroke-width="2"/>`;
|
||
|
||
let position = 0;
|
||
bar.pieces.forEach((piece, idx) => {
|
||
const scaledWidth = piece.length * scale;
|
||
const colorIdx = idx % colors.length;
|
||
svgContent += `
|
||
<rect x="${position}" y="150" width="${scaledWidth}" height="100" fill="${colors[colorIdx]}" stroke="#34495e" stroke-width="2"/>
|
||
<line x1="${position + scaledWidth}" y1="150" x2="${position + scaledWidth}" y2="250" stroke="#000" stroke-width="2"/>
|
||
<text x="${position + scaledWidth / 2}" y="130" class="bar-svg-piece-tag">${piece.tag}</text>
|
||
<text x="${position + scaledWidth / 2}" y="340" class="bar-svg-piece-length">${piece.length}</text>`;
|
||
position += scaledWidth;
|
||
});
|
||
|
||
const waste = bar.remaining;
|
||
if (waste > 0) {
|
||
const wasteWidth = waste * scale;
|
||
svgContent += `<rect x="${position}" y="150" width="${wasteWidth}" height="100" fill="#e74c3c" stroke="#c0392b" stroke-width="2" stroke-dasharray="10,10"/>
|
||
<text x="${position + wasteWidth / 2}" y="130" class="bar-svg-waste-tag">sobra</text>
|
||
<text x="${position + wasteWidth / 2}" y="340" class="bar-svg-waste-length">${waste}</text>`;
|
||
}
|
||
|
||
const svg = `<svg viewBox="0 0 6200 400" height="100">${svgContent}</svg>`;
|
||
|
||
const pieceDetails = groupPieces(bar.pieces);
|
||
const table = `
|
||
<table class="bar-table">
|
||
<tr><th>TAG</th><th>mm</th><th>Qtd</th></tr>
|
||
${pieceDetails.map(g => `<tr><td>${g.tag}</td><td>${g.length}</td><td>${g.count}</td></tr>`).join('')}
|
||
<tr class="row-waste"><td><strong>SOBRA</strong></td><td>${waste}</td><td>mm</td></tr>
|
||
</table>
|
||
`;
|
||
|
||
const effBar = `<div class="efficiency-bar"><div class="efficiency-fill" style="width: ${efficiency}%;">${efficiency}%</div></div>`;
|
||
|
||
const repetitionText = bar.count > 1 ? '<span class="repetition-text"> × ' + bar.count + '</span>' : '';
|
||
|
||
return `
|
||
<div class="bar-card">
|
||
<div class="bar-header">
|
||
<div><strong>BARRA ${barNum}</strong>${repetitionText}${noStockLabel}<br><span class="bar-subtitle">${bar.barType}</span></div>
|
||
<div class="bar-stats">
|
||
<div>Usado: <strong>${used}mm</strong></div>
|
||
<div>Peso: <strong>${bar.weight}kg</strong></div>
|
||
</div>
|
||
</div>
|
||
${svg}
|
||
${table}
|
||
${effBar}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
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 = '<div class="no-data grid-full-col">Calcule a otimização para ver os resultados</div>';
|
||
document.getElementById('barsContainer').innerHTML = '<div class="no-data">Resultados aparecerão após o cálculo</div>';
|
||
}
|
||
|
||
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 `
|
||
<div class="page">
|
||
<header>
|
||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||
<div>
|
||
<h1>📊 ${jobTitle}</h1>
|
||
<p>Gerado em: ${dateStr} às ${timeStr}</p>
|
||
</div>
|
||
<div style="text-align: right; font-size: 10pt;">
|
||
Página <strong>${pageNum}</strong> de <strong>${totalPages}</strong>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="content">
|
||
${pageBody}
|
||
</div>
|
||
|
||
<footer>
|
||
Otimizador de Corte | Total de Peças: ${totalPieces} | Aproveitamento Global: ${((1 - (lastResults.usedBars.reduce((s, b) => s + b.remaining, 0) / lastResults.usedBars.reduce((s, b) => s + b.length, 0))) * 100).toFixed(2)}%
|
||
</footer>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
return `
|
||
<!DOCTYPE html>
|
||
<html lang="pt-BR">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Relatório de Corte - ${dateStr}</title>
|
||
<style>
|
||
:root { --primary: #1a6b8f; --success: #27ae60; --warning: #e67e22; --danger: #e74c3c; --gray: #bdc3c7; }
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body { background: #555; font-family: 'Segoe UI', Arial, sans-serif; }
|
||
|
||
.page {
|
||
background: white;
|
||
width: 210mm;
|
||
height: 297mm;
|
||
margin: 20px auto;
|
||
padding: 10mm;
|
||
position: relative;
|
||
box-shadow: 0 0 10px rgba(0,0,0,0.3);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
@media print {
|
||
@page { size: A4; margin: 0; }
|
||
body { background: white; margin: 0; padding: 0; }
|
||
.page {
|
||
margin: 0;
|
||
box-shadow: none;
|
||
page-break-after: always;
|
||
width: 100%;
|
||
height: 100vh;
|
||
border: none;
|
||
}
|
||
}
|
||
|
||
header {
|
||
border-bottom: 2px solid var(--primary);
|
||
padding-bottom: 10px;
|
||
margin-bottom: 20px;
|
||
color: var(--primary);
|
||
}
|
||
|
||
header h1 { font-size: 16pt; margin-bottom: 5px; }
|
||
header p { font-size: 9pt; color: #666; }
|
||
|
||
footer {
|
||
position: absolute;
|
||
bottom: 10mm;
|
||
left: 10mm;
|
||
right: 10mm;
|
||
border-top: 1px solid var(--gray);
|
||
padding-top: 10px;
|
||
text-align: center;
|
||
font-size: 8pt;
|
||
color: #888;
|
||
}
|
||
|
||
.content { flex: 1; }
|
||
|
||
/* Estilos dos Cards */
|
||
.bar-section {
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
padding: 10px;
|
||
margin-bottom: 15px;
|
||
page-break-inside: avoid;
|
||
}
|
||
|
||
.bar-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
font-size: 10pt;
|
||
border-bottom: 1px solid #eee;
|
||
padding-bottom: 5px;
|
||
}
|
||
|
||
.bar-stats { font-size: 9pt; color: #666; }
|
||
|
||
svg { width: 100%; height: 80px; background: #f9f9f9; border: 1px solid #eee; margin-bottom: 8px; }
|
||
|
||
table { width: 100%; border-collapse: collapse; font-size: 8pt; }
|
||
th { background: var(--primary); color: white; text-align: left; padding: 4px; }
|
||
td { border-bottom: 1px solid #eee; padding: 3px 4px; }
|
||
tr:nth-child(even) { background: #f9f9f9; }
|
||
|
||
.summary-box {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 10px;
|
||
margin-top: 20px;
|
||
padding: 15px;
|
||
background: #f8f9fa;
|
||
border-radius: 5px;
|
||
border: 1px solid #ddd;
|
||
}
|
||
|
||
.summary-item { text-align: center; }
|
||
.summary-label { font-size: 8pt; color: #666; text-transform: uppercase; }
|
||
.summary-value { font-size: 14pt; font-weight: bold; color: var(--primary); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
${htmlContent}
|
||
</body>
|
||
</html>`;
|
||
}
|
||
|
||
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 = `<rect x="0" y="150" width="${bar.length * scale}" height="100" fill="#ecf0f1" stroke="#bdc3c7" />`;
|
||
let pos = 0;
|
||
bar.pieces.forEach((p, i) => {
|
||
const w = p.length * scale;
|
||
svgContent += `<rect x="${pos}" y="150" width="${w}" height="100" fill="${colors[i % colors.length]}" stroke="#34495e" />`;
|
||
svgContent += `<text x="${pos + w / 2}" y="130" font-size="120" fill="#333" text-anchor="middle" font-weight="bold">${p.tag}</text>`;
|
||
pos += w;
|
||
});
|
||
if (waste > 0) {
|
||
svgContent += `<rect x="${pos}" y="150" width="${waste * scale}" height="100" fill="#e74c3c" stroke="#c0392b" stroke-dasharray="10,10" opacity="0.5" />`;
|
||
svgContent += `<text x="${pos + (waste * scale) / 2}" y="130" font-size="100" fill="#c0392b" text-anchor="middle">sobra</text>`;
|
||
}
|
||
|
||
const repetition = bar.count > 1 ? `<span style="background:var(--warning); color:white; padding:1px 4px; border-radius:3px; font-size:8pt;">${bar.count} cópias idênticas</span>` : '';
|
||
|
||
return `
|
||
<div class="bar-section">
|
||
<div class="bar-header">
|
||
<div><strong>BARRA ${index}</strong> - ${bar.barType} ${repetition}</div>
|
||
<div class="bar-stats">Eficiência: <strong>${efficiency}%</strong> | Sobra: <span style="color:var(--danger)">${waste}mm</span></div>
|
||
</div>
|
||
<svg viewBox="0 0 6200 400">${svgContent}</svg>
|
||
<table>
|
||
<thead><tr><th>Peça</th><th>Comp. (mm)</th><th>Qtd no Corte</th></tr></thead>
|
||
<tbody>
|
||
${groups.map(g => `<tr><td>${g.tag}</td><td>${g.length}</td><td>${g.count}</td></tr>`).join('')}
|
||
${waste > 0 ? `<tr style="color:var(--danger); font-style:italic;"><td>SOBRA</td><td>${waste}</td><td>-</td></tr>` : ''}
|
||
</tbody>
|
||
</table>
|
||
</div>`;
|
||
}
|
||
|
||
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 `
|
||
<div style="margin-top: 30px; border-top: 2px solid #eee; padding-top: 20px;">
|
||
<h3>📌 Resumo Final</h3>
|
||
<div class="summary-box">
|
||
<div class="summary-item">
|
||
<div class="summary-label">Total de Barras</div>
|
||
<div class="summary-value">${data.usedBars.length}</div>
|
||
</div>
|
||
<div class="summary-item">
|
||
<div class="summary-label">Peso Total</div>
|
||
<div class="summary-value">${data.totalWeight.toFixed(1)} kg</div>
|
||
</div>
|
||
<div class="summary-item">
|
||
<div class="summary-label">Sobra Total</div>
|
||
<div class="summary-value" style="color:var(--danger)">${totalWaste} mm</div>
|
||
</div>
|
||
<div class="summary-item">
|
||
<div class="summary-label">Eficiência Global</div>
|
||
<div class="summary-value" style="color:var(--success)">${efficiency}%</div>
|
||
</div>
|
||
<div class="summary-item" style="grid-column: span 4; margin-top: 10px; border-top: 1px solid #eee; padding-top: 10px;">
|
||
<div class="summary-label">Processo: <strong style="color:var(--primary)">${PROCESS_NAMES[selectedProcess]}</strong></div>
|
||
<div style="font-size: 9pt; color: #666;">Perda estimada por corte (Kerf): <strong>${totalKerfLoss} mm</strong></div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
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
|