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 = `${svgContent}`; - - const pieceDetails = groupPieces(bar.pieces); - const table = ` - - - ${pieceDetails.map(g => ``).join('')} - -
TAGmmQtd
${g.tag}${g.length}${g.count}
SOBRA${waste}mm
- `; - - const effBar = `
${efficiency}%
`; - - const repetitionText = bar.count > 1 ? ' × ' + bar.count + '' : ''; - - return ` -
-
-
BARRA ${barNum}${repetitionText}${noStockLabel}
${bar.barType}
-
-
Usado: ${used}mm
-
Peso: ${bar.weight}kg
-
-
- ${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 ` -
-
-
-
-

📊 ${jobTitle}

-

Gerado em: ${dateStr} às ${timeStr}

-
-
- Página ${pageNum} de ${totalPages} -
-
-
- -
- ${pageBody} -
- -
- Otimizador de Corte | Total de Peças: ${totalPieces} | Aproveitamento Global: ${((1 - (lastResults.usedBars.reduce((s, b) => s + b.remaining, 0) / lastResults.usedBars.reduce((s, b) => s + b.length, 0))) * 100).toFixed(2)}% -
-
`; - }).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 ` -
-
-
BARRA ${index} - ${bar.barType} ${repetition}
-
Eficiência: ${efficiency}% | Sobra: ${waste}mm
-
- ${svgContent} - - - - ${groups.map(g => ``).join('')} - ${waste > 0 ? `` : ''} - -
PeçaComp. (mm)Qtd no Corte
${g.tag}${g.length}${g.count}
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 = `${svgContent}`; + + const pieceDetails = groupPieces(bar.pieces); + const table = ` + + + ${pieceDetails.map(g => ``).join('')} + +
TAGmmQtd
${g.tag}${g.length}${g.count}
SOBRA${waste}mm
+ `; + + const effBar = `
${efficiency}%
`; + + const repetitionText = bar.count > 1 ? ' × ' + bar.count + '' : ''; + + return ` +
+
+
BARRA ${barNum}${repetitionText}${noStockLabel}
${bar.barType}
+
+
Usado: ${used}mm
+
Peso: ${bar.weight}kg
+
+
+ ${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 ` +
+
+
+
+

📊 ${jobTitle}

+

Gerado em: ${dateStr} às ${timeStr}

+
+
+ Página ${pageNum} de ${totalPages} +
+
+
+ +
+ ${pageBody} +
+ +
+ Otimizador de Corte | Total de Peças: ${totalPieces} | Aproveitamento Global: ${((1 - (lastResults.usedBars.reduce((s, b) => s + b.remaining, 0) / lastResults.usedBars.reduce((s, b) => s + b.length, 0))) * 100).toFixed(2)}% +
+
`; + }).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 ` +
+
+
BARRA ${index} - ${bar.barType} ${repetition}
+
Eficiência: ${efficiency}% | Sobra: ${waste}mm
+
+ ${svgContent} + + + + ${groups.map(g => ``).join('')} + ${waste > 0 ? `` : ''} + +
PeçaComp. (mm)Qtd no Corte
${g.tag}${g.length}${g.count}
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 + } +}