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,662 @@
1
+ renderNavbar("Orders");
2
+ renderFooter();
3
+
4
+ const state = {
5
+ page: 1,
6
+ limit: 5,
7
+ totalCount: 0,
8
+ search: "",
9
+ orders: [],
10
+ };
11
+
12
+ const dom = {
13
+ tableBody: document.getElementById("ordersTableBody"),
14
+ mobileContainer: document.getElementById("ordersMobile"),
15
+ searchInput: document.querySelector(".order-search-input"),
16
+ resultsText: document.querySelector(".results-text"),
17
+ pagination: document.querySelector(".pagination-custom"),
18
+ modalElement: document.getElementById("createOrderModal"),
19
+ form: document.getElementById("createOrderForm"),
20
+ supplierSelect: document.getElementById("supplierSelect"),
21
+ orderDate: document.getElementById("orderDate"),
22
+ orderItemsBody: document.getElementById("orderItemsBody"),
23
+ subtotalValue: document.getElementById("subtotalValue"),
24
+ totalValue: document.getElementById("totalValue"),
25
+ addRowBtn: document.getElementById("addProductRowBtn"),
26
+ saveBtn: document.querySelector(".modal-save-btn"),
27
+ };
28
+
29
+ const modalInstance = window.bootstrap && dom.modalElement
30
+ ? bootstrap.Modal.getOrCreateInstance(dom.modalElement)
31
+ : null;
32
+
33
+ initOrdersPage();
34
+
35
+ function initOrdersPage() {
36
+ prepareSupplierSelect();
37
+ attachEventListeners();
38
+ resetOrderForm();
39
+ loadSuppliers();
40
+ loadOrders();
41
+ }
42
+
43
+ function attachEventListeners() {
44
+ dom.searchInput?.addEventListener("input", handleSearch);
45
+ dom.addRowBtn?.addEventListener("click", addProductRow);
46
+ dom.saveBtn?.addEventListener("click", createOrder);
47
+ dom.form?.addEventListener("submit", function (event) {
48
+ event.preventDefault();
49
+ createOrder();
50
+ });
51
+
52
+ dom.orderItemsBody?.addEventListener("input", function () {
53
+ updateOrderTotals();
54
+ });
55
+
56
+ dom.orderItemsBody?.addEventListener("click", function (event) {
57
+ const removeButton = event.target.closest(".remove-row-btn");
58
+ if (!removeButton) return;
59
+
60
+ const rows = dom.orderItemsBody.querySelectorAll("tr");
61
+ const row = removeButton.closest("tr");
62
+
63
+ if (rows.length === 1) {
64
+ row.outerHTML = createItemRow();
65
+ } else {
66
+ row.remove();
67
+ }
68
+
69
+ updateOrderTotals();
70
+ });
71
+
72
+ dom.tableBody?.addEventListener("click", handleOrderAction);
73
+ dom.mobileContainer?.addEventListener("click", handleOrderAction);
74
+
75
+ dom.modalElement?.addEventListener("hidden.bs.modal", function () {
76
+ resetOrderForm();
77
+ });
78
+ }
79
+
80
+ async function loadSuppliers() {
81
+ try {
82
+ const { data } = await getData("suppliers");
83
+ if (!Array.isArray(data) || !data.length) return;
84
+
85
+ const supplierNames = data
86
+ .map((supplier) => (
87
+ supplier?.name ||
88
+ supplier?.supplierName ||
89
+ supplier?.companyName ||
90
+ supplier?.contactPerson ||
91
+ ""
92
+ ).trim())
93
+ .filter(Boolean);
94
+
95
+ if (!supplierNames.length) return;
96
+
97
+ const uniqueSupplierNames = Array.from(new Set(supplierNames));
98
+ const currentValue = dom.supplierSelect.value;
99
+
100
+ dom.supplierSelect.innerHTML = [
101
+ '<option value="" disabled>Select a supplier</option>',
102
+ ...uniqueSupplierNames.map((name) => `<option value="${escapeHtml(name)}">${escapeHtml(name)}</option>`),
103
+ ].join("");
104
+
105
+ dom.supplierSelect.value = currentValue && uniqueSupplierNames.includes(currentValue)
106
+ ? currentValue
107
+ : "";
108
+ } catch (error) {
109
+ console.error("Unable to load suppliers:", error);
110
+ }
111
+ }
112
+
113
+ function prepareSupplierSelect() {
114
+ if (!dom.supplierSelect) return;
115
+
116
+ const firstOption = dom.supplierSelect.options[0];
117
+ if (firstOption) {
118
+ firstOption.value = "";
119
+ firstOption.disabled = true;
120
+ }
121
+
122
+ dom.supplierSelect.value = "";
123
+ }
124
+
125
+ async function loadOrders() {
126
+ try {
127
+ const { data } = await getData("orders");
128
+ state.orders = normalizeOrders(Array.isArray(data) ? data : []);
129
+ renderPage();
130
+ } catch (error) {
131
+ console.error("Unable to load orders:", error);
132
+ state.orders = [];
133
+ renderPage();
134
+ }
135
+ }
136
+
137
+ function normalizeOrders(orders) {
138
+ return orders
139
+ .map((order) => {
140
+ const items = Array.isArray(order.items)
141
+ ? order.items.map((item) => ({
142
+ product: item.product || item.name || "",
143
+ qty: Number(item.qty ?? item.quantity ?? 0),
144
+ cost: Number(item.cost ?? item.price ?? 0),
145
+ }))
146
+ : [];
147
+
148
+ const calculatedQuantity = items.reduce((sum, item) => sum + (Number(item.qty) || 0), 0);
149
+ const calculatedTotal = items.reduce((sum, item) => sum + ((Number(item.qty) || 0) * (Number(item.cost) || 0)), 0);
150
+
151
+ return {
152
+ ...order,
153
+ id: order.id ?? generateOrderId(),
154
+ supplier: order.supplier || "Unknown Supplier",
155
+ date: order.date || new Date().toISOString().split("T")[0],
156
+ items,
157
+ itemsCount: Number(order.itemsCount ?? order.itemCount ?? calculatedQuantity) || 0,
158
+ totalCost: Number(order.totalCost ?? order.total ?? calculatedTotal) || 0,
159
+ status: order.status || "Received",
160
+ };
161
+ })
162
+ .sort(function (a, b) {
163
+ return new Date(b.date) - new Date(a.date);
164
+ });
165
+ }
166
+
167
+ function handleSearch(event) {
168
+ state.search = event.target.value.trim().toLowerCase();
169
+ state.page = 1;
170
+ renderPage();
171
+ }
172
+
173
+ function getFilteredOrders() {
174
+ if (!state.search) return state.orders;
175
+
176
+ return state.orders.filter(function (order) {
177
+ return String(order.id).toLowerCase().includes(state.search)
178
+ || String(order.supplier).toLowerCase().includes(state.search);
179
+ });
180
+ }
181
+
182
+ function getPaginatedOrders(orders) {
183
+ const startIndex = (state.page - 1) * state.limit;
184
+ return orders.slice(startIndex, startIndex + state.limit);
185
+ }
186
+
187
+ function renderPage() {
188
+ const filteredOrders = getFilteredOrders();
189
+ const totalPages = Math.max(1, Math.ceil(filteredOrders.length / state.limit));
190
+
191
+ if (state.page > totalPages) {
192
+ state.page = totalPages;
193
+ }
194
+
195
+ state.totalCount = filteredOrders.length;
196
+
197
+ const paginatedOrders = getPaginatedOrders(filteredOrders);
198
+ renderOrders(paginatedOrders);
199
+ renderResultsText(filteredOrders.length, paginatedOrders.length);
200
+ renderPagination(filteredOrders.length);
201
+ }
202
+
203
+ function renderOrders(orders) {
204
+ dom.tableBody.innerHTML = "";
205
+ dom.mobileContainer.innerHTML = "";
206
+
207
+ if (!orders.length) {
208
+ dom.tableBody.innerHTML = `
209
+ <tr>
210
+ <td colspan="7" class="text-center py-5 text-muted">
211
+ No orders found. Create a new order to get started.
212
+ </td>
213
+ </tr>
214
+ `;
215
+
216
+ dom.mobileContainer.innerHTML = `
217
+ <div class="text-center py-4 px-3 bg-white rounded-3 shadow-sm text-muted">
218
+ No orders found. Create a new order to get started.
219
+ </div>
220
+ `;
221
+ return;
222
+ }
223
+
224
+ dom.tableBody.innerHTML = orders.map(renderTableRow).join("");
225
+ dom.mobileContainer.innerHTML = orders.map(renderMobileCard).join("");
226
+ }
227
+
228
+ function renderTableRow(order) {
229
+ return `
230
+ <tr>
231
+ <td class="fw-semibold">${escapeHtml(String(order.id))}</td>
232
+ <td><span class="supplier-link">${escapeHtml(order.supplier)}</span></td>
233
+ <td>${formatDate(order.date)}</td>
234
+ <td>${order.itemsCount}</td>
235
+ <td class="fw-semibold">${formatCurrency(order.totalCost)}</td>
236
+ <td>
237
+ <span class="status-badge ${getStatusClass(order.status)}">
238
+ <span class="dot"></span> ${escapeHtml(order.status)}
239
+ </span>
240
+ </td>
241
+ <td class="text-center">
242
+ <button class="action-btn delete" data-action="delete" data-order-id="${escapeHtml(String(order.id))}" aria-label="Delete order ${escapeHtml(String(order.id))}">
243
+ <i class="fa-regular fa-trash-can"></i>
244
+ </button>
245
+ </td>
246
+ </tr>
247
+ `;
248
+ }
249
+
250
+ function renderMobileCard(order) {
251
+ return `
252
+ <div class="order-mobile-card">
253
+ <div class="mobile-top">
254
+ <div>
255
+ <h6 class="mb-1">${escapeHtml(String(order.id))}</h6>
256
+ <span class="supplier-link">${escapeHtml(order.supplier)}</span>
257
+ </div>
258
+ <span class="status-badge ${getStatusClass(order.status)}">
259
+ <span class="dot"></span> ${escapeHtml(order.status)}
260
+ </span>
261
+ </div>
262
+
263
+ <div class="mobile-details">
264
+ <div><strong>Date:</strong> ${formatDate(order.date)}</div>
265
+ <div><strong>Items:</strong> ${order.itemsCount}</div>
266
+ <div><strong>Total:</strong> ${formatCurrency(order.totalCost)}</div>
267
+ </div>
268
+
269
+ <div class="actions mobile-actions">
270
+ <button class="action-btn delete" data-action="delete" data-order-id="${escapeHtml(String(order.id))}" aria-label="Delete order ${escapeHtml(String(order.id))}">
271
+ <i class="fa-regular fa-trash-can"></i>
272
+ </button>
273
+ </div>
274
+ </div>
275
+ `;
276
+ }
277
+
278
+ function renderResultsText(totalItems, currentPageItemsCount) {
279
+ if (!dom.resultsText) return;
280
+
281
+ if (!totalItems) {
282
+ dom.resultsText.textContent = "Showing 0 of 0 orders";
283
+ return;
284
+ }
285
+
286
+ const start = (state.page - 1) * state.limit + 1;
287
+ const end = start + currentPageItemsCount - 1;
288
+ dom.resultsText.textContent = `Showing ${start} to ${end} of ${totalItems} orders`;
289
+ }
290
+
291
+ function renderPagination(totalItems) {
292
+ if (!dom.pagination) return;
293
+
294
+ const totalPages = Math.ceil(totalItems / state.limit);
295
+
296
+ if (!totalItems || totalPages <= 1) {
297
+ dom.pagination.innerHTML = "";
298
+ return;
299
+ }
300
+
301
+ const pageButtons = [];
302
+
303
+ pageButtons.push(`
304
+ <button class="page-btn" data-page-nav="prev" ${state.page === 1 ? "disabled" : ""}>
305
+ <i class="fa-solid fa-angle-left"></i>
306
+ </button>
307
+ `);
308
+
309
+ for (let pageNumber = 1; pageNumber <= totalPages; pageNumber += 1) {
310
+ pageButtons.push(`
311
+ <button class="page-btn ${pageNumber === state.page ? "active" : ""}" data-page="${pageNumber}">
312
+ ${pageNumber}
313
+ </button>
314
+ `);
315
+ }
316
+
317
+ pageButtons.push(`
318
+ <button class="page-btn" data-page-nav="next" ${state.page === totalPages ? "disabled" : ""}>
319
+ <i class="fa-solid fa-angle-right"></i>
320
+ </button>
321
+ `);
322
+
323
+ dom.pagination.innerHTML = pageButtons.join("");
324
+
325
+ dom.pagination.querySelectorAll("[data-page]").forEach(function (button) {
326
+ button.addEventListener("click", function () {
327
+ state.page = Number(button.dataset.page);
328
+ renderPage();
329
+ });
330
+ });
331
+
332
+ const prevButton = dom.pagination.querySelector('[data-page-nav="prev"]');
333
+ const nextButton = dom.pagination.querySelector('[data-page-nav="next"]');
334
+
335
+ prevButton?.addEventListener("click", function () {
336
+ if (state.page > 1) {
337
+ state.page -= 1;
338
+ renderPage();
339
+ }
340
+ });
341
+
342
+ nextButton?.addEventListener("click", function () {
343
+ if (state.page < totalPages) {
344
+ state.page += 1;
345
+ renderPage();
346
+ }
347
+ });
348
+ }
349
+
350
+ function addProductRow() {
351
+ dom.orderItemsBody.insertAdjacentHTML("beforeend", createItemRow());
352
+ updateOrderTotals();
353
+ }
354
+
355
+ function createItemRow() {
356
+ return `
357
+ <tr>
358
+ <td>
359
+ <input type="text" class="form-control item-input product-input" placeholder="Product name">
360
+ </td>
361
+ <td>
362
+ <input type="number" class="form-control item-input qty-input" min="1" step="1" placeholder="0">
363
+ </td>
364
+ <td>
365
+ <input type="number" class="form-control item-input cost-input" min="0" step="0.01" placeholder="0.00">
366
+ </td>
367
+ <td class="text-end">
368
+ <button type="button" class="remove-row-btn" aria-label="Remove row">
369
+ <i class="fa-regular fa-trash-can"></i>
370
+ </button>
371
+ </td>
372
+ </tr>
373
+ `;
374
+ }
375
+
376
+ function collectOrderItems() {
377
+ const rows = Array.from(dom.orderItemsBody.querySelectorAll("tr"));
378
+ const items = [];
379
+
380
+ for (const row of rows) {
381
+ const productInput = row.querySelector(".product-input") || row.querySelector('input[type="text"]');
382
+ const qtyInput = row.querySelector(".qty-input");
383
+ const costInput = row.querySelector(".cost-input");
384
+
385
+ const product = productInput.value.trim();
386
+ const qtyValue = qtyInput.value;
387
+ const costValue = costInput.value;
388
+ const isBlankRow = !product && qtyValue === "" && costValue === "";
389
+
390
+ [productInput, qtyInput, costInput].forEach(function (input) {
391
+ input.setCustomValidity("");
392
+ });
393
+
394
+ if (isBlankRow) {
395
+ continue;
396
+ }
397
+
398
+ const qty = Number(qtyValue);
399
+ const cost = Number(costValue);
400
+
401
+ if (!product) {
402
+ productInput.setCustomValidity("Enter a product name.");
403
+ productInput.reportValidity();
404
+ productInput.setCustomValidity("");
405
+ return null;
406
+ }
407
+
408
+ if (!Number.isFinite(qty) || qty <= 0) {
409
+ qtyInput.setCustomValidity("Quantity must be greater than 0.");
410
+ qtyInput.reportValidity();
411
+ qtyInput.setCustomValidity("");
412
+ return null;
413
+ }
414
+
415
+ if (!Number.isFinite(cost) || cost < 0) {
416
+ costInput.setCustomValidity("Cost must be 0 or more.");
417
+ costInput.reportValidity();
418
+ costInput.setCustomValidity("");
419
+ return null;
420
+ }
421
+
422
+ items.push({
423
+ product,
424
+ qty,
425
+ cost: Number(cost.toFixed(2)),
426
+ lineTotal: Number((qty * cost).toFixed(2)),
427
+ });
428
+ }
429
+
430
+ if (!items.length) {
431
+ const firstProductInput = dom.orderItemsBody.querySelector(".product-input") || dom.orderItemsBody.querySelector('input[type="text"]');
432
+ firstProductInput?.setCustomValidity("Add at least one product.");
433
+ firstProductInput?.reportValidity();
434
+ firstProductInput?.setCustomValidity("");
435
+ return null;
436
+ }
437
+
438
+ return items;
439
+ }
440
+
441
+ function updateOrderTotals() {
442
+ const rows = Array.from(dom.orderItemsBody.querySelectorAll("tr"));
443
+ const subtotal = rows.reduce(function (sum, row) {
444
+ const qty = Number(row.querySelector(".qty-input")?.value || 0);
445
+ const cost = Number(row.querySelector(".cost-input")?.value || 0);
446
+ return sum + (qty * cost);
447
+ }, 0);
448
+
449
+ dom.subtotalValue.textContent = formatCurrency(subtotal);
450
+ dom.totalValue.textContent = formatCurrency(subtotal);
451
+ }
452
+
453
+ function resetOrderForm() {
454
+ dom.form?.reset();
455
+ prepareSupplierSelect();
456
+
457
+ if (dom.orderDate) {
458
+ dom.orderDate.value = new Date().toISOString().split("T")[0];
459
+ }
460
+
461
+ dom.orderItemsBody.innerHTML = createItemRow();
462
+ updateOrderTotals();
463
+ }
464
+
465
+ function normalizeText(value) {
466
+ return String(value || "").trim().toLowerCase();
467
+ }
468
+
469
+ function getProductStatus(quantity, minStock) {
470
+ return quantity <= Number(minStock || 0) ? "low_stock" : "in_stock";
471
+ }
472
+
473
+
474
+ async function syncOrderItemsToProducts(items, supplierName) {
475
+ const [{ data: productsData }, { data: suppliersData }] = await Promise.all([
476
+ getData("products"),
477
+ getData("suppliers"),
478
+ ]);
479
+
480
+ const products = Array.isArray(productsData) ? [...productsData] : [];
481
+ const suppliers = Array.isArray(suppliersData) ? suppliersData : [];
482
+
483
+ const supplier = suppliers.find((item) => {
484
+ return normalizeText(item.name) === normalizeText(supplierName);
485
+ });
486
+
487
+ const rawUserName = localStorage.getItem("userName");
488
+ const actor = rawUserName ? JSON.parse(rawUserName) : "Admin";
489
+
490
+ for (const item of items) {
491
+ const existingProduct = products.find((product) => {
492
+ const sameName =
493
+ normalizeText(product.name) === normalizeText(item.product);
494
+
495
+ const sameSupplier =
496
+ String(product.supplierId) === String(supplier?.id);
497
+
498
+ return sameName && sameSupplier;
499
+ });
500
+
501
+ if (existingProduct) {
502
+ const updatedQuantity = Number(existingProduct.quantity || 0) + Number(item.qty || 0);
503
+
504
+ const updatedProduct = {
505
+ ...existingProduct,
506
+ supplierId: supplier ? Number(supplier.id) : existingProduct.supplierId,
507
+ cost: item.cost,
508
+ quantity: updatedQuantity,
509
+ status: getProductStatus(updatedQuantity, existingProduct.minStock),
510
+ createdBy: actor,
511
+ };
512
+
513
+ await putData("products", existingProduct.id, updatedProduct);
514
+ await postData("stockMovements", updatedProduct);
515
+
516
+
517
+ existingProduct.quantity = updatedQuantity;
518
+ existingProduct.cost = item.cost;
519
+ existingProduct.status = updatedProduct.status;
520
+
521
+ if (supplier) {
522
+ existingProduct.supplierId = Number(supplier.id);
523
+ }
524
+ } else {
525
+ const nextId = String(products.length + 1);
526
+
527
+ const newProduct = {
528
+ id: nextId,
529
+ name: item.product,
530
+ sku: `P-${nextId.padStart(3, "0")}`,
531
+ supplierId: supplier ? Number(supplier.id) : null,
532
+ price: item.cost * 1.15,
533
+ cost: item.cost,
534
+ quantity: item.qty,
535
+ minStock: 2,
536
+ status: getProductStatus(item.qty, 5),
537
+ description: "",
538
+ createdAt: new Date().toISOString().split("T")[0],
539
+ createdBy: actor,
540
+ };
541
+
542
+ await postData("products", newProduct);
543
+ await postData("stockMovements", newProduct);
544
+ products.push(newProduct);
545
+ }
546
+ }
547
+ }
548
+
549
+ async function createOrder() {
550
+ const supplier = dom.supplierSelect.value.trim();
551
+
552
+ dom.supplierSelect.setCustomValidity("");
553
+
554
+ if (!supplier) {
555
+ dom.supplierSelect.setCustomValidity("Please select a supplier.");
556
+ dom.supplierSelect.reportValidity();
557
+ dom.supplierSelect.setCustomValidity("");
558
+ return;
559
+ }
560
+
561
+ const items = collectOrderItems();
562
+ if (!items) return;
563
+
564
+ const totalQuantity = items.reduce((sum, item) => sum + item.qty, 0);
565
+ const totalCost = items.reduce((sum, item) => sum + item.lineTotal, 0);
566
+
567
+ const order = {
568
+ id: generateOrderId(),
569
+ supplier,
570
+ date: dom.orderDate.value || new Date().toISOString().split("T")[0],
571
+ items,
572
+ itemsCount: totalQuantity,
573
+ totalCost: Number(totalCost.toFixed(2)),
574
+ status: "Received",
575
+ };
576
+
577
+ const originalButtonText = dom.saveBtn.innerHTML;
578
+ dom.saveBtn.disabled = true;
579
+ dom.saveBtn.innerHTML = "Saving...";
580
+
581
+ try {
582
+ await postData("orders", order);
583
+
584
+ await syncOrderItemsToProducts(items, supplier);
585
+
586
+ state.page = 1;
587
+ state.search = "";
588
+ if (dom.searchInput) dom.searchInput.value = "";
589
+ modalInstance?.hide();
590
+ resetOrderForm();
591
+ await loadOrders();
592
+ } catch (error) {
593
+ console.error("Order saved or partially saved, but sync failed:", error);
594
+ alert("The order was created, but syncing products failed. Please check product inventory.");
595
+ } finally {
596
+ dom.saveBtn.disabled = false;
597
+ dom.saveBtn.innerHTML = originalButtonText;
598
+ }
599
+ }
600
+
601
+ async function handleOrderAction(event) {
602
+ const deleteButton = event.target.closest('[data-action="delete"]');
603
+ if (!deleteButton) return;
604
+
605
+ const orderId = deleteButton.dataset.orderId;
606
+ await deleteOrder(orderId);
607
+ }
608
+
609
+ async function deleteOrder(orderId) {
610
+ const confirmed = window.confirm(`Delete order ${orderId}?`);
611
+ if (!confirmed) return;
612
+
613
+ try {
614
+ await deleteData("orders", orderId);
615
+ await loadOrders();
616
+ } catch (error) {
617
+ console.error("Unable to delete order:", error);
618
+ alert("Unable to delete the order. Please try again.");
619
+ }
620
+ }
621
+
622
+ function formatCurrency(value) {
623
+ return new Intl.NumberFormat("en-US", {
624
+ style: "currency",
625
+ currency: "USD",
626
+ }).format(Number(value) || 0);
627
+ }
628
+
629
+ function formatDate(dateString) {
630
+ if (!dateString) return "-";
631
+
632
+ const parsedDate = new Date(dateString);
633
+ if (Number.isNaN(parsedDate.getTime())) {
634
+ return escapeHtml(String(dateString));
635
+ }
636
+
637
+ return parsedDate.toLocaleDateString("en-US", {
638
+ year: "numeric",
639
+ month: "short",
640
+ day: "numeric",
641
+ });
642
+ }
643
+
644
+ function generateOrderId() {
645
+ return `PO-${Date.now().toString().slice(-8)}`;
646
+ }
647
+
648
+ function getStatusClass(status) {
649
+ return String(status || "Received")
650
+ .toLowerCase()
651
+ .trim()
652
+ .replace(/\s+/g, "-");
653
+ }
654
+
655
+ function escapeHtml(value) {
656
+ return String(value)
657
+ .replace(/&/g, "&amp;")
658
+ .replace(/</g, "&lt;")
659
+ .replace(/>/g, "&gt;")
660
+ .replace(/"/g, "&quot;")
661
+ .replace(/'/g, "&#39;");
662
+ }