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.
- package/README.md +25 -0
- package/api/index.js +13 -0
- package/backend/README.md +35 -0
- package/backend/data/db.json +1239 -0
- package/backend/package-lock.json +532 -0
- package/backend/package.json +8 -0
- package/frontend/README.md +22 -0
- package/frontend/assets/Icon.png +0 -0
- package/frontend/assets/IconSort.png +0 -0
- package/frontend/assets/activity-1.png +0 -0
- package/frontend/assets/activity-2.png +0 -0
- package/frontend/assets/activity-3.png +0 -0
- package/frontend/assets/activity-4.png +0 -0
- package/frontend/assets/card-icon-1.png +0 -0
- package/frontend/assets/card-icon-2.png +0 -0
- package/frontend/assets/card-icon-3.png +0 -0
- package/frontend/assets/card-icon-4.png +0 -0
- package/frontend/assets/login.png +0 -0
- package/frontend/assets/logo.png +0 -0
- package/frontend/categories.html +143 -0
- package/frontend/css/all.min.css +9 -0
- package/frontend/css/bootstrap.min.css +6 -0
- package/frontend/css/categories.css +359 -0
- package/frontend/css/dashboard.css +373 -0
- package/frontend/css/inventoryInsights.css +308 -0
- package/frontend/css/inventoryOverview.css +353 -0
- package/frontend/css/orders.css +632 -0
- package/frontend/css/products.css +364 -0
- package/frontend/css/signin.css +120 -0
- package/frontend/css/style.css +282 -0
- package/frontend/css/suppliers.css +136 -0
- package/frontend/dashboard.html +160 -0
- package/frontend/index.html +124 -0
- package/frontend/inventoryInsights.html +182 -0
- package/frontend/inventoryOverview.html +187 -0
- package/frontend/js/api.js +55 -0
- package/frontend/js/auth.js +70 -0
- package/frontend/js/bootstrap.bundle.min.js +7 -0
- package/frontend/js/categories.js +356 -0
- package/frontend/js/dashboard.js +341 -0
- package/frontend/js/inventoryInsights.js +396 -0
- package/frontend/js/inventoryOverview.js +503 -0
- package/frontend/js/orders.js +662 -0
- package/frontend/js/products.js +650 -0
- package/frontend/js/suppliers.js +535 -0
- package/frontend/js/utils.js +234 -0
- package/frontend/orders.html +216 -0
- package/frontend/products.html +152 -0
- package/frontend/suppliers.html +175 -0
- package/frontend/webfonts/fa-brands-400.woff2 +0 -0
- package/frontend/webfonts/fa-regular-400.woff2 +0 -0
- package/frontend/webfonts/fa-solid-900.woff2 +0 -0
- package/frontend/webfonts/fa-v4compatibility.woff2 +0 -0
- package/package.json +38 -0
- package/vercel.json +18 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
renderNavbar("InventoryOverview");
|
|
2
|
+
renderFooter();
|
|
3
|
+
|
|
4
|
+
const emptyState = document.getElementById("emptyState");
|
|
5
|
+
const inventoryTable = document.getElementById("inventoryTable");
|
|
6
|
+
const inventoryBody = document.getElementById("inventoryBody");
|
|
7
|
+
const inventorySearch = document.getElementById("inventorySearch");
|
|
8
|
+
const inventoryTableInfo = document.getElementById("inventoryTableInfo");
|
|
9
|
+
const inventoryPagination = document.getElementById("pagination");
|
|
10
|
+
const filterBtn = document.getElementById("inventoryFilterBtn");
|
|
11
|
+
|
|
12
|
+
const productModal = document.getElementById("productModal");
|
|
13
|
+
const addProductBtn = document.getElementById("addProductBtn");
|
|
14
|
+
const addFirstBtn = document.getElementById("addFirstBtn");
|
|
15
|
+
const modalCloseBtn = document.getElementById("modalClose");
|
|
16
|
+
const cancelBtn = document.getElementById("cancelBtn");
|
|
17
|
+
const saveOverviewProductBtn = document.getElementById("saveOverviewProduct");
|
|
18
|
+
|
|
19
|
+
const overviewProductName = document.getElementById("overviewProductName");
|
|
20
|
+
const overviewCategory = document.getElementById("overviewCategory");
|
|
21
|
+
const overviewSku = document.getElementById("overviewSku");
|
|
22
|
+
const overviewQuantity = document.getElementById("overviewQuantity");
|
|
23
|
+
const overviewPrice = document.getElementById("overviewPrice");
|
|
24
|
+
|
|
25
|
+
const exportBtn = document.querySelector(".export-btn");
|
|
26
|
+
const tabs = document.querySelectorAll(".inv-tab");
|
|
27
|
+
|
|
28
|
+
const state = {
|
|
29
|
+
page: 1,
|
|
30
|
+
limit: 5,
|
|
31
|
+
totalCount: 0,
|
|
32
|
+
activeTab: "all",
|
|
33
|
+
search: ""
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
let allProducts = [];
|
|
37
|
+
let allCategories = [];
|
|
38
|
+
|
|
39
|
+
function openOverviewModal() {
|
|
40
|
+
if (!productModal) return;
|
|
41
|
+
productModal.classList.add("show");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function closeOverviewModal() {
|
|
45
|
+
if (!productModal) return;
|
|
46
|
+
productModal.classList.remove("show");
|
|
47
|
+
resetModalForm();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resetModalForm() {
|
|
51
|
+
if (overviewProductName) overviewProductName.value = "";
|
|
52
|
+
if (overviewCategory) overviewCategory.value = "";
|
|
53
|
+
if (overviewSku) overviewSku.value = "";
|
|
54
|
+
if (overviewQuantity) overviewQuantity.value = "";
|
|
55
|
+
if (overviewPrice) overviewPrice.value = "";
|
|
56
|
+
|
|
57
|
+
clearValidationStyle(overviewProductName);
|
|
58
|
+
clearValidationStyle(overviewCategory);
|
|
59
|
+
clearValidationStyle(overviewSku);
|
|
60
|
+
clearValidationStyle(overviewQuantity);
|
|
61
|
+
clearValidationStyle(overviewPrice);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function clearValidationStyle(element) {
|
|
65
|
+
if (!element) return;
|
|
66
|
+
element.style.border = "";
|
|
67
|
+
element.setCustomValidity("");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getCurrentDate() {
|
|
71
|
+
return new Date().toISOString().split("T")[0];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function calculateStatus(quantity, minStock) {
|
|
75
|
+
if (quantity === 0) return "out_of_stock";
|
|
76
|
+
if (quantity <= minStock) return "low_stock";
|
|
77
|
+
return "in_stock";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getCategoryName(categoryId) {
|
|
81
|
+
const category = allCategories.find(function (cat) {
|
|
82
|
+
return String(cat.id) === String(categoryId);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return category ? category.name : "Unknown";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getStatusBadge(product) {
|
|
89
|
+
if (product.status === "out_of_stock" || Number(product.quantity) === 0) {
|
|
90
|
+
return `<span class="badge rounded-pill text-bg-danger">Out of Stock</span>`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (product.status === "low_stock") {
|
|
94
|
+
return `<span class="badge rounded-pill text-bg-warning">Low Stock</span>`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (product.status === "archived") {
|
|
98
|
+
return `<span class="badge rounded-pill text-bg-secondary">Archived</span>`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return `<span class="badge rounded-pill text-bg-success">In Stock</span>`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getStockCellClass(product) {
|
|
105
|
+
return Number(product.quantity) <= Number(product.minStock) ? "text-danger fw-bold" : "fw-bold";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getFilteredProducts(products) {
|
|
109
|
+
let result = [...products];
|
|
110
|
+
|
|
111
|
+
if (state.activeTab === "low") {
|
|
112
|
+
result = result.filter(function (product) {
|
|
113
|
+
return product.status === "low_stock";
|
|
114
|
+
});
|
|
115
|
+
} else if (state.activeTab === "out") {
|
|
116
|
+
result = result.filter(function (product) {
|
|
117
|
+
return product.status === "out_of_stock";
|
|
118
|
+
});
|
|
119
|
+
} else if (state.activeTab === "archived") {
|
|
120
|
+
result = result.filter(function (product) {
|
|
121
|
+
return product.status === "archived";
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (state.search) {
|
|
125
|
+
result = result.filter(function (product) {
|
|
126
|
+
const name = String(product.name || "").toLowerCase();
|
|
127
|
+
const sku = String(product.sku || "").toLowerCase();
|
|
128
|
+
const category = String(getCategoryName(product.categoryId) || "").toLowerCase();
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
name.includes(state.search) ||
|
|
132
|
+
sku.includes(state.search) ||
|
|
133
|
+
category.includes(state.search)
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function paginateArray(array, page, limit) {
|
|
142
|
+
const start = (page - 1) * limit;
|
|
143
|
+
const end = start + limit;
|
|
144
|
+
return array.slice(start, end);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function toggleEmptyState(showEmpty) {
|
|
148
|
+
if (!emptyState || !inventoryTable) return;
|
|
149
|
+
|
|
150
|
+
if (showEmpty) {
|
|
151
|
+
emptyState.classList.remove("d-none");
|
|
152
|
+
inventoryTable.classList.add("d-none");
|
|
153
|
+
} else {
|
|
154
|
+
emptyState.classList.add("d-none");
|
|
155
|
+
inventoryTable.classList.remove("d-none");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function loadCategoriesIntoModal() {
|
|
160
|
+
const categoriesResponse = await getData("categories");
|
|
161
|
+
allCategories = categoriesResponse.data || [];
|
|
162
|
+
|
|
163
|
+
if (!overviewCategory) return;
|
|
164
|
+
|
|
165
|
+
overviewCategory.innerHTML = `<option value="">Select Category</option>`;
|
|
166
|
+
|
|
167
|
+
overviewCategory.innerHTML += allCategories
|
|
168
|
+
.map(function (category) {
|
|
169
|
+
return `<option value="${category.id}">${category.name}</option>`;
|
|
170
|
+
})
|
|
171
|
+
.join("");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function renderInventoryTable(products) {
|
|
175
|
+
if (!inventoryBody) return;
|
|
176
|
+
|
|
177
|
+
inventoryBody.innerHTML = products
|
|
178
|
+
.map(function (product) {
|
|
179
|
+
return `
|
|
180
|
+
<tr>
|
|
181
|
+
<td><strong>${product.name}</strong></td>
|
|
182
|
+
<td>${getCategoryName(product.categoryId)}</td>
|
|
183
|
+
<td>${product.sku}</td>
|
|
184
|
+
<td class="${getStockCellClass(product)}">${product.quantity}</td>
|
|
185
|
+
<td>$${Number(product.price).toLocaleString()}</td>
|
|
186
|
+
<td>${getStatusBadge(product)}</td>
|
|
187
|
+
<td>
|
|
188
|
+
<button class="filter-btn delete-product-btn" data-id="${product.id}" type="button">
|
|
189
|
+
<i class="fa-solid fa-trash"></i>
|
|
190
|
+
</button>
|
|
191
|
+
</td>
|
|
192
|
+
</tr>
|
|
193
|
+
`;
|
|
194
|
+
})
|
|
195
|
+
.join("");
|
|
196
|
+
|
|
197
|
+
bindDeleteButtons();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function renderOverviewFooter(totalCount, currentCount) {
|
|
201
|
+
if (inventoryTableInfo) {
|
|
202
|
+
const start = totalCount === 0 ? 0 : (state.page - 1) * state.limit + 1;
|
|
203
|
+
const end = totalCount === 0 ? 0 : start + currentCount - 1;
|
|
204
|
+
inventoryTableInfo.textContent = `Showing ${start}-${end} of ${totalCount} items`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (inventoryPagination) {
|
|
208
|
+
inventoryPagination.innerHTML = "";
|
|
209
|
+
renderPagination(inventoryPagination, state, renderInventoryOverview);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function renderInventoryOverview() {
|
|
214
|
+
try {
|
|
215
|
+
const productsResponse = await getData("products");
|
|
216
|
+
const categoriesResponse = await getData("categories");
|
|
217
|
+
|
|
218
|
+
allProducts = productsResponse.data || [];
|
|
219
|
+
allCategories = categoriesResponse.data || [];
|
|
220
|
+
|
|
221
|
+
const filteredProducts = getFilteredProducts(allProducts);
|
|
222
|
+
state.totalCount = filteredProducts.length;
|
|
223
|
+
|
|
224
|
+
toggleEmptyState(filteredProducts.length === 0);
|
|
225
|
+
|
|
226
|
+
if (filteredProducts.length === 0) {
|
|
227
|
+
inventoryTable.classList.remove("d-none");
|
|
228
|
+
emptyState.classList.add("d-none");
|
|
229
|
+
|
|
230
|
+
inventoryBody.innerHTML = `
|
|
231
|
+
<tr>
|
|
232
|
+
<td colspan="7" class="text-center py-5">
|
|
233
|
+
<div class="d-flex flex-column align-items-center gap-3">
|
|
234
|
+
<h5 class="m-0">No matching products found</h5>
|
|
235
|
+
<p class="m-0 text-muted">
|
|
236
|
+
No results for "<strong>${inventorySearch.value.trim()}</strong>".
|
|
237
|
+
Would you like to add a new product or stay on this page?
|
|
238
|
+
</p>
|
|
239
|
+
<div class="d-flex gap-3 mt-2">
|
|
240
|
+
<button type="button" class="add-product-btn" id="searchAddProductBtn">
|
|
241
|
+
<i class="fa-solid fa-plus"></i> Add Product
|
|
242
|
+
</button>
|
|
243
|
+
<button type="button" class="filter-btn" id="stayOnPageBtn">
|
|
244
|
+
Stay Here
|
|
245
|
+
</button>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
</td>
|
|
249
|
+
</tr>
|
|
250
|
+
`;
|
|
251
|
+
|
|
252
|
+
if (inventoryTableInfo) {
|
|
253
|
+
inventoryTableInfo.textContent = "Showing 0 of 0 items";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (inventoryPagination) {
|
|
257
|
+
inventoryPagination.innerHTML = "";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const searchAddProductBtn = document.getElementById("searchAddProductBtn");
|
|
261
|
+
const stayOnPageBtn = document.getElementById("stayOnPageBtn");
|
|
262
|
+
|
|
263
|
+
if (searchAddProductBtn) {
|
|
264
|
+
searchAddProductBtn.addEventListener("click", function () {
|
|
265
|
+
openOverviewModal();
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (stayOnPageBtn) {
|
|
270
|
+
stayOnPageBtn.addEventListener("click", function () {
|
|
271
|
+
inventorySearch.value = "";
|
|
272
|
+
state.search = "";
|
|
273
|
+
state.page = 1;
|
|
274
|
+
|
|
275
|
+
renderInventoryOverview();
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const paginatedProducts = paginateArray(filteredProducts, state.page, state.limit);
|
|
283
|
+
renderInventoryTable(paginatedProducts);
|
|
284
|
+
renderOverviewFooter(filteredProducts.length, paginatedProducts.length);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
console.error("Failed to render inventory overview:", error);
|
|
287
|
+
showOverviewError();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function showOverviewError() {
|
|
292
|
+
toggleEmptyState(true);
|
|
293
|
+
|
|
294
|
+
if (emptyState) {
|
|
295
|
+
emptyState.innerHTML = `
|
|
296
|
+
<div class="empty-state">
|
|
297
|
+
<div class="empty-icon-wrap">
|
|
298
|
+
<i class="fa-solid fa-triangle-exclamation"></i>
|
|
299
|
+
</div>
|
|
300
|
+
<h3 class="fw-bold mt-4 mb-2">Failed to load inventory</h3>
|
|
301
|
+
<p class="empty-desc">
|
|
302
|
+
Please make sure json-server is running and api.js is included before this file.
|
|
303
|
+
</p>
|
|
304
|
+
</div>
|
|
305
|
+
`;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function buildOverviewProductObject() {
|
|
310
|
+
validateInputs(
|
|
311
|
+
/^[A-Za-z0-9\s\-]{3,}$/,
|
|
312
|
+
overviewProductName,
|
|
313
|
+
"Product name must be at least 3 characters"
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
validateSelect(overviewCategory);
|
|
317
|
+
|
|
318
|
+
validateInputs(
|
|
319
|
+
/^[A-Za-z0-9\-]{3,}$/,
|
|
320
|
+
overviewSku,
|
|
321
|
+
"SKU must contain letters, numbers or -"
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
validateInputs(
|
|
325
|
+
/^\d+$/,
|
|
326
|
+
overviewQuantity,
|
|
327
|
+
"Quantity must be a valid number"
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
validateInputs(
|
|
331
|
+
/^\d+(\.\d{1,2})?$/,
|
|
332
|
+
overviewPrice,
|
|
333
|
+
"Enter a valid price"
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const allValid =
|
|
337
|
+
overviewProductName.checkValidity() &&
|
|
338
|
+
overviewCategory.checkValidity() &&
|
|
339
|
+
overviewSku.checkValidity() &&
|
|
340
|
+
overviewQuantity.checkValidity() &&
|
|
341
|
+
overviewPrice.checkValidity();
|
|
342
|
+
|
|
343
|
+
if (!allValid) return null;
|
|
344
|
+
|
|
345
|
+
const quantity = Number(overviewQuantity.value);
|
|
346
|
+
const minStock = 5;
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
name: overviewProductName.value.trim(),
|
|
350
|
+
categoryId: String(overviewCategory.value),
|
|
351
|
+
sku: overviewSku.value.trim(),
|
|
352
|
+
quantity: quantity,
|
|
353
|
+
price: Number(overviewPrice.value),
|
|
354
|
+
minStock: minStock,
|
|
355
|
+
supplierId: "",
|
|
356
|
+
status: calculateStatus(quantity, minStock),
|
|
357
|
+
createdAt: getCurrentDate()
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function handleSaveOverviewProduct() {
|
|
362
|
+
const productObject = buildOverviewProductObject();
|
|
363
|
+
if (!productObject) return;
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
await postData("products", productObject);
|
|
367
|
+
closeOverviewModal();
|
|
368
|
+
state.page = 1;
|
|
369
|
+
await renderInventoryOverview();
|
|
370
|
+
} catch (error) {
|
|
371
|
+
console.error("Failed to save product:", error);
|
|
372
|
+
alert("Failed to save product");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function bindDeleteButtons() {
|
|
377
|
+
const deleteButtons = document.querySelectorAll(".delete-product-btn");
|
|
378
|
+
|
|
379
|
+
deleteButtons.forEach(function (button) {
|
|
380
|
+
button.addEventListener("click", async function () {
|
|
381
|
+
const productId = this.dataset.id;
|
|
382
|
+
const confirmed = confirm("Are you sure you want to delete this product?");
|
|
383
|
+
if (!confirmed) return;
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
await deleteData("products", productId);
|
|
387
|
+
|
|
388
|
+
const newTotal = Math.max(state.totalCount - 1, 0);
|
|
389
|
+
const totalPages = getTotalPages(newTotal, state.limit);
|
|
390
|
+
|
|
391
|
+
if (state.page > totalPages && totalPages > 0) {
|
|
392
|
+
state.page = totalPages;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
await renderInventoryOverview();
|
|
396
|
+
} catch (error) {
|
|
397
|
+
console.error("Failed to delete product:", error);
|
|
398
|
+
alert("Failed to delete product");
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function exportInventoryData() {
|
|
405
|
+
const filteredProducts = getFilteredProducts(allProducts);
|
|
406
|
+
|
|
407
|
+
if (!filteredProducts.length) {
|
|
408
|
+
alert("No inventory data to export");
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const rows = filteredProducts.map(function (product) {
|
|
413
|
+
return {
|
|
414
|
+
Product: product.name,
|
|
415
|
+
Category: getCategoryName(product.categoryId),
|
|
416
|
+
SKU: product.sku,
|
|
417
|
+
Stock: product.quantity,
|
|
418
|
+
Price: product.price,
|
|
419
|
+
Status: product.status
|
|
420
|
+
};
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const headers = Object.keys(rows[0]);
|
|
424
|
+
const csv = [
|
|
425
|
+
headers.join(","),
|
|
426
|
+
...rows.map(function (row) {
|
|
427
|
+
return headers
|
|
428
|
+
.map(function (header) {
|
|
429
|
+
return `"${String(row[header]).replace(/"/g, '""')}"`;
|
|
430
|
+
})
|
|
431
|
+
.join(",");
|
|
432
|
+
})
|
|
433
|
+
].join("\n");
|
|
434
|
+
|
|
435
|
+
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
|
436
|
+
const url = URL.createObjectURL(blob);
|
|
437
|
+
const link = document.createElement("a");
|
|
438
|
+
|
|
439
|
+
link.href = url;
|
|
440
|
+
link.download = "inventory-overview.csv";
|
|
441
|
+
document.body.appendChild(link);
|
|
442
|
+
link.click();
|
|
443
|
+
document.body.removeChild(link);
|
|
444
|
+
URL.revokeObjectURL(url);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function bindEvents() {
|
|
448
|
+
if (addProductBtn) addProductBtn.addEventListener("click", openOverviewModal);
|
|
449
|
+
if (addFirstBtn) addFirstBtn.addEventListener("click", openOverviewModal);
|
|
450
|
+
if (modalCloseBtn) modalCloseBtn.addEventListener("click", closeOverviewModal);
|
|
451
|
+
if (cancelBtn) cancelBtn.addEventListener("click", closeOverviewModal);
|
|
452
|
+
|
|
453
|
+
if (productModal) {
|
|
454
|
+
productModal.addEventListener("click", function (e) {
|
|
455
|
+
if (e.target === productModal) {
|
|
456
|
+
closeOverviewModal();
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (inventorySearch) {
|
|
462
|
+
inventorySearch.addEventListener("input", function () {
|
|
463
|
+
state.search = inventorySearch.value.trim().toLowerCase();
|
|
464
|
+
state.page = 1;
|
|
465
|
+
renderInventoryOverview();
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
tabs.forEach(function (tab) {
|
|
470
|
+
tab.addEventListener("click", function () {
|
|
471
|
+
tabs.forEach(function (btn) {
|
|
472
|
+
btn.classList.remove("active");
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
this.classList.add("active");
|
|
476
|
+
state.activeTab = this.dataset.tab;
|
|
477
|
+
state.page = 1;
|
|
478
|
+
renderInventoryOverview();
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
if (saveOverviewProductBtn) {
|
|
483
|
+
saveOverviewProductBtn.addEventListener("click", handleSaveOverviewProduct);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (exportBtn) {
|
|
487
|
+
exportBtn.addEventListener("click", exportInventoryData);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function initInventoryOverview() {
|
|
492
|
+
bindEvents();
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
await loadCategoriesIntoModal();
|
|
496
|
+
} catch (error) {
|
|
497
|
+
console.error("Failed to load categories:", error);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
await renderInventoryOverview();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
initInventoryOverview();
|