Spaces:
Running
Running
| // ui.js - Renders all UI components | |
| import { appState } from './state.js'; | |
| import { attachAllListeners } from './events.js'; | |
| import { getMonthlyProductionSummary, getMonthlyMaterialUsage } from './reportService.js'; | |
| let productionChart = null; | |
| let inventoryChart = null; | |
| Chart.defaults.color = 'hsl(210, 14%, 66%)'; | |
| Chart.defaults.borderColor = 'hsl(220, 13%, 30%)'; | |
| export function refreshUI() { | |
| renderKpiCards(); | |
| renderCharts(); | |
| renderProductInputs(); | |
| renderInventory(); | |
| renderProductionLog(); | |
| renderModals(); // Still needed to create modal structure initially | |
| renderReorderList(); | |
| attachAllListeners(); // Attaches non-persistent listeners | |
| } | |
| export function showToast(message, type = 'info') { | |
| const container = document.getElementById('toast-container'); | |
| const toast = document.createElement('div'); | |
| const icon = type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-times-circle' : 'fa-info-circle'; | |
| toast.className = `toast toast-${type}`; | |
| toast.innerHTML = `<i class="fas ${icon}"></i><span>${message}</span>`; | |
| container.appendChild(toast); | |
| setTimeout(() => toast.classList.add('show'), 10); | |
| setTimeout(() => { | |
| toast.classList.remove('show'); | |
| toast.addEventListener('transitionend', () => toast.remove()); | |
| }, 3000); | |
| } | |
| function animateValue(element, start, end, duration, prefix = '', suffix = '') { | |
| let startTimestamp = null; | |
| const step = (timestamp) => { | |
| if (!startTimestamp) startTimestamp = timestamp; | |
| const progress = Math.min((timestamp - startTimestamp) / duration, 1); | |
| const current = Math.floor(progress * (end - start) + start); | |
| element.textContent = `${prefix}${current.toLocaleString()}${suffix}`; | |
| if (progress < 1) { | |
| window.requestAnimationFrame(step); | |
| } | |
| }; | |
| window.requestAnimationFrame(step); | |
| } | |
| function renderKpiCards() { | |
| const kpiRow = document.getElementById('kpi-row'); | |
| if (!kpiRow) return; | |
| const totalStockItems = appState.materials.reduce((sum, mat) => sum + mat.currentStock, 0); | |
| const itemsBelowReorder = appState.materials.filter(m => m.currentStock <= m.reorderPoint).length; | |
| const oneMonthAgo = new Date(new Date().setMonth(new Date().getMonth() - 1)); | |
| const unitsProducedMonth = appState.productionLog | |
| .filter(entry => new Date(entry.date) > oneMonthAgo && entry.productName === 'COMPLETE ANTENNA UNIT') | |
| .reduce((sum, entry) => sum + entry.quantity, 0); | |
| const kpis = [ | |
| { id: 'kpi-stock', label: 'Total Stock Items', value: totalStockItems, suffix: ' pcs', color: 'var(--accent-green)' }, | |
| { id: 'kpi-units', label: 'Units Produced (Month)', value: unitsProducedMonth, suffix: '', color: 'var(--accent-blue)' }, | |
| { id: 'kpi-reorder', label: 'Items Below Reorder', value: itemsBelowReorder, suffix: '', color: 'var(--accent-yellow)' }, | |
| { id: 'kpi-materials', label: 'Materials to Order', value: itemsBelowReorder, suffix: '', color: 'var(--accent-red)' } | |
| ]; | |
| kpiRow.innerHTML = kpis.map((kpi, index) => ` | |
| <div class="dashboard-card p-4 flex items-center"> | |
| <div class="kpi-icon-wrapper mr-4" style="background-color: ${kpi.color}20; color: ${kpi.color};"> | |
| <i class="fas ${['fa-boxes', 'fa-cogs', 'fa-exclamation-triangle', 'fa-shopping-cart'][index]} fa-lg"></i> | |
| </div> | |
| <div> | |
| <p class="text-sm text-secondary">${kpi.label}</p> | |
| <p class="text-2xl font-bold text-primary" id="${kpi.id}-${index}">0</p> | |
| </div> | |
| </div> | |
| `).join(''); | |
| kpis.forEach((kpi, index) => { | |
| const element = document.getElementById(`${kpi.id}-${index}`); | |
| if (element) { | |
| animateValue(element, 0, kpi.value, 1500, kpi.prefix, kpi.suffix); | |
| } | |
| }); | |
| } | |
| function renderCharts() { | |
| renderProductionHistoryChart(); | |
| renderInventoryStatusChart(); | |
| } | |
| function renderProductionHistoryChart() { | |
| const ctx = document.getElementById('production-history-chart')?.getContext('2d'); | |
| if (!ctx) return; | |
| const labels = []; | |
| const data = []; | |
| for (let i = 6; i >= 0; i--) { | |
| const d = new Date(); | |
| d.setDate(d.getDate() - i); | |
| labels.push(d.toLocaleDateString([], { weekday: 'short' })); | |
| const totalProduced = appState.productionLog | |
| .filter(entry => new Date(entry.date).toDateString() === d.toDateString() && entry.productName === 'COMPLETE ANTENNA UNIT') | |
| .reduce((sum, entry) => sum + entry.quantity, 0); | |
| data.push(totalProduced); | |
| } | |
| if (productionChart) { | |
| productionChart.data.labels = labels; | |
| productionChart.data.datasets[0].data = data; | |
| productionChart.update(); | |
| } else { | |
| productionChart = new Chart(ctx, { | |
| type: 'bar', | |
| data: { | |
| labels: labels, | |
| datasets: [{ | |
| label: 'Complete Units Produced', | |
| data: data, | |
| backgroundColor: 'rgba(66, 153, 225, 0.5)', | |
| borderColor: 'rgba(66, 153, 225, 1)', | |
| borderWidth: 1, | |
| borderRadius: 4, | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, // Important for responsive containers | |
| plugins: { legend: { display: false } }, | |
| scales: { | |
| y: { beginAtZero: true, grid: { color: 'hsl(220, 13%, 30%)' } }, | |
| x: { grid: { display: false } } | |
| } | |
| } | |
| }); | |
| } | |
| } | |
| function renderInventoryStatusChart() { | |
| const ctx = document.getElementById('inventory-status-chart')?.getContext('2d'); | |
| if (!ctx) return; | |
| let okCount = 0, warningCount = 0, criticalCount = 0; | |
| appState.materials.forEach(m => { | |
| if (m.currentStock <= m.reorderPoint) criticalCount++; | |
| else if ((m.currentStock / m.maxStock) <= 0.5) warningCount++; | |
| else okCount++; | |
| }); | |
| const data = { | |
| labels: ['OK', 'Warning', 'Critical'], | |
| datasets: [{ | |
| data: [okCount, warningCount, criticalCount], | |
| backgroundColor: [ 'hsla(145, 63%, 49%, 0.7)', 'hsla(50, 91%, 64%, 0.7)', 'hsla(0, 89%, 69%, 0.7)' ], | |
| borderColor: 'hsl(220, 26%, 18%)', | |
| borderWidth: 2 | |
| }] | |
| }; | |
| if (inventoryChart) { | |
| inventoryChart.data.datasets[0].data = [okCount, warningCount, criticalCount]; | |
| inventoryChart.update(); | |
| } else { | |
| inventoryChart = new Chart(ctx, { | |
| type: 'doughnut', | |
| data: data, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, // Important for responsive containers | |
| plugins: { legend: { position: 'top', labels: { color: 'hsl(210, 14%, 66%)' } } } | |
| } | |
| }); | |
| } | |
| } | |
| function renderModals() { | |
| const resetModal = document.getElementById('reset-modal'); | |
| if (resetModal && resetModal.innerHTML === '') { | |
| resetModal.innerHTML = `<div class="modal-content dashboard-card p-6 max-w-sm w-full mx-4"><h3 class="text-lg font-semibold mb-2">Confirm Reset</h3><p class="text-secondary mb-4">Are you sure you want to reset all inventory data? This action cannot be undone.</p><div class="flex justify-end space-x-2"><button id="cancel-reset-btn" class="btn btn-secondary">Cancel</button><button id="confirm-reset-btn" class="btn btn-danger">Reset Data</button></div></div>`; | |
| } | |
| const reportsModal = document.getElementById('reports-modal'); | |
| if (reportsModal && reportsModal.innerHTML === '') { | |
| reportsModal.innerHTML = `<div class="modal-content dashboard-card p-6 max-w-2xl w-full mx-4"><div class="flex justify-between items-center mb-4"><h3 class="text-lg font-semibold">Generate Report</h3><button id="close-reports-modal-btn" class="text-secondary hover:text-primary text-2xl">×</button></div><div class="flex gap-4 mb-4"><button id="report-prod-summary" class="flex-1 btn btn-primary">Monthly Production</button><button id="report-mat-usage" class="flex-1 btn btn-primary" style="background-color: var(--accent-green);">Monthly Material Usage</button></div><div id="report-content" class="p-4 border border-border-color rounded bg-dark min-h-[300px]">Select a report to view data.</div></div>`; | |
| } | |
| } | |
| export function renderReport(type) { | |
| const content = document.getElementById('report-content'); | |
| let data, title, headers, rows; | |
| if (type === 'production') { | |
| data = getMonthlyProductionSummary(); | |
| title = 'Monthly Production Summary (Last 30 Days)'; | |
| headers = ['Product/Assembly', 'Total Units Produced']; | |
| rows = Object.entries(data).map(([name, qty]) => `<tr><td class="border-b border-border-color px-4 py-2">${name}</td><td class="border-b border-border-color px-4 py-2 text-right">${qty}</td></tr>`).join(''); | |
| } else { | |
| data = getMonthlyMaterialUsage(); | |
| title = 'Monthly Material Usage (Last 30 Days)'; | |
| headers = ['Material', 'Total Quantity Consumed']; | |
| rows = Object.entries(data).map(([name, qty]) => { | |
| const material = appState.materials.find(m => m.name === name); | |
| return `<tr><td class="border-b border-border-color px-4 py-2">${name}</td><td class="border-b border-border-color px-4 py-2 text-right">${qty} ${material?.unit || ''}</td></tr>`; | |
| }).join(''); | |
| } | |
| if (Object.keys(data).length === 0) { | |
| content.innerHTML = `<p class="text-center text-secondary pt-12">No data available for this period.</p>`; | |
| return; | |
| } | |
| content.innerHTML = `<h4 class="font-bold mb-2">${title}</h4><table class="table-auto w-full text-sm"><thead><tr><th class="border-b-2 border-border-color px-4 py-2 text-left">${headers[0]}</th><th class="border-b-2 border-border-color px-4 py-2 text-right">${headers[1]}</th></tr></thead><tbody>${rows}</tbody></table>`; | |
| } | |
| function renderProductInputs() { | |
| const container = document.getElementById('product-cards'); | |
| if (!container) return; | |
| container.innerHTML = ''; | |
| for (const productName in appState.productRecipes) { | |
| const cardHTML = `<div class="dashboard-card p-4 border-l-4 border-l-transparent"><h3 class="font-medium mb-3">${productName}</h3><div class="flex items-center space-x-3"><input type="number" min="0" class="input-field w-20" value="0"><button class="btn btn-primary flex-1 update-btn" data-product-name="${productName}">Produce</button></div></div>`; | |
| container.insertAdjacentHTML('beforeend', cardHTML); | |
| } | |
| } | |
| function renderInventory() { | |
| const container = document.getElementById('material-cards'); | |
| if (!container) return; | |
| container.innerHTML = ''; | |
| appState.materials.forEach(material => { | |
| const stockPercentage = (material.currentStock / material.maxStock) * 100; | |
| let statusColor = 'var(--accent-green)'; | |
| if (stockPercentage <= 50) statusColor = 'var(--accent-yellow)'; | |
| if (material.currentStock <= material.reorderPoint) statusColor = 'var(--accent-red)'; // Changed logic to use reorder point for red | |
| const cardHTML = `<div class="dashboard-card p-4 border-l-4" style="border-left-color: ${statusColor}" data-material-name="${material.name}"><div class="flex justify-between items-start mb-1"><h3 class="font-medium text-sm">${material.name}</h3><div class="text-xs text-secondary flex items-center gap-3"><span>MAX:</span><span class="font-semibold max-stock-value">${material.maxStock}</span><i class="fas fa-plus-circle icon-btn restock-icon" title="Restock"></i><i class="fas fa-pencil-alt icon-btn edit-max-stock" title="Edit Max Stock"></i></div></div><div class="flex justify-between items-baseline mb-2"><div class="text-2xl font-bold">${material.currentStock}</div><span class="text-sm text-secondary">${material.unit}</span></div><div class="w-full bg-dark rounded-full h-1.5"><div class="h-1.5 rounded-full" style="width: ${stockPercentage}%; background-color: ${statusColor};"></div></div><div class="restock-form mt-2 hidden"></div></div>`; | |
| container.insertAdjacentHTML('beforeend', cardHTML); | |
| }); | |
| } | |
| function renderProductionLog() { | |
| const list = document.getElementById('production-log-list'); | |
| if (!list) return; | |
| list.innerHTML = ''; | |
| if (appState.productionLog.length === 0) { list.innerHTML = `<li class="text-secondary text-center pt-4">No production recorded yet.</li>`; return; } | |
| [...appState.productionLog].reverse().forEach(entry => { | |
| const date = new Date(entry.date); | |
| const formattedDate = `${date.toLocaleDateString()} ${date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}`; | |
| const logHTML = `<li class="p-2 border-b border-border-color flex justify-between items-center text-sm"><div><span class="font-semibold text-accent-blue">${entry.quantity}x</span> <span class="text-primary">${entry.productName}</span></div><span class="text-xs text-secondary">${formattedDate}</span></li>`; | |
| list.insertAdjacentHTML('beforeend', logHTML); | |
| }); | |
| } | |
| function renderReorderList() { | |
| const list = document.getElementById('reorder-list'); | |
| const header = document.getElementById('reorder-header'); | |
| if (!list || !header) return; | |
| list.innerHTML = ''; | |
| header.querySelector('#generate-full-po-btn')?.remove(); // Remove old button | |
| const itemsToReorder = appState.materials.filter(m => m.currentStock <= m.reorderPoint); | |
| if (itemsToReorder.length === 0) { | |
| list.innerHTML = `<li class="text-secondary text-center pt-4">All stock levels are healthy.</li>`; | |
| return; | |
| } | |
| // Add the consolidated button | |
| const poButton = `<button id="generate-full-po-btn" class="btn btn-secondary text-sm">Generate Full Reorder Report</button>`; | |
| header.insertAdjacentHTML('beforeend', poButton); | |
| itemsToReorder.forEach(item => { | |
| const needed = item.maxStock - item.currentStock; | |
| const itemHTML = `<li class="p-3 rounded-md text-sm flex justify-between items-center" style="background-color: #f6e05e20;"><div class="flex-grow"><span class="font-semibold">${item.name}</span><span class="text-xs text-secondary block">Stock: ${item.currentStock} / ${item.reorderPoint} (Need ${needed})</span></div><div class="text-right"><span class="font-bold">${needed}</span><span class="text-xs text-secondary ml-1">${item.unit}</span></div></li>`; | |
| list.insertAdjacentHTML('beforeend', itemHTML); | |
| }); | |
| } |