Spaces:
Running
Running
Create ui.js
Browse files
ui.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ui.js - Renders all UI components
|
| 2 |
+
|
| 3 |
+
import { appState } from './state.js';
|
| 4 |
+
import { attachAllListeners } from './events.js';
|
| 5 |
+
|
| 6 |
+
// Master function to update the entire UI
|
| 7 |
+
export function refreshUI() {
|
| 8 |
+
renderProductInputs();
|
| 9 |
+
renderInventory();
|
| 10 |
+
renderProductionLog();
|
| 11 |
+
renderAnalytics();
|
| 12 |
+
renderReorderList();
|
| 13 |
+
renderDailyProductionSummary();
|
| 14 |
+
attachAllListeners(); // Re-attach listeners to new/updated elements
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export function showToast(message, type = 'info') {
|
| 18 |
+
const container = document.getElementById('toast-container');
|
| 19 |
+
const toast = document.createElement('div');
|
| 20 |
+
const icon = type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-times-circle' : 'fa-info-circle';
|
| 21 |
+
toast.className = `toast toast-${type}`;
|
| 22 |
+
toast.innerHTML = `<i class="fas ${icon}"></i><span>${message}</span>`;
|
| 23 |
+
container.appendChild(toast);
|
| 24 |
+
setTimeout(() => toast.classList.add('show'), 10);
|
| 25 |
+
setTimeout(() => {
|
| 26 |
+
toast.classList.remove('show');
|
| 27 |
+
toast.addEventListener('transitionend', () => toast.remove());
|
| 28 |
+
}, 3000);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// --- Internal Rendering Functions ---
|
| 32 |
+
|
| 33 |
+
function renderProductInputs() {
|
| 34 |
+
const container = document.getElementById('product-cards');
|
| 35 |
+
container.innerHTML = '';
|
| 36 |
+
for (const productName in appState.productRecipes) {
|
| 37 |
+
const cardHTML = `<div class="product-card bg-white rounded-lg shadow p-4 border border-gray-100" data-product-name="${productName}"><h3 class="font-medium text-gray-700 mb-3">${productName}</h3><div class="flex items-center space-x-3"><input type="number" min="0" class="input-number w-20 px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500" value="0"><button class="update-btn px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">Produce</button></div></div>`;
|
| 38 |
+
container.insertAdjacentHTML('beforeend', cardHTML);
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
function renderInventory() {
|
| 43 |
+
const container = document.getElementById('material-cards');
|
| 44 |
+
container.innerHTML = '';
|
| 45 |
+
appState.materials.forEach(material => {
|
| 46 |
+
const stockPercentage = (material.currentStock / material.maxStock) * 100;
|
| 47 |
+
let statusClass = 'status-ok', progressClass = 'progress-ok';
|
| 48 |
+
if (stockPercentage <= 50 && stockPercentage > 20) { statusClass = 'status-warning'; progressClass = 'progress-warning'; }
|
| 49 |
+
else if (stockPercentage <= 20) { statusClass = 'status-critical'; progressClass = 'progress-critical'; }
|
| 50 |
+
const cardHTML = `<div class="material-card bg-white rounded-lg shadow p-4 border-l-4 ${statusClass}" data-material-name="${material.name}"><div class="flex justify-between items-start mb-1"><h3 class="font-medium text-gray-700">${material.name}</h3><div class="text-xs text-gray-500 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 current-stock">${material.currentStock}</div><span class="text-sm text-gray-500 unit">${material.unit}</span></div><div class="w-full bg-gray-200 rounded-full h-2.5"><div class="h-2.5 rounded-full ${progressClass} progress-bar" style="width: ${stockPercentage}%"></div></div><div class="restock-form mt-2 hidden"></div></div>`;
|
| 51 |
+
container.insertAdjacentHTML('beforeend', cardHTML);
|
| 52 |
+
});
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
function renderProductionLog() {
|
| 56 |
+
const list = document.getElementById('production-log-list');
|
| 57 |
+
list.innerHTML = '';
|
| 58 |
+
if (appState.productionLog.length === 0) { list.innerHTML = `<li class="text-gray-500 text-center pt-4">No production recorded yet.</li>`; return; }
|
| 59 |
+
[...appState.productionLog].reverse().forEach(entry => {
|
| 60 |
+
const date = new Date(entry.date);
|
| 61 |
+
const formattedDate = `${date.toLocaleDateString()} ${date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}`;
|
| 62 |
+
const logHTML = `<li class="p-2 border-b border-gray-100 flex justify-between items-center text-sm"><div><span class="font-semibold text-blue-600">${entry.quantity}x</span> <span class="text-gray-800">${entry.productName}</span></div><span class="text-xs text-gray-400">${formattedDate}</span></li>`;
|
| 63 |
+
list.insertAdjacentHTML('beforeend', logHTML);
|
| 64 |
+
});
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
function renderAnalytics() {
|
| 68 |
+
const container = document.getElementById('analytics-content');
|
| 69 |
+
container.innerHTML = '';
|
| 70 |
+
const totalValue = appState.materials.reduce((sum, mat) => sum + (mat.currentStock * (mat.costPerUnit || 0)), 0);
|
| 71 |
+
let productCostsHTML = '';
|
| 72 |
+
for (const productName in appState.productRecipes) {
|
| 73 |
+
const recipe = appState.productRecipes[productName];
|
| 74 |
+
const cost = Object.keys(recipe).reduce((sum, matName) => {
|
| 75 |
+
const material = appState.materials.find(m => m.name === matName);
|
| 76 |
+
const quantity = recipe[matName];
|
| 77 |
+
return sum + (quantity * (material.costPerUnit || 0));
|
| 78 |
+
}, 0);
|
| 79 |
+
productCostsHTML += `<div class="flex justify-between text-sm"><span class="text-gray-600">${productName}</span><span class="font-medium">$${cost.toFixed(2)} / unit</span></div>`;
|
| 80 |
+
}
|
| 81 |
+
const analyticsHTML = `<div class="p-3 bg-gray-50 rounded-lg"><div class="flex justify-between items-center"><span class="text-gray-600">Total Inventory Value</span><span class="text-lg font-bold text-green-600">$${totalValue.toFixed(2)}</span></div></div><div><h4 class="font-semibold text-sm mb-2 text-gray-700 mt-3">Material Cost Per Product</h4><div class="space-y-1">${productCostsHTML}</div></div>`;
|
| 82 |
+
container.innerHTML = analyticsHTML;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
function renderReorderList() {
|
| 86 |
+
const list = document.getElementById('reorder-list');
|
| 87 |
+
list.innerHTML = '';
|
| 88 |
+
const itemsToReorder = appState.materials.filter(m => m.currentStock <= (m.reorderPoint || 0));
|
| 89 |
+
if (itemsToReorder.length === 0) { list.innerHTML = `<li class="text-gray-500 text-center pt-4">All stock levels are healthy.</li>`; return; }
|
| 90 |
+
itemsToReorder.forEach(item => {
|
| 91 |
+
const needed = item.maxStock - item.currentStock;
|
| 92 |
+
const itemHTML = `<li class="p-3 rounded-md bg-yellow-50 border border-yellow-200 text-sm" data-material-name="${item.name}"><div class="flex justify-between items-center"><div><span class="font-semibold text-yellow-800">${item.name}</span><span class="text-xs text-yellow-600 block">Stock: ${item.currentStock} / ${item.reorderPoint} (Need ${needed})</span></div><button class="restock-btn-reorder text-xs bg-yellow-400 text-yellow-900 font-bold py-1 px-3 rounded hover:bg-yellow-500">Restock</button></div><div class="restock-form mt-2 hidden"></div></li>`;
|
| 93 |
+
list.insertAdjacentHTML('beforeend', itemHTML);
|
| 94 |
+
});
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
function renderDailyProductionSummary() {
|
| 98 |
+
const container = document.getElementById('daily-production-summary');
|
| 99 |
+
container.innerHTML = '';
|
| 100 |
+
if (appState.productionLog.length === 0) { container.innerHTML = `<p class="text-gray-500 text-center pt-4">No production recorded yet.</p>`; return; }
|
| 101 |
+
const dailySummary = {};
|
| 102 |
+
appState.productionLog.forEach(entry => {
|
| 103 |
+
const date = new Date(entry.date).toLocaleDateString();
|
| 104 |
+
if (!dailySummary[date]) dailySummary[date] = {};
|
| 105 |
+
if (!dailySummary[date][entry.productName]) dailySummary[date][entry.productName] = 0;
|
| 106 |
+
dailySummary[date][entry.productName] += entry.quantity;
|
| 107 |
+
});
|
| 108 |
+
const sortedDates = Object.keys(dailySummary).sort((a, b) => new Date(b) - new Date(a));
|
| 109 |
+
sortedDates.forEach(date => {
|
| 110 |
+
let dailyEntryHTML = `<div class="p-3 rounded-md bg-blue-50 border border-blue-200 text-sm">`;
|
| 111 |
+
dailyEntryHTML += `<h5 class="font-semibold text-blue-800 mb-1">${date}</h5>`;
|
| 112 |
+
for (const productName in dailySummary[date]) { dailyEntryHTML += `<p class="text-gray-700 ml-2">${productName}: <span class="font-bold">${dailySummary[date][productName]} units</span></p>`; }
|
| 113 |
+
dailyEntryHTML += `</div>`;
|
| 114 |
+
container.insertAdjacentHTML('beforeend', dailyEntryHTML);
|
| 115 |
+
});
|
| 116 |
+
}
|