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,535 @@
1
+ // selectors
2
+ let showDatainTable = document.querySelector("tbody");
3
+ let searchSuppliersByName = document.querySelector("#searchBySupplierName");
4
+ let formSelect = document.querySelector("#formSelect");
5
+ let exportData = document.getElementById("downloadData");
6
+ let addSupplier = document.querySelector("#addSupplier");
7
+ let modal = document.querySelector(".modal");
8
+ let supplierName = document.querySelector("#supplierName");
9
+ let contactPerson = document.querySelector("#contactPerson");
10
+ let supplierMail = document.querySelector("#supplierMail");
11
+ let supplierPhone = document.querySelector("#supplierPhone");
12
+ let selectStatus = document.querySelector("#select");
13
+ let physicalAddress = document.querySelector("textarea");
14
+ let saveSupplier = document.querySelector("#saveSupplier");
15
+ let targetID = null;
16
+ const paginationContainer = document.querySelector("#pagination");
17
+ let allSuppliersData = [];
18
+ let filteredSuppliersData = [];
19
+ let sortSupplierName = document.querySelector("#sortSupplierName");
20
+
21
+ // initial render
22
+ renderNavbar("Suppliers");
23
+ loadAndRenderSuppliers();
24
+ setupInputValidation();
25
+ renderFooter();
26
+
27
+ // pagination state
28
+ const state = {
29
+ page: 1,
30
+ limit: 5,
31
+ totalCount: 0,
32
+ sortOrder: null // null | "asc" | "desc"
33
+ };
34
+
35
+ // -----------------------------
36
+ // Render only current table page
37
+ // -----------------------------
38
+ function renderTable() {
39
+ const start = (state.page - 1) * state.limit;
40
+ const end = start + state.limit;
41
+ const paginatedData = filteredSuppliersData.slice(start, end);
42
+
43
+ showDatainTable.innerHTML = "";
44
+
45
+ if (paginatedData.length === 0) {
46
+ showDatainTable.innerHTML = `
47
+ <tr>
48
+ <td colspan="7" style="text-align:center;">No Data Matched!!</td>
49
+ </tr>
50
+ `;
51
+ return;
52
+ }
53
+
54
+ renderSuppliersRows(paginatedData);
55
+ }
56
+
57
+ // -----------------------------
58
+ // Render table + pagination
59
+ // -----------------------------
60
+ function renderSuppliersPage() {
61
+ renderTable();
62
+ renderPagination(paginationContainer, state, renderSuppliersPage);
63
+ }
64
+
65
+ // -----------------------------
66
+ // Helpers
67
+ // -----------------------------
68
+ function firstLatterOfSuppliers(suppliersName) {
69
+ let arrStr = suppliersName.trim().split(" ");
70
+ let firstLetter = [];
71
+
72
+ for (let index = 0; index < arrStr.length; index++) {
73
+ if (arrStr[index]) {
74
+ firstLetter.push(arrStr[index].charAt(0).toUpperCase());
75
+ }
76
+ }
77
+
78
+ return firstLetter.join("");
79
+ }
80
+
81
+ function getNextSupplierId(data) {
82
+ if (!data.length) return 1;
83
+
84
+ let maxId = Math.max(...data.map((item) => Number(item.id)));
85
+ return maxId + 1;
86
+ }
87
+
88
+ function clearValidation(input) {
89
+ input.setCustomValidity("");
90
+ input.style.border = "";
91
+ }
92
+
93
+ function resetFormFields() {
94
+ supplierName.value = "";
95
+ contactPerson.value = "";
96
+ supplierPhone.value = "";
97
+ supplierMail.value = "";
98
+ physicalAddress.value = "";
99
+ selectStatus.value = "";
100
+
101
+ clearValidation(supplierName);
102
+ clearValidation(contactPerson);
103
+ clearValidation(supplierPhone);
104
+ clearValidation(supplierMail);
105
+ clearValidation(physicalAddress);
106
+ clearValidation(selectStatus);
107
+ }
108
+
109
+ function escapeCSVValue(value) {
110
+ const stringValue = String(value ?? "");
111
+ if (
112
+ stringValue.includes(",") ||
113
+ stringValue.includes('"') ||
114
+ stringValue.includes("\n")
115
+ ) {
116
+ return `"${stringValue.replace(/"/g, '""')}"`;
117
+ }
118
+ return stringValue;
119
+ }
120
+
121
+ function downloadFile(content, fileName, mimeType) {
122
+ const blob = new Blob([content], { type: mimeType });
123
+ const url = URL.createObjectURL(blob);
124
+
125
+ const link = document.createElement("a");
126
+ link.href = url;
127
+ link.download = fileName;
128
+ document.body.appendChild(link);
129
+ link.click();
130
+ link.remove();
131
+
132
+ URL.revokeObjectURL(url);
133
+ }
134
+
135
+ // -----------------------------
136
+ // Search + Filter + Sort logic
137
+ // -----------------------------
138
+ function applyFilters() {
139
+ let result = [...allSuppliersData];
140
+
141
+ const searchValue = searchSuppliersByName.value.trim().toLowerCase();
142
+ const selectedStatus = formSelect.value.trim();
143
+
144
+ // search by supplier name
145
+ if (searchValue !== "") {
146
+ result = result.filter(function (supplier) {
147
+ return supplier.name.toLowerCase().includes(searchValue);
148
+ });
149
+ }
150
+
151
+ // filter by status
152
+ if (selectedStatus !== "" && selectedStatus.toLowerCase() !== "all") {
153
+ result = result.filter(function (supplier) {
154
+ return supplier.status.toLowerCase() === selectedStatus.toLowerCase();
155
+ });
156
+ }
157
+
158
+ // sort by supplier name
159
+ if (state.sortOrder === "asc") {
160
+ result.sort(function (a, b) {
161
+ return a.name.localeCompare(b.name);
162
+ });
163
+ } else if (state.sortOrder === "desc") {
164
+ result.sort(function (a, b) {
165
+ return b.name.localeCompare(a.name);
166
+ });
167
+ }
168
+
169
+ filteredSuppliersData = result;
170
+ state.totalCount = filteredSuppliersData.length;
171
+ renderSuppliersPage();
172
+ }
173
+
174
+ // -----------------------------
175
+ // Validation
176
+ // -----------------------------
177
+ function validateInputs(regexForValidInput, inputElement, messageShowForUser) {
178
+ const inputValue = inputElement.value.trim();
179
+
180
+ if (regexForValidInput.test(inputValue)) {
181
+ inputElement.setCustomValidity("");
182
+ inputElement.style.border = "2px solid rgb(0, 208, 59)";
183
+ } else {
184
+ inputElement.setCustomValidity(messageShowForUser);
185
+ inputElement.reportValidity();
186
+ inputElement.style.border = "2px solid rgba(255, 89, 89, 0.89)";
187
+ }
188
+ }
189
+
190
+ function validateSelect(selectValidate) {
191
+ if (selectValidate.value === "") {
192
+ selectValidate.setCustomValidity("Please select value");
193
+ selectValidate.reportValidity();
194
+ selectValidate.style.border = "2px solid rgba(255, 89, 89, 0.89)";
195
+ } else {
196
+ selectValidate.setCustomValidity("");
197
+ selectValidate.style.border = "2px solid rgb(0, 208, 59)";
198
+ }
199
+ }
200
+
201
+ function validateSupplierForm() {
202
+ validateInputs(
203
+ /^[A-Za-z\s]{3,60}$/,
204
+ supplierName,
205
+ "Please Enter Valid Supplier Name"
206
+ );
207
+
208
+ validateInputs(
209
+ /^[A-Za-z\s]{3,60}$/,
210
+ contactPerson,
211
+ "Please Enter Valid Contact Person"
212
+ );
213
+
214
+ validateInputs(
215
+ /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
216
+ supplierMail,
217
+ "Please enter a valid email"
218
+ );
219
+
220
+ validateInputs(
221
+ /^01[0125][0-9]{8}$/,
222
+ supplierPhone,
223
+ "Enter valid Egyptian phone number"
224
+ );
225
+
226
+ validateInputs(
227
+ /^.{3,100}$/,
228
+ physicalAddress,
229
+ "Enter valid Physical Address"
230
+ );
231
+
232
+ validateSelect(selectStatus);
233
+
234
+ return (
235
+ supplierName.checkValidity() &&
236
+ contactPerson.checkValidity() &&
237
+ supplierMail.checkValidity() &&
238
+ supplierPhone.checkValidity() &&
239
+ physicalAddress.checkValidity() &&
240
+ selectStatus.checkValidity()
241
+ );
242
+ }
243
+
244
+ function setupInputValidation() {
245
+ supplierName.addEventListener("input", function () {
246
+ validateInputs(
247
+ /^[A-Za-z\s]{3,60}$/,
248
+ supplierName,
249
+ "Please Enter Valid Supplier Name"
250
+ );
251
+ });
252
+
253
+ contactPerson.addEventListener("input", function () {
254
+ validateInputs(
255
+ /^[A-Za-z\s]{3,60}$/,
256
+ contactPerson,
257
+ "Please Enter Valid Contact Person"
258
+ );
259
+ });
260
+
261
+ supplierMail.addEventListener("input", function () {
262
+ validateInputs(
263
+ /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
264
+ supplierMail,
265
+ "Please enter a valid email"
266
+ );
267
+ });
268
+
269
+ supplierPhone.addEventListener("input", function () {
270
+ validateInputs(
271
+ /^01[0125][0-9]{8}$/,
272
+ supplierPhone,
273
+ "Enter valid Egyptian phone number"
274
+ );
275
+ });
276
+
277
+ selectStatus.addEventListener("change", function () {
278
+ validateSelect(selectStatus);
279
+ });
280
+
281
+ physicalAddress.addEventListener("input", function () {
282
+ validateInputs(
283
+ /^.{3,100}$/,
284
+ physicalAddress,
285
+ "Enter valid Physical Address"
286
+ );
287
+ });
288
+ }
289
+
290
+ // -----------------------------
291
+ // Load data
292
+ // -----------------------------
293
+ async function loadAndRenderSuppliers() {
294
+ try {
295
+ let suppliersData = (await getData("suppliers")).data;
296
+
297
+ allSuppliersData = suppliersData;
298
+ state.page = 1;
299
+
300
+ applyFilters();
301
+ } catch (error) {
302
+ console.error("Failed to load suppliers:", error);
303
+ showDatainTable.innerHTML = `
304
+ <tr>
305
+ <td colspan="7" style="text-align:center;">Failed to load suppliers</td>
306
+ </tr>
307
+ `;
308
+ paginationContainer.innerHTML = "";
309
+ }
310
+ }
311
+
312
+ // -----------------------------
313
+ // Search by supplier name
314
+ // -----------------------------
315
+ searchSuppliersByName.addEventListener("input", function () {
316
+ state.page = 1;
317
+ applyFilters();
318
+ });
319
+
320
+ // -----------------------------
321
+ // Filter by status
322
+ // -----------------------------
323
+ formSelect.addEventListener("change", function () {
324
+ state.page = 1;
325
+ applyFilters();
326
+ });
327
+
328
+ // -----------------------------
329
+ // Render rows
330
+ // -----------------------------
331
+ function renderSuppliersRows(dataAfterFilter) {
332
+ dataAfterFilter.forEach((dataFilter) => {
333
+ let statusClass =
334
+ dataFilter.status === "Active" ? "status_active" : "status_inactive";
335
+
336
+ showDatainTable.innerHTML += `
337
+ <tr id="${dataFilter.id}">
338
+ <td>
339
+ <p>
340
+ <span class="first-latter rounded-4 text-center p-2" id="firstLatter">
341
+ ${firstLatterOfSuppliers(dataFilter.name)}
342
+ </span>
343
+ ${dataFilter.name}
344
+ </p>
345
+ </td>
346
+ <td>${dataFilter.contactPerson}</td>
347
+ <td>${dataFilter.phone}</td>
348
+ <td><a href="mailto:${dataFilter.email}">${dataFilter.email}</a></td>
349
+ <td>${dataFilter.address}</td>
350
+ <td>
351
+ <div class="${statusClass}">
352
+ ${dataFilter.status}
353
+ </div>
354
+ </td>
355
+ <td>
356
+ <button class="btn edit_supplier_btn" data-bs-toggle="modal" data-bs-target="#exampleModal" id="editSupplierBtn">
357
+ <i class="fa-solid fa-pen-to-square"></i>
358
+ </button>
359
+ <button class="btn delete_supplier_btn" id="deleteSupplierBtn">
360
+ <i class="fa-solid fa-trash-can"></i>
361
+ </button>
362
+ </td>
363
+ </tr>
364
+ `;
365
+ });
366
+ }
367
+
368
+ // -----------------------------
369
+ // Sort by supplier name
370
+ // click 1 => asc
371
+ // click 2 => desc
372
+ // click 3 => reset
373
+ // -----------------------------
374
+ sortSupplierName.addEventListener("click", function () {
375
+ if (state.sortOrder === null) {
376
+ state.sortOrder = "asc";
377
+ } else if (state.sortOrder === "asc") {
378
+ state.sortOrder = "desc";
379
+ } else {
380
+ state.sortOrder = null;
381
+ }
382
+
383
+ state.page = 1;
384
+ applyFilters();
385
+ });
386
+
387
+ // -----------------------------
388
+ // Export suppliers data
389
+ // Exports current filtered/sorted data
390
+ // -----------------------------
391
+ exportData.addEventListener("click", function () {
392
+ if (!filteredSuppliersData.length) {
393
+ alert("No supplier data available to export.");
394
+ return;
395
+ }
396
+
397
+ const headers = [
398
+ "ID",
399
+ "Supplier Name",
400
+ "Contact Person",
401
+ "Phone",
402
+ "Email",
403
+ "Address",
404
+ "Status"
405
+ ];
406
+
407
+ const rows = filteredSuppliersData.map(function (supplier) {
408
+ return [
409
+ escapeCSVValue(supplier.id),
410
+ escapeCSVValue(supplier.name),
411
+ escapeCSVValue(supplier.contactPerson),
412
+ escapeCSVValue(supplier.phone),
413
+ escapeCSVValue(supplier.email),
414
+ escapeCSVValue(supplier.address),
415
+ escapeCSVValue(supplier.status)
416
+ ].join(",");
417
+ });
418
+
419
+ const csvContent = [headers.join(","), ...rows].join("\n");
420
+ const fileName = `suppliers-${new Date().toISOString().slice(0, 10)}.csv`;
421
+
422
+ downloadFile(csvContent, fileName, "text/csv;charset=utf-8;");
423
+ });
424
+
425
+ // -----------------------------
426
+ // Add supplier
427
+ // -----------------------------
428
+ addSupplier.addEventListener("click", function () {
429
+ targetID = null;
430
+ resetFormFields();
431
+ });
432
+
433
+ // -----------------------------
434
+ // Save supplier
435
+ // -----------------------------
436
+ saveSupplier.addEventListener("click", async function (e) {
437
+ e.preventDefault();
438
+
439
+ if (!validateSupplierForm()) return;
440
+
441
+ try {
442
+ const supplierObject = {
443
+ name: supplierName.value.trim(),
444
+ contactPerson: contactPerson.value.trim(),
445
+ phone: supplierPhone.value.trim(),
446
+ email: supplierMail.value.trim(),
447
+ address: physicalAddress.value.trim(),
448
+ status: selectStatus.value
449
+ };
450
+
451
+ if (targetID) {
452
+ await putData("suppliers", targetID, {
453
+ id: String(targetID),
454
+ ...supplierObject
455
+ });
456
+ } else {
457
+ const freshSuppliers = (await getData("suppliers")).data;
458
+ const newSupplierId = getNextSupplierId(freshSuppliers);
459
+
460
+ await postData("suppliers", {
461
+ id: String(newSupplierId),
462
+ ...supplierObject
463
+ });
464
+ }
465
+
466
+ resetFormFields();
467
+ targetID = null;
468
+ await loadAndRenderSuppliers();
469
+ } catch (error) {
470
+ console.error("Save failed:", error);
471
+ }
472
+ });
473
+
474
+ // -----------------------------
475
+ // Edit / Delete
476
+ // -----------------------------
477
+ showDatainTable.addEventListener("click", async function (e) {
478
+ let editBtn = e.target.closest(".edit_supplier_btn");
479
+ let deleteBtn = e.target.closest(".delete_supplier_btn");
480
+
481
+ // Edit
482
+ if (editBtn) {
483
+ let rowDataSupp_ofTarget = editBtn.closest("tr");
484
+ targetID = rowDataSupp_ofTarget.id;
485
+
486
+ try {
487
+ const supplier = allSuppliersData.find(
488
+ (item) => String(item.id) === String(targetID)
489
+ );
490
+
491
+ if (!supplier) return;
492
+
493
+ supplierName.value = supplier.name;
494
+ contactPerson.value = supplier.contactPerson;
495
+ supplierPhone.value = supplier.phone;
496
+ supplierMail.value = supplier.email;
497
+ physicalAddress.value = supplier.address;
498
+ selectStatus.value = supplier.status;
499
+
500
+ clearValidation(supplierName);
501
+ clearValidation(contactPerson);
502
+ clearValidation(supplierPhone);
503
+ clearValidation(supplierMail);
504
+ clearValidation(physicalAddress);
505
+ clearValidation(selectStatus);
506
+ } catch (error) {
507
+ console.error("Edit failed:", error);
508
+ }
509
+ }
510
+
511
+ // Delete
512
+ if (deleteBtn) {
513
+ let row = deleteBtn.closest("tr");
514
+ let currentId = row.id;
515
+ let supplierRowName = row.children[0].innerText.trim();
516
+
517
+ if (confirm(`Are You Sure to Delete ${supplierRowName} ?`)) {
518
+ try {
519
+ await deleteData("suppliers", currentId);
520
+ await loadAndRenderSuppliers();
521
+
522
+ const maxPage = Math.ceil(state.totalCount / state.limit) || 1;
523
+ if (state.page > maxPage) {
524
+ state.page = maxPage;
525
+ }
526
+
527
+ applyFilters();
528
+ } catch (error) {
529
+ console.error("Delete failed:", error);
530
+ }
531
+ } else {
532
+ alert("Supplier wasn't deleted");
533
+ }
534
+ }
535
+ });