From 3b8f94f0114da3146e5f0bcfd753ce01639c2993 Mon Sep 17 00:00:00 2001 From: Thanakorn S Date: Thu, 9 Apr 2026 09:48:47 +0700 Subject: [PATCH] stock dash --- app/assets/js/custom.js | 7 +- app/assets/utils/db_auth.php | 8 + app/ics/api/engine_report/product_stats.php | 37 ++ app/ics/api/engine_report/stock_activity.php | 65 +++ app/ics/api/engine_report/stock_movement.php | 69 +++ app/ics/api/engine_report/stock_trend.php | 83 ++++ .../api/engine_report/warehouse_balance.php | 24 + .../warehouse_stats.php | 0 app/ics/stock_overview.php | 422 +++++++++++++----- app/include_header.php | 2 + 10 files changed, 612 insertions(+), 105 deletions(-) create mode 100644 app/ics/api/engine_report/product_stats.php create mode 100644 app/ics/api/engine_report/stock_activity.php create mode 100644 app/ics/api/engine_report/stock_movement.php create mode 100644 app/ics/api/engine_report/stock_trend.php create mode 100644 app/ics/api/engine_report/warehouse_balance.php rename app/ics/api/{engine => engine_report}/warehouse_stats.php (100%) diff --git a/app/assets/js/custom.js b/app/assets/js/custom.js index 69dd640..1f4337b 100644 --- a/app/assets/js/custom.js +++ b/app/assets/js/custom.js @@ -286,14 +286,16 @@ let isAjaxProcessing = false; function ajax_request(options) { - if (isAjaxProcessing) { + if (isAjaxProcessing && options.queueLock !== false) { // Instead of rejecting, we just return a "never-ending" promise // or a resolved promise that does nothing. console.warn("Request is busy... ignoring click."); return new Promise(() => { }); // This stays pending and won't trigger .then or .catch } - isAjaxProcessing = true; + if (options.queueLock !== false) { + isAjaxProcessing = true; + } // Auto prepare form data if (options.autoPrepare === true) { @@ -339,6 +341,7 @@ function ajax_request(options) { let isSendingFiles = (options.data instanceof FormData); if (options.debugMode) { + isAjaxProcessing = false; console.log("REQUEST DATA:", options.data); } diff --git a/app/assets/utils/db_auth.php b/app/assets/utils/db_auth.php index 931f080..242e77a 100644 --- a/app/assets/utils/db_auth.php +++ b/app/assets/utils/db_auth.php @@ -199,6 +199,14 @@ class WarehouseManager { $name = $this->getWarehouseName($warehouse_id); $table = "td_stock_" . $name; + if (empty($id)) { + return [ + 'table' => $table, + 'name' => $name, + 'row' => [] + ]; + } + $sql = "SELECT * FROM `$table` WHERE company_id = :company_id AND id = :id"; diff --git a/app/ics/api/engine_report/product_stats.php b/app/ics/api/engine_report/product_stats.php new file mode 100644 index 0000000..f3a52d5 --- /dev/null +++ b/app/ics/api/engine_report/product_stats.php @@ -0,0 +1,37 @@ +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)); + +?> \ No newline at end of file diff --git a/app/ics/api/engine_report/stock_activity.php b/app/ics/api/engine_report/stock_activity.php new file mode 100644 index 0000000..a34bae6 --- /dev/null +++ b/app/ics/api/engine_report/stock_activity.php @@ -0,0 +1,65 @@ +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)); + +?> \ No newline at end of file diff --git a/app/ics/api/engine_report/stock_movement.php b/app/ics/api/engine_report/stock_movement.php new file mode 100644 index 0000000..82a71c3 --- /dev/null +++ b/app/ics/api/engine_report/stock_movement.php @@ -0,0 +1,69 @@ +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)); + +?> \ No newline at end of file diff --git a/app/ics/api/engine_report/stock_trend.php b/app/ics/api/engine_report/stock_trend.php new file mode 100644 index 0000000..aa6af5c --- /dev/null +++ b/app/ics/api/engine_report/stock_trend.php @@ -0,0 +1,83 @@ +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)); + +?> \ No newline at end of file diff --git a/app/ics/api/engine_report/warehouse_balance.php b/app/ics/api/engine_report/warehouse_balance.php new file mode 100644 index 0000000..c4e9abd --- /dev/null +++ b/app/ics/api/engine_report/warehouse_balance.php @@ -0,0 +1,24 @@ +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)); + +?> \ No newline at end of file diff --git a/app/ics/api/engine/warehouse_stats.php b/app/ics/api/engine_report/warehouse_stats.php similarity index 100% rename from app/ics/api/engine/warehouse_stats.php rename to app/ics/api/engine_report/warehouse_stats.php diff --git a/app/ics/stock_overview.php b/app/ics/stock_overview.php index f59264c..ab307af 100644 --- a/app/ics/stock_overview.php +++ b/app/ics/stock_overview.php @@ -16,8 +16,16 @@
-

Stock Overview

+

Stock Overview

+
+
+ +
+
+ +
+
@@ -44,11 +52,11 @@
-
+
- Stock In (This Month) + Total Stock In

+456

Unit added

@@ -66,11 +74,11 @@
-
+
- Stock Out (This Month) + Total Stock Out

-289

Unit removed

@@ -88,7 +96,7 @@
-
+
@@ -113,20 +121,12 @@
-
+
-
Stock Movement
-
- - - - - -
@@ -142,11 +142,10 @@
-
+
-
Stock Level Trend
@@ -165,88 +164,13 @@
-
+

Recent Stock Activity

-
-
-
- - -
-
-
Product A
-

Stock In - -

-
-
-
- +50 - 2 hour ago -
- - -
-
-
-
- - -
-
-
Product B
-

Stock Out - -

-
-
-
- -25 - 4 hour ago -
- - -
-
-
-
- - -
-
-
Product C
-

Stock Out - -

-
-
-
- +100 - 6 hour ago -
- - -
-
-
-
- - -
-
-
Product D
-

Stock Out - -

-
-
-
- -30 - 8 hour ago -
- + +
+
@@ -262,19 +186,20 @@ diff --git a/app/include_header.php b/app/include_header.php index 91b0cb2..cef468c 100644 --- a/app/include_header.php +++ b/app/include_header.php @@ -43,6 +43,8 @@ // This kills the CDN's auto-scanner so it doesn't conflict with main.js Dropzone.autoDiscover = false; + +