Reports
This commit is contained in:
@@ -1250,4 +1250,183 @@ class ReportManager
|
|||||||
$sth->execute([':company_id' => $this->companyId]);
|
$sth->execute([':company_id' => $this->companyId]);
|
||||||
return $sth->fetch(PDO::FETCH_ASSOC) ?: [];
|
return $sth->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return paginated raw stock movement transactions from td_stock_<warehouse>
|
||||||
|
* within a date range (by month), with a balance brought forward.
|
||||||
|
*
|
||||||
|
* Each row is one individual transaction — no grouping. Full datetime is
|
||||||
|
* returned as stored in the td_stock_* table.
|
||||||
|
*
|
||||||
|
* Brought-forward balance is sourced from warehouse_balance for all months
|
||||||
|
* strictly before from_month, giving the cumulative net before the range.
|
||||||
|
*
|
||||||
|
* Used by reports/stock_movement.php.
|
||||||
|
*
|
||||||
|
* @param int $warehouse_id Warehouse to scope the report to.
|
||||||
|
* @param string $from_month Start of range in YYYY-MM format (inclusive).
|
||||||
|
* @param string $to_month End of range in YYYY-MM format (inclusive).
|
||||||
|
* @param int $page Current page number (1-based).
|
||||||
|
* @param int $per_page Rows per page (default 20).
|
||||||
|
* @return array Keys: rows (array), total_rows (int), brought_forward (float).
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Return summary stats + ordered SKU list for the stock movement report.
|
||||||
|
*
|
||||||
|
* Called once when the user clicks Search. Returns:
|
||||||
|
* - stats tile values (total_transactions, total_in, total_out, net)
|
||||||
|
* - ordered list of distinct product_skus present in the date range,
|
||||||
|
* used by the client to drive lazy-load requests one SKU at a time.
|
||||||
|
*
|
||||||
|
* @param int $warehouse_id
|
||||||
|
* @param string $from_month YYYY-MM inclusive start
|
||||||
|
* @param string $to_month YYYY-MM inclusive end
|
||||||
|
* @return array Keys: skus (string[]), total_transactions, total_in, total_out, net
|
||||||
|
*/
|
||||||
|
public function getStockMovementSummary(
|
||||||
|
int $warehouse_id,
|
||||||
|
string $from_month,
|
||||||
|
string $to_month
|
||||||
|
): array {
|
||||||
|
$table = $this->resolveWarehouseTable($warehouse_id);
|
||||||
|
if (!$table) return ['skus' => [], 'total_transactions' => 0, 'total_in' => 0, 'total_out' => 0, 'net' => 0];
|
||||||
|
|
||||||
|
$date_from = $from_month . '-01 00:00:00';
|
||||||
|
$date_to = date('Y-m-t 23:59:59', strtotime($to_month . '-01'));
|
||||||
|
|
||||||
|
// Ordered distinct SKUs in the range — drives lazy-load sequence
|
||||||
|
$sth = $this->pdo->prepare(
|
||||||
|
"SELECT DISTINCT product_sku
|
||||||
|
FROM `{$table}`
|
||||||
|
WHERE company_id = :company_id
|
||||||
|
AND date BETWEEN :date_from AND :date_to
|
||||||
|
ORDER BY product_sku ASC"
|
||||||
|
);
|
||||||
|
$sth->execute([':company_id' => $this->companyId, ':date_from' => $date_from, ':date_to' => $date_to]);
|
||||||
|
$skus = $sth->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
// Summary stats across full range
|
||||||
|
$sth = $this->pdo->prepare(
|
||||||
|
"SELECT
|
||||||
|
COUNT(*) AS total_transactions,
|
||||||
|
ROUND(SUM(COALESCE(`in`, 0)), 2) AS total_in,
|
||||||
|
ROUND(SUM(COALESCE(`out`, 0)), 2) AS total_out
|
||||||
|
FROM `{$table}`
|
||||||
|
WHERE company_id = :company_id
|
||||||
|
AND date BETWEEN :date_from AND :date_to"
|
||||||
|
);
|
||||||
|
$sth->execute([':company_id' => $this->companyId, ':date_from' => $date_from, ':date_to' => $date_to]);
|
||||||
|
$stats = $sth->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'skus' => $skus,
|
||||||
|
'total_transactions' => (int) ($stats['total_transactions'] ?? 0),
|
||||||
|
'total_in' => (float) ($stats['total_in'] ?? 0),
|
||||||
|
'total_out' => (float) ($stats['total_out'] ?? 0),
|
||||||
|
'net' => round(($stats['total_in'] ?? 0) - ($stats['total_out'] ?? 0), 2),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all transactions for a single product_sku within the date range,
|
||||||
|
* with a brought-forward balance and a running total column.
|
||||||
|
*
|
||||||
|
* Called once per SKU by the lazy-load scroll handler.
|
||||||
|
*
|
||||||
|
* Brought-forward: sum of (in - out) for this SKU in this warehouse
|
||||||
|
* for all months strictly before from_month.
|
||||||
|
*
|
||||||
|
* Running total: cumulative (brought_forward + in - out) across rows,
|
||||||
|
* computed in PHP after the DB fetch so no window function is needed.
|
||||||
|
*
|
||||||
|
* @param int $warehouse_id
|
||||||
|
* @param string $product_sku
|
||||||
|
* @param string $from_month YYYY-MM inclusive start
|
||||||
|
* @param string $to_month YYYY-MM inclusive end
|
||||||
|
* @return array Keys: sku, product_name, brought_forward (array with in/out/balance),
|
||||||
|
* rows (each row has running_total appended)
|
||||||
|
*/
|
||||||
|
public function getStockMovementBySku(
|
||||||
|
int $warehouse_id,
|
||||||
|
string $product_sku,
|
||||||
|
string $from_month,
|
||||||
|
string $to_month
|
||||||
|
): array {
|
||||||
|
$table = $this->resolveWarehouseTable($warehouse_id);
|
||||||
|
if (!$table) return ['sku' => $product_sku, 'rows' => [], 'brought_forward' => []];
|
||||||
|
|
||||||
|
$date_from = $from_month . '-01 00:00:00';
|
||||||
|
$date_to = date('Y-m-t 23:59:59', strtotime($to_month . '-01'));
|
||||||
|
|
||||||
|
// Brought-forward: all transactions for this SKU before the range
|
||||||
|
$sth = $this->pdo->prepare(
|
||||||
|
"SELECT
|
||||||
|
ROUND(COALESCE(SUM(COALESCE(`in`, 0)), 0), 2) AS bf_in,
|
||||||
|
ROUND(COALESCE(SUM(COALESCE(`out`, 0)), 0), 2) AS bf_out
|
||||||
|
FROM `{$table}`
|
||||||
|
WHERE company_id = :company_id
|
||||||
|
AND product_sku = :sku
|
||||||
|
AND date < :date_from"
|
||||||
|
);
|
||||||
|
$sth->execute([':company_id' => $this->companyId, ':sku' => $product_sku, ':date_from' => $date_from]);
|
||||||
|
$bf = $sth->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$bf_in = (float)($bf['bf_in'] ?? 0);
|
||||||
|
$bf_out = (float)($bf['bf_out'] ?? 0);
|
||||||
|
$bf_bal = round($bf_in - $bf_out, 2);
|
||||||
|
|
||||||
|
// Product name
|
||||||
|
$sth = $this->pdo->prepare(
|
||||||
|
"SELECT product_name FROM md_product
|
||||||
|
WHERE company_id = :company_id AND sku = :sku LIMIT 1"
|
||||||
|
);
|
||||||
|
$sth->execute([':company_id' => $this->companyId, ':sku' => $product_sku]);
|
||||||
|
$product_name = $sth->fetchColumn() ?: $product_sku;
|
||||||
|
|
||||||
|
// All transactions for this SKU in range, sorted by date then id
|
||||||
|
$sth = $this->pdo->prepare(
|
||||||
|
"SELECT
|
||||||
|
s.id,
|
||||||
|
s.date,
|
||||||
|
s.type,
|
||||||
|
s.product_sku,
|
||||||
|
s.lot_number,
|
||||||
|
s.serial_number,
|
||||||
|
s.zone,
|
||||||
|
s.aisle,
|
||||||
|
s.rack,
|
||||||
|
ROUND(COALESCE(s.`in`, 0), 2) AS stock_in,
|
||||||
|
ROUND(COALESCE(s.`out`, 0), 2) AS stock_out,
|
||||||
|
c.contact_name,
|
||||||
|
s.description
|
||||||
|
FROM `{$table}` s
|
||||||
|
LEFT JOIN md_contact c
|
||||||
|
ON c.company_id = s.company_id
|
||||||
|
AND c.id = s.contact_id
|
||||||
|
WHERE s.company_id = :company_id
|
||||||
|
AND s.product_sku = :sku
|
||||||
|
AND s.date BETWEEN :date_from AND :date_to
|
||||||
|
ORDER BY s.date ASC, s.id ASC"
|
||||||
|
);
|
||||||
|
$sth->execute([':company_id' => $this->companyId, ':sku' => $product_sku, ':date_from' => $date_from, ':date_to' => $date_to]);
|
||||||
|
$rows = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Append running_total to each row (carried forward from brought-forward balance)
|
||||||
|
$running = $bf_bal;
|
||||||
|
foreach ($rows as &$row) {
|
||||||
|
$running += (float)$row['stock_in'] - (float)$row['stock_out'];
|
||||||
|
$row['running_total'] = round($running, 2);
|
||||||
|
}
|
||||||
|
unset($row);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'sku' => $product_sku,
|
||||||
|
'product_name' => $product_name,
|
||||||
|
'brought_forward' => [
|
||||||
|
'in' => $bf_in,
|
||||||
|
'out' => $bf_out,
|
||||||
|
'balance' => $bf_bal,
|
||||||
|
],
|
||||||
|
'rows' => $rows,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -254,14 +254,15 @@ class StockManager {
|
|||||||
*/
|
*/
|
||||||
public function saveStockIn(array $data, array $logging, string $uuid): void
|
public function saveStockIn(array $data, array $logging, string $uuid): void
|
||||||
{
|
{
|
||||||
$id = (int)$data['id'];
|
$id = (int)($data['id'] ?? 0);
|
||||||
$warehouse_id = (int)$data['warehouse'];
|
$warehouse_id = (int)($data['warehouse'] ?? 0);
|
||||||
$whMgmt = new WarehouseManager($this->pdo, $this->company_id);
|
$whMgmt = new WarehouseManager($this->pdo, $this->company_id);
|
||||||
|
|
||||||
$ctx = $whMgmt->getStockContext($warehouse_id, $id);
|
$ctx = $whMgmt->getStockContext($warehouse_id, $id);
|
||||||
$row = $ctx['row'] ?? [];
|
$row = $ctx['row'] ?? [];
|
||||||
$table = $ctx['table'];
|
$table = $ctx['table'];
|
||||||
$table_log = json_decode($row['log'] ?? '[]', true) ?: [];
|
$raw_log = $row['log'] ?? [];
|
||||||
|
$table_log = is_array($raw_log) ? $raw_log : (json_decode($raw_log, true) ?: []);
|
||||||
$table_log[] = $logging;
|
$table_log[] = $logging;
|
||||||
|
|
||||||
if ($id > 0) {
|
if ($id > 0) {
|
||||||
@@ -276,8 +277,8 @@ class StockManager {
|
|||||||
)->execute([
|
)->execute([
|
||||||
':id' => $id,
|
':id' => $id,
|
||||||
':company_id' => $this->company_id,
|
':company_id' => $this->company_id,
|
||||||
':contact_id' => (int)$data['contact_id'],
|
':contact_id' => (int)($data['contact_id'] ?? 0),
|
||||||
':description' => $data['description'],
|
':description' => $data['description'] ?? '',
|
||||||
':log' => json_encode($table_log),
|
':log' => json_encode($table_log),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -316,11 +317,11 @@ class StockManager {
|
|||||||
':zone' => $data['zone'],
|
':zone' => $data['zone'],
|
||||||
':aisle' => $data['aisle'],
|
':aisle' => $data['aisle'],
|
||||||
':rack' => $data['rack'],
|
':rack' => $data['rack'],
|
||||||
':contact_id' => (int)$data['contact_id'],
|
':contact_id' => (int)($data['contact_id'] ?? 0),
|
||||||
':description' => $data['description'],
|
':description' => $data['description'] ?? '',
|
||||||
':log' => json_encode($table_log),
|
':log' => json_encode($table_log),
|
||||||
':lot_number' => $lot_number,
|
':lot_number' => $lot_number,
|
||||||
':serial_number' => $data['serial_number'] ?: null,
|
':serial_number' => ($data['serial_number'] ?? null) ?: null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$td_stock_id = (int)$this->pdo->lastInsertId();
|
$td_stock_id = (int)$this->pdo->lastInsertId();
|
||||||
@@ -366,14 +367,15 @@ class StockManager {
|
|||||||
*/
|
*/
|
||||||
public function saveStockOut(array $data, array $logging, string $uuid): void
|
public function saveStockOut(array $data, array $logging, string $uuid): void
|
||||||
{
|
{
|
||||||
$id = (int)$data['id'];
|
$id = (int)($data['id'] ?? 0);
|
||||||
$warehouse_id = (int)$data['warehouse'];
|
$warehouse_id = (int)($data['warehouse'] ?? 0);
|
||||||
$whMgmt = new WarehouseManager($this->pdo, $this->company_id);
|
$whMgmt = new WarehouseManager($this->pdo, $this->company_id);
|
||||||
|
|
||||||
$ctx = $whMgmt->getStockContext($warehouse_id, $id);
|
$ctx = $whMgmt->getStockContext($warehouse_id, $id);
|
||||||
$row = $ctx['row'] ?? [];
|
$row = $ctx['row'] ?? [];
|
||||||
$table = $ctx['table'];
|
$table = $ctx['table'];
|
||||||
$table_log = json_decode($row['log'] ?? '[]', true) ?: [];
|
$raw_log = $row['log'] ?? [];
|
||||||
|
$table_log = is_array($raw_log) ? $raw_log : (json_decode($raw_log, true) ?: []);
|
||||||
$table_log[] = $logging;
|
$table_log[] = $logging;
|
||||||
|
|
||||||
if ($id > 0) {
|
if ($id > 0) {
|
||||||
@@ -388,8 +390,8 @@ class StockManager {
|
|||||||
)->execute([
|
)->execute([
|
||||||
':id' => $id,
|
':id' => $id,
|
||||||
':company_id' => $this->company_id,
|
':company_id' => $this->company_id,
|
||||||
':contact_id' => (int)$data['contact_id'],
|
':contact_id' => (int)($data['contact_id'] ?? 0),
|
||||||
':description' => $data['description'],
|
':description' => $data['description'] ?? '',
|
||||||
':log' => json_encode($table_log),
|
':log' => json_encode($table_log),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -447,8 +449,8 @@ class StockManager {
|
|||||||
':zone' => $data['zone'],
|
':zone' => $data['zone'],
|
||||||
':aisle' => $data['aisle'],
|
':aisle' => $data['aisle'],
|
||||||
':rack' => $data['rack'],
|
':rack' => $data['rack'],
|
||||||
':contact_id' => (int)$data['contact_id'],
|
':contact_id' => (int)($data['contact_id'] ?? 0),
|
||||||
':description' => $data['description'],
|
':description' => $data['description'] ?? '',
|
||||||
':log' => json_encode($table_log),
|
':log' => json_encode($table_log),
|
||||||
':ref_id' => $ref_id,
|
':ref_id' => $ref_id,
|
||||||
':lot_number' => $lot_number,
|
':lot_number' => $lot_number,
|
||||||
@@ -504,7 +506,7 @@ class StockManager {
|
|||||||
$whMgmt = new WarehouseManager($this->pdo, $this->company_id);
|
$whMgmt = new WarehouseManager($this->pdo, $this->company_id);
|
||||||
|
|
||||||
$contact_id = (int)($data['contact_id'] ?? 0);
|
$contact_id = (int)($data['contact_id'] ?? 0);
|
||||||
$description = ($data['description'] ?? '');
|
$description = ($data['description'] ?? '' ?? '');
|
||||||
|
|
||||||
if ($id > 0) {
|
if ($id > 0) {
|
||||||
|
|
||||||
@@ -527,9 +529,11 @@ class StockManager {
|
|||||||
throw new Exception("Paired destination record missing — data integrity issue.");
|
throw new Exception("Paired destination record missing — data integrity issue.");
|
||||||
}
|
}
|
||||||
|
|
||||||
$from_log = json_decode($from_row['log'] ?? '[]', true) ?: [];
|
$raw_from = $from_row['log'] ?? [];
|
||||||
|
$from_log = is_array($raw_from) ? $raw_from : (json_decode($raw_from, true) ?: []);
|
||||||
$from_log[] = $logging;
|
$from_log[] = $logging;
|
||||||
$to_log = json_decode($to_row['log'] ?? '[]', true) ?: [];
|
$raw_to = $to_row['log'] ?? [];
|
||||||
|
$to_log = is_array($raw_to) ? $raw_to : (json_decode($raw_to, true) ?: []);
|
||||||
$to_log[] = $logging;
|
$to_log[] = $logging;
|
||||||
|
|
||||||
$this->pdo->prepare(
|
$this->pdo->prepare(
|
||||||
|
|||||||
@@ -34,9 +34,18 @@ class db_statement extends PDOStatement {
|
|||||||
if (!is_array($args)) {
|
if (!is_array($args)) {
|
||||||
$args = func_get_args();
|
$args = func_get_args();
|
||||||
}else{
|
}else{
|
||||||
|
// Cast all values to string before XSS processing.
|
||||||
|
// json_decode requires a string — integers, booleans, and nulls
|
||||||
|
// passed as bound parameters would otherwise cause a TypeError.
|
||||||
|
// null is preserved as-is so PDO can bind NULL columns correctly.
|
||||||
|
$args = array_map(fn($v) => is_null($v) ? null : (string)$v, $args);
|
||||||
|
|
||||||
// escaping array
|
// escaping array
|
||||||
// prevent store XSS
|
// prevent store XSS
|
||||||
foreach($args as &$item){
|
foreach($args as &$item){
|
||||||
|
|
||||||
|
if (is_null($item)) continue;
|
||||||
|
|
||||||
// decode the JSON data
|
// decode the JSON data
|
||||||
// set second parameter boolean TRUE for associative array output.
|
// set second parameter boolean TRUE for associative array output.
|
||||||
$result = json_decode($item);
|
$result = json_decode($item);
|
||||||
@@ -45,6 +54,12 @@ class db_statement extends PDOStatement {
|
|||||||
$tmp = json_decode($item,true);
|
$tmp = json_decode($item,true);
|
||||||
foreach((array)$tmp as &$ii){
|
foreach((array)$tmp as &$ii){
|
||||||
|
|
||||||
|
// Inner values may be arrays (nested JSON objects) — cast to string
|
||||||
|
if (!is_string($ii)) {
|
||||||
|
$ii = json_encode($ii);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$result = json_decode($ii);
|
$result = json_decode($ii);
|
||||||
if (json_last_error() === JSON_ERROR_NONE) {
|
if (json_last_error() === JSON_ERROR_NONE) {
|
||||||
// json inside json
|
// json inside json
|
||||||
|
|||||||
+29
-17
@@ -14,6 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
|
|
||||||
<!-- Main -->
|
<!-- Main -->
|
||||||
<li class="nav-text-space">
|
<li class="nav-text-space">
|
||||||
<small class="nav-text text-muted">Main</small>
|
<small class="nav-text text-muted">Main</small>
|
||||||
@@ -26,28 +27,14 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
|
||||||
<a class="nav-link" href="<?php echo $server_url?>dashboard/occupy_rack.php">
|
|
||||||
<i class="ti ti-receipt"></i>
|
|
||||||
<span class="nav-text">Occupied Racks</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<a class="nav-link" href="<?php echo $server_url?>dashboard/product_lot.php">
|
|
||||||
<i class="ti ti-receipt"></i>
|
|
||||||
<span class="nav-text">Product Lots</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a class="nav-link" href="<?php echo $server_url?>dashboard/low_stock_products.php">
|
<a class="nav-link" href="<?php echo $server_url?>dashboard/low_stock_products.php">
|
||||||
<i class="ti ti-alert-triangle"></i>
|
<i class="ti ti-alert-triangle"></i>
|
||||||
<span class="nav-text">Low Stock Products</span>
|
<span class="nav-text">Low Stock</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- Product -->
|
<!-- Master Data -->
|
||||||
<li class="nav-text-space">
|
<li class="nav-text-space">
|
||||||
<small class="nav-text text-muted">Master Data</small>
|
<small class="nav-text text-muted">Master Data</small>
|
||||||
</li>
|
</li>
|
||||||
@@ -106,15 +93,40 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<!-- Reports -->
|
||||||
|
<li class="nav-text-space">
|
||||||
|
<small class="nav-text text-muted">Reports</small>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a class="nav-link" href="<?php echo $server_url?>ics/expired_stock.php">
|
<a class="nav-link" href="<?php echo $server_url?>reports/stock_movement.php">
|
||||||
|
<i class="ti ti-chart-bar"></i>
|
||||||
|
<span class="nav-text">Stock Movement</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a class="nav-link" href="<?php echo $server_url?>reports/expired_stock.php">
|
||||||
<i class="ti ti-clock"></i>
|
<i class="ti ti-clock"></i>
|
||||||
<span class="nav-text">Expired Stock</span>
|
<span class="nav-text">Expired Stock</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a class="nav-link" href="<?php echo $server_url?>reports/occupy_rack.php">
|
||||||
|
<i class="ti ti-layout-grid"></i>
|
||||||
|
<span class="nav-text">Rack Occupancy</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a class="nav-link" href="<?php echo $server_url?>reports/product_lot.php">
|
||||||
|
<i class="ti ti-packages"></i>
|
||||||
|
<span class="nav-text">Product Lots</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li><br><br><br><br></li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
+3
-1
@@ -6,7 +6,9 @@
|
|||||||
|
|
||||||
$report = new ReportManager($pdo2, $company_id);
|
$report = new ReportManager($pdo2, $company_id);
|
||||||
|
|
||||||
$items = $report->getExpiredStock();
|
$warehouse_id = (int)($data['warehouse_id'] ?? 0);
|
||||||
|
|
||||||
|
$items = $report->getExpiredStock($warehouse_id);
|
||||||
|
|
||||||
$expired = 0;
|
$expired = 0;
|
||||||
$critical = 0;
|
$critical = 0;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require '../../../assets/utils/db_auth.php';
|
||||||
|
require '../../../assets/utils/classes/ReportManager.php';
|
||||||
|
|
||||||
|
$data = json_decode($_POST['json'] ?? '{}', true) ?: [];
|
||||||
|
$warehouse_id = (int)($data['warehouse_id'] ?? 0);
|
||||||
|
$from_month = $data['from_month'] ?? '';
|
||||||
|
$to_month = $data['to_month'] ?? '';
|
||||||
|
|
||||||
|
if (!$warehouse_id || !$from_month || !$to_month) {
|
||||||
|
$answer['message'] = 'warehouse_id, from_month, and to_month are required.';
|
||||||
|
exit(json_encode($answer));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($from_month > $to_month) [$from_month, $to_month] = [$to_month, $from_month];
|
||||||
|
|
||||||
|
$report = new ReportManager($pdo2, $company_id);
|
||||||
|
|
||||||
|
$answer['output'] = $report->getStockMovementSummary($warehouse_id, $from_month, $to_month);
|
||||||
|
$answer['success'] = 1;
|
||||||
|
exit(json_encode($answer));
|
||||||
|
?>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require '../../../assets/utils/db_auth.php';
|
||||||
|
require '../../../assets/utils/classes/ReportManager.php';
|
||||||
|
|
||||||
|
$data = json_decode($_POST['json'] ?? '{}', true) ?: [];
|
||||||
|
$warehouse_id = (int)($data['warehouse_id'] ?? 0);
|
||||||
|
$product_sku = trim($data['product_sku'] ?? '');
|
||||||
|
$from_month = $data['from_month'] ?? '';
|
||||||
|
$to_month = $data['to_month'] ?? '';
|
||||||
|
|
||||||
|
if (!$warehouse_id || !$product_sku || !$from_month || !$to_month) {
|
||||||
|
$answer['message'] = 'warehouse_id, product_sku, from_month, and to_month are required.';
|
||||||
|
exit(json_encode($answer));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($from_month > $to_month) [$from_month, $to_month] = [$to_month, $from_month];
|
||||||
|
|
||||||
|
$report = new ReportManager($pdo2, $company_id);
|
||||||
|
|
||||||
|
$answer['output'] = $report->getStockMovementBySku($warehouse_id, $product_sku, $from_month, $to_month);
|
||||||
|
$answer['success'] = 1;
|
||||||
|
exit(json_encode($answer));
|
||||||
|
?>
|
||||||
@@ -202,9 +202,12 @@
|
|||||||
if (!warehouse_id) return;
|
if (!warehouse_id) return;
|
||||||
|
|
||||||
return ajax_request({
|
return ajax_request({
|
||||||
url: "<?php echo $server_url?>ics/api/engine_report/expired_stock.php",
|
url: "<?php echo $server_url?>reports/api/engine_report/expired_stock.php",
|
||||||
autoPrepare: true,
|
autoPrepare: true,
|
||||||
checkRequired: 0,
|
checkRequired: 0,
|
||||||
|
data: {
|
||||||
|
warehouse_id: warehouse_id
|
||||||
|
},
|
||||||
action: 'read',
|
action: 'read',
|
||||||
debugMode: 0,
|
debugMode: 0,
|
||||||
onSuccess: function(res) {
|
onSuccess: function(res) {
|
||||||
@@ -272,7 +272,7 @@
|
|||||||
function retrieve_rack_occupancy() {
|
function retrieve_rack_occupancy() {
|
||||||
|
|
||||||
return ajax_request({
|
return ajax_request({
|
||||||
url: "<?php echo $server_url?>dashboard/api/engine_report/rack_occupancy.php",
|
url: "<?php echo $server_url?>reports/api/engine_report/rack_occupancy.php",
|
||||||
autoPrepare: true,
|
autoPrepare: true,
|
||||||
checkRequired: 0,
|
checkRequired: 0,
|
||||||
action: 'read',
|
action: 'read',
|
||||||
@@ -476,7 +476,7 @@
|
|||||||
modal.show();
|
modal.show();
|
||||||
|
|
||||||
ajax_request({
|
ajax_request({
|
||||||
url: "<?php echo $server_url?>dashboard/api/engine_report/rack_log.php",
|
url: "<?php echo $server_url?>reports/api/engine_report/rack_log.php",
|
||||||
autoPrepare: true,
|
autoPrepare: true,
|
||||||
checkRequired: 0,
|
checkRequired: 0,
|
||||||
action: 'read',
|
action: 'read',
|
||||||
@@ -200,7 +200,7 @@
|
|||||||
function retrieve_product_lot() {
|
function retrieve_product_lot() {
|
||||||
|
|
||||||
return ajax_request({
|
return ajax_request({
|
||||||
url: "<?php echo $server_url?>dashboard/api/engine_report/product_lot.php",
|
url: "<?php echo $server_url?>reports/api/engine_report/product_lot.php",
|
||||||
autoPrepare: true,
|
autoPrepare: true,
|
||||||
checkRequired: 0,
|
checkRequired: 0,
|
||||||
action: 'read',
|
action: 'read',
|
||||||
@@ -324,7 +324,7 @@
|
|||||||
modal.show();
|
modal.show();
|
||||||
|
|
||||||
ajax_request({
|
ajax_request({
|
||||||
url: "<?php echo $server_url?>dashboard/api/engine_report/lot_stock_log.php",
|
url: "<?php echo $server_url?>reports/api/engine_report/lot_stock_log.php",
|
||||||
autoPrepare: true,
|
autoPrepare: true,
|
||||||
checkRequired: 0,
|
checkRequired: 0,
|
||||||
action: 'read',
|
action: 'read',
|
||||||
@@ -0,0 +1,374 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require '../config.php';
|
||||||
|
require '../include_header.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<?php require '../include_topbar.php';?>
|
||||||
|
<?php require '../include_sidebar.php';?>
|
||||||
|
|
||||||
|
<main id="content" class="content py-15">
|
||||||
|
<div class="container-fluid">
|
||||||
|
|
||||||
|
<!-- Page header -->
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3">
|
||||||
|
<div>
|
||||||
|
<h1 class="fs-4 mb-0">Stock Movement</h1>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2 flex-nowrap">
|
||||||
|
<select id="warehouse" class="form-select form-select-sm" style="min-width:140px;max-width:180px;">
|
||||||
|
<option value="">Warehouse...</option>
|
||||||
|
</select>
|
||||||
|
<input type="month" id="from_month" class="form-control form-control-sm" style="width:150px;">
|
||||||
|
<span class="text-muted small text-nowrap">to</span>
|
||||||
|
<input type="month" id="to_month" class="form-control form-control-sm" style="width:150px;">
|
||||||
|
<button class="btn btn-primary btn-sm text-nowrap" onclick="do_search();">
|
||||||
|
<i class="ti ti-search"></i> Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Summary stats -->
|
||||||
|
<div class="row mb-4 g-3">
|
||||||
|
<div class="col-xl-3 col-md-6 col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-4 d-flex align-items-center gap-3">
|
||||||
|
<span class="icon-shape icon-sm bg-primary text-white rounded-2">
|
||||||
|
<i class="ti ti-list fs-5"></i>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<div class="fs-3 fw-bold mb-0" id="stat_transactions">—</div>
|
||||||
|
<div class="text-muted small">Total Transactions</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-3 col-md-6 col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-4 d-flex align-items-center gap-3">
|
||||||
|
<span class="icon-shape icon-sm bg-success text-white rounded-2">
|
||||||
|
<i class="ti ti-package-import fs-5"></i>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<div class="fs-3 fw-bold mb-0 text-success" id="stat_in">—</div>
|
||||||
|
<div class="text-muted small">Total Stock In</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-3 col-md-6 col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-4 d-flex align-items-center gap-3">
|
||||||
|
<span class="icon-shape icon-sm bg-danger text-white rounded-2">
|
||||||
|
<i class="ti ti-package-export fs-5"></i>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<div class="fs-3 fw-bold mb-0 text-danger" id="stat_out">—</div>
|
||||||
|
<div class="text-muted small">Total Stock Out</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-3 col-md-6 col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-4 d-flex align-items-center gap-3">
|
||||||
|
<span class="icon-shape icon-sm bg-warning text-white rounded-2">
|
||||||
|
<i class="ti ti-arrows-diff fs-5"></i>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<div class="fs-3 fw-bold mb-0" id="stat_net">—</div>
|
||||||
|
<div class="text-muted small">Net Movement</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="fs-4 mb-0">Movement Detail</h2>
|
||||||
|
<span id="result_summary" class="text-muted small"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table mb-0 text-nowrap table-hover table-centered table-sm" id="tbl_movement">
|
||||||
|
<thead class="table-light border-light">
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>SKU</th>
|
||||||
|
<th>Lot</th>
|
||||||
|
<th>Location</th>
|
||||||
|
<th>Contact</th>
|
||||||
|
<th class="text-end">Stock In</th>
|
||||||
|
<th class="text-end">Stock Out</th>
|
||||||
|
<th class="text-end">Running Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tbl_body">
|
||||||
|
<tr>
|
||||||
|
<td colspan="9" class="text-center py-5 text-muted">
|
||||||
|
Select a warehouse and date range, then click Search.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Lazy load sentinel + loader -->
|
||||||
|
<div id="lazy_sentinel" class="py-3 text-center text-muted small" style="display:none;">
|
||||||
|
<div id="lazy_loader"><i class="ti ti-loader-2 me-1"></i>Loading...</div>
|
||||||
|
<div id="lazy_done" style="display:none;">All products loaded.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<?php require "../include_ending.php";?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ── State ─────────────────────────────────────────────────────────────
|
||||||
|
var sku_queue = []; // ordered list of SKUs returned by summary call
|
||||||
|
var sku_index = 0; // next SKU to load
|
||||||
|
var is_loading = false;
|
||||||
|
var search_params = {}; // warehouse_id, from_month, to_month — fixed per search
|
||||||
|
|
||||||
|
var type_badge = {
|
||||||
|
'in': '<span class="badge bg-success">In</span>',
|
||||||
|
'out': '<span class="badge bg-danger">Out</span>',
|
||||||
|
'transfer': '<span class="badge bg-info text-dark">Transfer</span>',
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Default months ────────────────────────────────────────────────────
|
||||||
|
(function() {
|
||||||
|
var now = new Date();
|
||||||
|
var month = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0');
|
||||||
|
$('#from_month').val(month);
|
||||||
|
$('#to_month').val(month);
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
// ── Warehouse dropdown ────────────────────────────────────────────────
|
||||||
|
function retrieve_warehouse() {
|
||||||
|
return ajax_request({
|
||||||
|
url: "<?php echo $server_url?>ics/api/engine/retrieve_warehouse.php",
|
||||||
|
autoPrepare: true, checkRequired: 0, action: 'read',
|
||||||
|
onSuccess: function(res) {
|
||||||
|
var options = '';
|
||||||
|
$.each(res.output, function(i, item) {
|
||||||
|
options += `<option value="${item.id}">${item.warehouse_name}</option>`;
|
||||||
|
});
|
||||||
|
$('#warehouse').html(options);
|
||||||
|
if (res.output && res.output.length > 0) {
|
||||||
|
$('#warehouse').val(res.output[0].id);
|
||||||
|
do_search();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Search — fetch summary, reset state, start lazy load ─────────────
|
||||||
|
function do_search() {
|
||||||
|
var warehouse_id = $('#warehouse').val();
|
||||||
|
var from_month = $('#from_month').val();
|
||||||
|
var to_month = $('#to_month').val();
|
||||||
|
if (!warehouse_id || !from_month || !to_month) return;
|
||||||
|
|
||||||
|
// Reset state
|
||||||
|
sku_queue = [];
|
||||||
|
sku_index = 0;
|
||||||
|
is_loading = false;
|
||||||
|
search_params = { warehouse_id: warehouse_id, from_month: from_month, to_month: to_month };
|
||||||
|
|
||||||
|
// Reset UI
|
||||||
|
$('#tbl_body').html('<tr><td colspan="9" class="text-center py-4 text-muted">Loading…</td></tr>');
|
||||||
|
$('#lazy_sentinel').hide();
|
||||||
|
$('#lazy_done').hide();
|
||||||
|
$('#lazy_loader').show();
|
||||||
|
$('#result_summary').text('');
|
||||||
|
|
||||||
|
// Reset stats
|
||||||
|
$('#stat_transactions, #stat_in, #stat_out, #stat_net').text('—');
|
||||||
|
|
||||||
|
return ajax_request({
|
||||||
|
url: "<?php echo $server_url?>reports/api/engine_report/stock_movement.php",
|
||||||
|
autoPrepare: true, checkRequired: 0, action: 'read',
|
||||||
|
data: { warehouse_id: warehouse_id, from_month: from_month, to_month: to_month },
|
||||||
|
onSuccess: function(res) {
|
||||||
|
var d = res.output;
|
||||||
|
|
||||||
|
// Populate stats
|
||||||
|
$('#stat_transactions').text(d.total_transactions);
|
||||||
|
$('#stat_in').text(fmt(d.total_in));
|
||||||
|
$('#stat_out').text(fmt(d.total_out));
|
||||||
|
var net = d.net;
|
||||||
|
$('#stat_net')
|
||||||
|
.text((net >= 0 ? '+' : '') + fmt(net))
|
||||||
|
.removeClass('text-success text-danger')
|
||||||
|
.addClass(net >= 0 ? 'text-success' : 'text-danger');
|
||||||
|
|
||||||
|
if (!d.skus || d.skus.length === 0) {
|
||||||
|
$('#tbl_body').html('<tr><td colspan="9" class="text-center py-5 text-muted">No movement data found.</td></tr>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sku_queue = d.skus;
|
||||||
|
sku_index = 0;
|
||||||
|
$('#tbl_body').html('');
|
||||||
|
$('#result_summary').text(d.skus.length + ' product' + (d.skus.length > 1 ? 's' : ''));
|
||||||
|
$('#lazy_sentinel').show();
|
||||||
|
|
||||||
|
// Start loading — sentinel_is_visible() will keep pulling until window is filled
|
||||||
|
load_next_sku();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Load one SKU group, then keep going if sentinel still visible ─────
|
||||||
|
function load_next_sku() {
|
||||||
|
if (is_loading || sku_index >= sku_queue.length) {
|
||||||
|
if (sku_index >= sku_queue.length) {
|
||||||
|
$('#lazy_loader').hide();
|
||||||
|
$('#lazy_done').show();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
is_loading = true;
|
||||||
|
var sku = sku_queue[sku_index];
|
||||||
|
|
||||||
|
return ajax_request({
|
||||||
|
url: "<?php echo $server_url?>reports/api/engine_report/stock_movement_sku.php",
|
||||||
|
autoPrepare: true, checkRequired: 0, action: 'read',
|
||||||
|
queueLock: false,
|
||||||
|
data: {
|
||||||
|
warehouse_id : search_params.warehouse_id,
|
||||||
|
product_sku : sku,
|
||||||
|
from_month : search_params.from_month,
|
||||||
|
to_month : search_params.to_month,
|
||||||
|
},
|
||||||
|
onSuccess: function(res) {
|
||||||
|
append_sku_group(res.output);
|
||||||
|
sku_index++;
|
||||||
|
is_loading = false;
|
||||||
|
|
||||||
|
if (sku_index >= sku_queue.length) {
|
||||||
|
$('#lazy_loader').hide();
|
||||||
|
$('#lazy_done').show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If sentinel is still within the viewport (or above it),
|
||||||
|
// the table hasn't filled the window yet — keep loading immediately.
|
||||||
|
if (sentinel_is_visible()) {
|
||||||
|
load_next_sku();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Returns true if the sentinel is visible in the current viewport ───
|
||||||
|
function sentinel_is_visible() {
|
||||||
|
var el = document.getElementById('lazy_sentinel');
|
||||||
|
var rect = el.getBoundingClientRect();
|
||||||
|
return rect.top <= (window.innerHeight || document.documentElement.clientHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Render one SKU group into the table ───────────────────────────────
|
||||||
|
function append_sku_group(d) {
|
||||||
|
var bf = d.brought_forward;
|
||||||
|
var rows = d.rows;
|
||||||
|
|
||||||
|
// SKU header row
|
||||||
|
var header = `<tr class="table-secondary">
|
||||||
|
<td colspan="9" class="py-2 fw-semibold">
|
||||||
|
<code>${d.sku}</code>
|
||||||
|
<span class="text-muted fw-normal ms-2">${d.product_name}</span>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
|
||||||
|
// Brought-forward row
|
||||||
|
var bf_in_val = bf.in > 0 ? fmt(bf.in) : '<span class="text-muted">—</span>';
|
||||||
|
var bf_out_val = bf.out > 0 ? fmt(bf.out) : '<span class="text-muted">—</span>';
|
||||||
|
var bf_bal_cls = bf.balance >= 0 ? 'text-success' : 'text-danger';
|
||||||
|
var bf_row = `<tr class="table-light text-muted fst-italic">
|
||||||
|
<td class="py-2" colspan="6">Brought Forward</td>
|
||||||
|
<td class="py-2 text-end text-success">${bf_in_val}</td>
|
||||||
|
<td class="py-2 text-end text-danger">${bf_out_val}</td>
|
||||||
|
<td class="py-2 text-end fw-semibold ${bf_bal_cls}">${fmt(bf.balance)}</td>
|
||||||
|
</tr>`;
|
||||||
|
|
||||||
|
// Transaction rows
|
||||||
|
var body = '';
|
||||||
|
if (rows.length === 0) {
|
||||||
|
body = `<tr>
|
||||||
|
<td colspan="9" class="text-center py-2 text-muted fst-italic">No transactions in range.</td>
|
||||||
|
</tr>`;
|
||||||
|
} else {
|
||||||
|
$.each(rows, function(i, row) {
|
||||||
|
var in_val = row.stock_in > 0 ? fmt(row.stock_in) : '<span class="text-muted">—</span>';
|
||||||
|
var out_val = row.stock_out > 0 ? fmt(row.stock_out) : '<span class="text-muted">—</span>';
|
||||||
|
var rt_cls = row.running_total >= 0 ? 'text-success' : 'text-danger';
|
||||||
|
body += `<tr>
|
||||||
|
<td class="py-2">${row.date}</td>
|
||||||
|
<td class="py-2">${type_badge[row.type] || row.type}</td>
|
||||||
|
<td class="py-2"><code>${row.product_sku}</code></td>
|
||||||
|
<td class="py-2">${row.lot_number || '<span class="text-muted">—</span>'}</td>
|
||||||
|
<td class="py-2">${row.zone}-${row.aisle}-${row.rack}</td>
|
||||||
|
<td class="py-2">${row.contact_name || '<span class="text-muted">—</span>'}</td>
|
||||||
|
<td class="py-2 text-end text-success">${in_val}</td>
|
||||||
|
<td class="py-2 text-end text-danger">${out_val}</td>
|
||||||
|
<td class="py-2 text-end fw-semibold ${rt_cls}">${fmt(row.running_total)}</td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#tbl_body').append(header + bf_row + body);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Intersection Observer for lazy load ───────────────────────────────
|
||||||
|
var observer = new IntersectionObserver(function(entries) {
|
||||||
|
if (entries[0].isIntersecting && !is_loading && sku_index < sku_queue.length) {
|
||||||
|
load_next_sku();
|
||||||
|
}
|
||||||
|
}, { rootMargin: '200px' });
|
||||||
|
|
||||||
|
observer.observe(document.getElementById('lazy_sentinel'));
|
||||||
|
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────
|
||||||
|
function fmt(n) {
|
||||||
|
return parseFloat(n || 0).toLocaleString(undefined, {
|
||||||
|
minimumFractionDigits: 2, maximumFractionDigits: 2
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Boot ──────────────────────────────────────────────────────────────
|
||||||
|
$(async function() {
|
||||||
|
try { await retrieve_warehouse(); } catch(e) { console.log(e); }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user