diff --git a/.agent/rules/GEMINI.md b/.agent/rules/GEMINI.md
new file mode 100644
index 0000000..7bee6af
--- /dev/null
+++ b/.agent/rules/GEMINI.md
@@ -0,0 +1,22 @@
+---
+trigger: always_on
+---
+
+# GEMINI.md - Antigravity Kit (Auto-Generated)
+
+
+## TIER 0: UNIVERSAL RULES (Always Active)
+
+### 🌍 Global User Rules (MANDATORY)
+
+1. **Idioma**: Conversar e desenvolver sempre em **Português do Brasil**.
+2. **Automação Terminais**: Executar comandos de terminal, leitura de arquivos e edições de forma automática. Comandos de desenvolvimento (build, install, lint, git, etc.) são sempre 'SafeToAutoRun'.
+3. **Memória & Monitoramento**: Após cada evolução ou atualização no app, é OBRIGATÓRIO:
+ - Atualizar a memória do app em '/root/Antigravity_Memory/knowledge/{AppName}.md'.
+ - Atualizar o status e credenciais no 'app.py' do Cronos Dashboard.
+ - Realizar 'git commit', 'git push' e executar o 'update.sh' do Cronos para deploy automático.
+
+### 🌐 Language Handling
+
+
+---
diff --git a/.gitignore b/.gitignore
index 2685228..6842ae8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,6 @@
-node_modules
-dist
-.vercel
-.vscode
-.DS_Store
-*.local
+node_modules
+dist
+.vercel
+.vscode
+.DS_Store
+*.local
diff --git a/src/main.js b/src/main.js
index 933a25a..6b8393a 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,1187 +1,1187 @@
-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 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
diff --git a/src/style.css b/src/style.css
index b93a757..6108818 100644
--- a/src/style.css
+++ b/src/style.css
@@ -1,1323 +1,1323 @@
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
-
- :root {
- --primary: #1a6b8f;
- --primary-light: #2d8bb0;
- --success: #27ae60;
- --warning: #e67e22;
- --danger: #e74c3c;
- --gray-light: #ecf0f1;
- --gray: #bdc3c7;
- --text: #2c3e50;
- --text-light: #7f8c8d;
- --bg: #f5f7fa;
- }
-
- body {
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- background: var(--bg);
- color: var(--text);
- line-height: 1.6;
- padding: 10px;
- }
-
- .container {
- max-width: 1400px;
- margin: 0 auto;
- }
-
- header {
- background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
- color: white;
- padding: 20px;
- border-radius: 8px;
- margin-bottom: 20px;
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
- position: relative;
- }
-
- header h1 {
- font-size: 28px;
- margin-bottom: 5px;
- }
-
- header p {
- font-size: 14px;
- opacity: 0.95;
- }
-
- .layout {
- display: grid;
- grid-template-columns: 380px 1fr;
- gap: 20px;
- margin-bottom: 20px;
- }
-
- .panel {
- background: white;
- padding: 20px;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
- border: 1px solid var(--gray);
- }
-
- .panel h2 {
- font-size: 16px;
- color: var(--primary);
- margin-bottom: 15px;
- padding-bottom: 10px;
- border-bottom: 2px solid var(--primary);
- }
-
- .form-group {
- margin-bottom: 12px;
- }
-
- label {
- display: block;
- font-size: 11px;
- font-weight: 600;
- color: var(--text-light);
- text-transform: uppercase;
- margin-bottom: 4px;
- }
-
- input {
- width: 100%;
- padding: 8px;
- border: 1px solid var(--gray);
- border-radius: 4px;
- font-size: 13px;
- }
-
- button {
- width: 100%;
- padding: 10px;
- border: none;
- border-radius: 4px;
- font-size: 14px;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.3s ease;
- }
-
- .btn-primary {
- background: var(--primary);
- color: white;
- }
-
- .btn-primary:hover {
- background: var(--primary-light);
- }
-
- .btn-success {
- background: var(--success);
- color: white;
- }
-
- .btn-success:hover {
- background: #229954;
- }
-
- .btn-danger {
- background: var(--danger);
- color: white;
- width: auto;
- padding: 4px 8px;
- font-size: 11px;
- margin-left: 5px;
- }
-
- .btn-danger:hover {
- background: #c0392b;
- }
-
- .section-divider {
- margin: 18px 0;
- border: none;
- border-top: 1px solid var(--gray);
- }
-
- .list-container {
- max-height: 280px;
- overflow-y: auto;
- color: var(--text-light);
- }
-
- .piece-balloon {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- background: var(--gray-light);
- padding: 8px 12px;
- border-radius: 20px;
- margin: 4px;
- font-size: 12px;
- border-left: 3px solid var(--primary);
- }
-
- .piece-balloon-tag {
- font-weight: bold;
- color: var(--primary);
- }
-
- .piece-balloon-info {
- display: flex;
- flex-direction: column;
- font-size: 10px;
- color: var(--text-light);
- }
-
- .results {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
- gap: 12px;
- margin-top: 15px;
- }
-
- .result-card {
- background: white;
- padding: 12px;
- border-radius: 4px;
- text-align: center;
- border-left: 4px solid var(--primary);
- }
-
- .result-card.success {
- border-left-color: var(--success);
- }
-
- .result-card.warning {
- border-left-color: var(--warning);
- }
-
- .result-label {
- font-size: 10px;
- color: var(--text-light);
- text-transform: uppercase;
- margin-bottom: 6px;
- }
-
- .result-value {
- font-size: 22px;
- font-weight: bold;
- color: var(--primary);
- }
-
- .result-card.success .result-value {
- color: var(--success);
- }
-
- .result-card.warning .result-value {
- color: var(--warning);
- }
-
- .bars-container {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
- gap: 20px;
- }
-
- .bar-card {
- background: white;
- padding: 15px;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
- border: 1px solid var(--gray);
- }
-
- .bar-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 10px;
- padding-bottom: 8px;
- border-bottom: 2px solid var(--primary);
- }
-
- .bar-title {
- font-weight: bold;
- color: var(--primary);
- font-size: 13px;
- }
-
- .bar-stats {
- font-size: 11px;
- color: var(--text-light);
- text-align: right;
- }
-
- svg {
- width: 100%;
- height: auto;
- border: 1px solid var(--gray);
- border-radius: 4px;
- background: var(--gray-light);
- margin-bottom: 10px;
- }
-
- .bar-table {
- width: 100%;
- font-size: 10px;
- border-collapse: collapse;
- margin-bottom: 8px;
- }
-
- .bar-table th {
- background: var(--primary);
- color: white;
- padding: 5px;
- text-align: left;
- font-weight: bold;
- }
-
- .bar-table td {
- padding: 4px 5px;
- border-bottom: 1px solid var(--gray);
- }
-
- .bar-table tr:nth-child(even) {
- background: #f9f9f9;
- }
-
- .efficiency-bar {
- width: 100%;
- height: 18px;
- background: #e0e0e0;
- border-radius: 4px;
- overflow: hidden;
- margin-bottom: 8px;
- }
-
- .efficiency-fill {
- height: 100%;
- background: linear-gradient(90deg, var(--success) 0%, var(--primary-light) 100%);
- display: flex;
- align-items: center;
- justify-content: center;
- color: white;
- font-weight: bold;
- font-size: 9px;
- }
-
- .export-buttons {
- display: flex;
- gap: 10px;
- margin-top: 15px;
- }
-
- .export-buttons button {
- flex: 1;
- padding: 10px;
- }
-
- @media (max-width: 1024px) {
- .layout {
- grid-template-columns: 1fr;
- }
- }
-
- @media (max-width: 768px) {
- header h1 {
- font-size: 20px;
- }
-
- .bars-container {
- grid-template-columns: 1fr;
- }
- }
-
- .no-data {
- text-align: center;
- padding: 20px;
- color: var(--text-light);
- font-size: 12px;
- }
-
- .pieces-balloons {
- display: flex;
- flex-wrap: wrap;
- gap: 4px;
- padding: 8px;
- background: var(--gray-light);
- border-radius: 4px;
- min-height: 40px;
- align-content: flex-start;
- }
-
- /* New classes extracted from inline styles */
- .job-title-group {
- background: #f0f7fa;
- padding: 10px;
- border-radius: 4px;
- border: 1px solid #bce0fd;
- margin-bottom: 15px;
- }
-
- .job-title-label {
- color: var(--primary);
- font-size: 12px;
- }
-
- .job-title-input {
- font-weight: bold;
- }
-
- .job-actions {
- display: flex;
- gap: 5px;
- margin-top: 8px;
- }
-
- .btn-small {
- font-size: 12px;
- padding: 6px;
- }
-
- .btn-secondary {
- background-color: #7f8c8d;
- }
-
- .hidden {
- display: none;
- }
-
- .section-title {
- font-size: 13px;
- color: var(--primary);
- margin: 15px 0 10px 0;
- }
-
- .grid-2-col {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 8px;
- }
-
- .action-row {
- display: flex;
- gap: 5px;
- }
-
- .mt-10 {
- margin-top: 10px;
- }
-
- .mt-20 {
- margin-top: 20px;
- }
-
- .mb-10 {
- margin-bottom: 10px;
- }
-
- .import-btn {
- flex: 1;
- padding: 8px;
- }
-
- .feedback-msg {
- font-size: 11px;
- color: var(--success);
- margin-bottom: 10px;
- text-align: center;
- display: none;
- }
-
- .btn-calc {
- padding: 12px;
- }
-
- .btn-clear {
- width: auto;
- margin-top: 8px;
- width: 100%;
- }
-
- .full-width {
- width: 100%;
- }
-
- .grid-full-col {
- grid-column: 1/-1;
- }
-
- /* TOAST NOTIFICATIONS */
- .toast-container {
- position: fixed;
- top: 20px;
- right: 20px;
- z-index: 1000;
- display: flex;
- flex-direction: column;
- gap: 10px;
- }
-
- .toast {
- background: white;
- padding: 15px 20px;
- border-radius: 8px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- display: flex;
- align-items: center;
- gap: 12px;
- min-width: 300px;
- animation: slideIn 0.3s ease-out forwards;
- border-left: 5px solid var(--primary);
- opacity: 0;
- transform: translateX(100%);
- }
-
- .toast.show {
- opacity: 1;
- transform: translateX(0);
- }
-
- .toast.success {
- border-left-color: var(--success);
- }
-
- .toast.error {
- border-left-color: var(--danger);
- }
-
- .toast.warning {
- border-left-color: var(--warning);
- }
-
- .toast-icon {
- font-size: 20px;
- }
-
- .toast-content {
- flex: 1;
- }
-
- .toast-title {
- font-weight: bold;
- font-size: 14px;
- margin-bottom: 2px;
- color: var(--text);
- }
-
- .toast-message {
- font-size: 12px;
- color: var(--text-light);
- }
-
- .toast-close {
- background: none;
- border: none;
- color: var(--text-light);
- cursor: pointer;
- font-size: 16px;
- padding: 0;
- width: auto;
- }
-
- @keyframes slideIn {
- from {
- opacity: 0;
- transform: translateX(100%);
- }
-
- to {
- opacity: 1;
- transform: translateX(0);
- }
- }
-
- /* MODAL */
- .modal-overlay {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.5);
- z-index: 2000;
- display: flex;
- justify-content: center;
- align-items: center;
- opacity: 0;
- visibility: hidden;
- transition: all 0.3s ease;
- }
-
- .modal-overlay.show {
- opacity: 1;
- visibility: visible;
- }
-
- .modal {
- background: white;
- padding: 25px;
- border-radius: 8px;
- width: 90%;
- max-width: 400px;
- box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
- transform: translateY(-20px);
- transition: transform 0.3s ease;
- }
-
- .modal-overlay.show .modal {
- transform: translateY(0);
- }
-
- .modal-header {
- margin-bottom: 15px;
- }
-
- .modal-title {
- font-size: 18px;
- font-weight: bold;
- color: var(--primary);
- }
-
- .modal-body {
- font-size: 14px;
- color: var(--text);
- margin-bottom: 25px;
- line-height: 1.5;
- }
-
- .modal-actions {
- display: flex;
- justify-content: flex-end;
- gap: 10px;
- }
-
- .modal-btn {
- padding: 8px 16px;
- border-radius: 4px;
- font-weight: 600;
- font-size: 13px;
- cursor: pointer;
- border: none;
- transition: background 0.2s;
- }
-
- .modal-btn-cancel {
- background: var(--gray-light);
- color: var(--text);
- }
-
- .modal-btn-cancel:hover {
- background: var(--gray);
- }
-
- .import-btn {
- flex: 1;
- padding: 8px;
- }
-
- .feedback-msg {
- font-size: 11px;
- color: var(--success);
- margin-bottom: 10px;
- text-align: center;
- display: none;
- }
-
- .btn-calc {
- padding: 12px;
- }
-
- .btn-clear {
- width: auto;
- margin-top: 8px;
- width: 100%;
- }
-
- .full-width {
- width: 100%;
- }
-
- .grid-full-col {
- grid-column: 1/-1;
- }
-
- /* TOAST NOTIFICATIONS */
- .toast-container {
- position: fixed;
- top: 20px;
- right: 20px;
- z-index: 1000;
- display: flex;
- flex-direction: column;
- gap: 10px;
- }
-
- .toast {
- background: white;
- padding: 15px 20px;
- border-radius: 8px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- display: flex;
- align-items: center;
- gap: 12px;
- min-width: 300px;
- animation: slideIn 0.3s ease-out forwards;
- border-left: 5px solid var(--primary);
- opacity: 0;
- transform: translateX(100%);
- }
-
- .toast.show {
- opacity: 1;
- transform: translateX(0);
- }
-
- .toast.success {
- border-left-color: var(--success);
- }
-
- .toast.error {
- border-left-color: var(--danger);
- }
-
- .toast.warning {
- border-left-color: var(--warning);
- }
-
- .toast-icon {
- font-size: 20px;
- }
-
- .toast-content {
- flex: 1;
- }
-
- .toast-title {
- font-weight: bold;
- font-size: 14px;
- margin-bottom: 2px;
- color: var(--text);
- }
-
- .toast-message {
- font-size: 12px;
- color: var(--text-light);
- }
-
- .toast-close {
- background: none;
- border: none;
- color: var(--text-light);
- cursor: pointer;
- font-size: 16px;
- padding: 0;
- width: auto;
- }
-
- @keyframes slideIn {
- from {
- opacity: 0;
- transform: translateX(100%);
- }
-
- to {
- opacity: 1;
- transform: translateX(0);
- }
- }
-
- /* MODAL */
- .modal-overlay {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.5);
- z-index: 2000;
- display: flex;
- justify-content: center;
- align-items: center;
- opacity: 0;
- visibility: hidden;
- transition: all 0.3s ease;
- }
-
- .modal-overlay.show {
- opacity: 1;
- visibility: visible;
- }
-
- .modal {
- background: white;
- padding: 25px;
- border-radius: 8px;
- width: 90%;
- max-width: 400px;
- box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
- transform: translateY(-20px);
- transition: transform 0.3s ease;
- }
-
- .modal-overlay.show .modal {
- transform: translateY(0);
- }
-
- .modal-header {
- margin-bottom: 15px;
- }
-
- .modal-title {
- font-size: 18px;
- font-weight: bold;
- color: var(--primary);
- }
-
- .modal-body {
- font-size: 14px;
- color: var(--text);
- margin-bottom: 25px;
- line-height: 1.5;
- }
-
- .modal-actions {
- display: flex;
- justify-content: flex-end;
- gap: 10px;
- }
-
- .modal-btn {
- padding: 8px 16px;
- border-radius: 4px;
- font-weight: 600;
- font-size: 13px;
- cursor: pointer;
- border: none;
- transition: background 0.2s;
- }
-
- .modal-btn-cancel {
- background: var(--gray-light);
- color: var(--text);
- }
-
- .modal-btn-cancel:hover {
- background: var(--gray);
- }
-
- .modal-btn-confirm {
- background: var(--primary);
- color: white;
- }
-
- .modal-btn-confirm:hover {
- background: var(--primary-light);
- }
-
- /* Process Selection Styles */
- .process-section {
- margin-bottom: 20px;
- padding: 5px 0;
- }
-
- .section-label {
- display: block;
- font-weight: 600;
- margin-bottom: 10px;
- color: var(--primary);
- width: 100%;
- }
-
- .full-width {
- width: 100%;
- }
-
- .grid-full-col {
- grid-column: 1/-1;
- }
-
- /* TOAST NOTIFICATIONS */
- .toast-container {
- position: fixed;
- top: 20px;
- right: 20px;
- z-index: 1000;
- display: flex;
- flex-direction: column;
- gap: 10px;
- }
-
- .toast {
- background: white;
- padding: 15px 20px;
- border-radius: 8px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- display: flex;
- align-items: center;
- gap: 12px;
- min-width: 300px;
- animation: slideIn 0.3s ease-out forwards;
- border-left: 5px solid var(--primary);
- opacity: 0;
- transform: translateX(100%);
- }
-
- .toast.show {
- opacity: 1;
- transform: translateX(0);
- }
-
- .toast.success {
- border-left-color: var(--success);
- }
-
- .toast.error {
- border-left-color: var(--danger);
- }
-
- .toast.warning {
- border-left-color: var(--warning);
- }
-
- .toast-icon {
- font-size: 20px;
- }
-
- .toast-content {
- flex: 1;
- }
-
- .toast-title {
- font-weight: bold;
- font-size: 14px;
- margin-bottom: 2px;
- color: var(--text);
- }
-
- .toast-message {
- font-size: 12px;
- color: var(--text-light);
- }
-
- .toast-close {
- background: none;
- border: none;
- color: var(--text-light);
- cursor: pointer;
- font-size: 16px;
- padding: 0;
- width: auto;
- }
-
- @keyframes slideIn {
- from {
- opacity: 0;
- transform: translateX(100%);
- }
-
- to {
- opacity: 1;
- transform: translateX(0);
- }
- }
-
- /* MODAL */
- .modal-overlay {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.5);
- z-index: 2000;
- display: flex;
- justify-content: center;
- align-items: center;
- opacity: 0;
- visibility: hidden;
- transition: all 0.3s ease;
- }
-
- .modal-overlay.show {
- opacity: 1;
- visibility: visible;
- }
-
- .modal {
- background: white;
- padding: 25px;
- border-radius: 8px;
- width: 90%;
- max-width: 400px;
- box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
- transform: translateY(-20px);
- transition: transform 0.3s ease;
- }
-
- .modal-overlay.show .modal {
- transform: translateY(0);
- }
-
- .modal-header {
- margin-bottom: 15px;
- }
-
- .modal-title {
- font-size: 18px;
- font-weight: bold;
- color: var(--primary);
- }
-
- .modal-body {
- font-size: 14px;
- color: var(--text);
- margin-bottom: 25px;
- line-height: 1.5;
- }
-
- .modal-actions {
- display: flex;
- justify-content: flex-end;
- gap: 10px;
- }
-
- .modal-btn {
- padding: 8px 16px;
- border-radius: 4px;
- font-weight: 600;
- font-size: 13px;
- cursor: pointer;
- border: none;
- transition: background 0.2s;
- }
-
- .modal-btn-cancel {
- background: var(--gray-light);
- color: var(--text);
- }
-
- .modal-btn-cancel:hover {
- background: var(--gray);
- }
-
- .modal-btn-confirm {
- background: var(--primary);
- color: white;
- }
-
- .modal-btn-confirm:hover {
- background: var(--primary-light);
- }
-
- /* Process Selection Styles */
- .process-section {
- margin-bottom: 20px;
- padding: 5px 0;
- }
-
- .section-label {
- display: block;
- font-weight: 600;
- margin-bottom: 10px;
- color: var(--primary);
- font-size: 0.85em;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- }
-
- .process-options {
- display: flex;
- gap: 10px;
- flex-wrap: wrap;
- }
-
- .process-option {
- cursor: pointer;
- -webkit-user-select: none;
- user-select: none;
- }
-
- /* Hide the actual radio input */
- .process-option input[type="radio"] {
- display: none;
- }
-
- /* Style the span to look like a balloon/chip */
- .process-option .process-name {
- display: inline-block;
- padding: 6px 12px;
- border-radius: 20px;
- background: #f1f3f5;
- color: #6c757d;
- font-size: 0.85em;
- font-weight: 600;
- border: 1px solid transparent;
- transition: all 0.2s ease;
- }
-
- .process-option:hover .process-name {
- background: #e9ecef;
- color: #495057;
- }
-
- /* Selected State */
- .process-option input[type="radio"]:checked+.process-name {
- background: var(--primary);
- color: white;
- box-shadow: 0 2px 5px rgba(26, 107, 143, 0.3);
- transform: translateY(-1px);
- }
-
-
- .app-logo {
- height: 40px;
- vertical-align: middle;
- margin-right: 10px;
- }
-
- /* THEME TOGGLE */
- .theme-toggle-btn {
- position: absolute;
- top: 20px;
- right: 20px;
- background: rgba(255, 255, 255, 0.2);
- border: 1px solid rgba(255, 255, 255, 0.3);
- color: white;
- width: 40px;
- height: 40px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- transition: all 0.2s ease;
- font-size: 20px;
- z-index: 10;
- }
-
- .theme-toggle-btn:hover {
- background: rgba(255, 255, 255, 0.35);
- transform: scale(1.05);
- }
-
- /* DARK MODE */
- body.dark-theme {
- --bg: #121212;
- --text: #e0e0e0;
- --text-light: #aaa;
- --gray-light: #2c2c2c;
- --gray: #444;
- --primary: #3498db;
- /* Brighter blue for contrast */
- --primary-light: #5dade2;
- background-color: #121212;
- }
-
- body.dark-theme .panel,
- body.dark-theme .bar-card,
- body.dark-theme .result-card,
- body.dark-theme .toast,
- body.dark-theme .modal {
- background-color: #1e1e1e;
- border-color: #333;
- color: #e0e0e0;
- }
-
- body.dark-theme input {
- background-color: #2c2c2c;
- border-color: #444;
- color: white;
- }
-
- body.dark-theme .job-title-group {
- background-color: #252525;
- border-color: #444;
- }
-
- body.dark-theme .process-option .process-name,
- body.dark-theme .piece-balloon,
- body.dark-theme .pieces-balloons {
- background-color: #2c2c2c;
- color: #ccc;
- }
-
- body.dark-theme .process-option input[type="radio"]:checked+.process-name {
- background-color: var(--primary);
- color: white;
- }
-
- body.dark-theme .bar-table tr:nth-child(even) {
- background-color: #252525;
- }
-
- body.dark-theme .bar-table td,
- body.dark-theme .bar-table th {
- border-color: #444;
- }
-
- body.dark-theme .item-card {
- border-bottom-color: #333 !important;
- /* Force override inline style if possible, else ignored */
- }
-
- body.dark-theme .item-card div[style*="color: #333"] {
- width: auto;
- }
-
- .dynamic-action-btn.edit {
- color: var(--primary);
- }
-
- .dynamic-action-btn.remove {
- color: var(--danger);
- }
-
- .scraps-list {
- max-height: 100px;
- overflow-y: auto;
- font-size: 11px;
- margin-top: 5px;
- }
-
- .kerf-info {
- font-size: 11px;
- color: var(--text-light);
- margin-top: 5px;
- }
-
- .no-stock-badge {
- color: var(--danger);
- font-weight: bold;
- margin-left: 10px;
- }
-
- .bar-svg-piece-tag {
- font-size: 120px;
- font-weight: bold;
- text-anchor: middle;
- fill: #2c3e50;
- }
-
- .bar-svg-piece-length {
- font-size: 80px;
- font-weight: bold;
- text-anchor: middle;
- fill: #333;
- }
-
- .bar-svg-waste-tag {
- font-size: 100px;
- font-weight: bold;
- text-anchor: middle;
- fill: #c0392b;
- }
-
- .bar-svg-waste-length {
- font-size: 80px;
- font-weight: bold;
- text-anchor: middle;
- fill: #c0392b;
- }
-
- body.dark-theme .dynamic-flex-row {
- border-bottom-color: #333;
- }
-
- body.dark-theme .bar-svg-piece-tag {
- fill: #eee;
- }
-
- body.dark-theme .bar-svg-piece-length {
- fill: #ccc;
- }
-
- .dynamic-flex-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 8px 10px;
- border-bottom: 1px solid #eee;
- }
-
- .dynamic-item-info {
- font-size: 14px;
- color: var(--text);
- }
-
- .dynamic-item-subinfo {
- color: var(--text-light);
- margin-left: 8px;
- }
-
- .dynamic-item-actions {
- display: flex;
- gap: 10px;
- }
-
- .dynamic-action-btn {
- background: none;
- border: none;
- cursor: pointer;
- font-size: 16px;
- padding: 0;
- display: inline-block;
- width: auto;
- }
-
- .result-subtext {
- font-size: 10px;
- color: var(--text-light);
- }
-
- .medium-size {
- font-size: 16px;
- }
-
- .medium-small {
- font-size: 14px;
- }
-
- .small-danger {
- font-size: 16px;
- color: var(--danger);
- }
-
- .danger {
- color: var(--danger) !important;
- }
-
- .primary {
- color: var(--primary) !important;
- }
-
- .simulation-warning {
- grid-column: span 2;
- border-left-color: var(--danger) !important;
- }
-
- .row-waste {
- background-color: #ffe6e6;
- }
-
- body.dark-theme .row-waste {
- background-color: #3d1a1a;
- }
-
- .no-scraps {
- font-size: 11px;
- color: var(--success);
- }
-
- .repetition-text {
- color: var(--warning);
- font-weight: bold;
- }
-
- .bar-subtitle {
- font-size: 11px;
- color: var(--text-light);
- }
-
- .process-info {
- border-left: 4px solid var(--primary);
+ * {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ }
+
+ :root {
+ --primary: #1a6b8f;
+ --primary-light: #2d8bb0;
+ --success: #27ae60;
+ --warning: #e67e22;
+ --danger: #e74c3c;
+ --gray-light: #ecf0f1;
+ --gray: #bdc3c7;
+ --text: #2c3e50;
+ --text-light: #7f8c8d;
+ --bg: #f5f7fa;
+ }
+
+ body {
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ background: var(--bg);
+ color: var(--text);
+ line-height: 1.6;
+ padding: 10px;
+ }
+
+ .container {
+ max-width: 1400px;
+ margin: 0 auto;
+ }
+
+ header {
+ background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
+ color: white;
+ padding: 20px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ position: relative;
+ }
+
+ header h1 {
+ font-size: 28px;
+ margin-bottom: 5px;
+ }
+
+ header p {
+ font-size: 14px;
+ opacity: 0.95;
+ }
+
+ .layout {
+ display: grid;
+ grid-template-columns: 380px 1fr;
+ gap: 20px;
+ margin-bottom: 20px;
+ }
+
+ .panel {
+ background: white;
+ padding: 20px;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ border: 1px solid var(--gray);
+ }
+
+ .panel h2 {
+ font-size: 16px;
+ color: var(--primary);
+ margin-bottom: 15px;
+ padding-bottom: 10px;
+ border-bottom: 2px solid var(--primary);
+ }
+
+ .form-group {
+ margin-bottom: 12px;
+ }
+
+ label {
+ display: block;
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--text-light);
+ text-transform: uppercase;
+ margin-bottom: 4px;
+ }
+
+ input {
+ width: 100%;
+ padding: 8px;
+ border: 1px solid var(--gray);
+ border-radius: 4px;
+ font-size: 13px;
+ }
+
+ button {
+ width: 100%;
+ padding: 10px;
+ border: none;
+ border-radius: 4px;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ }
+
+ .btn-primary {
+ background: var(--primary);
+ color: white;
+ }
+
+ .btn-primary:hover {
+ background: var(--primary-light);
+ }
+
+ .btn-success {
+ background: var(--success);
+ color: white;
+ }
+
+ .btn-success:hover {
+ background: #229954;
+ }
+
+ .btn-danger {
+ background: var(--danger);
+ color: white;
+ width: auto;
+ padding: 4px 8px;
+ font-size: 11px;
+ margin-left: 5px;
+ }
+
+ .btn-danger:hover {
+ background: #c0392b;
+ }
+
+ .section-divider {
+ margin: 18px 0;
+ border: none;
+ border-top: 1px solid var(--gray);
+ }
+
+ .list-container {
+ max-height: 280px;
+ overflow-y: auto;
+ color: var(--text-light);
+ }
+
+ .piece-balloon {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ background: var(--gray-light);
+ padding: 8px 12px;
+ border-radius: 20px;
+ margin: 4px;
+ font-size: 12px;
+ border-left: 3px solid var(--primary);
+ }
+
+ .piece-balloon-tag {
+ font-weight: bold;
+ color: var(--primary);
+ }
+
+ .piece-balloon-info {
+ display: flex;
+ flex-direction: column;
+ font-size: 10px;
+ color: var(--text-light);
+ }
+
+ .results {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
+ gap: 12px;
+ margin-top: 15px;
+ }
+
+ .result-card {
+ background: white;
+ padding: 12px;
+ border-radius: 4px;
+ text-align: center;
+ border-left: 4px solid var(--primary);
+ }
+
+ .result-card.success {
+ border-left-color: var(--success);
+ }
+
+ .result-card.warning {
+ border-left-color: var(--warning);
+ }
+
+ .result-label {
+ font-size: 10px;
+ color: var(--text-light);
+ text-transform: uppercase;
+ margin-bottom: 6px;
+ }
+
+ .result-value {
+ font-size: 22px;
+ font-weight: bold;
+ color: var(--primary);
+ }
+
+ .result-card.success .result-value {
+ color: var(--success);
+ }
+
+ .result-card.warning .result-value {
+ color: var(--warning);
+ }
+
+ .bars-container {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
+ gap: 20px;
+ }
+
+ .bar-card {
+ background: white;
+ padding: 15px;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ border: 1px solid var(--gray);
+ }
+
+ .bar-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 10px;
+ padding-bottom: 8px;
+ border-bottom: 2px solid var(--primary);
+ }
+
+ .bar-title {
+ font-weight: bold;
+ color: var(--primary);
+ font-size: 13px;
+ }
+
+ .bar-stats {
+ font-size: 11px;
+ color: var(--text-light);
+ text-align: right;
+ }
+
+ svg {
+ width: 100%;
+ height: auto;
+ border: 1px solid var(--gray);
+ border-radius: 4px;
+ background: var(--gray-light);
+ margin-bottom: 10px;
+ }
+
+ .bar-table {
+ width: 100%;
+ font-size: 10px;
+ border-collapse: collapse;
+ margin-bottom: 8px;
+ }
+
+ .bar-table th {
+ background: var(--primary);
+ color: white;
+ padding: 5px;
+ text-align: left;
+ font-weight: bold;
+ }
+
+ .bar-table td {
+ padding: 4px 5px;
+ border-bottom: 1px solid var(--gray);
+ }
+
+ .bar-table tr:nth-child(even) {
+ background: #f9f9f9;
+ }
+
+ .efficiency-bar {
+ width: 100%;
+ height: 18px;
+ background: #e0e0e0;
+ border-radius: 4px;
+ overflow: hidden;
+ margin-bottom: 8px;
+ }
+
+ .efficiency-fill {
+ height: 100%;
+ background: linear-gradient(90deg, var(--success) 0%, var(--primary-light) 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ font-weight: bold;
+ font-size: 9px;
+ }
+
+ .export-buttons {
+ display: flex;
+ gap: 10px;
+ margin-top: 15px;
+ }
+
+ .export-buttons button {
+ flex: 1;
+ padding: 10px;
+ }
+
+ @media (max-width: 1024px) {
+ .layout {
+ grid-template-columns: 1fr;
+ }
+ }
+
+ @media (max-width: 768px) {
+ header h1 {
+ font-size: 20px;
+ }
+
+ .bars-container {
+ grid-template-columns: 1fr;
+ }
+ }
+
+ .no-data {
+ text-align: center;
+ padding: 20px;
+ color: var(--text-light);
+ font-size: 12px;
+ }
+
+ .pieces-balloons {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ padding: 8px;
+ background: var(--gray-light);
+ border-radius: 4px;
+ min-height: 40px;
+ align-content: flex-start;
+ }
+
+ /* New classes extracted from inline styles */
+ .job-title-group {
+ background: #f0f7fa;
+ padding: 10px;
+ border-radius: 4px;
+ border: 1px solid #bce0fd;
+ margin-bottom: 15px;
+ }
+
+ .job-title-label {
+ color: var(--primary);
+ font-size: 12px;
+ }
+
+ .job-title-input {
+ font-weight: bold;
+ }
+
+ .job-actions {
+ display: flex;
+ gap: 5px;
+ margin-top: 8px;
+ }
+
+ .btn-small {
+ font-size: 12px;
+ padding: 6px;
+ }
+
+ .btn-secondary {
+ background-color: #7f8c8d;
+ }
+
+ .hidden {
+ display: none;
+ }
+
+ .section-title {
+ font-size: 13px;
+ color: var(--primary);
+ margin: 15px 0 10px 0;
+ }
+
+ .grid-2-col {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 8px;
+ }
+
+ .action-row {
+ display: flex;
+ gap: 5px;
+ }
+
+ .mt-10 {
+ margin-top: 10px;
+ }
+
+ .mt-20 {
+ margin-top: 20px;
+ }
+
+ .mb-10 {
+ margin-bottom: 10px;
+ }
+
+ .import-btn {
+ flex: 1;
+ padding: 8px;
+ }
+
+ .feedback-msg {
+ font-size: 11px;
+ color: var(--success);
+ margin-bottom: 10px;
+ text-align: center;
+ display: none;
+ }
+
+ .btn-calc {
+ padding: 12px;
+ }
+
+ .btn-clear {
+ width: auto;
+ margin-top: 8px;
+ width: 100%;
+ }
+
+ .full-width {
+ width: 100%;
+ }
+
+ .grid-full-col {
+ grid-column: 1/-1;
+ }
+
+ /* TOAST NOTIFICATIONS */
+ .toast-container {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .toast {
+ background: white;
+ padding: 15px 20px;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ min-width: 300px;
+ animation: slideIn 0.3s ease-out forwards;
+ border-left: 5px solid var(--primary);
+ opacity: 0;
+ transform: translateX(100%);
+ }
+
+ .toast.show {
+ opacity: 1;
+ transform: translateX(0);
+ }
+
+ .toast.success {
+ border-left-color: var(--success);
+ }
+
+ .toast.error {
+ border-left-color: var(--danger);
+ }
+
+ .toast.warning {
+ border-left-color: var(--warning);
+ }
+
+ .toast-icon {
+ font-size: 20px;
+ }
+
+ .toast-content {
+ flex: 1;
+ }
+
+ .toast-title {
+ font-weight: bold;
+ font-size: 14px;
+ margin-bottom: 2px;
+ color: var(--text);
+ }
+
+ .toast-message {
+ font-size: 12px;
+ color: var(--text-light);
+ }
+
+ .toast-close {
+ background: none;
+ border: none;
+ color: var(--text-light);
+ cursor: pointer;
+ font-size: 16px;
+ padding: 0;
+ width: auto;
+ }
+
+ @keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateX(100%);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+ }
+
+ /* MODAL */
+ .modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.5);
+ z-index: 2000;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.3s ease;
+ }
+
+ .modal-overlay.show {
+ opacity: 1;
+ visibility: visible;
+ }
+
+ .modal {
+ background: white;
+ padding: 25px;
+ border-radius: 8px;
+ width: 90%;
+ max-width: 400px;
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
+ transform: translateY(-20px);
+ transition: transform 0.3s ease;
+ }
+
+ .modal-overlay.show .modal {
+ transform: translateY(0);
+ }
+
+ .modal-header {
+ margin-bottom: 15px;
+ }
+
+ .modal-title {
+ font-size: 18px;
+ font-weight: bold;
+ color: var(--primary);
+ }
+
+ .modal-body {
+ font-size: 14px;
+ color: var(--text);
+ margin-bottom: 25px;
+ line-height: 1.5;
+ }
+
+ .modal-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+ }
+
+ .modal-btn {
+ padding: 8px 16px;
+ border-radius: 4px;
+ font-weight: 600;
+ font-size: 13px;
+ cursor: pointer;
+ border: none;
+ transition: background 0.2s;
+ }
+
+ .modal-btn-cancel {
+ background: var(--gray-light);
+ color: var(--text);
+ }
+
+ .modal-btn-cancel:hover {
+ background: var(--gray);
+ }
+
+ .import-btn {
+ flex: 1;
+ padding: 8px;
+ }
+
+ .feedback-msg {
+ font-size: 11px;
+ color: var(--success);
+ margin-bottom: 10px;
+ text-align: center;
+ display: none;
+ }
+
+ .btn-calc {
+ padding: 12px;
+ }
+
+ .btn-clear {
+ width: auto;
+ margin-top: 8px;
+ width: 100%;
+ }
+
+ .full-width {
+ width: 100%;
+ }
+
+ .grid-full-col {
+ grid-column: 1/-1;
+ }
+
+ /* TOAST NOTIFICATIONS */
+ .toast-container {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .toast {
+ background: white;
+ padding: 15px 20px;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ min-width: 300px;
+ animation: slideIn 0.3s ease-out forwards;
+ border-left: 5px solid var(--primary);
+ opacity: 0;
+ transform: translateX(100%);
+ }
+
+ .toast.show {
+ opacity: 1;
+ transform: translateX(0);
+ }
+
+ .toast.success {
+ border-left-color: var(--success);
+ }
+
+ .toast.error {
+ border-left-color: var(--danger);
+ }
+
+ .toast.warning {
+ border-left-color: var(--warning);
+ }
+
+ .toast-icon {
+ font-size: 20px;
+ }
+
+ .toast-content {
+ flex: 1;
+ }
+
+ .toast-title {
+ font-weight: bold;
+ font-size: 14px;
+ margin-bottom: 2px;
+ color: var(--text);
+ }
+
+ .toast-message {
+ font-size: 12px;
+ color: var(--text-light);
+ }
+
+ .toast-close {
+ background: none;
+ border: none;
+ color: var(--text-light);
+ cursor: pointer;
+ font-size: 16px;
+ padding: 0;
+ width: auto;
+ }
+
+ @keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateX(100%);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+ }
+
+ /* MODAL */
+ .modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.5);
+ z-index: 2000;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.3s ease;
+ }
+
+ .modal-overlay.show {
+ opacity: 1;
+ visibility: visible;
+ }
+
+ .modal {
+ background: white;
+ padding: 25px;
+ border-radius: 8px;
+ width: 90%;
+ max-width: 400px;
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
+ transform: translateY(-20px);
+ transition: transform 0.3s ease;
+ }
+
+ .modal-overlay.show .modal {
+ transform: translateY(0);
+ }
+
+ .modal-header {
+ margin-bottom: 15px;
+ }
+
+ .modal-title {
+ font-size: 18px;
+ font-weight: bold;
+ color: var(--primary);
+ }
+
+ .modal-body {
+ font-size: 14px;
+ color: var(--text);
+ margin-bottom: 25px;
+ line-height: 1.5;
+ }
+
+ .modal-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+ }
+
+ .modal-btn {
+ padding: 8px 16px;
+ border-radius: 4px;
+ font-weight: 600;
+ font-size: 13px;
+ cursor: pointer;
+ border: none;
+ transition: background 0.2s;
+ }
+
+ .modal-btn-cancel {
+ background: var(--gray-light);
+ color: var(--text);
+ }
+
+ .modal-btn-cancel:hover {
+ background: var(--gray);
+ }
+
+ .modal-btn-confirm {
+ background: var(--primary);
+ color: white;
+ }
+
+ .modal-btn-confirm:hover {
+ background: var(--primary-light);
+ }
+
+ /* Process Selection Styles */
+ .process-section {
+ margin-bottom: 20px;
+ padding: 5px 0;
+ }
+
+ .section-label {
+ display: block;
+ font-weight: 600;
+ margin-bottom: 10px;
+ color: var(--primary);
+ width: 100%;
+ }
+
+ .full-width {
+ width: 100%;
+ }
+
+ .grid-full-col {
+ grid-column: 1/-1;
+ }
+
+ /* TOAST NOTIFICATIONS */
+ .toast-container {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .toast {
+ background: white;
+ padding: 15px 20px;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ min-width: 300px;
+ animation: slideIn 0.3s ease-out forwards;
+ border-left: 5px solid var(--primary);
+ opacity: 0;
+ transform: translateX(100%);
+ }
+
+ .toast.show {
+ opacity: 1;
+ transform: translateX(0);
+ }
+
+ .toast.success {
+ border-left-color: var(--success);
+ }
+
+ .toast.error {
+ border-left-color: var(--danger);
+ }
+
+ .toast.warning {
+ border-left-color: var(--warning);
+ }
+
+ .toast-icon {
+ font-size: 20px;
+ }
+
+ .toast-content {
+ flex: 1;
+ }
+
+ .toast-title {
+ font-weight: bold;
+ font-size: 14px;
+ margin-bottom: 2px;
+ color: var(--text);
+ }
+
+ .toast-message {
+ font-size: 12px;
+ color: var(--text-light);
+ }
+
+ .toast-close {
+ background: none;
+ border: none;
+ color: var(--text-light);
+ cursor: pointer;
+ font-size: 16px;
+ padding: 0;
+ width: auto;
+ }
+
+ @keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateX(100%);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+ }
+
+ /* MODAL */
+ .modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.5);
+ z-index: 2000;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.3s ease;
+ }
+
+ .modal-overlay.show {
+ opacity: 1;
+ visibility: visible;
+ }
+
+ .modal {
+ background: white;
+ padding: 25px;
+ border-radius: 8px;
+ width: 90%;
+ max-width: 400px;
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
+ transform: translateY(-20px);
+ transition: transform 0.3s ease;
+ }
+
+ .modal-overlay.show .modal {
+ transform: translateY(0);
+ }
+
+ .modal-header {
+ margin-bottom: 15px;
+ }
+
+ .modal-title {
+ font-size: 18px;
+ font-weight: bold;
+ color: var(--primary);
+ }
+
+ .modal-body {
+ font-size: 14px;
+ color: var(--text);
+ margin-bottom: 25px;
+ line-height: 1.5;
+ }
+
+ .modal-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+ }
+
+ .modal-btn {
+ padding: 8px 16px;
+ border-radius: 4px;
+ font-weight: 600;
+ font-size: 13px;
+ cursor: pointer;
+ border: none;
+ transition: background 0.2s;
+ }
+
+ .modal-btn-cancel {
+ background: var(--gray-light);
+ color: var(--text);
+ }
+
+ .modal-btn-cancel:hover {
+ background: var(--gray);
+ }
+
+ .modal-btn-confirm {
+ background: var(--primary);
+ color: white;
+ }
+
+ .modal-btn-confirm:hover {
+ background: var(--primary-light);
+ }
+
+ /* Process Selection Styles */
+ .process-section {
+ margin-bottom: 20px;
+ padding: 5px 0;
+ }
+
+ .section-label {
+ display: block;
+ font-weight: 600;
+ margin-bottom: 10px;
+ color: var(--primary);
+ font-size: 0.85em;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ }
+
+ .process-options {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+ }
+
+ .process-option {
+ cursor: pointer;
+ -webkit-user-select: none;
+ user-select: none;
+ }
+
+ /* Hide the actual radio input */
+ .process-option input[type="radio"] {
+ display: none;
+ }
+
+ /* Style the span to look like a balloon/chip */
+ .process-option .process-name {
+ display: inline-block;
+ padding: 6px 12px;
+ border-radius: 20px;
+ background: #f1f3f5;
+ color: #6c757d;
+ font-size: 0.85em;
+ font-weight: 600;
+ border: 1px solid transparent;
+ transition: all 0.2s ease;
+ }
+
+ .process-option:hover .process-name {
+ background: #e9ecef;
+ color: #495057;
+ }
+
+ /* Selected State */
+ .process-option input[type="radio"]:checked+.process-name {
+ background: var(--primary);
+ color: white;
+ box-shadow: 0 2px 5px rgba(26, 107, 143, 0.3);
+ transform: translateY(-1px);
+ }
+
+
+ .app-logo {
+ height: 40px;
+ vertical-align: middle;
+ margin-right: 10px;
+ }
+
+ /* THEME TOGGLE */
+ .theme-toggle-btn {
+ position: absolute;
+ top: 20px;
+ right: 20px;
+ background: rgba(255, 255, 255, 0.2);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ color: white;
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ font-size: 20px;
+ z-index: 10;
+ }
+
+ .theme-toggle-btn:hover {
+ background: rgba(255, 255, 255, 0.35);
+ transform: scale(1.05);
+ }
+
+ /* DARK MODE */
+ body.dark-theme {
+ --bg: #121212;
+ --text: #e0e0e0;
+ --text-light: #aaa;
+ --gray-light: #2c2c2c;
+ --gray: #444;
+ --primary: #3498db;
+ /* Brighter blue for contrast */
+ --primary-light: #5dade2;
+ background-color: #121212;
+ }
+
+ body.dark-theme .panel,
+ body.dark-theme .bar-card,
+ body.dark-theme .result-card,
+ body.dark-theme .toast,
+ body.dark-theme .modal {
+ background-color: #1e1e1e;
+ border-color: #333;
+ color: #e0e0e0;
+ }
+
+ body.dark-theme input {
+ background-color: #2c2c2c;
+ border-color: #444;
+ color: white;
+ }
+
+ body.dark-theme .job-title-group {
+ background-color: #252525;
+ border-color: #444;
+ }
+
+ body.dark-theme .process-option .process-name,
+ body.dark-theme .piece-balloon,
+ body.dark-theme .pieces-balloons {
+ background-color: #2c2c2c;
+ color: #ccc;
+ }
+
+ body.dark-theme .process-option input[type="radio"]:checked+.process-name {
+ background-color: var(--primary);
+ color: white;
+ }
+
+ body.dark-theme .bar-table tr:nth-child(even) {
+ background-color: #252525;
+ }
+
+ body.dark-theme .bar-table td,
+ body.dark-theme .bar-table th {
+ border-color: #444;
+ }
+
+ body.dark-theme .item-card {
+ border-bottom-color: #333 !important;
+ /* Force override inline style if possible, else ignored */
+ }
+
+ body.dark-theme .item-card div[style*="color: #333"] {
+ width: auto;
+ }
+
+ .dynamic-action-btn.edit {
+ color: var(--primary);
+ }
+
+ .dynamic-action-btn.remove {
+ color: var(--danger);
+ }
+
+ .scraps-list {
+ max-height: 100px;
+ overflow-y: auto;
+ font-size: 11px;
+ margin-top: 5px;
+ }
+
+ .kerf-info {
+ font-size: 11px;
+ color: var(--text-light);
+ margin-top: 5px;
+ }
+
+ .no-stock-badge {
+ color: var(--danger);
+ font-weight: bold;
+ margin-left: 10px;
+ }
+
+ .bar-svg-piece-tag {
+ font-size: 120px;
+ font-weight: bold;
+ text-anchor: middle;
+ fill: #2c3e50;
+ }
+
+ .bar-svg-piece-length {
+ font-size: 80px;
+ font-weight: bold;
+ text-anchor: middle;
+ fill: #333;
+ }
+
+ .bar-svg-waste-tag {
+ font-size: 100px;
+ font-weight: bold;
+ text-anchor: middle;
+ fill: #c0392b;
+ }
+
+ .bar-svg-waste-length {
+ font-size: 80px;
+ font-weight: bold;
+ text-anchor: middle;
+ fill: #c0392b;
+ }
+
+ body.dark-theme .dynamic-flex-row {
+ border-bottom-color: #333;
+ }
+
+ body.dark-theme .bar-svg-piece-tag {
+ fill: #eee;
+ }
+
+ body.dark-theme .bar-svg-piece-length {
+ fill: #ccc;
+ }
+
+ .dynamic-flex-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 10px;
+ border-bottom: 1px solid #eee;
+ }
+
+ .dynamic-item-info {
+ font-size: 14px;
+ color: var(--text);
+ }
+
+ .dynamic-item-subinfo {
+ color: var(--text-light);
+ margin-left: 8px;
+ }
+
+ .dynamic-item-actions {
+ display: flex;
+ gap: 10px;
+ }
+
+ .dynamic-action-btn {
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 16px;
+ padding: 0;
+ display: inline-block;
+ width: auto;
+ }
+
+ .result-subtext {
+ font-size: 10px;
+ color: var(--text-light);
+ }
+
+ .medium-size {
+ font-size: 16px;
+ }
+
+ .medium-small {
+ font-size: 14px;
+ }
+
+ .small-danger {
+ font-size: 16px;
+ color: var(--danger);
+ }
+
+ .danger {
+ color: var(--danger) !important;
+ }
+
+ .primary {
+ color: var(--primary) !important;
+ }
+
+ .simulation-warning {
+ grid-column: span 2;
+ border-left-color: var(--danger) !important;
+ }
+
+ .row-waste {
+ background-color: #ffe6e6;
+ }
+
+ body.dark-theme .row-waste {
+ background-color: #3d1a1a;
+ }
+
+ .no-scraps {
+ font-size: 11px;
+ color: var(--success);
+ }
+
+ .repetition-text {
+ color: var(--warning);
+ font-weight: bold;
+ }
+
+ .bar-subtitle {
+ font-size: 11px;
+ color: var(--text-light);
+ }
+
+ .process-info {
+ border-left: 4px solid var(--primary);
}
\ No newline at end of file
diff --git a/vite.config.js b/vite.config.js
index b12b387..c8fb4a2 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,9 +1,9 @@
-export default {
- root: './',
- build: {
- outDir: 'dist',
- },
- preview: {
- allowedHosts: true
- }
-}
+export default {
+ root: './',
+ build: {
+ outDir: 'dist',
+ },
+ preview: {
+ allowedHosts: true
+ }
+}