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,650 @@
1
+ renderNavbar("Products");
2
+ renderFooter();
3
+
4
+ // ===============================
5
+ // State
6
+ // ===============================
7
+ const state = {
8
+ page: 1,
9
+ limit: 5,
10
+ totalCount: 0,
11
+ editingId: null
12
+ };
13
+
14
+ // ===============================
15
+ // Selectors
16
+ // ===============================
17
+ const tableBody = document.getElementById("tableBody");
18
+ const paginationContainer = document.getElementById("pagination");
19
+ const searchByProductName = document.getElementById("searchByProductName");
20
+ const formSelect = document.getElementById("formSelect");
21
+
22
+ const addProductBtn = document.getElementById("addProductBtn");
23
+ const exportBtn = document.getElementById("exportBtn");
24
+
25
+ const modal = document.getElementById("modal");
26
+ const modalOverlay = document.getElementById("modalOverlay");
27
+ const modalType = document.getElementById("modalType");
28
+
29
+ const productForm = document.getElementById("productData");
30
+ const productName = document.getElementById("productName");
31
+ const SKU = document.getElementById("productSKU");
32
+ const productPrice = document.getElementById("productPrice");
33
+ const categorySelect = document.getElementById("category");
34
+ const supplierSelect = document.getElementById("supplier");
35
+ const initialQty = document.getElementById("initialQty");
36
+ const reorderLevel = document.getElementById("reorderLevel");
37
+ const cancelBtn = document.getElementById("cancelBtn");
38
+ const saveProduct = document.getElementById("saveProduct");
39
+ const sortBtn = document.querySelector('.sorting');
40
+
41
+
42
+ // ===============================
43
+ // Helpers
44
+ // ===============================
45
+ function getCurrentDate() {
46
+ const today = new Date();
47
+ return today.toISOString().split("T")[0];
48
+ }
49
+
50
+ function escapeHTML(value) {
51
+ return String(value)
52
+ .replaceAll("&", "&")
53
+ .replaceAll("<", "&lt;")
54
+ .replaceAll(">", "&gt;")
55
+ .replaceAll('"', "&quot;")
56
+ .replaceAll("'", "&#39;");
57
+ }
58
+
59
+ function resetForm() {
60
+ productForm.reset();
61
+ state.editingId = null;
62
+ modalType.textContent = "Add";
63
+
64
+ [
65
+ productName,
66
+ SKU,
67
+ productPrice,
68
+ categorySelect,
69
+ supplierSelect,
70
+ initialQty,
71
+ reorderLevel
72
+ ].forEach(function (field) {
73
+ field.setCustomValidity("");
74
+ field.style.border = "";
75
+ });
76
+ }
77
+
78
+ function calculateStatus(quantity, minStock) {
79
+ if (quantity === 0) return "out_of_stock";
80
+ if (quantity <= minStock) return "low_stock";
81
+ return "in_stock";
82
+ }
83
+
84
+ function getStockClass(status) {
85
+ const normalized = String(status || "").toLowerCase();
86
+
87
+ if (normalized === "out_of_stock") return "stock-critical";
88
+ if (normalized === "low_stock") return "stock-warning";
89
+ return "stock-normal";
90
+ }
91
+
92
+ function getStatusText(status) {
93
+ const normalized = String(status || "").toLowerCase();
94
+
95
+ if (normalized === "in_stock") return "In Stock";
96
+ if (normalized === "low_stock") return "Low Stock";
97
+ if (normalized === "out_of_stock") return "Out of Stock";
98
+ return "Unknown";
99
+ }
100
+
101
+ function getProductIcon(category) {
102
+ const cat = String(category || "").toLowerCase();
103
+
104
+ if (cat.includes("accessories")) return "fa-headphones";
105
+ if (cat.includes("displays")) return "fa-desktop";
106
+ if (cat.includes("computers")) return "fa-laptop";
107
+ if (cat.includes("furniture")) return "fa-chair";
108
+ if (cat.includes("office")) return "fa-print";
109
+ if (cat.includes("network")) return "fa-network-wired";
110
+ if (cat.includes("storage")) return "fa-hard-drive";
111
+ if (cat.includes("mobile")) return "fa-mobile-screen";
112
+ if (cat.includes("camera")) return "fa-camera";
113
+ if (cat.includes("power")) return "fa-plug";
114
+
115
+ return "fa-box";
116
+ }
117
+
118
+ function filterProductsByStatus(products, filterValue) {
119
+ if (!filterValue) return products;
120
+
121
+ return products.filter(function (product) {
122
+ return String(product.status || "").toLowerCase() === filterValue.toLowerCase();
123
+ });
124
+ }
125
+
126
+ // ===============================
127
+ // Maps
128
+ // ===============================
129
+ async function getCategoriesMap() {
130
+ const response = await getData("categories");
131
+ const categories = response.data;
132
+
133
+ const map = {};
134
+ categories.forEach(function (cat) {
135
+ map[String(cat.id)] = cat.name;
136
+ });
137
+
138
+ return map;
139
+ }
140
+
141
+ async function getSuppliersMap() {
142
+ const response = await getData("suppliers");
143
+ const suppliers = response.data;
144
+
145
+ const map = {};
146
+ suppliers.forEach(function (supp) {
147
+ map[String(supp.id)] = supp.name;
148
+ });
149
+
150
+ return map;
151
+ }
152
+
153
+ // ===============================
154
+ // Select rendering
155
+ // ===============================
156
+ async function renderCategoriesSelect() {
157
+ const categoriesMap = await getCategoriesMap();
158
+
159
+ let html = `<option value="" selected>Select</option>`;
160
+ for (const [id, name] of Object.entries(categoriesMap)) {
161
+ html += `<option value="${escapeHTML(id)}">${escapeHTML(name)}</option>`;
162
+ }
163
+
164
+ categorySelect.innerHTML = html;
165
+ }
166
+
167
+ async function renderSuppliersSelect() {
168
+ const suppliersMap = await getSuppliersMap();
169
+
170
+ let html = `<option value="" selected>Select</option>`;
171
+ for (const [id, name] of Object.entries(suppliersMap)) {
172
+ html += `<option value="${escapeHTML(id)}">${escapeHTML(name)}</option>`;
173
+ }
174
+
175
+ supplierSelect.innerHTML = html;
176
+ }
177
+
178
+ // ===============================
179
+ // Validation
180
+ // ===============================
181
+ function getProductFormData() {
182
+ validateInputs(
183
+ /^[A-Za-z0-9\s-]{3,100}$/,
184
+ productName,
185
+ "Product name must be at least 3 characters"
186
+ );
187
+
188
+ validateInputs(
189
+ /^[A-Za-z0-9-]{3,}$/,
190
+ SKU,
191
+ "SKU must contain letters, numbers or -"
192
+ );
193
+
194
+ validateInputs(
195
+ /^\d+(\.\d{1,2})?$/,
196
+ productPrice,
197
+ "Enter a valid price"
198
+ );
199
+
200
+ validateInputs(
201
+ /^\d+$/,
202
+ initialQty,
203
+ "Quantity must be a valid number"
204
+ );
205
+
206
+ validateInputs(
207
+ /^\d+$/,
208
+ reorderLevel,
209
+ "Reorder level must be a valid number"
210
+ );
211
+
212
+ validateSelect(categorySelect);
213
+ validateSelect(supplierSelect);
214
+
215
+ const allValid =
216
+ productName.checkValidity() &&
217
+ SKU.checkValidity() &&
218
+ productPrice.checkValidity() &&
219
+ initialQty.checkValidity() &&
220
+ reorderLevel.checkValidity() &&
221
+ categorySelect.checkValidity() &&
222
+ supplierSelect.checkValidity();
223
+
224
+ if (!allValid) return null;
225
+
226
+ const quantity = Number(initialQty.value);
227
+ const minStock = Number(reorderLevel.value);
228
+
229
+ return {
230
+ name: productName.value.trim(),
231
+ sku: SKU.value.trim(),
232
+ categoryId: String(categorySelect.value),
233
+ supplierId: String(supplierSelect.value),
234
+ price: Number(productPrice.value),
235
+ quantity: quantity,
236
+ minStock: minStock,
237
+ status: calculateStatus(quantity, minStock),
238
+ createdAt: getCurrentDate()
239
+ };
240
+ }
241
+
242
+ // ^ Build Product stock movement
243
+
244
+ function buildAddProductStockMovement(createdProduct) {
245
+ const rawUserName = localStorage.getItem("userName");
246
+ const actor = rawUserName ? JSON.parse(rawUserName) : "Admin";
247
+
248
+ return {
249
+ productId: String(createdProduct.id),
250
+ productName: createdProduct.name,
251
+ type: "PRODUCT_ADDED",
252
+ action: "IN",
253
+ quantity: Number(createdProduct.quantity),
254
+ previousQuantity: 0,
255
+ newQuantity: Number(createdProduct.quantity),
256
+ title: `${createdProduct.name} added to inventory`,
257
+ reason: "Initial stock",
258
+ createdAt: new Date().toISOString(),
259
+ createdBy: actor
260
+ };
261
+ }
262
+
263
+ // ===============================
264
+ // Form fill for edit
265
+ // ===============================
266
+ function fillFormWithProduct(product) {
267
+ productName.value = product.name || "";
268
+ SKU.value = product.sku || "";
269
+ productPrice.value = product.price ?? "";
270
+ categorySelect.value = String(product.categoryId ?? "");
271
+ supplierSelect.value = String(product.supplierId ?? "");
272
+ initialQty.value = product.quantity ?? "";
273
+ reorderLevel.value = product.minStock ?? "";
274
+ }
275
+
276
+ // ^ Sort functionality (sort / return original order)
277
+ let isSortedByName = false;
278
+ let originalProductsOrder = [];
279
+
280
+ sortBtn.addEventListener("click", async function () {
281
+ const productsResponse = await getData("products");
282
+ let products = productsResponse.data;
283
+
284
+ const searchValue = searchByProductName.value.trim().toLowerCase();
285
+ const filterValue = formSelect.value;
286
+
287
+ if (searchValue) {
288
+ products = products.filter(function (product) {
289
+ return String(product.name || "")
290
+ .toLowerCase()
291
+ .includes(searchValue);
292
+ });
293
+ }
294
+
295
+ products = filterProductsByStatus(products, filterValue);
296
+
297
+ if (!isSortedByName) {
298
+ originalProductsOrder = [...products];
299
+
300
+ products.sort(function (a, b) {
301
+ return String(a.name || "").localeCompare(String(b.name || ""));
302
+ });
303
+
304
+ isSortedByName = true;
305
+ } else {
306
+ products = [...originalProductsOrder];
307
+ isSortedByName = false;
308
+ }
309
+
310
+ state.page = 1;
311
+ state.totalCount = products.length;
312
+
313
+ const categoryMap = await getCategoriesMap();
314
+
315
+ const start = (state.page - 1) * state.limit;
316
+ const end = start + state.limit;
317
+ const paginatedProducts = products.slice(start, end);
318
+
319
+ if (!paginatedProducts.length) {
320
+ tableBody.innerHTML = `
321
+ <tr>
322
+ <td colspan="7" style="text-align:center;">No products found</td>
323
+ </tr>
324
+ `;
325
+ renderPagination(paginationContainer, state, renderProducts);
326
+ return;
327
+ }
328
+
329
+ tableBody.innerHTML = paginatedProducts
330
+ .map(function (product) {
331
+ const categoryName = categoryMap[String(product.categoryId)] || "Unknown";
332
+ const stockClass = getStockClass(product.status);
333
+ const statusText = getStatusText(product.status);
334
+ const iconClass = getProductIcon(categoryName);
335
+
336
+ return `
337
+ <tr>
338
+ <td>
339
+ <div class="product-cell">
340
+ <div class="product-icon">
341
+ <i class="fa-solid ${iconClass}"></i>
342
+ </div>
343
+ <div class="product-info">
344
+ <h6>${escapeHTML(product.name)}</h6>
345
+ </div>
346
+ </div>
347
+ </td>
348
+ <td>${escapeHTML(categoryName)}</td>
349
+ <td>$${Number(product.price).toLocaleString()}</td>
350
+ <td class="${stockClass}">${product.quantity}</td>
351
+ <td class="status ${stockClass}">
352
+ <i class="fa-solid fa-circle"></i> ${statusText}
353
+ </td>
354
+ <td>
355
+ <button class="editProductBtn" data-id="${product.id}">Edit</button>
356
+ </td>
357
+ <td>
358
+ <button class="removeProductBtn" data-id="${product.id}">Remove</button>
359
+ </td>
360
+ </tr>
361
+ `;
362
+ })
363
+ .join("");
364
+
365
+ renderPagination(paginationContainer, state, renderProducts);
366
+ });
367
+
368
+ //^ Render products
369
+
370
+ async function renderProducts() {
371
+ try {
372
+ const productsResponse = await getData("products");
373
+ let products = productsResponse.data;
374
+
375
+ const searchValue = searchByProductName.value.trim().toLowerCase();
376
+ const filterValue = formSelect.value;
377
+
378
+ if (searchValue) {
379
+ products = products.filter(function (product) {
380
+ return String(product.name || "")
381
+ .toLowerCase()
382
+ .includes(searchValue);
383
+ });
384
+ }
385
+
386
+ products = filterProductsByStatus(products, filterValue);
387
+
388
+ state.totalCount = products.length;
389
+
390
+ const categoryMap = await getCategoriesMap();
391
+
392
+ const start = (state.page - 1) * state.limit;
393
+ const end = start + state.limit;
394
+ const paginatedProducts = products.slice(start, end);
395
+
396
+ if (!paginatedProducts.length) {
397
+ tableBody.innerHTML = `
398
+ <tr>
399
+ <td colspan="7" style="text-align:center;">No products found</td>
400
+ </tr>
401
+ `;
402
+ renderPagination(paginationContainer, state, renderProducts);
403
+ return;
404
+ }
405
+
406
+ tableBody.innerHTML = paginatedProducts
407
+ .map(function (product) {
408
+ const categoryName = categoryMap[String(product.categoryId)] || "Unknown";
409
+ const stockClass = getStockClass(product.status);
410
+ const statusText = getStatusText(product.status);
411
+ const iconClass = getProductIcon(categoryName);
412
+
413
+ return `
414
+ <tr>
415
+ <td>
416
+ <div class="product-cell">
417
+ <div class="product-icon">
418
+ <i class="fa-solid ${iconClass}"></i>
419
+ </div>
420
+ <div class="product-info">
421
+ <h6>${escapeHTML(product.name)}</h6>
422
+ </div>
423
+ </div>
424
+ </td>
425
+ <td>${escapeHTML(categoryName)}</td>
426
+ <td>$${Number(product.price).toLocaleString()}</td>
427
+ <td class="${stockClass}">${product.quantity}</td>
428
+ <td class="status ${stockClass}">
429
+ <i class="fa-solid fa-circle"></i> ${statusText}
430
+ </td>
431
+ <td>
432
+ <button class="editProductBtn" data-id="${product.id}">Edit</button>
433
+ </td>
434
+ <td>
435
+ <button class="removeProductBtn" data-id="${product.id}">Remove</button>
436
+ </td>
437
+ </tr>
438
+ `;
439
+ })
440
+ .join("");
441
+
442
+ renderPagination(paginationContainer, state, renderProducts);
443
+ } catch (error) {
444
+ console.error(error);
445
+ tableBody.innerHTML = `
446
+ <tr>
447
+ <td colspan="7" style="text-align:center; color:red;">Failed to load products</td>
448
+ </tr>
449
+ `;
450
+ }
451
+ }
452
+
453
+ // ===============================
454
+ // Add / Update
455
+ // ===============================
456
+ saveProduct.addEventListener("click", async function (e) {
457
+ e.preventDefault();
458
+
459
+ const productObject = getProductFormData();
460
+ if (!productObject) return;
461
+
462
+ try {
463
+ if (state.editingId) {
464
+ const oldProductResponse = await getData(`products/${state.editingId}`);
465
+ const oldProduct = oldProductResponse.data;
466
+
467
+ const updatedProduct = {
468
+ ...oldProduct,
469
+ ...productObject
470
+ };
471
+
472
+ await putData("products", state.editingId, updatedProduct);
473
+ } else {
474
+ const createdProduct = await postData("products", productObject);
475
+
476
+ const stockMovementObject = buildAddProductStockMovement(createdProduct);
477
+ await postData("stockMovements", stockMovementObject);
478
+ }
479
+
480
+ closeModal();
481
+ resetForm();
482
+ state.page = 1;
483
+ await renderProducts();
484
+ } catch (error) {
485
+ console.error(error);
486
+ alert("Failed to save product");
487
+ }
488
+ });
489
+
490
+ // ===============================
491
+ // Event delegation
492
+ // ===============================
493
+ document.addEventListener("click", async function (e) {
494
+ const editBtn = e.target.closest(".editProductBtn");
495
+ if (editBtn) {
496
+ try {
497
+ const id = editBtn.dataset.id;
498
+ const response = await getData(`products/${id}`);
499
+ const product = response.data;
500
+
501
+ state.editingId = id;
502
+ modalType.textContent = "Edit";
503
+ fillFormWithProduct(product);
504
+ showModal();
505
+ } catch (error) {
506
+ console.error(error);
507
+ alert("Failed to load product data");
508
+ }
509
+ return;
510
+ }
511
+
512
+ const removeBtn = e.target.closest(".removeProductBtn");
513
+ if (removeBtn) {
514
+ const id = removeBtn.dataset.id;
515
+ const confirmed = confirm("Are you sure you want to remove this product?");
516
+ if (!confirmed) return;
517
+
518
+ try {
519
+ await deleteData("products", id);
520
+
521
+ const possiblePages = Math.ceil((state.totalCount - 1) / state.limit);
522
+ if (state.page > possiblePages && state.page > 1) {
523
+ state.page--;
524
+ }
525
+
526
+ await renderProducts();
527
+ } catch (error) {
528
+ console.error(error);
529
+ alert("Failed to remove product");
530
+ }
531
+ }
532
+ });
533
+
534
+ // ===============================
535
+ // Modal actions
536
+ // ===============================
537
+ addProductBtn.addEventListener("click", function () {
538
+ resetForm();
539
+ modalType.textContent = "Add";
540
+ showModal();
541
+ });
542
+
543
+ cancelBtn.addEventListener("click", function (e) {
544
+ e.preventDefault();
545
+ closeModal();
546
+ resetForm();
547
+ });
548
+
549
+ modalOverlay.addEventListener("click", function () {
550
+ closeModal();
551
+ resetForm();
552
+ });
553
+
554
+ document.addEventListener("keydown", function (e) {
555
+ if (e.key === "Escape" && !modal.classList.contains("hidden")) {
556
+ closeModal();
557
+ resetForm();
558
+ }
559
+ });
560
+
561
+ // ===============================
562
+ // Search / filter
563
+ // ===============================
564
+ searchByProductName.addEventListener("input", function () {
565
+ state.page = 1;
566
+ renderProducts();
567
+ });
568
+
569
+ formSelect.addEventListener("change", function () {
570
+ state.page = 1;
571
+ renderProducts();
572
+ });
573
+
574
+
575
+ // ^ Export button
576
+
577
+ exportBtn.addEventListener("click", async function () {
578
+ try {
579
+ let products = (await getData("products")).data;
580
+ const categoryMap = await getCategoriesMap();
581
+ const suppliersMap = await getSuppliersMap();
582
+
583
+ const searchValue = searchByProductName.value.trim().toLowerCase();
584
+ const filterValue = formSelect.value;
585
+
586
+ if (searchValue) {
587
+ products = products.filter(function (product) {
588
+ return String(product.name || "")
589
+ .toLowerCase()
590
+ .includes(searchValue);
591
+ });
592
+ }
593
+
594
+ products = filterProductsByStatus(products, filterValue);
595
+
596
+ const csvRows = [
597
+ [
598
+ "ID",
599
+ "Name",
600
+ "SKU",
601
+ "Category",
602
+ "Supplier",
603
+ "Price",
604
+ "Quantity",
605
+ "Min Stock",
606
+ "Status",
607
+ "Created At"
608
+ ].join(",")
609
+ ];
610
+
611
+ products.forEach(function (product) {
612
+ csvRows.push([
613
+ `"${product.id || ""}"`,
614
+ `"${String(product.name || "").replaceAll('"', '""')}"`,
615
+ `"${String(product.sku || "").replaceAll('"', '""')}"`,
616
+ `"${String(categoryMap[String(product.categoryId)] || "").replaceAll('"', '""')}"`,
617
+ `"${String(suppliersMap[String(product.supplierId)] || "").replaceAll('"', '""')}"`,
618
+ `"${product.price || ""}"`,
619
+ `"${product.quantity || ""}"`,
620
+ `"${product.minStock || ""}"`,
621
+ `"${getStatusText(product.status)}"`,
622
+ `"${product.createdAt || ""}"`
623
+ ].join(","));
624
+ });
625
+
626
+ const blob = new Blob([csvRows.join("\n")], {
627
+ type: "text/csv;charset=utf-8;"
628
+ });
629
+
630
+ const url = URL.createObjectURL(blob);
631
+ const a = document.createElement("a");
632
+ a.href = url;
633
+ a.download = "products-export.csv";
634
+ document.body.appendChild(a);
635
+ a.click();
636
+ a.remove();
637
+ URL.revokeObjectURL(url);
638
+ } catch (error) {
639
+ console.error(error);
640
+ alert("Failed to export products");
641
+ }
642
+ });
643
+
644
+ //^ Initialization
645
+
646
+ document.addEventListener("DOMContentLoaded", async function () {
647
+ await renderCategoriesSelect();
648
+ await renderSuppliersSelect();
649
+ await renderProducts();
650
+ });