stock dash
This commit is contained in:
@@ -286,14 +286,16 @@ let isAjaxProcessing = false;
|
|||||||
|
|
||||||
function ajax_request(options) {
|
function ajax_request(options) {
|
||||||
|
|
||||||
if (isAjaxProcessing) {
|
if (isAjaxProcessing && options.queueLock !== false) {
|
||||||
// Instead of rejecting, we just return a "never-ending" promise
|
// Instead of rejecting, we just return a "never-ending" promise
|
||||||
// or a resolved promise that does nothing.
|
// or a resolved promise that does nothing.
|
||||||
console.warn("Request is busy... ignoring click.");
|
console.warn("Request is busy... ignoring click.");
|
||||||
return new Promise(() => { }); // This stays pending and won't trigger .then or .catch
|
return new Promise(() => { }); // This stays pending and won't trigger .then or .catch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.queueLock !== false) {
|
||||||
isAjaxProcessing = true;
|
isAjaxProcessing = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Auto prepare form data
|
// Auto prepare form data
|
||||||
if (options.autoPrepare === true) {
|
if (options.autoPrepare === true) {
|
||||||
@@ -339,6 +341,7 @@ function ajax_request(options) {
|
|||||||
let isSendingFiles = (options.data instanceof FormData);
|
let isSendingFiles = (options.data instanceof FormData);
|
||||||
|
|
||||||
if (options.debugMode) {
|
if (options.debugMode) {
|
||||||
|
isAjaxProcessing = false;
|
||||||
console.log("REQUEST DATA:", options.data);
|
console.log("REQUEST DATA:", options.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -199,6 +199,14 @@ class WarehouseManager {
|
|||||||
$name = $this->getWarehouseName($warehouse_id);
|
$name = $this->getWarehouseName($warehouse_id);
|
||||||
$table = "td_stock_" . $name;
|
$table = "td_stock_" . $name;
|
||||||
|
|
||||||
|
if (empty($id)) {
|
||||||
|
return [
|
||||||
|
'table' => $table,
|
||||||
|
'name' => $name,
|
||||||
|
'row' => []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
$sql = "SELECT * FROM `$table`
|
$sql = "SELECT * FROM `$table`
|
||||||
WHERE company_id = :company_id AND id = :id";
|
WHERE company_id = :company_id AND id = :id";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
require '../../../assets/utils/db_auth.php';
|
||||||
|
|
||||||
|
|
||||||
|
$sql = "SELECT SUM(total_in) as total_in, SUM(total_out) as total_out, b.min_stock
|
||||||
|
FROM warehouse_balance a
|
||||||
|
LEFT JOIN md_product b ON
|
||||||
|
a.company_id = b.company_id AND
|
||||||
|
a.product_sku = b.sku
|
||||||
|
WHERE
|
||||||
|
a.company_id = :company_id
|
||||||
|
GROUP BY a.product_sku
|
||||||
|
";
|
||||||
|
$sth = $pdo2->prepare($sql);
|
||||||
|
$sth->execute([
|
||||||
|
":company_id" => $company_id
|
||||||
|
]);
|
||||||
|
db_check($sth, $answer);
|
||||||
|
|
||||||
|
$output = $sth->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// calculate stock balance
|
||||||
|
$low_stock = 0;
|
||||||
|
foreach($output as $key => $value) {
|
||||||
|
$output[$key]["low_stock"] = ($value["total_in"] - $value["total_out"]) < $value["min_stock"]?true:false;
|
||||||
|
if ($output[$key]["low_stock"]) {
|
||||||
|
$low_stock++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$answer["output"] = $output;
|
||||||
|
$answer["low_stock"] = $low_stock;
|
||||||
|
$answer["success"] = 1;
|
||||||
|
exit(json_encode($answer));
|
||||||
|
|
||||||
|
?>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
require '../../../assets/utils/db_auth.php';
|
||||||
|
|
||||||
|
$id = 0;
|
||||||
|
$warehouse_id = (int)$data['warehouse'];
|
||||||
|
|
||||||
|
$whMgmt = new WarehouseManager($pdo2, $company_id);
|
||||||
|
$warehouse = $whMgmt->getStockContext($warehouse_id, $id);
|
||||||
|
$table = $warehouse['table'];
|
||||||
|
|
||||||
|
|
||||||
|
// ── Last 10 stock in/out transactions ────────────────────────────────
|
||||||
|
$sql = "SELECT
|
||||||
|
a.id,
|
||||||
|
a.date,
|
||||||
|
a.product_sku,
|
||||||
|
a.`in` AS stock_in,
|
||||||
|
a.`out` AS stock_out,
|
||||||
|
a.type,
|
||||||
|
a.description,
|
||||||
|
b.product_name
|
||||||
|
FROM `{$table}` a
|
||||||
|
LEFT JOIN md_product b ON
|
||||||
|
a.company_id = b.company_id and
|
||||||
|
a.product_sku = b.sku
|
||||||
|
WHERE
|
||||||
|
a.company_id = {$company_id} and
|
||||||
|
(a.`in` > 0 OR a.`out` > 0)
|
||||||
|
ORDER BY a.date DESC, a.id DESC
|
||||||
|
LIMIT 10";
|
||||||
|
|
||||||
|
$sth = $pdo2->prepare($sql);
|
||||||
|
$sth->execute();
|
||||||
|
$rows = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// ── Build output ─────────────────────────────────────────────────────
|
||||||
|
$activities = [];
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$is_in = (float)$row['stock_in'] > 0;
|
||||||
|
$qty = $is_in ? (float)$row['stock_in'] : (float)$row['stock_out'];
|
||||||
|
|
||||||
|
$activities[] = [
|
||||||
|
'product_name' => $row['product_name'] ?: $row['product_sku'],
|
||||||
|
'sku' => $row['product_sku'],
|
||||||
|
'direction' => $is_in ? 'in' : 'out',
|
||||||
|
'qty' => round($qty, 2),
|
||||||
|
'type' => $row['type'],
|
||||||
|
'description' => $row['description'],
|
||||||
|
'date' => $row['date'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Response ─────────────────────────────────────────────────────────
|
||||||
|
$output = [
|
||||||
|
'activities' => $activities,
|
||||||
|
];
|
||||||
|
|
||||||
|
$answer["output"] = $output;
|
||||||
|
$answer["success"] = 1;
|
||||||
|
exit(json_encode($answer));
|
||||||
|
|
||||||
|
?>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
require '../../../assets/utils/db_auth.php';
|
||||||
|
|
||||||
|
$id = 0;
|
||||||
|
$warehouse_id = (int)$data['warehouse'];
|
||||||
|
|
||||||
|
$whMgmt = new WarehouseManager($pdo2, $company_id);
|
||||||
|
$warehouse = $whMgmt->getStockContext($warehouse_id, $id);
|
||||||
|
$table = $warehouse['table'];
|
||||||
|
|
||||||
|
|
||||||
|
// ── Always last 12 months ────────────────────────────────────────────
|
||||||
|
$now = new DateTime();
|
||||||
|
$start = (clone $now)->modify('-12 months')->format('Y-m-d 00:00:00');
|
||||||
|
$end = $now->format('Y-m-d 23:59:59');
|
||||||
|
|
||||||
|
// ── Query: group by month ────────────────────────────────────────────
|
||||||
|
$sql = "SELECT
|
||||||
|
DATE_FORMAT(a.date, '%b') AS `label`,
|
||||||
|
DATE_FORMAT(a.date, '%Y-%m') AS `sort_key`,
|
||||||
|
ROUND(SUM(a.`in`), 2) AS `stock_in`,
|
||||||
|
ROUND(SUM(a.`out`), 2) AS `stock_out`
|
||||||
|
FROM `{$table}` a
|
||||||
|
WHERE
|
||||||
|
a.company_id = {$company_id} and
|
||||||
|
a.date >= '{$start}' and
|
||||||
|
a.date <= '{$end}'
|
||||||
|
GROUP BY `sort_key`, `label`
|
||||||
|
ORDER BY `sort_key` ASC";
|
||||||
|
|
||||||
|
$sth = $pdo2->prepare($sql);
|
||||||
|
$sth->execute();
|
||||||
|
$rows = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// ── Index DB results by sort_key (YYYY-MM) ──────────────────────────
|
||||||
|
$dataByMonth = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$dataByMonth[$row['sort_key']] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Always build 12 months, fill zeros where no data ────────────────
|
||||||
|
$labels = [];
|
||||||
|
$stock_in = [];
|
||||||
|
$stock_out = [];
|
||||||
|
|
||||||
|
for ($i = 11; $i >= 0; $i--) {
|
||||||
|
$dt = (clone $now)->modify("-{$i} months");
|
||||||
|
$key = $dt->format('Y-m'); // e.g. "2025-04"
|
||||||
|
$label = $dt->format('M'); // e.g. "Apr"
|
||||||
|
|
||||||
|
$labels[] = $label;
|
||||||
|
$stock_in[] = isset($dataByMonth[$key]) ? round((float)$dataByMonth[$key]['stock_in'], 2) : 0;
|
||||||
|
$stock_out[] = isset($dataByMonth[$key]) ? round((float)$dataByMonth[$key]['stock_out'], 2) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Response ─────────────────────────────────────────────────────────
|
||||||
|
$output = [
|
||||||
|
'labels' => $labels,
|
||||||
|
'stock_in' => $stock_in,
|
||||||
|
'stock_out' => $stock_out,
|
||||||
|
];
|
||||||
|
|
||||||
|
$answer["output"] = $output;
|
||||||
|
$answer["success"] = 1;
|
||||||
|
exit(json_encode($answer));
|
||||||
|
|
||||||
|
?>
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
require '../../../assets/utils/db_auth.php';
|
||||||
|
|
||||||
|
$id = 0;
|
||||||
|
$warehouse_id = (int)$data['warehouse'];
|
||||||
|
|
||||||
|
$whMgmt = new WarehouseManager($pdo2, $company_id);
|
||||||
|
$warehouse = $whMgmt->getStockContext($warehouse_id, $id);
|
||||||
|
$table = $warehouse['table'];
|
||||||
|
|
||||||
|
|
||||||
|
// ── Always last 12 months ────────────────────────────────────────────
|
||||||
|
$now = new DateTime();
|
||||||
|
$start = (clone $now)->modify('-12 months')->format('Y-m-d 00:00:00');
|
||||||
|
$end = $now->format('Y-m-d 23:59:59');
|
||||||
|
|
||||||
|
// ── Get cumulative balance BEFORE the 12-month window ────────────────
|
||||||
|
$sql_prior = "SELECT
|
||||||
|
ROUND(COALESCE(SUM(a.`in`), 0) - COALESCE(SUM(a.`out`), 0), 2) AS `balance`
|
||||||
|
FROM `{$table}` a
|
||||||
|
WHERE
|
||||||
|
a.company_id = {$company_id} and
|
||||||
|
a.date < '{$start}'";
|
||||||
|
|
||||||
|
$sth = $pdo2->prepare($sql_prior);
|
||||||
|
$sth->execute();
|
||||||
|
$prior_balance = (float)$sth->fetchColumn();
|
||||||
|
|
||||||
|
// ── Get monthly in/out for the 12-month window ──────────────────────
|
||||||
|
$sql = "SELECT
|
||||||
|
DATE_FORMAT(a.date, '%Y-%m') AS `sort_key`,
|
||||||
|
ROUND(SUM(a.`in`), 2) AS `stock_in`,
|
||||||
|
ROUND(SUM(a.`out`), 2) AS `stock_out`
|
||||||
|
FROM `{$table}` a
|
||||||
|
WHERE
|
||||||
|
a.company_id = {$company_id} and
|
||||||
|
a.date >= '{$start}' and
|
||||||
|
a.date <= '{$end}'
|
||||||
|
GROUP BY `sort_key`
|
||||||
|
ORDER BY `sort_key` ASC";
|
||||||
|
|
||||||
|
$sth = $pdo2->prepare($sql);
|
||||||
|
$sth->execute();
|
||||||
|
$rows = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// ── Index DB results by sort_key (YYYY-MM) ──────────────────────────
|
||||||
|
$dataByMonth = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$dataByMonth[$row['sort_key']] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Always build 12 months, accumulate running balance ──────────────
|
||||||
|
$labels = [];
|
||||||
|
$balance = [];
|
||||||
|
$running = $prior_balance;
|
||||||
|
|
||||||
|
for ($i = 11; $i >= 0; $i--) {
|
||||||
|
$dt = (clone $now)->modify("-{$i} months");
|
||||||
|
$key = $dt->format('Y-m');
|
||||||
|
$label = $dt->format('M');
|
||||||
|
|
||||||
|
$month_in = isset($dataByMonth[$key]) ? (float)$dataByMonth[$key]['stock_in'] : 0;
|
||||||
|
$month_out = isset($dataByMonth[$key]) ? (float)$dataByMonth[$key]['stock_out'] : 0;
|
||||||
|
|
||||||
|
$running += ($month_in - $month_out);
|
||||||
|
|
||||||
|
$labels[] = $label;
|
||||||
|
$balance[] = round($running, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Response ─────────────────────────────────────────────────────────
|
||||||
|
$output = [
|
||||||
|
'labels' => $labels,
|
||||||
|
'balance' => $balance,
|
||||||
|
];
|
||||||
|
|
||||||
|
$answer["output"] = $output;
|
||||||
|
$answer["success"] = 1;
|
||||||
|
exit(json_encode($answer));
|
||||||
|
|
||||||
|
?>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
require '../../../assets/utils/db_auth.php';
|
||||||
|
|
||||||
|
|
||||||
|
$sql = "SELECT SUM(total_in) as total_in, SUM(total_out) as total_out, COUNT(DISTINCT warehouse_id) as total_warehouse
|
||||||
|
FROM warehouse_balance a
|
||||||
|
WHERE
|
||||||
|
a.company_id = :company_id
|
||||||
|
";
|
||||||
|
$sth = $pdo2->prepare($sql);
|
||||||
|
$sth->execute([
|
||||||
|
":company_id" => $company_id
|
||||||
|
]);
|
||||||
|
db_check($sth, $answer);
|
||||||
|
|
||||||
|
$output = $sth->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$answer["output"] = $output;
|
||||||
|
$answer["success"] = 1;
|
||||||
|
exit(json_encode($answer));
|
||||||
|
|
||||||
|
?>
|
||||||
+317
-101
@@ -16,11 +16,19 @@
|
|||||||
<div class="row ">
|
<div class="row ">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="mb-6">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row g-5 mb-5">
|
<div class="row g-5 mb-5">
|
||||||
<div class="col-xl-3 col-md-6 col-12">
|
<div class="col-xl-3 col-md-6 col-12">
|
||||||
<div class="card" id="capacity">
|
<div class="card" id="capacity">
|
||||||
@@ -44,11 +52,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-xl-3 col-md-6 col-12">
|
<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="card-body p-5">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<div>
|
<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>
|
<h3 class="fw-bold mb-0 text-success">+456</h3>
|
||||||
<p class="mb-0 small text-secondary">Unit added</p>
|
<p class="mb-0 small text-secondary">Unit added</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,11 +74,11 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xl-3 col-md-6 col-12">
|
<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="card-body p-5">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<div>
|
<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>
|
<h3 class="fw-bold mb-0 text-danger">-289</h3>
|
||||||
<p class="mb-0 small text-secondary">Unit removed</p>
|
<p class="mb-0 small text-secondary">Unit removed</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,7 +96,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xl-3 col-md-6 col-12">
|
<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="card-body p-5">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -113,21 +121,13 @@
|
|||||||
<div class="row g-5 mb-5">
|
<div class="row g-5 mb-5">
|
||||||
<div class="col-lg-6 col-12">
|
<div class="col-lg-6 col-12">
|
||||||
|
|
||||||
<div class="card">
|
<div class="card" id="stock_movement">
|
||||||
<div class="card-body p-5">
|
<div class="card-body p-5">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
|
||||||
|
|
||||||
<h5 class="mb-0">Stock Movement</h5>
|
<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>
|
<div>
|
||||||
<div id="stockMovementChart"></div>
|
<div id="stockMovementChart"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,11 +142,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-lg-6 col-12">
|
<div class="col-lg-6 col-12">
|
||||||
|
|
||||||
<div class="card">
|
<div class="card" id="stock_trend">
|
||||||
<div class="card-body p-5">
|
<div class="card-body p-5">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
|
||||||
|
|
||||||
<h5 class="mb-0">Stock Level Trend</h5>
|
<h5 class="mb-0">Stock Level Trend</h5>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -165,88 +164,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card" id="stock_activity">
|
||||||
<div class="card-body p-5">
|
<div class="card-body p-5">
|
||||||
<h3 class="fs-5">Recent Stock Activity</h3>
|
<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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -262,19 +186,20 @@
|
|||||||
<?php require "../include_ending.php";?>
|
<?php require "../include_ending.php";?>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
|
||||||
function retrieve_warehouse_stats() {
|
function retrieve_warehouse_stats() {
|
||||||
|
|
||||||
return ajax_request({
|
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,
|
autoPrepare: true,
|
||||||
checkRequired: 0,
|
checkRequired: 0,
|
||||||
|
queueLock: false, // Allow this request to be sent even if another request is processing
|
||||||
action: 'read',
|
action: 'read',
|
||||||
onSuccess: function(res) {
|
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();
|
$("#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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── 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() {
|
$(function() {
|
||||||
|
|
||||||
|
retrieve_warehouse().done(function() {
|
||||||
|
retrieve_stock_movement();
|
||||||
|
retrieve_stock_trend();
|
||||||
|
retrieve_stock_activity();
|
||||||
|
});
|
||||||
|
|
||||||
retrieve_warehouse_stats();
|
retrieve_warehouse_stats();
|
||||||
|
retrieve_warehouse_balance();
|
||||||
|
retrieve_product_stats();
|
||||||
|
|
||||||
|
$(`#warehouse`).change(function() {
|
||||||
|
retrieve_stock_movement();
|
||||||
|
retrieve_stock_trend();
|
||||||
|
retrieve_stock_activity();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -44,6 +44,8 @@
|
|||||||
Dropzone.autoDiscover = false;
|
Dropzone.autoDiscover = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
|
||||||
|
|
||||||
<!-- theme script -->
|
<!-- theme script -->
|
||||||
<link rel="stylesheet" href="<?php echo $server_url?>assets/css/main.css">
|
<link rel="stylesheet" href="<?php echo $server_url?>assets/css/main.css">
|
||||||
<script type="module" src="<?php echo $server_url?>assets/js/main.js"></script>
|
<script type="module" src="<?php echo $server_url?>assets/js/main.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user