inventrack 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +25 -0
  2. package/api/index.js +13 -0
  3. package/backend/README.md +35 -0
  4. package/backend/data/db.json +1239 -0
  5. package/backend/package-lock.json +532 -0
  6. package/backend/package.json +8 -0
  7. package/frontend/README.md +22 -0
  8. package/frontend/assets/Icon.png +0 -0
  9. package/frontend/assets/IconSort.png +0 -0
  10. package/frontend/assets/activity-1.png +0 -0
  11. package/frontend/assets/activity-2.png +0 -0
  12. package/frontend/assets/activity-3.png +0 -0
  13. package/frontend/assets/activity-4.png +0 -0
  14. package/frontend/assets/card-icon-1.png +0 -0
  15. package/frontend/assets/card-icon-2.png +0 -0
  16. package/frontend/assets/card-icon-3.png +0 -0
  17. package/frontend/assets/card-icon-4.png +0 -0
  18. package/frontend/assets/login.png +0 -0
  19. package/frontend/assets/logo.png +0 -0
  20. package/frontend/categories.html +143 -0
  21. package/frontend/css/all.min.css +9 -0
  22. package/frontend/css/bootstrap.min.css +6 -0
  23. package/frontend/css/categories.css +359 -0
  24. package/frontend/css/dashboard.css +373 -0
  25. package/frontend/css/inventoryInsights.css +308 -0
  26. package/frontend/css/inventoryOverview.css +353 -0
  27. package/frontend/css/orders.css +632 -0
  28. package/frontend/css/products.css +364 -0
  29. package/frontend/css/signin.css +120 -0
  30. package/frontend/css/style.css +282 -0
  31. package/frontend/css/suppliers.css +136 -0
  32. package/frontend/dashboard.html +160 -0
  33. package/frontend/index.html +124 -0
  34. package/frontend/inventoryInsights.html +182 -0
  35. package/frontend/inventoryOverview.html +187 -0
  36. package/frontend/js/api.js +55 -0
  37. package/frontend/js/auth.js +70 -0
  38. package/frontend/js/bootstrap.bundle.min.js +7 -0
  39. package/frontend/js/categories.js +356 -0
  40. package/frontend/js/dashboard.js +341 -0
  41. package/frontend/js/inventoryInsights.js +396 -0
  42. package/frontend/js/inventoryOverview.js +503 -0
  43. package/frontend/js/orders.js +662 -0
  44. package/frontend/js/products.js +650 -0
  45. package/frontend/js/suppliers.js +535 -0
  46. package/frontend/js/utils.js +234 -0
  47. package/frontend/orders.html +216 -0
  48. package/frontend/products.html +152 -0
  49. package/frontend/suppliers.html +175 -0
  50. package/frontend/webfonts/fa-brands-400.woff2 +0 -0
  51. package/frontend/webfonts/fa-regular-400.woff2 +0 -0
  52. package/frontend/webfonts/fa-solid-900.woff2 +0 -0
  53. package/frontend/webfonts/fa-v4compatibility.woff2 +0 -0
  54. package/package.json +38 -0
  55. package/vercel.json +18 -0
@@ -0,0 +1,396 @@
1
+ renderNavbar("InventoryInsights");
2
+ renderFooter();
3
+
4
+ const state = {
5
+ inventorySort: "value",
6
+ lowStockExpanded: false,
7
+ products: [],
8
+ categories: [],
9
+ suppliers: []
10
+ };
11
+
12
+ const elements = {
13
+ lowStockCount: document.getElementById("lowStockCount"),
14
+ lowStockTrend: document.getElementById("lowStockTrend"),
15
+ outOfStockCount: document.getElementById("outOfStockCount"),
16
+ outOfStockTrend: document.getElementById("outOfStockTrend"),
17
+ inventoryValueTotal: document.getElementById("inventoryValueTotal"),
18
+ inventoryValueTrend: document.getElementById("inventoryValueTrend"),
19
+ totalProductsCount: document.getElementById("totalProductsCount"),
20
+ totalProductsTrend: document.getElementById("totalProductsTrend"),
21
+ lowStockTableBody: document.getElementById("lowStockTableBody"),
22
+ lowStockMobileList: document.getElementById("lowStockMobileList"),
23
+ inventoryValueTableBody: document.getElementById("inventoryValueTableBody"),
24
+ inventoryValueMobileList: document.getElementById("inventoryValueMobileList"),
25
+ inventoryValueSort: document.getElementById("inventoryValueSort"),
26
+ toggleLowStockAlerts: document.getElementById("toggleLowStockAlerts")
27
+ };
28
+
29
+ function normalizeNumber(value) {
30
+ const number = Number(value);
31
+ return Number.isFinite(number) ? number : 0;
32
+ }
33
+
34
+ function normalizeText(value, fallback = "—") {
35
+ if (value === null || value === undefined) return fallback;
36
+ const text = String(value).trim();
37
+ return text || fallback;
38
+ }
39
+
40
+ function escapeHtml(value) {
41
+ return String(value)
42
+ .replaceAll("&", "&")
43
+ .replaceAll("<", "&lt;")
44
+ .replaceAll(">", "&gt;")
45
+ .replaceAll('"', "&quot;")
46
+ .replaceAll("'", "&#39;");
47
+ }
48
+
49
+ function formatCurrency(value) {
50
+ return new Intl.NumberFormat("en-US", {
51
+ style: "currency",
52
+ currency: "USD",
53
+ minimumFractionDigits: 2,
54
+ maximumFractionDigits: 2
55
+ }).format(normalizeNumber(value));
56
+ }
57
+
58
+ function formatCount(value) {
59
+ return new Intl.NumberFormat("en-US").format(normalizeNumber(value));
60
+ }
61
+
62
+ function formatDate(dateString) {
63
+ if (!dateString) return "—";
64
+ const date = new Date(dateString);
65
+ if (Number.isNaN(date.getTime())) return normalizeText(dateString);
66
+ return date.toLocaleDateString("en-CA");
67
+ }
68
+
69
+ function getMaps() {
70
+ const categoriesMap = new Map(
71
+ state.categories.map((category) => [String(category.id), category.name])
72
+ );
73
+ const suppliersMap = new Map(
74
+ state.suppliers.map((supplier) => [String(supplier.id), supplier.name])
75
+ );
76
+ return { categoriesMap, suppliersMap };
77
+ }
78
+
79
+ function isOutOfStock(product) {
80
+ const quantity = normalizeNumber(product.quantity);
81
+ const status = String(product.status || "").toLowerCase();
82
+ return quantity <= 0 || status === "out_of_stock";
83
+ }
84
+
85
+ function isLowStock(product) {
86
+ if (isOutOfStock(product)) return true;
87
+ const quantity = normalizeNumber(product.quantity);
88
+ const minStock = normalizeNumber(product.minStock);
89
+ const status = String(product.status || "").toLowerCase();
90
+ return status === "low_stock" || (minStock > 0 && quantity <= minStock);
91
+ }
92
+
93
+ function getLowStockSeverity(product) {
94
+ if (isOutOfStock(product)) {
95
+ return {
96
+ label: "OUT OF STOCK",
97
+ badgeClass: "critical",
98
+ qtyClass: "qty-danger",
99
+ priority: 0
100
+ };
101
+ }
102
+
103
+ const quantity = normalizeNumber(product.quantity);
104
+ const minStock = Math.max(normalizeNumber(product.minStock), 1);
105
+ const isCritical = quantity <= Math.max(1, Math.floor(minStock / 2));
106
+
107
+ return isCritical
108
+ ? {
109
+ label: "CRITICAL",
110
+ badgeClass: "critical",
111
+ qtyClass: "qty-danger",
112
+ priority: 1
113
+ }
114
+ : {
115
+ label: "WARNING",
116
+ badgeClass: "warning",
117
+ qtyClass: "qty-warning",
118
+ priority: 2
119
+ };
120
+ }
121
+
122
+ function enrichProducts() {
123
+ const { categoriesMap, suppliersMap } = getMaps();
124
+
125
+ return state.products.map((product) => {
126
+ const quantity = normalizeNumber(product.quantity);
127
+ const price = normalizeNumber(product.price);
128
+ const cost = normalizeNumber(product.cost);
129
+ const minStock = normalizeNumber(product.minStock);
130
+ return {
131
+ ...product,
132
+ quantity,
133
+ price,
134
+ cost,
135
+ minStock,
136
+ categoryName: categoriesMap.get(String(product.categoryId)) || "Unknown Category",
137
+ supplierName: suppliersMap.get(String(product.supplierId)) || "Unknown Supplier",
138
+ totalValue: quantity * price,
139
+ totalCostValue: quantity * cost,
140
+ createdAtFormatted: formatDate(product.createdAt)
141
+ };
142
+ });
143
+ }
144
+
145
+ function getLowStockProducts() {
146
+ return enrichProducts()
147
+ .filter(isLowStock)
148
+ .sort((first, second) => {
149
+ const firstSeverity = getLowStockSeverity(first);
150
+ const secondSeverity = getLowStockSeverity(second);
151
+ if (firstSeverity.priority !== secondSeverity.priority) {
152
+ return firstSeverity.priority - secondSeverity.priority;
153
+ }
154
+ if (first.quantity !== second.quantity) return first.quantity - second.quantity;
155
+ if (first.minStock !== second.minStock) return second.minStock - first.minStock;
156
+ return first.name.localeCompare(second.name);
157
+ });
158
+ }
159
+
160
+ function getSortedInventoryProducts() {
161
+ const products = enrichProducts();
162
+ const sortBy = state.inventorySort;
163
+
164
+ return products.sort((first, second) => {
165
+ if (sortBy === "quantity") {
166
+ if (second.quantity !== first.quantity) return second.quantity - first.quantity;
167
+ return second.totalValue - first.totalValue;
168
+ }
169
+
170
+ if (sortBy === "price") {
171
+ if (second.price !== first.price) return second.price - first.price;
172
+ return second.totalValue - first.totalValue;
173
+ }
174
+
175
+ if (second.totalValue !== first.totalValue) return second.totalValue - first.totalValue;
176
+ return second.quantity - first.quantity;
177
+ });
178
+ }
179
+
180
+ function renderInsightCards() {
181
+ const products = enrichProducts();
182
+ const lowStockProducts = products.filter(isLowStock);
183
+ const outOfStockProducts = products.filter(isOutOfStock);
184
+ const criticalLowStockCount = lowStockProducts.filter((product) => getLowStockSeverity(product).priority <= 1).length;
185
+ const warningLowStockCount = Math.max(lowStockProducts.length - criticalLowStockCount, 0);
186
+ const totalInventoryValue = products.reduce((sum, product) => sum + product.totalValue, 0);
187
+ const averageInventoryValue = products.length ? totalInventoryValue / products.length : 0;
188
+
189
+ elements.lowStockCount.textContent = formatCount(lowStockProducts.length);
190
+ elements.lowStockTrend.textContent = lowStockProducts.length
191
+ ? `${formatCount(criticalLowStockCount)} critical • ${formatCount(warningLowStockCount)} warning`
192
+ : "Everything is above reorder level";
193
+ elements.lowStockTrend.className = `insight-trend ${lowStockProducts.length ? "negative" : "positive"}`;
194
+
195
+ elements.outOfStockCount.textContent = formatCount(outOfStockProducts.length);
196
+ elements.outOfStockTrend.textContent = outOfStockProducts.length
197
+ ? `${formatCount(outOfStockProducts.length)} unavailable right now`
198
+ : "All tracked products are available";
199
+ elements.outOfStockTrend.className = `insight-trend ${outOfStockProducts.length ? "negative" : "positive"}`;
200
+
201
+ elements.inventoryValueTotal.textContent = formatCurrency(totalInventoryValue);
202
+ elements.inventoryValueTrend.textContent = `${formatCurrency(averageInventoryValue)} average value per product`;
203
+ elements.inventoryValueTrend.className = "insight-trend positive";
204
+
205
+ elements.totalProductsCount.textContent = formatCount(products.length);
206
+ elements.totalProductsTrend.textContent = `${formatCount(state.categories.length)} categories • ${formatCount(state.suppliers.length)} suppliers`;
207
+ elements.totalProductsTrend.className = "insight-trend purple-text";
208
+ }
209
+
210
+ function renderLowStockReport() {
211
+ const lowStockProducts = getLowStockProducts();
212
+ const visibleProducts = state.lowStockExpanded ? lowStockProducts : lowStockProducts.slice(0, 5);
213
+
214
+ if (!lowStockProducts.length) {
215
+ elements.lowStockTableBody.innerHTML = `
216
+ <tr>
217
+ <td colspan="7" class="text-center py-4">No low stock alerts found.</td>
218
+ </tr>
219
+ `;
220
+
221
+ elements.lowStockMobileList.innerHTML = `
222
+ <div class="report-mobile-item">
223
+ <p class="mb-0">No low stock alerts found.</p>
224
+ </div>
225
+ `;
226
+
227
+ elements.toggleLowStockAlerts.textContent = "No Alerts";
228
+ elements.toggleLowStockAlerts.classList.add("disabled");
229
+ elements.toggleLowStockAlerts.setAttribute("aria-disabled", "true");
230
+ return;
231
+ }
232
+
233
+ elements.toggleLowStockAlerts.classList.remove("disabled");
234
+ elements.toggleLowStockAlerts.removeAttribute("aria-disabled");
235
+ elements.toggleLowStockAlerts.textContent =
236
+ lowStockProducts.length <= 5
237
+ ? `${formatCount(lowStockProducts.length)} alert${lowStockProducts.length > 1 ? "s" : ""}`
238
+ : state.lowStockExpanded
239
+ ? "Show Top Alerts"
240
+ : `View All Alerts (${formatCount(lowStockProducts.length)})`;
241
+
242
+ elements.lowStockTableBody.innerHTML = visibleProducts
243
+ .map((product) => {
244
+ const severity = getLowStockSeverity(product);
245
+ return `
246
+ <tr>
247
+ <td class="fw-semibold">${escapeHtml(normalizeText(product.name))}</td>
248
+ <td>${escapeHtml(normalizeText(product.sku))}</td>
249
+ <td>${escapeHtml(normalizeText(product.categoryName))}</td>
250
+ <td>${escapeHtml(normalizeText(product.supplierName))}</td>
251
+ <td class="${severity.qtyClass}">${formatCount(product.quantity)}</td>
252
+ <td>${formatCount(product.minStock)}</td>
253
+ <td>
254
+ <span class="table-badge ${severity.badgeClass}">${severity.label}</span>
255
+ </td>
256
+ </tr>
257
+ `;
258
+ })
259
+ .join("");
260
+
261
+ elements.lowStockMobileList.innerHTML = visibleProducts
262
+ .map((product) => {
263
+ const severity = getLowStockSeverity(product);
264
+ return `
265
+ <div class="report-mobile-item">
266
+ <h6>${escapeHtml(normalizeText(product.name))}</h6>
267
+ <p><strong>SKU:</strong> ${escapeHtml(normalizeText(product.sku))}</p>
268
+ <p><strong>Category:</strong> ${escapeHtml(normalizeText(product.categoryName))}</p>
269
+ <p><strong>Supplier:</strong> ${escapeHtml(normalizeText(product.supplierName))}</p>
270
+ <p><strong>Quantity:</strong> <span class="${severity.qtyClass}">${formatCount(product.quantity)}</span></p>
271
+ <p><strong>Reorder Level:</strong> ${formatCount(product.minStock)}</p>
272
+ <span class="table-badge ${severity.badgeClass}">${severity.label}</span>
273
+ </div>
274
+ `;
275
+ })
276
+ .join("");
277
+ }
278
+
279
+ function renderInventoryValueReport() {
280
+ const products = getSortedInventoryProducts();
281
+
282
+ if (!products.length) {
283
+ elements.inventoryValueTableBody.innerHTML = `
284
+ <tr>
285
+ <td colspan="6" class="text-center py-4">No products available.</td>
286
+ </tr>
287
+ `;
288
+
289
+ elements.inventoryValueMobileList.innerHTML = `
290
+ <div class="report-mobile-item">
291
+ <p class="mb-0">No products available.</p>
292
+ </div>
293
+ `;
294
+ return;
295
+ }
296
+
297
+ elements.inventoryValueTableBody.innerHTML = products
298
+ .map((product) => `
299
+ <tr>
300
+ <td class="fw-semibold">${escapeHtml(normalizeText(product.name))}</td>
301
+ <td>${escapeHtml(normalizeText(product.sku))}</td>
302
+ <td>${escapeHtml(normalizeText(product.categoryName))}</td>
303
+ <td>${formatCurrency(product.price)}</td>
304
+ <td>${formatCount(product.quantity)}</td>
305
+ <td class="fw-bold">${formatCurrency(product.totalValue)}</td>
306
+ </tr>
307
+ `)
308
+ .join("");
309
+
310
+ elements.inventoryValueMobileList.innerHTML = products
311
+ .map((product) => `
312
+ <div class="report-mobile-item">
313
+ <h6>${escapeHtml(normalizeText(product.name))}</h6>
314
+ <p><strong>SKU:</strong> ${escapeHtml(normalizeText(product.sku))}</p>
315
+ <p><strong>Category:</strong> ${escapeHtml(normalizeText(product.categoryName))}</p>
316
+ <p><strong>Price:</strong> ${formatCurrency(product.price)}</p>
317
+ <p><strong>Quantity:</strong> ${formatCount(product.quantity)}</p>
318
+ <p><strong>Total Value:</strong> <span class="fw-bold">${formatCurrency(product.totalValue)}</span></p>
319
+ </div>
320
+ `)
321
+ .join("");
322
+ }
323
+
324
+ function renderErrorState(message) {
325
+ const fallbackMessage = normalizeText(message, "Unable to load inventory data.");
326
+
327
+ elements.lowStockCount.textContent = "—";
328
+ elements.outOfStockCount.textContent = "—";
329
+ elements.inventoryValueTotal.textContent = "—";
330
+ elements.totalProductsCount.textContent = "—";
331
+ elements.lowStockTrend.textContent = fallbackMessage;
332
+ elements.outOfStockTrend.textContent = fallbackMessage;
333
+ elements.inventoryValueTrend.textContent = fallbackMessage;
334
+ elements.totalProductsTrend.textContent = fallbackMessage;
335
+
336
+ elements.lowStockTableBody.innerHTML = `
337
+ <tr>
338
+ <td colspan="7" class="text-center py-4">${escapeHtml(fallbackMessage)}</td>
339
+ </tr>
340
+ `;
341
+ elements.inventoryValueTableBody.innerHTML = `
342
+ <tr>
343
+ <td colspan="6" class="text-center py-4">${escapeHtml(fallbackMessage)}</td>
344
+ </tr>
345
+ `;
346
+ elements.lowStockMobileList.innerHTML = `
347
+ <div class="report-mobile-item">
348
+ <p class="mb-0">${escapeHtml(fallbackMessage)}</p>
349
+ </div>
350
+ `;
351
+ elements.inventoryValueMobileList.innerHTML = `
352
+ <div class="report-mobile-item">
353
+ <p class="mb-0">${escapeHtml(fallbackMessage)}</p>
354
+ </div>
355
+ `;
356
+ elements.toggleLowStockAlerts.textContent = "Unable to Load Alerts";
357
+ elements.toggleLowStockAlerts.classList.add("disabled");
358
+ }
359
+
360
+ function attachEventListeners() {
361
+ elements.inventoryValueSort.addEventListener("change", (event) => {
362
+ state.inventorySort = event.target.value;
363
+ renderInventoryValueReport();
364
+ });
365
+
366
+ elements.toggleLowStockAlerts.addEventListener("click", (event) => {
367
+ event.preventDefault();
368
+ if (!getLowStockProducts().length || getLowStockProducts().length <= 5) return;
369
+ state.lowStockExpanded = !state.lowStockExpanded;
370
+ renderLowStockReport();
371
+ });
372
+ }
373
+
374
+ async function loadInventoryInsights() {
375
+ try {
376
+ const [productsResponse, categoriesResponse, suppliersResponse] = await Promise.all([
377
+ getData("products"),
378
+ getData("categories"),
379
+ getData("suppliers")
380
+ ]);
381
+
382
+ state.products = Array.isArray(productsResponse.data) ? productsResponse.data : [];
383
+ state.categories = Array.isArray(categoriesResponse.data) ? categoriesResponse.data : [];
384
+ state.suppliers = Array.isArray(suppliersResponse.data) ? suppliersResponse.data : [];
385
+
386
+ renderInsightCards();
387
+ renderLowStockReport();
388
+ renderInventoryValueReport();
389
+ } catch (error) {
390
+ console.error("Failed to load inventory insights:", error);
391
+ renderErrorState("Unable to connect to the database. Make sure json-server is running on port 3000.");
392
+ }
393
+ }
394
+
395
+ attachEventListeners();
396
+ loadInventoryInsights();