// 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) => `
`).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 = `Confirm Reset
Are you sure you want to reset all inventory data? This action cannot be undone.
`;
}
const reportsModal = document.getElementById('reports-modal');
if (reportsModal && reportsModal.innerHTML === '') {
reportsModal.innerHTML = `Generate Report
Select a report to view data.
`;
}
}
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}
${headers[0]} | ${headers[1]} |
${rows}
`;
}
function renderProductInputs() {
const container = document.getElementById('product-cards');
if (!container) return;
container.innerHTML = '';
for (const productName in appState.productRecipes) {
const cardHTML = ``;
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.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 `
`;
}).join('');
modal.innerHTML = `
Create Custom Purchase Order
${materialRows}
`;
modal.classList.remove('hidden');
}