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,341 @@
|
|
|
1
|
+
renderNavbar("Dashboard");
|
|
2
|
+
renderFooter();
|
|
3
|
+
|
|
4
|
+
const tableBody = document.getElementById("tableBody");
|
|
5
|
+
const totalProducts = document.getElementById("totalProducts");
|
|
6
|
+
const lowStockItems = document.getElementById("lowStockItems");
|
|
7
|
+
const totalSuppliers = document.getElementById("totalSuppliers");
|
|
8
|
+
const inventoryValue = document.getElementById("inventoryValue");
|
|
9
|
+
|
|
10
|
+
const summaryCards = document.querySelector(".summary-cards");
|
|
11
|
+
const chartContainer = document.querySelector(".inventory-chart .container");
|
|
12
|
+
const activitiesContainer = document.querySelector(".activities");
|
|
13
|
+
const tableFooter = document.querySelector(".table-footer");
|
|
14
|
+
|
|
15
|
+
const state = {
|
|
16
|
+
page: 1,
|
|
17
|
+
limit: 5,
|
|
18
|
+
totalCount: 0
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
async function renderDashboard() {
|
|
22
|
+
try {
|
|
23
|
+
const productsResponse = await getData("products");
|
|
24
|
+
const categoriesResponse = await getData("categories");
|
|
25
|
+
const suppliersResponse = await getData("suppliers");
|
|
26
|
+
const stockMovementsResponse = await getData("stockMovements");
|
|
27
|
+
|
|
28
|
+
const products = productsResponse.data;
|
|
29
|
+
const categories = categoriesResponse.data;
|
|
30
|
+
const suppliers = suppliersResponse.data;
|
|
31
|
+
const stockMovements = stockMovementsResponse.data;
|
|
32
|
+
|
|
33
|
+
renderSummary(products, suppliers);
|
|
34
|
+
renderChart(products, categories);
|
|
35
|
+
renderRecentActivity(products, suppliers, stockMovements);
|
|
36
|
+
renderLowStockTable(products, categories);
|
|
37
|
+
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error("Failed to render dashboard:", error);
|
|
40
|
+
renderDashboardError();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function renderSummary(products, suppliers) {
|
|
45
|
+
const totalProductsCount = products.length;
|
|
46
|
+
|
|
47
|
+
const lowStockCount = products.filter(function (product) {
|
|
48
|
+
return product.status === "low_stock" || product.status === "out_of_stock";
|
|
49
|
+
}).length;
|
|
50
|
+
|
|
51
|
+
const totalSuppliersCount = suppliers.length;
|
|
52
|
+
|
|
53
|
+
const totalInventoryValue = products.reduce(function (total, product) {
|
|
54
|
+
return total + (Number(product.price) * Number(product.quantity));
|
|
55
|
+
}, 0);
|
|
56
|
+
|
|
57
|
+
totalProducts.textContent = totalProductsCount;
|
|
58
|
+
lowStockItems.textContent = lowStockCount;
|
|
59
|
+
totalSuppliers.textContent = totalSuppliersCount;
|
|
60
|
+
inventoryValue.innerHTML = `$${formatNumber(totalInventoryValue)}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function renderChart(products, categories) {
|
|
64
|
+
if (!chartContainer) return;
|
|
65
|
+
|
|
66
|
+
const categoryTotals = categories.map(function (category) {
|
|
67
|
+
const totalQty = products
|
|
68
|
+
.filter(function (product) {
|
|
69
|
+
return String(product.categoryId) === String(category.id);
|
|
70
|
+
})
|
|
71
|
+
.reduce(function (sum, product) {
|
|
72
|
+
return sum + Number(product.quantity);
|
|
73
|
+
}, 0);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
name: category.name,
|
|
77
|
+
quantity: totalQty
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const maxQty = Math.max(
|
|
82
|
+
...categoryTotals.map(function (item) {
|
|
83
|
+
return item.quantity;
|
|
84
|
+
}),
|
|
85
|
+
1
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
chartContainer.innerHTML = categoryTotals
|
|
89
|
+
.map(function (item) {
|
|
90
|
+
const height = Math.max((item.quantity / maxQty) * 180, 10);
|
|
91
|
+
|
|
92
|
+
return `
|
|
93
|
+
<div class="chart-col d-flex flex-column justify-content-end align-items-center" style="height: 300px;">
|
|
94
|
+
<div class="level" style="height:${height}px;"></div>
|
|
95
|
+
<small style="margin-top: 8px; text-align: center; line-height: 1.2; min-height: 30px; display: flex; align-items: flex-start; justify-content: center;">
|
|
96
|
+
${item.name}
|
|
97
|
+
</small>
|
|
98
|
+
</div>
|
|
99
|
+
`;
|
|
100
|
+
})
|
|
101
|
+
.join("");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function renderRecentActivity(products, suppliers, stockMovements) {
|
|
105
|
+
if (!activitiesContainer) return;
|
|
106
|
+
if (!stockMovements.length) {
|
|
107
|
+
activitiesContainer.innerHTML = `
|
|
108
|
+
<p>No recent activity available.</p>
|
|
109
|
+
`;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const activities = stockMovements
|
|
114
|
+
.slice(-4)
|
|
115
|
+
.reverse()
|
|
116
|
+
.map(function (movement, index) {
|
|
117
|
+
|
|
118
|
+
const icons = ["fa-box", "fa-truck", "fa-layer-group", "fa-triangle-exclamation"];
|
|
119
|
+
const iconClasses = ["icon-purple", "icon-green", "icon-yellow", "icon-red"];
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
iconClass: iconClasses[index % iconClasses.length],
|
|
123
|
+
icon: icons[index % icons.length],
|
|
124
|
+
text: movement.name || movement.productName,
|
|
125
|
+
meta: `${movement.createdAt} by ${movement.createdBy}`
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
activitiesContainer.innerHTML = activities
|
|
130
|
+
.map(function (activity) {
|
|
131
|
+
return `
|
|
132
|
+
<div class="container d-flex align-items-start">
|
|
133
|
+
<div class="${activity.iconClass} icon d-flex align-items-center justify-content-center">
|
|
134
|
+
<i class="fa-solid ${activity.icon}"></i>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="activity-caption">
|
|
137
|
+
<p>
|
|
138
|
+
${activity.text}<br>
|
|
139
|
+
<span>${activity.meta}</span>
|
|
140
|
+
</p>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
`;
|
|
144
|
+
})
|
|
145
|
+
.join("");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function renderLowStockTable(products, categories) {
|
|
149
|
+
const flaggedProducts = products.filter(function (product) {
|
|
150
|
+
return product.status === "low_stock" || product.status === "out_of_stock";
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
state.totalCount = flaggedProducts.length;
|
|
154
|
+
|
|
155
|
+
const start = (state.page - 1) * state.limit;
|
|
156
|
+
const end = start + state.limit;
|
|
157
|
+
const paginatedProducts = flaggedProducts.slice(start, end);
|
|
158
|
+
|
|
159
|
+
if (!paginatedProducts.length) {
|
|
160
|
+
tableBody.innerHTML = `
|
|
161
|
+
<tr>
|
|
162
|
+
<td colspan="6" class="text-center">No low stock items found</td>
|
|
163
|
+
</tr>
|
|
164
|
+
`;
|
|
165
|
+
|
|
166
|
+
renderLowStockFooter(0, flaggedProducts.length);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
tableBody.innerHTML = paginatedProducts
|
|
171
|
+
.map(function (product) {
|
|
172
|
+
const category = categories.find(function (cat) {
|
|
173
|
+
return String(cat.id) === String(product.categoryId);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const categoryName = category ? category.name : "Unknown";
|
|
177
|
+
const stockClass = getStockClass(product.quantity, product.minStock);
|
|
178
|
+
const statusInfo = getStatusInfo(product.status, product.quantity, product.minStock);
|
|
179
|
+
const productIcon = getProductIcon(product.name, categoryName);
|
|
180
|
+
|
|
181
|
+
return `
|
|
182
|
+
<tr>
|
|
183
|
+
<td>
|
|
184
|
+
<div class="product-cell">
|
|
185
|
+
<div class="product-icon">
|
|
186
|
+
<i class="fa-solid ${productIcon}"></i>
|
|
187
|
+
</div>
|
|
188
|
+
<div class="product-info">
|
|
189
|
+
<h6>${product.name}</h6>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</td>
|
|
193
|
+
<td>${categoryName}</td>
|
|
194
|
+
<td class="${stockClass}">${product.quantity}</td>
|
|
195
|
+
<td>${product.minStock}</td>
|
|
196
|
+
<td><span class="status ${statusInfo.className}">${statusInfo.text}</span></td>
|
|
197
|
+
<td><button class="reorder-btn" data-id="${product.id}">Reorder</button></td>
|
|
198
|
+
</tr>
|
|
199
|
+
`;
|
|
200
|
+
})
|
|
201
|
+
.join("");
|
|
202
|
+
|
|
203
|
+
renderLowStockFooter(paginatedProducts.length, flaggedProducts.length);
|
|
204
|
+
attachReorderEvents();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function renderLowStockFooter(currentCount, totalCount) {
|
|
208
|
+
if (!tableFooter) return;
|
|
209
|
+
|
|
210
|
+
tableFooter.innerHTML = `
|
|
211
|
+
<span>Showing ${currentCount} of ${totalCount} flagged items</span>
|
|
212
|
+
<div class="pagination" id="pagination"></div>
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
const paginationContainer = document.getElementById("pagination");
|
|
216
|
+
renderPagination(paginationContainer, state, rerenderLowStockOnly);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function rerenderLowStockOnly() {
|
|
220
|
+
try {
|
|
221
|
+
const productsResponse = await getData("products");
|
|
222
|
+
const categoriesResponse = await getData("categories");
|
|
223
|
+
|
|
224
|
+
renderLowStockTable(productsResponse.data, categoriesResponse.data);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error("Failed to rerender low stock table:", error);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function getStatusInfo(status, quantity, minStock) {
|
|
231
|
+
if (status === "out_of_stock" || quantity === 0) {
|
|
232
|
+
return {
|
|
233
|
+
text: "Out of Stock",
|
|
234
|
+
className: "out"
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (status === "low_stock" || quantity <= minStock) {
|
|
239
|
+
if (quantity <= Math.ceil(minStock / 2)) {
|
|
240
|
+
return {
|
|
241
|
+
text: "Critical",
|
|
242
|
+
className: "critical"
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
text: "Low Stock",
|
|
248
|
+
className: "low"
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
text: "In Stock",
|
|
254
|
+
className: "low"
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function getProductIcon(productName, categoryName) {
|
|
259
|
+
const name = `${productName} ${categoryName}`.toLowerCase();
|
|
260
|
+
|
|
261
|
+
if (name.includes("keyboard")) return "fa-keyboard";
|
|
262
|
+
if (name.includes("mouse")) return "fa-computer-mouse";
|
|
263
|
+
if (name.includes("headset") || name.includes("headphones")) return "fa-headphones";
|
|
264
|
+
if (name.includes("monitor") || name.includes("display")) return "fa-desktop";
|
|
265
|
+
if (name.includes("laptop")) return "fa-laptop";
|
|
266
|
+
if (name.includes("phone") || name.includes("smartphone")) return "fa-mobile-screen";
|
|
267
|
+
if (name.includes("tablet")) return "fa-tablet-screen-button";
|
|
268
|
+
if (name.includes("camera") || name.includes("webcam")) return "fa-camera";
|
|
269
|
+
if (name.includes("printer")) return "fa-print";
|
|
270
|
+
if (name.includes("router")) return "fa-wifi";
|
|
271
|
+
if (name.includes("switch")) return "fa-network-wired";
|
|
272
|
+
if (name.includes("storage") || name.includes("ssd") || name.includes("hdd") || name.includes("flash")) return "fa-hard-drive";
|
|
273
|
+
if (name.includes("chair")) return "fa-chair";
|
|
274
|
+
if (name.includes("desk") || name.includes("table")) return "fa-table";
|
|
275
|
+
if (name.includes("cable")) return "fa-plug";
|
|
276
|
+
if (name.includes("power")) return "fa-bolt";
|
|
277
|
+
return "fa-box";
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function attachReorderEvents() {
|
|
281
|
+
const reorderButtons = document.querySelectorAll(".reorder-btn");
|
|
282
|
+
reorderButtons.forEach(function (button) {
|
|
283
|
+
button.addEventListener("click", function () {
|
|
284
|
+
const productId = button.dataset.id;
|
|
285
|
+
alert(`Reorder action for product ID: ${productId}`);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ^ Static Events
|
|
291
|
+
// function attachStaticEvents() {
|
|
292
|
+
// const bulkOrderBtn = document.getElementById("bulkOrderBtn");
|
|
293
|
+
// const viewActivityBtn = document.getElementById("viewActivityBtn");
|
|
294
|
+
// if (bulkOrderBtn) {
|
|
295
|
+
// bulkOrderBtn.addEventListener("click", function () {
|
|
296
|
+
// alert("Bulk order feature will be connected later.");
|
|
297
|
+
// });
|
|
298
|
+
// }
|
|
299
|
+
// if (viewActivityBtn) {
|
|
300
|
+
// viewActivityBtn.addEventListener("click", function () {
|
|
301
|
+
// alert("View all activity feature will be connected later.");
|
|
302
|
+
// });
|
|
303
|
+
// }
|
|
304
|
+
// }
|
|
305
|
+
|
|
306
|
+
function renderDashboardError() {
|
|
307
|
+
if (summaryCards) {
|
|
308
|
+
summaryCards.innerHTML = `
|
|
309
|
+
<div class="card w-100">
|
|
310
|
+
<h4>Failed to load dashboard data</h4>
|
|
311
|
+
<p>Please make sure json-server is running on port 3000.</p>
|
|
312
|
+
</div>
|
|
313
|
+
`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (chartContainer) {
|
|
317
|
+
chartContainer.innerHTML = `<p class="text-center w-100">Chart unavailable</p>`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (activitiesContainer) {
|
|
321
|
+
activitiesContainer.innerHTML = `<p>No recent activity available.</p>`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (tableBody) {
|
|
325
|
+
tableBody.innerHTML = `
|
|
326
|
+
<tr>
|
|
327
|
+
<td colspan="6" class="text-center">Unable to load low stock items</td>
|
|
328
|
+
</tr>
|
|
329
|
+
`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (tableFooter) {
|
|
333
|
+
tableFooter.innerHTML = "";
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function formatNumber(number) {
|
|
338
|
+
return Number(number).toLocaleString();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
renderDashboard();
|