Spaces:
Running
Running
Update ui.js
Browse files
ui.js
CHANGED
|
@@ -1,27 +1,23 @@
|
|
| 1 |
-
// ui.js - Renders all UI components
|
| 2 |
|
| 3 |
import { appState } from './state.js';
|
| 4 |
import { attachAllListeners } from './events.js';
|
| 5 |
import { getMonthlyProductionSummary, getMonthlyMaterialUsage } from './reportService.js';
|
| 6 |
|
| 7 |
-
// Chart instances to prevent re-creation
|
| 8 |
let productionChart = null;
|
| 9 |
let inventoryChart = null;
|
| 10 |
|
| 11 |
-
// Set Chart.js defaults for our dark theme
|
| 12 |
Chart.defaults.color = 'hsl(210, 14%, 66%)';
|
| 13 |
Chart.defaults.borderColor = 'hsl(220, 13%, 30%)';
|
| 14 |
|
| 15 |
-
// Master function to update the entire UI
|
| 16 |
export function refreshUI() {
|
| 17 |
renderKpiCards();
|
| 18 |
renderCharts();
|
| 19 |
renderProductInputs();
|
| 20 |
renderInventory();
|
| 21 |
renderProductionLog();
|
| 22 |
-
renderAnalytics();
|
| 23 |
-
renderReorderList();
|
| 24 |
renderModals();
|
|
|
|
| 25 |
attachAllListeners();
|
| 26 |
}
|
| 27 |
|
|
@@ -39,13 +35,13 @@ export function showToast(message, type = 'info') {
|
|
| 39 |
}, 3000);
|
| 40 |
}
|
| 41 |
|
| 42 |
-
function animateValue(element, start, end, duration, prefix = '') {
|
| 43 |
let startTimestamp = null;
|
| 44 |
const step = (timestamp) => {
|
| 45 |
if (!startTimestamp) startTimestamp = timestamp;
|
| 46 |
const progress = Math.min((timestamp - startTimestamp) / duration, 1);
|
| 47 |
const current = Math.floor(progress * (end - start) + start);
|
| 48 |
-
element.textContent = `${prefix}${current.toLocaleString()}`;
|
| 49 |
if (progress < 1) {
|
| 50 |
window.requestAnimationFrame(step);
|
| 51 |
}
|
|
@@ -57,7 +53,7 @@ function renderKpiCards() {
|
|
| 57 |
const kpiRow = document.getElementById('kpi-row');
|
| 58 |
if (!kpiRow) return;
|
| 59 |
|
| 60 |
-
const
|
| 61 |
const itemsBelowReorder = appState.materials.filter(m => m.currentStock <= m.reorderPoint).length;
|
| 62 |
const oneMonthAgo = new Date(new Date().setMonth(new Date().getMonth() - 1));
|
| 63 |
const unitsProducedMonth = appState.productionLog
|
|
@@ -65,20 +61,20 @@ function renderKpiCards() {
|
|
| 65 |
.reduce((sum, entry) => sum + entry.quantity, 0);
|
| 66 |
|
| 67 |
const kpis = [
|
| 68 |
-
{ id: 'kpi-
|
| 69 |
-
{ id: 'kpi-units', label: 'Units Produced (Month)', value: unitsProducedMonth,
|
| 70 |
-
{ id: 'kpi-reorder', label: 'Items Below Reorder', value: itemsBelowReorder,
|
| 71 |
-
{ id: 'kpi-materials', label: 'Materials to Order', value: itemsBelowReorder,
|
| 72 |
];
|
| 73 |
|
| 74 |
kpiRow.innerHTML = kpis.map((kpi, index) => `
|
| 75 |
<div class="dashboard-card p-4 flex items-center">
|
| 76 |
<div class="kpi-icon-wrapper mr-4" style="background-color: ${kpi.color}20; color: ${kpi.color};">
|
| 77 |
-
<i class="fas ${['fa-
|
| 78 |
</div>
|
| 79 |
<div>
|
| 80 |
<p class="text-sm text-secondary">${kpi.label}</p>
|
| 81 |
-
<p class="text-2xl font-bold text-primary" id="${kpi.id}-${index}"
|
| 82 |
</div>
|
| 83 |
</div>
|
| 84 |
`).join('');
|
|
@@ -86,7 +82,7 @@ function renderKpiCards() {
|
|
| 86 |
kpis.forEach((kpi, index) => {
|
| 87 |
const element = document.getElementById(`${kpi.id}-${index}`);
|
| 88 |
if (element) {
|
| 89 |
-
animateValue(element, 0, kpi.value, 1500, kpi.prefix);
|
| 90 |
}
|
| 91 |
});
|
| 92 |
}
|
|
@@ -112,14 +108,11 @@ function renderProductionHistoryChart() {
|
|
| 112 |
data.push(totalProduced);
|
| 113 |
}
|
| 114 |
|
| 115 |
-
// ** BUG FIX STARTS HERE **
|
| 116 |
if (productionChart) {
|
| 117 |
-
// If chart exists, just update its data and refresh
|
| 118 |
productionChart.data.labels = labels;
|
| 119 |
productionChart.data.datasets[0].data = data;
|
| 120 |
productionChart.update();
|
| 121 |
} else {
|
| 122 |
-
// If chart does not exist (first render), create it
|
| 123 |
productionChart = new Chart(ctx, {
|
| 124 |
type: 'bar',
|
| 125 |
data: {
|
|
@@ -134,8 +127,7 @@ function renderProductionHistoryChart() {
|
|
| 134 |
}]
|
| 135 |
},
|
| 136 |
options: {
|
| 137 |
-
responsive: true,
|
| 138 |
-
maintainAspectRatio: false,
|
| 139 |
plugins: { legend: { display: false } },
|
| 140 |
scales: {
|
| 141 |
y: { beginAtZero: true, grid: { color: 'hsl(220, 13%, 30%)' } },
|
|
@@ -144,7 +136,6 @@ function renderProductionHistoryChart() {
|
|
| 144 |
}
|
| 145 |
});
|
| 146 |
}
|
| 147 |
-
// ** BUG FIX ENDS HERE **
|
| 148 |
}
|
| 149 |
|
| 150 |
function renderInventoryStatusChart() {
|
|
@@ -168,7 +159,6 @@ function renderInventoryStatusChart() {
|
|
| 168 |
}]
|
| 169 |
};
|
| 170 |
|
| 171 |
-
// ** BUG FIX STARTS HERE **
|
| 172 |
if (inventoryChart) {
|
| 173 |
inventoryChart.data.datasets[0].data = [okCount, warningCount, criticalCount];
|
| 174 |
inventoryChart.update();
|
|
@@ -177,13 +167,11 @@ function renderInventoryStatusChart() {
|
|
| 177 |
type: 'doughnut',
|
| 178 |
data: data,
|
| 179 |
options: {
|
| 180 |
-
responsive: true,
|
| 181 |
-
maintainAspectRatio: false,
|
| 182 |
plugins: { legend: { position: 'top', labels: { color: 'hsl(210, 14%, 66%)' } } }
|
| 183 |
}
|
| 184 |
});
|
| 185 |
}
|
| 186 |
-
// ** BUG FIX ENDS HERE **
|
| 187 |
}
|
| 188 |
|
| 189 |
function renderModals() {
|
|
@@ -249,24 +237,6 @@ function renderProductionLog() {
|
|
| 249 |
});
|
| 250 |
}
|
| 251 |
|
| 252 |
-
function renderAnalytics() {
|
| 253 |
-
const container = document.getElementById('analytics-content');
|
| 254 |
-
container.innerHTML = '';
|
| 255 |
-
const totalValue = appState.materials.reduce((sum, mat) => sum + (mat.currentStock * (mat.costPerUnit || 0)), 0);
|
| 256 |
-
let productCostsHTML = '';
|
| 257 |
-
for (const productName in appState.productRecipes) {
|
| 258 |
-
const recipe = appState.productRecipes[productName];
|
| 259 |
-
const cost = Object.keys(recipe).reduce((sum, matName) => {
|
| 260 |
-
const material = appState.materials.find(m => m.name === matName);
|
| 261 |
-
const quantity = recipe[matName];
|
| 262 |
-
return sum + (quantity * (material?.costPerUnit || 0));
|
| 263 |
-
}, 0);
|
| 264 |
-
productCostsHTML += `<div class="flex justify-between text-sm"><span class="text-secondary">${productName}</span><span class="font-medium">$${cost.toFixed(2)} / unit</span></div>`;
|
| 265 |
-
}
|
| 266 |
-
const analyticsHTML = `<div class="p-3 bg-dark rounded-lg"><div class="flex justify-between items-center"><span class="text-secondary">Total Inventory Value</span><span class="text-lg font-bold" style="color: var(--accent-green);">$${totalValue.toFixed(2)}</span></div></div><div><h4 class="font-semibold text-sm mb-2 mt-3">Material Cost Per Product</h4><div class="space-y-1">${productCostsHTML}</div></div>`;
|
| 267 |
-
container.innerHTML = analyticsHTML;
|
| 268 |
-
}
|
| 269 |
-
|
| 270 |
function renderReorderList() {
|
| 271 |
const list = document.getElementById('reorder-list');
|
| 272 |
list.innerHTML = '';
|
|
@@ -274,7 +244,7 @@ function renderReorderList() {
|
|
| 274 |
if (itemsToReorder.length === 0) { list.innerHTML = `<li class="text-secondary text-center pt-4">All stock levels are healthy.</li>`; return; }
|
| 275 |
itemsToReorder.forEach(item => {
|
| 276 |
const needed = item.maxStock - item.currentStock;
|
| 277 |
-
const itemHTML = `<li class="p-3 rounded-md text-sm" style="background-color: #f6e05e20;
|
| 278 |
list.insertAdjacentHTML('beforeend', itemHTML);
|
| 279 |
});
|
| 280 |
}
|
|
|
|
| 1 |
+
// ui.js - Renders all UI components
|
| 2 |
|
| 3 |
import { appState } from './state.js';
|
| 4 |
import { attachAllListeners } from './events.js';
|
| 5 |
import { getMonthlyProductionSummary, getMonthlyMaterialUsage } from './reportService.js';
|
| 6 |
|
|
|
|
| 7 |
let productionChart = null;
|
| 8 |
let inventoryChart = null;
|
| 9 |
|
|
|
|
| 10 |
Chart.defaults.color = 'hsl(210, 14%, 66%)';
|
| 11 |
Chart.defaults.borderColor = 'hsl(220, 13%, 30%)';
|
| 12 |
|
|
|
|
| 13 |
export function refreshUI() {
|
| 14 |
renderKpiCards();
|
| 15 |
renderCharts();
|
| 16 |
renderProductInputs();
|
| 17 |
renderInventory();
|
| 18 |
renderProductionLog();
|
|
|
|
|
|
|
| 19 |
renderModals();
|
| 20 |
+
renderReorderList();
|
| 21 |
attachAllListeners();
|
| 22 |
}
|
| 23 |
|
|
|
|
| 35 |
}, 3000);
|
| 36 |
}
|
| 37 |
|
| 38 |
+
function animateValue(element, start, end, duration, prefix = '', suffix = '') {
|
| 39 |
let startTimestamp = null;
|
| 40 |
const step = (timestamp) => {
|
| 41 |
if (!startTimestamp) startTimestamp = timestamp;
|
| 42 |
const progress = Math.min((timestamp - startTimestamp) / duration, 1);
|
| 43 |
const current = Math.floor(progress * (end - start) + start);
|
| 44 |
+
element.textContent = `${prefix}${current.toLocaleString()}${suffix}`;
|
| 45 |
if (progress < 1) {
|
| 46 |
window.requestAnimationFrame(step);
|
| 47 |
}
|
|
|
|
| 53 |
const kpiRow = document.getElementById('kpi-row');
|
| 54 |
if (!kpiRow) return;
|
| 55 |
|
| 56 |
+
const totalStockItems = appState.materials.reduce((sum, mat) => sum + mat.currentStock, 0);
|
| 57 |
const itemsBelowReorder = appState.materials.filter(m => m.currentStock <= m.reorderPoint).length;
|
| 58 |
const oneMonthAgo = new Date(new Date().setMonth(new Date().getMonth() - 1));
|
| 59 |
const unitsProducedMonth = appState.productionLog
|
|
|
|
| 61 |
.reduce((sum, entry) => sum + entry.quantity, 0);
|
| 62 |
|
| 63 |
const kpis = [
|
| 64 |
+
{ id: 'kpi-stock', label: 'Total Stock Items', value: totalStockItems, suffix: ' pcs', color: 'var(--accent-green)' },
|
| 65 |
+
{ id: 'kpi-units', label: 'Units Produced (Month)', value: unitsProducedMonth, suffix: '', color: 'var(--accent-blue)' },
|
| 66 |
+
{ id: 'kpi-reorder', label: 'Items Below Reorder', value: itemsBelowReorder, suffix: '', color: 'var(--accent-yellow)' },
|
| 67 |
+
{ id: 'kpi-materials', label: 'Materials to Order', value: itemsBelowReorder, suffix: '', color: 'var(--accent-red)' }
|
| 68 |
];
|
| 69 |
|
| 70 |
kpiRow.innerHTML = kpis.map((kpi, index) => `
|
| 71 |
<div class="dashboard-card p-4 flex items-center">
|
| 72 |
<div class="kpi-icon-wrapper mr-4" style="background-color: ${kpi.color}20; color: ${kpi.color};">
|
| 73 |
+
<i class="fas ${['fa-boxes', 'fa-cogs', 'fa-exclamation-triangle', 'fa-shopping-cart'][index]} fa-lg"></i>
|
| 74 |
</div>
|
| 75 |
<div>
|
| 76 |
<p class="text-sm text-secondary">${kpi.label}</p>
|
| 77 |
+
<p class="text-2xl font-bold text-primary" id="${kpi.id}-${index}">0</p>
|
| 78 |
</div>
|
| 79 |
</div>
|
| 80 |
`).join('');
|
|
|
|
| 82 |
kpis.forEach((kpi, index) => {
|
| 83 |
const element = document.getElementById(`${kpi.id}-${index}`);
|
| 84 |
if (element) {
|
| 85 |
+
animateValue(element, 0, kpi.value, 1500, kpi.prefix, kpi.suffix);
|
| 86 |
}
|
| 87 |
});
|
| 88 |
}
|
|
|
|
| 108 |
data.push(totalProduced);
|
| 109 |
}
|
| 110 |
|
|
|
|
| 111 |
if (productionChart) {
|
|
|
|
| 112 |
productionChart.data.labels = labels;
|
| 113 |
productionChart.data.datasets[0].data = data;
|
| 114 |
productionChart.update();
|
| 115 |
} else {
|
|
|
|
| 116 |
productionChart = new Chart(ctx, {
|
| 117 |
type: 'bar',
|
| 118 |
data: {
|
|
|
|
| 127 |
}]
|
| 128 |
},
|
| 129 |
options: {
|
| 130 |
+
responsive: true, maintainAspectRatio: false,
|
|
|
|
| 131 |
plugins: { legend: { display: false } },
|
| 132 |
scales: {
|
| 133 |
y: { beginAtZero: true, grid: { color: 'hsl(220, 13%, 30%)' } },
|
|
|
|
| 136 |
}
|
| 137 |
});
|
| 138 |
}
|
|
|
|
| 139 |
}
|
| 140 |
|
| 141 |
function renderInventoryStatusChart() {
|
|
|
|
| 159 |
}]
|
| 160 |
};
|
| 161 |
|
|
|
|
| 162 |
if (inventoryChart) {
|
| 163 |
inventoryChart.data.datasets[0].data = [okCount, warningCount, criticalCount];
|
| 164 |
inventoryChart.update();
|
|
|
|
| 167 |
type: 'doughnut',
|
| 168 |
data: data,
|
| 169 |
options: {
|
| 170 |
+
responsive: true, maintainAspectRatio: false,
|
|
|
|
| 171 |
plugins: { legend: { position: 'top', labels: { color: 'hsl(210, 14%, 66%)' } } }
|
| 172 |
}
|
| 173 |
});
|
| 174 |
}
|
|
|
|
| 175 |
}
|
| 176 |
|
| 177 |
function renderModals() {
|
|
|
|
| 237 |
});
|
| 238 |
}
|
| 239 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
function renderReorderList() {
|
| 241 |
const list = document.getElementById('reorder-list');
|
| 242 |
list.innerHTML = '';
|
|
|
|
| 244 |
if (itemsToReorder.length === 0) { list.innerHTML = `<li class="text-secondary text-center pt-4">All stock levels are healthy.</li>`; return; }
|
| 245 |
itemsToReorder.forEach(item => {
|
| 246 |
const needed = item.maxStock - item.currentStock;
|
| 247 |
+
const itemHTML = `<li class="p-3 rounded-md text-sm flex justify-between items-center" style="background-color: #f6e05e20;" data-material-name="${item.name}"><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><button class="generate-po-btn btn btn-secondary text-xs">Generate PO</button></li>`;
|
| 248 |
list.insertAdjacentHTML('beforeend', itemHTML);
|
| 249 |
});
|
| 250 |
}
|