stock dash

This commit is contained in:
Thanakorn S
2026-04-09 09:48:47 +07:00
parent a4f474b5df
commit 3b8f94f011
10 changed files with 612 additions and 105 deletions
+319 -103
View File
@@ -16,8 +16,16 @@
<div class="row ">
<div class="col-12">
<div class="mb-6">
<h1 class="fs-3 mb-0">Stock Overview</h1>
<h1 class="fs-3 mb-0" style="float: left;">Stock Overview</h1>
<div class="d-flex gap-2" style="float: right;">
<div class="mb-3 col-lg-6">
<input class="form-control" value='Warehouse' disabled>
</div>
<div class="mb-3 col-lg-6">
<select name="warehouse" id="warehouse" class="form-select" required></select>
</div>
</div>
</div>
</div>
</div>
@@ -44,11 +52,11 @@
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="card ">
<div class="card " id="total_stock_in">
<div class="card-body p-5">
<div class="d-flex justify-content-between">
<div>
<span class="mb-4 d-block">Stock In (This Month)</span>
<span class="mb-4 d-block">Total Stock In</span>
<h3 class="fw-bold mb-0 text-success">+456</h3>
<p class="mb-0 small text-secondary">Unit added</p>
</div>
@@ -66,11 +74,11 @@
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="card ">
<div class="card " id="total_stock_out">
<div class="card-body p-5">
<div class="d-flex justify-content-between">
<div>
<span class="mb-4 d-block">Stock Out (This Month)</span>
<span class="mb-4 d-block">Total Stock Out</span>
<h3 class="fw-bold mb-0 text-danger">-289</h3>
<p class="mb-0 small text-secondary">Unit removed</p>
</div>
@@ -88,7 +96,7 @@
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="card ">
<div class="card " id="low_stock">
<div class="card-body p-5">
<div class="d-flex justify-content-between">
<div>
@@ -113,20 +121,12 @@
<div class="row g-5 mb-5">
<div class="col-lg-6 col-12">
<div class="card">
<div class="card" id="stock_movement">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">Stock Movement</h5>
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-light text-dark btn-icon btn-md">1W</button>
<button type="button" class="btn btn-outline-light text-dark btn-icon btn-md">1M</button>
<button type="button" class="btn btn-outline-light text-dark btn-icon btn-md">3M</button>
<button type="button" class="btn btn-outline-light text-dark btn-icon btn-md">6M</button>
<button type="button" class="btn btn-primary btn-icon btn-md">1Y</button>
</div>
</div>
<div>
<div id="stockMovementChart"></div>
@@ -142,11 +142,10 @@
</div>
<div class="col-lg-6 col-12">
<div class="card">
<div class="card" id="stock_trend">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">Stock Level Trend</h5>
</div>
@@ -165,88 +164,13 @@
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card" id="stock_activity">
<div class="card-body p-5">
<h3 class="fs-5">Recent Stock Activity</h3>
<div class="border-bottom d-flex justify-content-between align-items-center mb-4 pb-3">
<div class="d-flex gap-3 align-items-center py-3">
<div class="icon-shape icon-md bg-success bg-opacity-10 text-success rounded-2">
<i class="ti ti-trending-up"></i>
</div>
<div>
<h5 class="mb-0">Product A</h5>
<p class="mb-0 small text-secondary">Stock In
</p>
</div>
</div>
<div class="d-flex flex-column align-items-end">
<span class="fs-5 text-success">+50</span>
<span>2 hour ago</span>
</div>
</div>
<div class="border-bottom d-flex justify-content-between align-items-center mb-4 pb-3">
<div class="d-flex gap-3 align-items-center py-3">
<div class="icon-shape icon-md bg-danger bg-opacity-10 text-danger rounded-2">
<i class="ti ti-trending-down"></i>
</div>
<div>
<h5 class="mb-0">Product B</h5>
<p class="mb-0 small text-secondary">Stock Out
</p>
</div>
</div>
<div class="d-flex flex-column align-items-end">
<span class="fs-5 text-danger">-25</span>
<span>4 hour ago</span>
</div>
</div>
<div class="border-bottom d-flex justify-content-between align-items-center mb-4 pb-3">
<div class="d-flex gap-3 align-items-center py-3">
<div class="icon-shape icon-md bg-success bg-opacity-10 text-success rounded-2">
<i class="ti ti-trending-up"></i>
</div>
<div>
<h5 class="mb-0">Product C</h5>
<p class="mb-0 small text-secondary">Stock Out
</p>
</div>
</div>
<div class="d-flex flex-column align-items-end">
<span class="fs-5 text-success">+100</span>
<span>6 hour ago</span>
</div>
</div>
<div class="border-bottom d-flex justify-content-between align-items-center mb-4 pb-3">
<div class="d-flex gap-3 align-items-center py-3">
<div class="icon-shape icon-md bg-danger bg-opacity-10 text-danger rounded-2">
<i class="ti ti-trending-down"></i>
</div>
<div>
<h5 class="mb-0">Product D</h5>
<p class="mb-0 small text-secondary">Stock Out
</p>
</div>
</div>
<div class="d-flex flex-column align-items-end">
<span class="fs-5 text-danger">-30</span>
<span>8 hour ago</span>
</div>
<!-- overlay -->
<div class="card-loader-overlay">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
</div>
@@ -262,19 +186,20 @@
<?php require "../include_ending.php";?>
<script>
function retrieve_warehouse_stats() {
function retrieve_warehouse_stats() {
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine/warehouse_stats.php",
url: "<?php echo $server_url?>ics/api/engine_report/warehouse_stats.php",
autoPrepare: true,
checkRequired: 0,
queueLock: false, // Allow this request to be sent even if another request is processing
action: 'read',
onSuccess: function(res) {
console.log(res);
$("#capacity h3").text(format_number(res.output.total_capacity,2));
$("#capacity p").text("Accross "+ format_number(res.output.total_warehouse) + " warehouses");
$("#capacity h3").text(format_number(res.output.total_capacity, 2));
$("#capacity p").text("Accross " + format_number(res.output.total_warehouse) + " warehouses");
$("#capacity .card-loader-overlay").hide();
}
@@ -282,9 +207,300 @@
}
function retrieve_warehouse_balance() {
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine_report/warehouse_balance.php",
autoPrepare: true,
checkRequired: 0,
debugMode: 0,
queueLock: false, // Allow this request to be sent even if another request is processing
action: 'read',
onSuccess: function(res) {
console.log(res);
$("#total_stock_in h3").text(format_number(res.output.total_in, 2));
$("#total_stock_in p").text("Accross " + format_number(res.output.total_warehouse) + " warehouses");
$("#total_stock_in .card-loader-overlay").hide();
$("#total_stock_out h3").text(format_number(res.output.total_out, 2));
$("#total_stock_out p").text("Accross " + format_number(res.output.total_warehouse) + " warehouses");
$("#total_stock_out .card-loader-overlay").hide();
}
});
}
function retrieve_product_stats() {
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine_report/product_stats.php",
autoPrepare: true,
checkRequired: 0,
debugMode: 0,
queueLock: false, // Allow this request to be sent even if another request is processing
action: 'read',
onSuccess: function(res) {
$("#low_stock h3").text(format_number(res.low_stock, 2));
$("#low_stock .card-loader-overlay").hide();
}
});
}
function retrieve_stock_movement() {
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine_report/stock_movement.php",
autoPrepare: true,
checkRequired: 0,
debugMode: 0,
queueLock: false, // Allow this request to be sent even if another request is processing
action: 'read',
onSuccess: function(res) {
var d = res.output;
// Clear previous chart safely
if (stockMovementChart && typeof stockMovementChart.destroy === 'function') {
stockMovementChart.destroy();
} else {
// First load: clear any chart rendered by main.js
$("#stockMovementChart").empty();
}
var n = {
series: [
{ name: "Stock In", data: d.stock_in || [] },
{ name: "Stock Out", data: d.stock_out || [] }
],
chart: { type: "bar", height: 350, toolbar: { show: !1 } },
plotOptions: { bar: { horizontal: !1, columnWidth: "55%", borderRadius: 5, borderRadiusApplication: "end" } },
colors: ["#E66239", "#00C951"],
dataLabels: { enabled: !1 },
stroke: { show: !0, width: 2, colors: ["transparent"] },
xaxis: { categories: d.labels || [] },
yaxis: { title: { text: "Quantity" } },
fill: { opacity: 1 },
tooltip: { y: { formatter: function(a) { return Number(a).toLocaleString() + " units" } } }
};
stockMovementChart = new ApexCharts(document.querySelector("#stockMovementChart"), n);
stockMovementChart.render();
$("#stockMovementChart").closest(".card").find(".card-loader-overlay").hide();
}
});
}
function retrieve_stock_trend() {
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine_report/stock_trend.php",
autoPrepare: true,
checkRequired: 0,
debugMode: 0,
queueLock: false, // Allow this request to be sent even if another request is processing
action: 'read',
onSuccess: function(res) {
var d = res.output;
// Clear previous chart safely
if (stockLevelTrendChart && typeof stockLevelTrendChart.destroy === 'function') {
stockLevelTrendChart.destroy();
} else {
$("#stockLevelTrendChart").empty();
}
var n = {
series: [
{ name: "Stock Balance", data: d.balance || [] }
],
chart: { height: 350, type: "area", toolbar: { show: !1 } },
colors: ["#E66239"],
dataLabels: { enabled: !1 },
stroke: { curve: "smooth", width: 3 },
fill: {
type: "gradient",
gradient: {
shadeIntensity: 1,
inverseColors: !1,
opacityFrom: 0.45,
opacityTo: 0.05,
stops: [20, 100]
}
},
xaxis: {
categories: d.labels || [],
axisBorder: { show: !1 },
axisTicks: { show: !1 }
},
yaxis: {
title: { text: "Quantity" },
labels: {
formatter: function(a) {
return Number(a).toLocaleString();
}
}
},
tooltip: {
y: {
formatter: function(a) {
return Number(a).toLocaleString() + " units";
}
}
},
grid: {
show: !0,
borderColor: "#e2e8f0"
}
};
stockLevelTrendChart = new ApexCharts(document.querySelector("#stockLevelTrendChart"), n);
stockLevelTrendChart.render();
$("#stockLevelTrendChart").closest(".card").find(".card-loader-overlay").hide();
}
});
}
function retrieve_stock_activity() {
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine_report/stock_activity.php",
autoPrepare: true,
checkRequired: 0,
debugMode: 0,
queueLock: false, // Allow this request to be sent even if another request is processing
action: 'read',
onSuccess: function(res) {
var d = res.output;
var $container = $("#stock_activity .card-body");
// Keep the title, remove old activity rows
$container.find(".stock-activity-item").remove();
$container.find(".stock-activity-empty").remove();
if (!d.activities || d.activities.length === 0) {
$container.append('<div class="stock-activity-empty text-center text-muted py-4">No recent activity</div>');
$("#stock_activity .card-loader-overlay").hide();
return;
}
var html = '';
d.activities.forEach(function(item) {
var isIn = item.direction === 'in';
var colorCls = isIn ? 'success' : 'danger';
var icon = isIn ? 'ti-trending-up' : 'ti-trending-down';
var sign = isIn ? '+' : '-';
var label = isIn ? 'Stock In' : 'Stock Out';
var qty = Number(item.qty).toLocaleString();
var timeAgo = time_ago(item.date);
html += '<div class="border-bottom d-flex justify-content-between align-items-center mb-4 pb-3 stock-activity-item">'
+ '<div class="d-flex gap-3 align-items-center py-3">'
+ '<div class="icon-shape icon-md bg-' + colorCls + ' bg-opacity-10 text-' + colorCls + ' rounded-2">'
+ '<i class="ti ' + icon + '"></i>'
+ '</div>'
+ '<div>'
+ '<h5 class="mb-0">' + escapeHtml(item.product_name) + '</h5>'
+ '<p class="mb-0 small text-secondary">' + label + '</p>'
+ '</div>'
+ '</div>'
+ '<div class="d-flex flex-column align-items-end">'
+ '<span class="fs-5 text-' + colorCls + '">' + sign + qty + '</span>'
+ '<span class="small text-muted">' + timeAgo + '</span>'
+ '</div>'
+ '</div>';
});
$container.append(html);
$("#stock_activity .card-loader-overlay").hide();
}
});
}
function retrieve_warehouse() {
return ajax_request({
url: "<?php echo $server_url?>inventory/api/engine/warehouse.php",
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
var option = ``;
$.each(res.output, function(key, item) {
option += `<option value='${item.id}'>${item.warehouse_name}</option>`;
})
$(`select#warehouse`).html(option);
}
});
}
// ── Helper: escape HTML ────────────────────────────────────────────
function escapeHtml(str) {
if (!str) return '';
return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── Helper: time ago from datetime string ─────────────────────────
function time_ago(dateStr) {
if (!dateStr) return '';
var now = new Date();
var past = new Date(dateStr.replace(/-/g, '/')); // cross-browser parse
var diff = Math.floor((now - past) / 1000); // seconds
if (diff < 60) return 'Just now';
if (diff < 3600) return Math.floor(diff / 60) + ' min ago';
if (diff < 86400) return Math.floor(diff / 3600) + ' hour ago';
if (diff < 2592000) return Math.floor(diff / 86400) + ' day ago';
if (diff < 31536000) return Math.floor(diff / 2592000) + ' month ago';
return Math.floor(diff / 31536000) + ' year ago';
}
$(function() {
retrieve_warehouse().done(function() {
retrieve_stock_movement();
retrieve_stock_trend();
retrieve_stock_activity();
});
retrieve_warehouse_stats();
retrieve_warehouse_balance();
retrieve_product_stats();
$(`#warehouse`).change(function() {
retrieve_stock_movement();
retrieve_stock_trend();
retrieve_stock_activity();
});
});
</script>