// 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(); renderReorderList(); attachAllListeners(); } 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 = `${message}`; 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) => `

${kpi.label}

0

`).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, 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.reorderPoint * 1.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, plugins: { legend: { position: 'top', labels: { color: 'hsl(210, 14%, 66%)' } } } } }); } } function renderModals() { const resetModal = document.getElementById('reset-modal'); if (resetModal && resetModal.innerHTML === '') { resetModal.innerHTML = ``; } const reportsModal = document.getElementById('reports-modal'); if (reportsModal && reportsModal.innerHTML === '') { reportsModal.innerHTML = ``; } } 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]) => `${name}${qty}`).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 `${name}${qty} ${material?.unit || ''}`; }).join(''); } if (Object.keys(data).length === 0) { content.innerHTML = `

No data available for this period.

`; return; } content.innerHTML = `

${title}

${rows}
${headers[0]}${headers[1]}
`; } function renderProductInputs() { const container = document.getElementById('product-cards'); if (!container) return; container.innerHTML = ''; for (const productName in appState.productRecipes) { const cardHTML = `

${productName}

`; container.insertAdjacentHTML('beforeend', cardHTML); } } function renderInventory() { const container = document.getElementById('material-cards'); if (!container) return; container.innerHTML = ''; appState.materials.forEach(material => { const safeStockLevel = material.reorderPoint * 2; const stockPercentage = Math.min((material.currentStock / safeStockLevel) * 100, 100); let statusColor = 'var(--accent-green)'; if (material.currentStock <= material.reorderPoint * 1.5) statusColor = 'var(--accent-yellow)'; if (material.currentStock <= material.reorderPoint) statusColor = 'var(--accent-red)'; const cardHTML = `

${material.name}

${material.currentStock}
${material.unit}
`; 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 = `
  • No production recorded yet.
  • `; 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 = `
  • ${entry.quantity}x ${entry.productName}
    ${formattedDate}
  • `; 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('#open-po-modal-btn')?.remove(); const itemsToReorder = appState.materials.filter(m => m.currentStock <= m.reorderPoint * 1.5); if (itemsToReorder.length === 0) { list.innerHTML = `
  • All stock levels are healthy.
  • `; return; } const poButton = ``; header.insertAdjacentHTML('beforeend', poButton); itemsToReorder.forEach(item => { const needed = Math.max(1, (item.reorderPoint * 2) - item.currentStock); const itemHTML = `
  • ${item.name}Stock: ${item.currentStock} / Reorder at: ${item.reorderPoint}
    Suggests ${needed}${item.unit}
  • `; list.insertAdjacentHTML('beforeend', itemHTML); }); } export function renderCustomPOModal(materials) { const modal = document.getElementById('custom-po-modal'); if (!modal) return; const materialRows = materials.map(material => { const suggestedQty = Math.max(1, (material.reorderPoint * 2) - material.currentStock); return `
    ${material.name} Stock: ${material.currentStock} | Reorder: ${material.reorderPoint}
    `; }).join(''); modal.innerHTML = ` `; modal.classList.remove('hidden'); }