Lot, Serial, ExpiryDate

This commit is contained in:
Thanakorn S
2026-04-25 14:37:00 +07:00
parent 92d116fd21
commit 3cc5baba3f
35 changed files with 3518 additions and 1890 deletions
+33 -13
View File
@@ -42,25 +42,45 @@
} else {
// Upsert md_lot if lot_number + expiry_date provided
$lot_number = $data["lot_number"] ?: null;
$expiry_date = $data["expiry_date"] ?: null;
if ($lot_number && $expiry_date) {
$sth = $pdo->prepare("
INSERT INTO md_lot (company_id, product_sku, lot_number, expiry_date)
VALUES (:company_id, :product_sku, :lot_number, :expiry_date)
ON DUPLICATE KEY UPDATE expiry_date = expiry_date
");
$sth->execute([
":company_id" => $company_id,
":product_sku" => $data["product_sku"],
":lot_number" => $lot_number,
":expiry_date" => $expiry_date,
]);
}
// INSERT new stock_in record
$sql = "INSERT INTO `$table`
(uuid, company_id, `date`, product_sku, `in`, zone, aisle, rack, contact_id, `description`, `log`, `type`)
(uuid, company_id, `date`, product_sku, `in`, zone, aisle, rack, contact_id, `description`, `log`, `type`, `lot_number`, `serial_number`)
VALUES
(:uuid, :company_id, :date, :product_sku, :quantity, :zone, :aisle, :rack, :contact_id, :description, :log, 'in')";
(:uuid, :company_id, :date, :product_sku, :quantity, :zone, :aisle, :rack, :contact_id, :description, :log, 'in', :lot_number, :serial_number)";
$sth = $pdo->prepare($sql);
$sth->execute([
":uuid" => $uuid,
":company_id" => $company_id,
":date" => date('Y-m-d H:i:s'),
":product_sku" => $data["product_sku"],
":quantity" => $data["quantity"],
":zone" => $data["zone"],
":aisle" => $data["aisle"],
":rack" => $data["rack"],
":contact_id" => (int)$data["contact_id"],
":description" => $data["description"],
":log" => json_encode($table_log),
":uuid" => $uuid,
":company_id" => $company_id,
":date" => date('Y-m-d H:i:s'),
":product_sku" => $data["product_sku"],
":quantity" => $data["quantity"],
":zone" => $data["zone"],
":aisle" => $data["aisle"],
":rack" => $data["rack"],
":contact_id" => (int)$data["contact_id"],
":description" => $data["description"],
":log" => json_encode($table_log),
":lot_number" => $lot_number,
":serial_number" => $data["serial_number"] ?: null,
]);
// Grab the newly-generated ID to link the rack
+36 -17
View File
@@ -54,41 +54,60 @@
);
}
// Defensive check — UI cascade should already prevent mismatches
// Defensive checks — UI cascade should already prevent mismatches
if ($source_stock['product_sku'] !== $data["product_sku"]) {
throw new Exception(
"Rack holds {$source_stock['product_sku']}, not {$data['product_sku']}."
);
}
// If user selected a specific lot, verify it matches the rack's stock
if (!empty($data["lot_number"]) && $source_stock['lot_number'] !== $data["lot_number"]) {
throw new Exception(
"Rack holds lot '{$source_stock['lot_number']}', not '{$data['lot_number']}'."
);
}
// If user selected a specific serial, verify it matches the rack's stock
if (!empty($data["serial_number"]) && $source_stock['serial_number'] !== $data["serial_number"]) {
throw new Exception(
"Rack holds serial '{$source_stock['serial_number']}', not '{$data['serial_number']}'."
);
}
// Quantity comes from the source stock_in row, NOT user input.
// Under 1:1, taking from this rack = taking the full batch.
$quantity = (int) $source_stock['in'];
$ref_id = (int) $source_stock['id'];
$quantity = (int) $source_stock['in'];
$ref_id = (int) $source_stock['id'];
$lot_number = $source_stock['lot_number'] ?? null;
$serial_number = $source_stock['serial_number'] ?? null;
// INSERT new stock_out record.
// lot_number and serial_number carried from source stock_in row.
// ref_id links this stock_out row back to the stock_in batch it consumed.
$sql = "INSERT INTO `$table`
(uuid, company_id, `date`, product_sku, `out`, zone, aisle, rack,
contact_id, `description`, `log`, `type`, ref_id)
contact_id, `description`, `log`, `type`, ref_id, lot_number, serial_number)
VALUES
(:uuid, :company_id, :date, :product_sku, :quantity, :zone, :aisle, :rack,
:contact_id, :description, :log, 'out', :ref_id)";
:contact_id, :description, :log, 'out', :ref_id, :lot_number, :serial_number)";
$sth = $pdo->prepare($sql);
$sth->execute([
":uuid" => $uuid,
":company_id" => $company_id,
":date" => date('Y-m-d H:i:s'),
":product_sku" => $data["product_sku"],
":quantity" => $quantity,
":zone" => $data["zone"],
":aisle" => $data["aisle"],
":rack" => $data["rack"],
":contact_id" => (int)$data["contact_id"],
":description" => $data["description"],
":log" => json_encode($table_log),
":ref_id" => $ref_id,
":uuid" => $uuid,
":company_id" => $company_id,
":date" => date('Y-m-d H:i:s'),
":product_sku" => $data["product_sku"],
":quantity" => $quantity,
":zone" => $data["zone"],
":aisle" => $data["aisle"],
":rack" => $data["rack"],
":contact_id" => (int)$data["contact_id"],
":description" => $data["description"],
":log" => json_encode($table_log),
":ref_id" => $ref_id,
":lot_number" => $lot_number,
":serial_number" => $serial_number,
]);
// Release the rack — stock is now gone from this location.
+23 -5
View File
@@ -117,7 +117,21 @@
);
}
$quantity = (int) $source_stock['in'];
// Defensive lot/serial checks
if (!empty($data["lot_number"]) && $source_stock['lot_number'] !== $data["lot_number"]) {
throw new Exception(
"Source rack holds lot '{$source_stock['lot_number']}', not '{$data['lot_number']}'."
);
}
if (!empty($data["serial_number"]) && $source_stock['serial_number'] !== $data["serial_number"]) {
throw new Exception(
"Source rack holds serial '{$source_stock['serial_number']}', not '{$data['serial_number']}'."
);
}
$quantity = (int) $source_stock['in'];
$lot_number = $source_stock['lot_number'] ?? null;
$serial_number = $source_stock['serial_number'] ?? null;
// Resolve both td_stock_<wh> tables
$from_ctx = $whMgmt->getStockContext($from_warehouse, 0);
@@ -134,11 +148,11 @@
$sql = "INSERT INTO `$from_table`
(uuid, company_id, `date`, product_sku, `out`,
ref_warehouse, zone, aisle, rack,
contact_id, `description`, `log`, `type`)
contact_id, `description`, `log`, `type`, lot_number, serial_number)
VALUES
(:uuid, :company_id, :date, :product_sku, :quantity,
:ref_warehouse, :zone, :aisle, :rack,
:contact_id, :description, :log, 'transfer')";
:contact_id, :description, :log, 'transfer', :lot_number, :serial_number)";
$pdo->prepare($sql)->execute([
":uuid" => $transfer_uuid,
":company_id" => $company_id,
@@ -152,6 +166,8 @@
":contact_id" => $contact_id,
":description" => $description,
":log" => json_encode($table_log),
":lot_number" => $lot_number,
":serial_number" => $serial_number,
]);
$from_stock_id = (int) $pdo->lastInsertId();
@@ -159,11 +175,11 @@
$sql = "INSERT INTO `$to_table`
(uuid, company_id, `date`, product_sku, `in`,
ref_warehouse, ref_id, zone, aisle, rack,
contact_id, `description`, `log`, `type`)
contact_id, `description`, `log`, `type`, lot_number, serial_number)
VALUES
(:uuid, :company_id, :date, :product_sku, :quantity,
:ref_warehouse, :ref_id, :zone, :aisle, :rack,
:contact_id, :description, :log, 'transfer')";
:contact_id, :description, :log, 'transfer', :lot_number, :serial_number)";
$pdo->prepare($sql)->execute([
":uuid" => $transfer_uuid,
":company_id" => $company_id,
@@ -178,6 +194,8 @@
":contact_id" => $contact_id,
":description" => $description,
":log" => json_encode($table_log),
":lot_number" => $lot_number,
":serial_number" => $serial_number,
]);
$to_stock_id = (int) $pdo->lastInsertId();
@@ -0,0 +1,64 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/WarehouseManager.php';
// Returns active lots for a given product_sku
// "Active" = EXISTS in an occupied md_rack (product_sku IS NOT NULL)
$product_sku = $data['product_sku'] ?? '';
if (empty($product_sku)) {
$answer['output'] = [];
$answer['success'] = 1;
exit(json_encode($answer));
}
// Discover all warehouses
$sth = $pdo2->prepare(
"SELECT id, warehouse_name FROM md_warehouse
WHERE company_id = :company_id AND status = 1"
);
$sth->execute([':company_id' => $company_id]);
$warehouses = $sth->fetchAll(PDO::FETCH_ASSOC);
$active_lots = [];
foreach ($warehouses as $wh) {
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $wh['warehouse_name']);
$table = "td_stock_{$safe}";
$sth = $pdo2->prepare(
"SELECT DISTINCT s.lot_number, l.expiry_date
FROM `{$table}` s
LEFT JOIN md_lot l
ON l.company_id = s.company_id
AND l.product_sku = s.product_sku
AND l.lot_number = s.lot_number
WHERE s.company_id = :company_id
AND s.product_sku = :product_sku
AND s.type = 'in'
AND s.lot_number IS NOT NULL
AND EXISTS (
SELECT 1 FROM md_rack r
WHERE r.company_id = s.company_id
AND r.td_stock_id = s.id
AND r.product_sku IS NOT NULL
)"
);
$sth->execute([
':company_id' => $company_id,
':product_sku' => $product_sku,
]);
foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $row) {
$key = $row['lot_number'];
if (!isset($active_lots[$key])) {
$active_lots[$key] = $row;
}
}
}
$answer['output'] = array_values($active_lots);
$answer['success'] = 1;
exit(json_encode($answer));
?>
@@ -0,0 +1,64 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
// Returns active serials for a given product_sku + optional lot_number
// "Active" = EXISTS in an occupied md_rack
$product_sku = $data['product_sku'] ?? '';
$lot_number = $data['lot_number'] ?? '';
if (empty($product_sku)) {
$answer['output'] = [];
$answer['success'] = 1;
exit(json_encode($answer));
}
// Discover all warehouses
$sth = $pdo2->prepare(
"SELECT id, warehouse_name FROM md_warehouse
WHERE company_id = :company_id AND status = 1"
);
$sth->execute([':company_id' => $company_id]);
$warehouses = $sth->fetchAll(PDO::FETCH_ASSOC);
$active_serials = [];
foreach ($warehouses as $wh) {
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $wh['warehouse_name']);
$table = "td_stock_{$safe}";
$sql = "SELECT DISTINCT s.serial_number
FROM `{$table}` s
WHERE s.company_id = :company_id
AND s.product_sku = :product_sku
AND s.type = 'in'
AND s.serial_number IS NOT NULL
AND EXISTS (
SELECT 1 FROM md_rack r
WHERE r.company_id = s.company_id
AND r.td_stock_id = s.id
AND r.product_sku IS NOT NULL
)";
$params = [
':company_id' => $company_id,
':product_sku' => $product_sku,
];
if (!empty($lot_number)) {
$sql .= " AND s.lot_number = :lot_number";
$params[':lot_number'] = $lot_number;
}
$sth = $pdo2->prepare($sql);
$sth->execute($params);
foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $row) {
$active_serials[$row['serial_number']] = $row['serial_number'];
}
}
$answer['output'] = array_values($active_serials);
$answer['success'] = 1;
exit(json_encode($answer));
?>
+42 -55
View File
@@ -1,64 +1,51 @@
<?php
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/WarehouseManager.php';
// Get warehouse and zone from the form
$warehouse = isset($data['warehouse']) ? (int)$data['warehouse'] : 0;
$zone = isset($data['zone']) ? $data['zone'] : '';
if (isset($data['warehouse_from']) && $data['type'] == "from") {
$warehouse = (int)$data['warehouse_from'];
$zone = $data['zone_from'];
}
if (isset($data['warehouse_to']) && $data['type'] == "to") {
$warehouse = (int)$data['warehouse_to'];
$zone = $data['zone_to'];
$type = $data['type'] ?? '';
$whMgmt = new WarehouseManager($pdo2, $company_id);
// Resolve warehouse and zone
$warehouse = (int)($data['warehouse'] ?? 0);
$zone = $data['zone'] ?? '';
if (isset($data['warehouse_from']) && $type === 'from') { $warehouse = (int)$data['warehouse_from']; $zone = $data['zone_from'] ?? $zone; }
if (isset($data['warehouse_to']) && $type === 'to') { $warehouse = (int)$data['warehouse_to']; $zone = $data['zone_to'] ?? $zone; }
if ($type === 'to') {
$output = $whMgmt->getAislesIn($warehouse, $zone);
} elseif ($type === 'from') {
$output = $whMgmt->getAislesOut(
$warehouse,
$zone,
$data['product_sku'] ?? '',
$data['lot_number'] ?? null,
$data['serial_number'] ?? null
);
} else {
$sth = $pdo2->prepare(
"SELECT DISTINCT aisle FROM md_rack
WHERE company_id = :company_id AND warehouse = :warehouse AND zone = :zone
ORDER BY CAST(aisle AS UNSIGNED), aisle"
);
$sth->execute([':company_id' => $company_id, ':warehouse' => $warehouse, ':zone' => $zone]);
$output = array_column($sth->fetchAll(PDO::FETCH_ASSOC), 'aisle');
}
// Filter aisles by operation type and optional SKU:
// 'to' = destination → aisles with at least one empty rack
// 'from' + sku → aisles currently holding that specific SKU
// 'from' (no sku) → aisles with any occupied rack
// other = all aisles in the zone (admin / read-only pages)
$product_sku_filter = "";
$params = [
":company_id" => $company_id,
":warehouse" => $warehouse,
":zone" => $zone,
];
if ($data["type"] == "to") {
$product_sku_filter = "AND product_sku IS NULL";
} elseif ($data["type"] == "from") {
if (!empty($data["product_sku"])) {
$product_sku_filter = "AND product_sku = :product_sku";
$params[":product_sku"] = $data["product_sku"];
} else {
$product_sku_filter = "AND product_sku IS NOT NULL";
}
// Edit mode
if (!empty($data['id'])) {
$sth = $pdo2->prepare(
"SELECT DISTINCT aisle FROM md_rack
WHERE company_id = :company_id AND warehouse = :warehouse AND zone = :zone
ORDER BY CAST(aisle AS UNSIGNED), aisle"
);
$sth->execute([':company_id' => $company_id, ':warehouse' => $warehouse, ':zone' => $zone]);
$output = array_column($sth->fetchAll(PDO::FETCH_ASSOC), 'aisle');
}
if(!empty($data["id"])){
$product_sku_filter = "";
unset($params[":product_sku"]);
}
$sql = "SELECT DISTINCT aisle
FROM md_rack
WHERE
company_id = :company_id AND
warehouse = :warehouse AND
`zone` = :zone
$product_sku_filter
ORDER BY CAST(aisle AS UNSIGNED), aisle";
$sth = $pdo2->prepare($sql);
$sth->execute($params);
db_check($sth, $answer);
// Return flat array (matches original response shape for the frontend)
$answer["output"] = array_column($sth->fetchAll(PDO::FETCH_ASSOC), 'aisle');
$answer["success"] = 1;
$answer['output'] = $output;
$answer['success'] = 1;
exit(json_encode($answer));
?>
+26
View File
@@ -0,0 +1,26 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
$sql = "SELECT *
FROM md_lot
WHERE
company_id = :company_id AND
product_sku = :product_sku AND
lot_number LIKE :keyword
ORDER BY lot_number ASC
LIMIT 50";
$sth = $pdo2->prepare($sql);
$sth->execute([
":company_id" => $company_id,
":product_sku" => $data["product_sku"] ?? '',
":keyword" => '%'.$data["keyword"].'%'
]);
db_check($sth, $answer);
$answer["result"] = $sth->fetchAll(PDO::FETCH_ASSOC);
$answer["success"] = 1;
exit(json_encode($answer));
?>
+43 -59
View File
@@ -1,69 +1,53 @@
<?php
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/WarehouseManager.php';
// Get warehouse, zone, and aisle from the form
$warehouse = (int)$data['warehouse'];
$zone = $data['zone'];
$aisle = $data['aisle'];
$type = $data['type'] ?? '';
$whMgmt = new WarehouseManager($pdo2, $company_id);
if (isset($data['warehouse_from']) && $data['type'] == "from") {
$warehouse = (int)$data['warehouse_from'];
$zone = $data['zone_from'];
$aisle = $data['aisle_from'];
}
if (isset($data['warehouse_to']) && $data['type'] == "to") {
$warehouse = (int)$data['warehouse_to'];
$zone = $data['zone_to'];
$aisle = $data['aisle_to'];
// Resolve warehouse, zone and aisle
$warehouse = (int)($data['warehouse'] ?? 0);
$zone = $data['zone'] ?? '';
$aisle = $data['aisle'] ?? '';
if (isset($data['warehouse_from']) && $type === 'from') { $warehouse = (int)$data['warehouse_from']; $zone = $data['zone_from'] ?? $zone; $aisle = $data['aisle_from'] ?? $aisle; }
if (isset($data['warehouse_to']) && $type === 'to') { $warehouse = (int)$data['warehouse_to']; $zone = $data['zone_to'] ?? $zone; $aisle = $data['aisle_to'] ?? $aisle; }
if ($type === 'to') {
$output = $whMgmt->getRacksIn($warehouse, $zone, $aisle);
} elseif ($type === 'from') {
$output = $whMgmt->getRacksOut(
$warehouse,
$zone,
$aisle,
$data['product_sku'] ?? '',
$data['lot_number'] ?? null,
$data['serial_number'] ?? null
);
} else {
$sth = $pdo2->prepare(
"SELECT DISTINCT rack FROM md_rack
WHERE company_id = :company_id AND warehouse = :warehouse AND zone = :zone AND aisle = :aisle
ORDER BY CAST(rack AS UNSIGNED), rack"
);
$sth->execute([':company_id' => $company_id, ':warehouse' => $warehouse, ':zone' => $zone, ':aisle' => $aisle]);
$output = array_column($sth->fetchAll(PDO::FETCH_ASSOC), 'rack');
}
// Filter racks by operation type and optional SKU:
// 'to' = destination → empty racks only (product_sku IS NULL)
// 'from' + sku → racks currently holding that specific SKU
// 'from' (no sku) → any occupied rack (product_sku IS NOT NULL)
// other = all racks in the aisle (admin / read-only pages)
$product_sku_filter = "";
$params = [
":company_id" => $company_id,
":warehouse" => $warehouse,
":zone" => $zone,
":aisle" => $aisle,
];
if ($data["type"] == "to") {
$product_sku_filter = "AND product_sku IS NULL";
} elseif ($data["type"] == "from") {
if (!empty($data["product_sku"])) {
$product_sku_filter = "AND product_sku = :product_sku";
$params[":product_sku"] = $data["product_sku"];
} else {
$product_sku_filter = "AND product_sku IS NOT NULL";
}
// Edit mode
if (!empty($data['id'])) {
$sth = $pdo2->prepare(
"SELECT DISTINCT rack FROM md_rack
WHERE company_id = :company_id AND warehouse = :warehouse AND zone = :zone AND aisle = :aisle
ORDER BY CAST(rack AS UNSIGNED), rack"
);
$sth->execute([':company_id' => $company_id, ':warehouse' => $warehouse, ':zone' => $zone, ':aisle' => $aisle]);
$output = array_column($sth->fetchAll(PDO::FETCH_ASSOC), 'rack');
}
if(!empty($data["id"])){
$product_sku_filter = "";
unset($params[":product_sku"]);
}
$sql = "SELECT DISTINCT rack
FROM md_rack
WHERE
company_id = :company_id AND
warehouse = :warehouse AND
`zone` = :zone AND
aisle = :aisle
$product_sku_filter
ORDER BY CAST(rack AS UNSIGNED), rack";
$sth = $pdo2->prepare($sql);
$sth->execute($params);
db_check($sth, $answer);
// Return flat array (matches original response shape for the frontend)
$answer["output"] = array_column($sth->fetchAll(PDO::FETCH_ASSOC), 'rack');
$answer["success"] = 1;
$answer['output'] = $output;
$answer['success'] = 1;
exit(json_encode($answer));
?>
+5 -1
View File
@@ -25,7 +25,7 @@
$table = "td_stock_" . $warehouse;
$sql = "SELECT a.* , a.in as quantity, b.contact_name, c.product_name
$sql = "SELECT a.* , a.in as quantity, b.contact_name, c.product_name, d.expiry_date
FROM `{$table}` a
LEFT JOIN md_contact b ON
a.company_id = b.company_id and
@@ -33,6 +33,10 @@
LEFT JOIN md_product c ON
a.company_id = c.company_id and
a.product_sku = c.sku
LEFT JOIN md_lot d ON
a.company_id = d.company_id and
a.product_sku = d.product_sku and
a.lot_number = d.lot_number
WHERE
a.company_id = :company_id and
a.id = :id ";
+42 -49
View File
@@ -1,59 +1,52 @@
<?php
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/WarehouseManager.php';
// Get warehouse from the form
$warehouse = (int)$data['warehouse'];
$type = $data['type'] ?? '';
$whMgmt = new WarehouseManager($pdo2, $company_id);
if(isset($data['warehouse_from'])&&$data['type']=="from"){
$warehouse = (int)$data['warehouse_from'];
}
if(isset($data['warehouse_to'])&&$data['type']=="to"){
$warehouse = (int)$data['warehouse_to'];
// Resolve warehouse — supports warehouse, warehouse_from, warehouse_to
$warehouse = (int)($data['warehouse'] ?? 0);
if (isset($data['warehouse_from']) && $type === 'from') $warehouse = (int)$data['warehouse_from'];
if (isset($data['warehouse_to']) && $type === 'to') $warehouse = (int)$data['warehouse_to'];
if ($type === 'to') {
// Stock_in destination — empty racks only
$output = $whMgmt->getZonesIn($warehouse);
} elseif ($type === 'from') {
// Stock_out source — filter by sku + lot + serial
$output = $whMgmt->getZonesOut(
$warehouse,
$data['product_sku'] ?? '',
$data['lot_number'] ?? null,
$data['serial_number'] ?? null
);
} else {
// Admin / read-only — all zones in the warehouse
$sth = $pdo2->prepare(
"SELECT DISTINCT zone FROM md_rack
WHERE company_id = :company_id AND warehouse = :warehouse
ORDER BY CAST(zone AS UNSIGNED), zone"
);
$sth->execute([':company_id' => $company_id, ':warehouse' => $warehouse]);
$output = $sth->fetchAll(PDO::FETCH_ASSOC);
}
// Filter zones by operation type and optional SKU:
// 'to' = destination → zones with at least one empty rack
// 'from' + sku → zones currently holding that specific SKU
// 'from' (no sku) → zones with any occupied rack
// other = all zones in the warehouse (admin / read-only pages)
$product_sku_filter = "";
$params = [
":company_id" => $company_id,
":warehouse" => $warehouse,
];
if ($data["type"] == "to") {
$product_sku_filter = "AND product_sku IS NULL";
} elseif ($data["type"] == "from") {
if (!empty($data["product_sku"])) {
$product_sku_filter = "AND product_sku = :product_sku";
$params[":product_sku"] = $data["product_sku"];
} else {
$product_sku_filter = "AND product_sku IS NOT NULL";
}
// Edit mode — return all zones regardless of occupancy
if (!empty($data['id'])) {
$sth = $pdo2->prepare(
"SELECT DISTINCT zone FROM md_rack
WHERE company_id = :company_id AND warehouse = :warehouse
ORDER BY CAST(zone AS UNSIGNED), zone"
);
$sth->execute([':company_id' => $company_id, ':warehouse' => $warehouse]);
$output = $sth->fetchAll(PDO::FETCH_ASSOC);
}
if(!empty($data["id"])){
$product_sku_filter = "";
unset($params[":product_sku"]);
}
$sql = "SELECT DISTINCT zone
FROM md_rack
WHERE
company_id = :company_id AND
warehouse = :warehouse
$product_sku_filter
ORDER BY CAST(zone AS UNSIGNED), zone";
$sth = $pdo2->prepare($sql);
$sth->execute($params);
db_check($sth, $answer);
// Frontend expects objects with a `zone` property — keep that shape.
$answer["output"] = $sth->fetchAll(PDO::FETCH_ASSOC);
$answer["success"] = 1;
$answer['output'] = $output;
$answer['success'] = 1;
exit(json_encode($answer));
?>
@@ -0,0 +1,32 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/Reports.php';
$report = new Reports($pdo2, $company_id);
$items = $report->getExpiredStock();
$expired = 0;
$critical = 0;
$warning = 0;
$caution = 0;
foreach ($items as $item) {
if ($item['status'] === 'expired') { $expired++; }
if ($item['status'] === 'critical') { $critical++; }
if ($item['status'] === 'warning') { $warning++; }
if ($item['status'] === 'caution') { $caution++; }
}
$answer['output'] = [
'items' => $items,
'expired' => $expired,
'critical' => $critical,
'warning' => $warning,
'caution' => $caution,
];
$answer['success'] = 1;
exit(json_encode($answer));
?>
@@ -0,0 +1,30 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/Reports.php';
$warehouse_id = (int)($data['warehouse'] ?? 0);
if (!$warehouse_id) {
$answer['success'] = 0;
$answer['message'] = 'Warehouse is required';
exit(json_encode($answer));
}
$report = new Reports($pdo2, $company_id);
$balance = $report->getWarehouseBalance($warehouse_id);
$answer['output'] = [
'total_capacity' => $report->getWarehouseCapacity($warehouse_id),
'space_used' => $report->getWarehouseSpaceUsed($warehouse_id),
'total_in' => $balance['total_in'],
'total_out' => $balance['total_out'],
'low_stock' => $report->getWarehouseLowStockCount($warehouse_id),
'stock_movement' => $report->getWarehouseStockMovement($warehouse_id),
'stock_trend' => $report->getWarehouseStockTrend($warehouse_id),
'activities' => $report->getWarehouseActivity($warehouse_id),
];
$answer['success'] = 1;
exit(json_encode($answer));
?>
-351
View File
@@ -1,351 +0,0 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php';?>
<?php require '../include_sidebar.php';?>
<!-- MAIN CONTENT -->
<main id="content" class="content py-15">
<div class="container-fluid">
<div class="row ">
<div class="col-12">
<div class="mb-6 ">
<h1 class="fs-3 mb-1">Expired / Near Expiry Products
</h1>
<p class="mb-0">Monitor and manage products approaching or past expiry dates</p>
</div>
</div>
</div>
<div class="row mb-5 g-5">
<div class="col-xl-3 col-md-6 col-12">
<div class="card bg-primary bg-opacity-10 border-primary border-opacity-25">
<div class="card-body p-5">
<div class="">
<span class="icon-shape icon-md bg-primary text-white rounded-2">
<i class="ti ti-trash fs-4"></i>
</span>
</div>
<div class="d-flex flex-column mt-3">
<span class="fs-2 fw-bold mb-0 d-block">3</span>
<span class="fs-5">Expired Products</span>
<small class="text-muted">
Requires immediate action</small>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="card bg-primary bg-opacity-10 border-primary border-opacity-25">
<div class="card-body p-5">
<div class="">
<span class="icon-shape icon-md bg-primary text-white rounded-2">
<i class="ti ti-alert-triangle fs-4"></i>
</span>
</div>
<div class="d-flex flex-column mt-3">
<span class="fs-2 fw-bold mb-0 d-block">2</span>
<span class="fs-5">Critical (≤7 days)</span>
<small class="text-muted">
Expiring within a week</small>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="card bg-primary bg-opacity-10 border-primary border-opacity-25">
<div class="card-body p-5">
<div class="">
<span class="icon-shape icon-md bg-primary text-white rounded-2">
<i class="ti ti-clock fs-4"></i>
</span>
</div>
<div class="d-flex flex-column mt-3">
<span class="fs-2 fw-bold mb-0 d-block">2</span>
<span class="fs-5">Warning (8-14 days)</span>
<small class="text-muted">
Expiring in 2 weeks</small>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="card bg-primary bg-opacity-10 border-primary border-opacity-25">
<div class="card-body p-5">
<div class="">
<span class="icon-shape icon-md bg-primary text-white rounded-2">
<i class="ti ti-calendar-clock fs-4"></i>
</span>
</div>
<div class="d-flex flex-column mt-3">
<span class="fs-2 fw-bold mb-0 d-block">2</span>
<span class="fs-5">Caution (15-30 days)</span>
<small class="text-muted">
Monitor closely</small>
</div>
</div>
</div>
</div>
</div>
<div class="row g-5 mb-5">
<div class="col-12">
<div class="card">
<div class="card-body p-5">
<h2 class="fs-4 mb-4">Filter Products</h2>
<div class="row g-5">
<div class="col-lg-7">
<div class="position-relative">
<input type="search" class="form-control ps-9" placeholder="Search by product name or SKU">
<span class="position-absolute top-25 ms-4">
<i class="ti ti-search text-muted"></i>
</span>
</div>
</div>
<div class="col-lg-5">
<div class="d-flex gap-3">
<select class="form-select">
<option selected>All Status</option>
<option value="Expired">Expired</option>
<option value="Critical">Critical</option>
<option value="Warning">Warning</option>
<option value="Caution">Caution</option>
</select>
<select class="form-select">
<option selected>All Categories</option>
<option value="Category 1">Category 1</option>
<option value="Category 2">Category 2</option>
<option value="Category 3">Category 3</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row g-5 mb-5">
<div class="col-12">
<div class="card ">
<div class="card-body p-5">
<div>
<h2 class="fs-4 mb-2">Expired Products</h2>
<p>Products that have passed their expiry date and need to be removed</p>
</div>
<div class="table-responsive">
<table class="table mb-0 text-nowrap table-hover table-centered">
<thead class="table-light">
<tr>
<th class="py-3">Product Name</th>
<th class="py-3">SKU</th>
<th class="py-3">Category</th>
<th class="py-3">Expiry Date</th>
<th class="py-3">Days Overdue</th>
<th class="py-3">Quantity</th>
<th class="py-3">Status</th>
<th class="py-3">Action</th>
</tr>
</thead>
<tbody>
<tr>
<td class="py-3">Wireless Mouse</td>
<td class="py-3">WM-001</td>
<td class="py-3">Electronics</td>
<td class="py-3">2025-11-15</td>
<td class="py-3">47 days</td>
<td class="py-3">12</td>
<td class="py-3"><span class="badge bg-danger">Expired</span></td>
<td class="py-3">
<a href="#" class="link-danger"><i class="ti ti-trash ms-2 fs-5"></i></a>
</td>
</tr>
<tr>
<td class="py-3">USB-C Cable</td>
<td class="py-3">UC-002</td>
<td class="py-3">Accessories</td>
<td class="py-3">2025-12-01</td>
<td class="py-3">31 days</td>
<td class="py-3">8</td>
<td class="py-3"><span class="badge bg-danger">Expired</span></td>
<td class="py-3">
<a href="#" class="link-danger"><i class="ti ti-trash ms-2 fs-5"></i></a>
</td>
</tr>
<tr>
<td class="py-3">Keyboard</td>
<td class="py-3">KB-004</td>
<td class="py-3">Electronics</td>
<td class="py-3">2025-12-20</td>
<td class="py-3">12 days</td>
<td class="py-3">5</td>
<td class="py-3"><span class="badge bg-danger">Expired</span></td>
<td class="py-3">
<a href="#" class="link-danger"><i class="ti ti-trash ms-2 fs-5"></i></a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="row g-5">
<div class="col-12">
<div class="card ">
<div class="card-body p-5">
<div>
<h2 class="fs-4 mb-2">Near Expiry Products</h2>
<p>Products approaching their expiry date that may need attention</p>
</div>
<div class="table-responsive">
<table class="table mb-0 text-nowrap table-hover table-centered">
<thead class="table-light">
<tr>
<th class="py-3">Product Name</th>
<th class="py-3">SKU</th>
<th class="py-3">Category</th>
<th class="py-3">Expiry Date</th>
<th class="py-3">Days Remaining</th>
<th class="py-3">Quantity</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td class="py-3">Laptop Stand</td>
<td class="py-3">LS-003</td>
<td class="py-3">Furniture</td>
<td class="py-3">2026-01-05</td>
<td class="py-3 text-primary">4 days</td>
<td class="py-3">15</td>
<td class="py-3"><span class="badge bg-primary">Critical (≤7 days)</span></td>
<td class="py-3">
<a href="#" class="btn btn-light">Mark For Sale</a>
</td>
</tr>
<tr>
<td class="py-3">Monitor</td>
<td class="py-3"> MN-005</td>
<td class="py-3">Electronics</td>
<td class="py-3">2026-01-08</td>
<td class="py-3">7 days</td>
<td class="py-3">10</td>
<td class="py-3"><span class="badge bg-primary">Critical (≤7 days)</span></td>
<td class="py-3">
<a href="#" class="btn btn-light">Mark For Sale</a>
</td>
</tr>
<tr>
<td class="py-3">Desk Lamp</td>
<td class="py-3">DL-006</td>
<td class="py-3">Furniture</td>
<td class="py-3">2026-01-12</td>
<td class="py-3">11 days</td>
<td class="py-3">20</td>
<td class="py-3"><span class="badge bg-warning">Warning (8-14 days)</span></td>
<td class="py-3">
<a href="#" class="btn btn-light">Mark For Sale</a>
</td>
</tr>
<tr>
<td class="py-3">Webcam HD</td>
<td class="py-3">WC-007</td>
<td class="py-3">Electronics</td>
<td class="py-3">2026-01-18</td>
<td class="py-3">17 days</td>
<td class="py-3">18</td>
<td class="py-3"><span class="badge bg-warning">Caution (15-30 days)</span></td>
<td class="py-3">
<a href="#" class="btn btn-light">Mark For Sale</a>
</td>
</tr>
<tr>
<td class="py-3">HDMI Cable</td>
<td class="py-3">HD-008</td>
<td class="py-3">Accessories</td>
<td class="py-3">2026-01-25</td>
<td class="py-3">24 days</td>
<td class="py-3">30</td>
<td class="py-3"><span class="badge bg-warning">Caution (15-30 days)</span></td>
<td class="py-3">
<a href="#" class="btn btn-light">Mark For Sale</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<footer class="text-center py-2 mt-6 text-secondary ">
<p class="mb-0">Copyright © 2026 InApp Inventory Dashboard. Developed by <a href="https://codescandy.com/"
target="_blank" class="text-primary">CodesCandy</a> </p>
</footer>
</div>
</div>
<div class="position-fixed end-0 bottom-0 m-4">
<a href="https://codescandy.com/template/inventory-admin-dashboard-template/" class="btn btn-dark"
target="_blank"> Download Now</a>
</div>
</div>
</main>
<?php require "../include_ending.php";?>
<!-- Bootstrap JS -->
</body>
</html>
+362
View File
@@ -0,0 +1,362 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php';?>
<?php require '../include_sidebar.php';?>
<!-- MAIN CONTENT -->
<main id="content" class="content py-15">
<div class="container-fluid">
<!-- Page header -->
<div class="row">
<div class="col-12">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-6 gap-3">
<div>
<h1 class="fs-3 mb-1">Expired / Near Expiry Products</h1>
<p class="mb-0">Monitor and manage products approaching or past expiry dates</p>
</div>
<div class="d-flex align-items-center gap-3">
<label class="mb-0 text-nowrap fw-semibold">Warehouse</label>
<select id="warehouse" class="form-select" style="min-width:200px;" required>
<option value="">Select warehouse...</option>
</select>
</div>
</div>
</div>
</div>
<!-- Main content -->
<div id="overview_content">
<!-- Summary stats -->
<div class="row mb-5 g-5">
<div class="col-xl-3 col-md-6 col-12">
<div class="card bg-danger bg-opacity-10 border-danger border-opacity-25">
<div class="card-body p-5">
<span class="icon-shape icon-md bg-danger text-white rounded-2">
<i class="ti ti-trash fs-4"></i>
</span>
<div class="d-flex flex-column mt-3">
<span class="fs-2 fw-bold mb-0 d-block" id="stat_expired">—</span>
<span class="fs-5">Expired Products</span>
<small class="text-muted">Requires immediate action</small>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="card bg-danger bg-opacity-10 border-danger border-opacity-25">
<div class="card-body p-5">
<span class="icon-shape icon-md bg-danger text-white rounded-2">
<i class="ti ti-alert-triangle fs-4"></i>
</span>
<div class="d-flex flex-column mt-3">
<span class="fs-2 fw-bold mb-0 d-block" id="stat_critical">—</span>
<span class="fs-5">Critical (≤7 days)</span>
<small class="text-muted">Expiring within a week</small>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="card bg-warning bg-opacity-10 border-warning border-opacity-25">
<div class="card-body p-5">
<span class="icon-shape icon-md bg-warning text-white rounded-2">
<i class="ti ti-clock fs-4"></i>
</span>
<div class="d-flex flex-column mt-3">
<span class="fs-2 fw-bold mb-0 d-block" id="stat_warning">—</span>
<span class="fs-5">Warning (8–14 days)</span>
<small class="text-muted">Expiring in 2 weeks</small>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="card bg-warning bg-opacity-10 border-warning border-opacity-25">
<div class="card-body p-5">
<span class="icon-shape icon-md bg-warning text-white rounded-2">
<i class="ti ti-calendar-clock fs-4"></i>
</span>
<div class="d-flex flex-column mt-3">
<span class="fs-2 fw-bold mb-0 d-block" id="stat_caution">—</span>
<span class="fs-5">Caution (15–30 days)</span>
<small class="text-muted">Monitor closely</small>
</div>
</div>
</div>
</div>
</div>
<!-- Filter -->
<div class="row g-5 mb-5">
<div class="col-12">
<div class="card">
<div class="card-body p-5">
<h2 class="fs-4 mb-4">Filter Products</h2>
<div class="row g-5">
<div class="col-lg-7">
<div class="position-relative">
<input type="search" id="search_expired" class="form-control ps-9"
placeholder="Search by product name, SKU, or lot number…">
<span class="position-absolute top-25 ms-4">
<i class="ti ti-search text-muted"></i>
</span>
</div>
</div>
<div class="col-lg-5">
<select class="form-select" id="filter_status">
<option value="">All Status</option>
<option value="expired">Expired</option>
<option value="critical">Critical</option>
<option value="warning">Warning</option>
<option value="caution">Caution</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Expired table -->
<div class="row g-5 mb-5">
<div class="col-12">
<div class="card">
<div class="card-body p-5">
<h2 class="fs-4 mb-2">Expired Products</h2>
<p class="mb-4">Products that have passed their expiry date and need to be removed</p>
<div class="table-responsive">
<table class="table mb-0 text-nowrap table-hover table-centered" id="tbl_expired">
<thead class="table-danger border-light">
<tr>
<th>Product</th>
<th>SKU</th>
<th>Lot Number</th>
<th>Location</th>
<th>Expiry Date</th>
<th>Days Overdue</th>
<th>Quantity</th>
<th>Status</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Near expiry table -->
<div class="row g-5 mb-5">
<div class="col-12">
<div class="card">
<div class="card-body p-5">
<h2 class="fs-4 mb-2">Near Expiry Products</h2>
<p class="mb-4">Products approaching their expiry date that may need attention</p>
<div class="table-responsive">
<table class="table mb-0 text-nowrap table-hover table-centered" id="tbl_near_expiry">
<thead class="table-warning border-light">
<tr>
<th>Product</th>
<th>SKU</th>
<th>Lot Number</th>
<th>Location</th>
<th>Expiry Date</th>
<th>Days Remaining</th>
<th>Quantity</th>
<th>Status</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div><!-- /overview_content -->
</div>
</main>
<?php require "../include_ending.php";?>
<script>
var all_items = [];
var search_term = '';
var filter_status = '';
// ── Fetch + populate ─────────────────────────────────────────────────
function retrieve_expired_stock() {
var warehouse_id = $('#warehouse').val();
if (!warehouse_id) return;
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine_report/expired_stock.php",
autoPrepare: true,
checkRequired: 0,
action: 'read',
debugMode: 0,
onSuccess: function(res) {
var data = res.output;
$('#stat_expired').text(data.expired);
$('#stat_critical').text(data.critical);
$('#stat_warning').text(data.warning);
$('#stat_caution').text(data.caution);
all_items = data.items || [];
render_tables();
}
});
}
// ── Render both tables from filtered data ────────────────────────────
function render_tables() {
var kw = search_term.toLowerCase();
var fs = filter_status;
var filtered = all_items.filter(function(item) {
var match_search = !kw ||
item.product_name.toLowerCase().includes(kw) ||
item.product_sku.toLowerCase().includes(kw) ||
(item.lot_number || '').toLowerCase().includes(kw);
var match_status = !fs || item.status === fs;
return match_search && match_status;
});
render_expired(filtered.filter(function(i) { return i.status === 'expired'; }));
render_near_expiry(filtered.filter(function(i) { return i.status !== 'expired'; }));
}
function status_badge(status) {
var map = {
expired: '<span class="badge bg-danger">Expired</span>',
critical: '<span class="badge bg-danger">Critical (≤7 days)</span>',
warning: '<span class="badge bg-warning text-dark">Warning (8–14 days)</span>',
caution: '<span class="badge bg-warning text-dark">Caution (15–30 days)</span>',
};
return map[status] || '';
}
function render_expired(items) {
if (items.length === 0) {
$('table#tbl_expired tbody').html(
'<tr><td colspan="8" class="text-center py-5 text-muted">No expired products.</td></tr>'
);
return;
}
var body = '';
$.each(items, function(i, item) {
body += `<tr>
<td class="py-3">${item.product_name}</td>
<td class="py-3">${item.product_sku}</td>
<td class="py-3">${item.lot_number || '<span class="text-muted">—</span>'}</td>
<td class="py-3">${item.zone}-${item.aisle}-${item.rack}</td>
<td class="py-3">${item.expiry_date}</td>
<td class="py-3 text-danger fw-semibold">${Math.abs(item.days_remaining)} days</td>
<td class="py-3">${item.quantity}</td>
<td class="py-3">${status_badge(item.status)}</td>
</tr>`;
});
$('table#tbl_expired tbody').html(body);
}
function render_near_expiry(items) {
if (items.length === 0) {
$('table#tbl_near_expiry tbody').html(
'<tr><td colspan="8" class="text-center py-5 text-muted">No near-expiry products.</td></tr>'
);
return;
}
var body = '';
$.each(items, function(i, item) {
body += `<tr>
<td class="py-3">${item.product_name}</td>
<td class="py-3">${item.product_sku}</td>
<td class="py-3">${item.lot_number || '<span class="text-muted">—</span>'}</td>
<td class="py-3">${item.zone}-${item.aisle}-${item.rack}</td>
<td class="py-3">${item.expiry_date}</td>
<td class="py-3 text-warning fw-semibold">${item.days_remaining} days</td>
<td class="py-3">${item.quantity}</td>
<td class="py-3">${status_badge(item.status)}</td>
</tr>`;
});
$('table#tbl_near_expiry tbody').html(body);
}
// ── Search + filter ──────────────────────────────────────────────────
function debounce(fn, ms) {
var t;
return function() { clearTimeout(t); t = setTimeout(fn, ms); };
}
$('#search_expired').on('input', debounce(function() {
search_term = $(this).val().trim();
render_tables();
}, 200));
$('#filter_status').on('change', function() {
filter_status = $(this).val();
render_tables();
});
// ── Load 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 option = '';
$.each(res.output, function(key, item) {
option += `<option value='${item.id}'>${item.warehouse_name}</option>`;
});
$('#warehouse').html(option);
// Auto-select first warehouse on page load
if (res.output && res.output.length > 0) {
$('#warehouse').val(res.output[0].id);
retrieve_expired_stock();
}
}
});
}
// ── Boot ─────────────────────────────────────────────────────────────
$(async function() {
try {
await retrieve_warehouse();
} catch(e) {
console.log(e);
}
});
$('#warehouse').on('change', function() {
retrieve_expired_stock();
});
</script>
</body>
</html>
-203
View File
@@ -1,203 +0,0 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php';?>
<?php require '../include_sidebar.php';?>
<!-- MAIN CONTENT -->
<main id="content" class="content py-15">
<div class="container-fluid">
<div class="row ">
<div class="col-12">
<div class="mb-6 d-flex justify-content-between align-items-center">
<h1 class="fs-3 mb-0">Low Stock Products
</h1>
<a href="#!" class="btn btn-primary">
<span><i class="ti ti-plus"></i></span>
<span>Restock All</span></a>
</div>
</div>
</div>
<div class="row mb-5 g-5">
<div class="col-lg-4 col-12">
<div class="card border border-warning border-opacity-25">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center">
<span>Total Low Stock</span>
<span>
<i class="ti ti-alert-triangle fs-3 text-warning"></i>
</span>
</div>
<div class="d-flex flex-column mt-3">
<span class="fs-3 fw-bold mb-0 d-block text-warning">6</span>
<small class="text-muted">
Products need attention</small>
</div>
</div>
</div>
</div>
<div class="col-lg-4 col-12">
<div class="card border border-danger border-opacity-25">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center">
<span>Critical Level</span>
<span>
<i class="ti ti-activity-heartbeat fs-3 text-danger"></i>
</span>
</div>
<div class="d-flex flex-column mt-3">
<span class="fs-3 fw-bold mb-0 d-block text-danger">3</span>
<small class="text-muted">
Immediate restock needed</small>
</div>
</div>
</div>
</div>
<div class="col-lg-4 col-12">
<div class="card border border-success border-opacity-25">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center">
<span>Warning Level</span>
<span>
<i class="ti ti-box-seam fs-3 text-success"></i>
</span>
</div>
<div class="d-flex flex-column mt-3">
<span class="fs-3 fw-bold mb-0 d-block text-success">3</span>
<small class="text-muted">
Restock soon</small>
</div>
</div>
</div>
</div>
</div>
<div class="row g-5">
<div class="col-12">
<div class="card">
<div class="card-body p-5">
<h2 class="mb-4 fs-5">Low Stock Items</h2>
<div class="table-responsive">
<table class="table table-hover mb-0 table-centered">
<thead class="table-light">
<tr>
<th>Product</th>
<th>SKU</th>
<th>Current Stock</th>
<th>Min Stock</th>
<th>Warehouse</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td class="py-3">Product A</td>
<td class="py-3 text-secondary">SKU-001</td>
<td class="py-3 text-danger">5</td>
<td class="py-3">20</td>
<td class="py-3">Main Warehouse</td>
<td class="py-3"><span class="badge bg-danger rounded-pill">Critical</span></td>
<td class="py-3"><a href="#!" class="btn btn-sm btn-secondary">Restock</a></td>
</tr>
</td>
</tr>
<tr>
<td class="py-3">Product B</td>
<td class="py-3 text-secondary">SKU-002</td>
<td class="py-3 text-warning">12</td>
<td class="py-3">25</td>
<td class="py-3">Main Warehouse</td>
<td class="py-3"><span
class="badge bg-secondary bg-opacity-10 text-dark rounded-pill">Warning</span></td>
<td class="py-3"><a href="#!" class="btn btn-sm btn-secondary">Restock</a></td>
</tr>
</td>
</tr>
<tr>
<td class="py-3">Product C</td>
<td class="py-3 text-secondary">SKU-003</td>
<td class="py-3 text-danger">3</td>
<td class="py-3">15</td>
<td class="py-3">Secondary Warehouse</td>
<td class="py-3"><span class="badge bg-danger rounded-pill">Critical</span></td>
<td class="py-3"><a href="#!" class="btn btn-sm btn-secondary">Restock</a></td>
</tr>
</td>
</tr>
<tr>
<td class="py-3">Product D</td>
<td class="py-3 text-secondary">SKU-004</td>
<td class="py-3 text-warning">18</td>
<td class="py-3">30</td>
<td class="py-3">Main Warehouse</td>
<td class="py-3"><span
class="badge bg-secondary bg-opacity-10 text-dark rounded-pill">Warning</span></td>
<td class="py-3"><a href="#!" class="btn btn-sm btn-secondary">Restock</a></td>
</tr>
</td>
</tr>
<tr>
<td class="py-3">Product E</td>
<td class="py-3 text-secondary">SKU-005</td>
<td class="py-3 text-danger">2</td>
<td class="py-3">10</td>
<td class="py-3">Secondary Warehouse</td>
<td class="py-3"><span class="badge bg-danger rounded-pill">Critical</span></td>
<td class="py-3"><a href="#!" class="btn btn-sm btn-secondary">Restock</a></td>
</tr>
</td>
</tr>
<tr>
<td class="py-3">Product F</td>
<td class="py-3 text-secondary">SKU-006</td>
<td class="py-3 text-warning">8</td>
<td class="py-3">20</td>
<td class="py-3">Main Warehouse</td>
<td class="py-3"><span
class="badge bg-secondary bg-opacity-10 text-dark rounded-pill">Warning</span></td>
<td class="py-3"><a href="#!" class="btn btn-sm btn-secondary">Restock</a></td>
</tr>
</td>
</tr>
<!-- Table rows will be dynamically populated here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require "../include_ending.php";?>
<!-- Bootstrap JS -->
</body>
</html>
+94 -2
View File
@@ -44,7 +44,23 @@
<input type="number" class="form-control" id="quantity" placeholder="Enter quantity" required>
</div>
<!-- Lot Number -->
<div class="mb-3 col-lg-6">
<label for="lot_number" class="form-label">Lot Number <span class="text-muted fw-normal">(optional)</span></label>
<input type="text" class="form-control lot_search" id="lot_number" placeholder="Type to search or enter new lot..." autocomplete="off">
</div>
<!-- Expiry Date (only relevant when lot_number is set) -->
<div class="mb-3 col-lg-6">
<label for="expiry_date" class="form-label">Expiry Date <span class="text-muted fw-normal">(optional)</span></label>
<input type="text" class="form-control" id="expiry_date" placeholder="DD/MM/YYYY" autocomplete="off">
</div>
<!-- Serial Number -->
<div class="mb-3 col-lg-6">
<label for="serial_number" class="form-label">Serial Number <span class="text-muted fw-normal">(optional)</span></label>
<input type="text" class="form-control" id="serial_number" placeholder="Enter serial number">
</div>
<!-- Warehouse -->
<div class="mb-3 col-lg-6">
@@ -79,7 +95,6 @@
<input type="text" class="form-control contact_search" id="contact" placeholder="Enter contact name">
</div>
<!-- Notes / Description -->
<div class="mb-3 col-lg-12">
<label for="description" class="form-label">Notes / Description</label>
@@ -111,7 +126,10 @@
checkRequired: 1,
debugMode: 0,
data: {
"contact_id": $("#contact").attr('data-id')
"contact_id": $("#contact").attr('data-id'),
"lot_number": $("#lot_number").val(),
"expiry_date": $("#expiry_date").val(),
"serial_number": $("#serial_number").val()
},
action: 'manage',
onSuccess: function(res) {
@@ -271,6 +289,16 @@
.attr('data-id', data.contact_id);
$('#product_name').val(data.product_name);
$('#lot_number').val(data.lot_number || '');
$('#serial_number').val(data.serial_number || '');
if (data.lot_number) {
if (expiryPicker) { expiryPicker.setDate(data.expiry_date || '', true, 'Y-m-d'); }
$('#expiry_date').prop('disabled', true).attr('title', 'Expiry date is locked for existing lots')
.val(data.expiry_date);
if (expiryPicker && expiryPicker.altInput) { expiryPicker.altInput.disabled = true; }
} else {
if (expiryPicker) { expiryPicker.clear(); }
}
$('#product_sku').attr('secondary', data.product_sku);
$('#product_sku,#quantity').prop('disabled', true);
@@ -291,16 +319,80 @@
}
// ── Prefill for restock flow (coming from low-stock-products.php) ────
function prefill_from_restock() {
var params = new URLSearchParams(window.location.search);
var sku = params.get('sku');
var name = params.get('name');
var qty = params.get('qty');
var wh = params.get('wh');
// Not a restock flow — nothing to prefill
if (!sku) return false;
// Product fields (locked — the point of restock is this specific SKU)
$('#product_sku')
.val(sku)
.attr('secondary', sku)
.prop('disabled', true);
$('#product_name')
.val(name || '')
.attr('secondary', name || '');
// already disabled in markup
// Suggested quantity (prefilled value only; user can still edit)
if (qty) {
$('#quantity').val(qty);
}
// Warehouse: lock it (can't restock to a different warehouse than the one that's low)
if (wh) {
$('#warehouse').val(wh).prop('disabled', true);
// Chain-load location selects for the locked warehouse.
// Unlike edit mode, we DON'T preselect zone/aisle/rack — the user picks.
retrieve_zone('to', function() {
retrieve_aisle('to', function() {
retrieve_rack('to');
});
});
}
return true;
}
$(async function() {
try {
await retrieve_warehouse('to');
await retrieve_for_edit();
<?php if(!empty($_GET["sku"])){ ?>
prefill_from_restock();
<?php } ?>
} catch (e) {
console.log(e);
}
});
// ── Flatpickr: display dd/mm/yyyy, send yyyy-mm-dd to API ──────────
var expiryPicker = flatpickr("#expiry_date", {
dateFormat: "Y-m-d", // actual value stored in input (sent to API)
altInput: true, // show a separate visible input
altFormat: "d/m/Y", // visible format: 28/04/2026
allowInput: true,
onReady: function(_, __, fp) {
// Apply Bootstrap form-control class to the alt input
fp.altInput.classList.add("form-control");
},
onChange: function(_, dateStr) {
// If lot is new (not existing), keep expiry_date unlocked
}
});
<?php if(empty($_GET["id"])){ ?>
$("select#warehouse").change(function() {
retrieve_zone('to');
+183 -97
View File
@@ -2,22 +2,19 @@
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php';?>
<?php require '../include_sidebar.php';?>
<!-- MAIN CONTENT -->
<main id="content" class="content py-15">
<div class="container-fluid">
<div class="row ">
<div class="row">
<div class="col-12">
<div class="mb-6">
<h1 class="fs-3 mb-0">Stock Out</h1>
</div>
</div>
</div>
@@ -25,9 +22,10 @@
<div class="col-lg-8 col-12">
<div class="card">
<div class="card-body p-5">
<h2 class="fs-5 mb-4">Add Stock</h2>
<h2 class="fs-5 mb-4">Remove Stock</h2>
<form class="row gx-5">
<!-- Product Name -->
<!-- Step 1: Product -->
<div class="mb-3 col-lg-3">
<label for="product_sku" class="form-label">SKU</label>
<input type="text" id="product_sku" class="form-control product_search"
@@ -38,36 +36,50 @@
<input type="text" id="product_name" class="form-control" autocomplete="off" disabled>
</div>
<!-- SKU or Product Code -->
<div class="mb-3 col-lg-6">
<!-- Step 2: Lot Number (active lots for selected SKU) -->
<div class="mb-3 col-lg-3">
<label for="lot_number" class="form-label">Lot Number <span class="text-muted fw-normal">(optional)</span></label>
<select id="lot_number" class="form-select">
<option value="">Select lot...</option>
</select>
</div>
<!-- Step 3: Serial Number (active serials for selected SKU + lot) -->
<div class="mb-3 col-lg-3">
<label for="serial_number" class="form-label">Serial Number <span class="text-muted fw-normal">(optional)</span></label>
<select id="serial_number" class="form-select">
<option value="">Select serial...</option>
</select>
</div>
<!-- Warehouse -->
<!-- Step 4: Warehouse -->
<div class="mb-3 col-lg-6">
<label for="warehouse" class="form-label">Warehouse</label>
<select name="warehouse" id="warehouse" class="form-select" required></select>
<select name="warehouse" id="warehouse" class="form-select" required disabled>
<option value="">Select warehouse...</option>
</select>
</div>
<!-- Step 5: Zone -->
<div class="mb-3 col-lg-6">
<label for="zone" class="form-label">Zone</label>
<select name="zone" id="zone" class="form-select" required>
<select name="zone" id="zone" class="form-select" required disabled>
<option value="">Please select zone</option>
</select>
</div>
<!-- Step 6: Aisle -->
<div class="mb-3 col-lg-6">
<label for="aisle" class="form-label">Aisle</label>
<select name="aisle" id="aisle" class="form-select" required>
<select name="aisle" id="aisle" class="form-select" required disabled>
<option value="">Please select aisle</option>
</select>
</div>
<!-- Step 7: Rack -->
<div class="mb-3 col-lg-6">
<label for="rack" class="form-label">Rack</label>
<select name="rack" id="rack" class="form-select" required>
<select name="rack" id="rack" class="form-select" required disabled>
<option value="">Please select rack</option>
</select>
</div>
@@ -78,7 +90,6 @@
<input type="text" class="form-control contact_search" id="contact" placeholder="Enter contact name">
</div>
<!-- Notes / Description -->
<div class="mb-3 col-lg-12">
<label for="description" class="form-label">Notes / Description</label>
@@ -87,15 +98,13 @@
<!-- Submit Button -->
<div class="mb-3 col-lg-12">
<button type="submit" class="btn btn-primary" onclick="manage_stock_out();">Add Stock</button>
<button type="submit" class="btn btn-primary" onclick="manage_stock_out();">Remove Stock</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</main>
@@ -114,67 +123,121 @@
},
action: 'manage',
onSuccess: function(res) {
window.location.href = "<?php echo $server_url?>ics/stock_out.php";
}
});
}
// ── Step 1: Product selected → load active lots ───────────────────────
function retrieve_active_lot() {
function retrieve_warehouse(type) {
var sku = $('#product_sku').attr('secondary') || $('#product_sku').val();
if (!sku) return;
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine/retrieve_active_lot.php",
autoPrepare: true,
checkRequired: 0,
noLoading: true,
action: 'read',
data: { "product_sku": sku },
onSuccess: function(res) {
var option = '<option value="">Select lot... (optional)</option>';
$.each(res.output, function(i, item) {
var label = item.lot_number;
if (item.expiry_date) label += ' — exp: ' + item.expiry_date;
option += `<option value="${item.lot_number}">${label}</option>`;
});
$('#lot_number').html(option).prop('disabled', false);
$('#serial_number').html('<option value="">Select serial... (optional)</option>').prop('disabled', false);
// Reset downstream
reset_location();
}
});
}
// ── Step 2: Lot selected → load active serials ────────────────────────
function retrieve_active_serial() {
var sku = $('#product_sku').attr('secondary') || $('#product_sku').val();
if (!sku) return;
var lot = $('#lot_number').val();
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine/retrieve_active_serial.php",
autoPrepare: true,
checkRequired: 0,
noLoading: true,
action: 'read',
data: { "product_sku": sku, "lot_number": lot },
onSuccess: function(res) {
var option = '<option value="">Select serial... (optional)</option>';
$.each(res.output, function(i, serial) {
option += `<option value="${serial}">${serial}</option>`;
});
$('#serial_number').html(option).prop('disabled', false);
// Reset downstream
reset_location();
}
});
}
// ── Step 4: Load warehouse filtered by SKU ────────────────────────────
function retrieve_warehouse() {
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine/retrieve_warehouse.php",
autoPrepare: true,
checkRequired: 0,
data: {
"type": type
},
noLoading: true,
data: { "type": 'from' },
action: 'read',
onSuccess: function(res) {
var option = `<option value="">Please select warehouse</option>`;
var option = '<option value="">Select warehouse...</option>';
$.each(res.output, function(key, item) {
option += `<option value='${item.id}'>${item.warehouse_name}</option>`;
})
$(`select#warehouse`).html(option);
});
$('#warehouse').html(option).prop('disabled', false);
}
});
}
function retrieve_zone(type,callback) {
// ── Steps 5-7: Zone → Aisle → Rack ───────────────────────────────────
function retrieve_zone(callback) {
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine/retrieve_zone.php",
autoPrepare: true,
checkRequired: 0,
noLoading: true,
data : {
"type": type
data: {
"type": 'from',
"product_sku": $('#product_sku').attr('secondary') || $('#product_sku').val(),
"lot_number": $('#lot_number').val(),
"serial_number": $('#serial_number').val()
},
debugMode: 0,
action: 'read',
onSuccess: function(res) {
var option = `<option value="">Please select zone</option>`;
var option = '<option value="">Please select zone</option>';
$.each(res.output, function(key, item) {
option += `<option value='${item.zone}'>${item.zone}</option>`;
})
$(`select#zone`).html(option);
if (callback) callback(); // ← add this
});
$('#zone').html(option).prop('disabled', false);
if (callback) callback();
}
});
}
function retrieve_aisle(type, callback) {
function retrieve_aisle(callback) {
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine/retrieve_aisle.php",
@@ -182,25 +245,24 @@
checkRequired: 0,
noLoading: true,
data: {
"type": type
"type": 'from',
"product_sku": $('#product_sku').attr('secondary') || $('#product_sku').val(),
"lot_number": $('#lot_number').val(),
"serial_number": $('#serial_number').val()
},
action: 'read',
onSuccess: function(res) {
var option = `<option value="">Please select aisle</option>`;
var option = '<option value="">Please select aisle</option>';
$.each(res.output, function(key, item) {
option += `<option value='${item}'>${item}</option>`;
})
$(`select#aisle`).html(option);
if (callback) callback(); // ← add this
});
$('#aisle').html(option).prop('disabled', false);
if (callback) callback();
}
});
}
function retrieve_rack(type, callback) {
function retrieve_rack(callback) {
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine/retrieve_rack.php",
@@ -208,24 +270,34 @@
checkRequired: 0,
noLoading: true,
data: {
"type": type
"type": 'from',
"product_sku": $('#product_sku').attr('secondary') || $('#product_sku').val(),
"lot_number": $('#lot_number').val(),
"serial_number": $('#serial_number').val()
},
action: 'read',
onSuccess: function(res) {
var option = `<option value="">Please select rack</option>`;
var option = '<option value="">Please select rack</option>';
$.each(res.output, function(key, item) {
option += `<option value='${item}'>${item}</option>`;
})
$(`select#rack`).html(option);
if (callback) callback(); // ← add this
});
$('#rack').html(option).prop('disabled', false);
if (callback) callback();
}
});
}
// ── Reset location dropdowns ──────────────────────────────────────────
function reset_location() {
$('#warehouse').html('<option value="">Select warehouse...</option>').prop('disabled', true);
$('#zone').html('<option value="">Please select zone</option>').prop('disabled', true);
$('#aisle').html('<option value="">Please select aisle</option>').prop('disabled', true);
$('#rack').html('<option value="">Please select rack</option>').prop('disabled', true);
}
// ── Edit mode ─────────────────────────────────────────────────────────
function retrieve_for_edit() {
<?php if(empty($_GET["id"])){ ?>
@@ -242,24 +314,25 @@
var data = res.output;
// 1. Set warehouse (already populated from page load)
$(`#warehouse`).val(<?php echo $_GET["wh"];?>).attr('disabled', true); // set warehouse BEFORE retrieve_zone
// Lock lot/serial — not editable in edit mode
$('#lot_number').html(`<option value="${data.lot_number || ''}">${data.lot_number || '—'}</option>`)
.val(data.lot_number || '').prop('disabled', true);
$('#serial_number').html(`<option value="${data.serial_number || ''}">${data.serial_number || '—'}</option>`)
.val(data.serial_number || '').prop('disabled', true);
// 2. Retrieve zone (autoPrepare reads warehouse value)
retrieve_zone('from', function() {
$(`select#zone`).val(data.zone).attr('disabled', true); // set zone BEFORE retrieve_aisle
$('#warehouse').val('<?php echo $_GET["wh"];?>').prop('disabled', true);
// 3. Retrieve aisle (autoPrepare reads zone value)
retrieve_aisle('from', function() {
$(`#aisle`).val(data.aisle).attr('disabled', true); // set aisle BEFORE retrieve_rack
retrieve_zone(function() {
$('#zone').val(data.zone).prop('disabled', true);
// 4. Retrieve rack (autoPrepare reads aisle value)
retrieve_rack('from', function() {
$(`#rack`).val(data.rack).attr('disabled', true);
retrieve_aisle(function() {
$('#aisle').val(data.aisle).prop('disabled', true);
retrieve_rack(function() {
$('#rack').val(data.rack).prop('disabled', true);
// retrieve for edit
$.each(data, function(key, item) {
if (!['warehouse', 'zone', 'aisle', 'rack'].includes(key)) {
if (!['warehouse', 'zone', 'aisle', 'rack', 'lot_number', 'serial_number'].includes(key)) {
$(`#${key}`).val(item);
}
});
@@ -270,50 +343,63 @@
$('#product_name').val(data.product_name);
$('#product_sku').attr('secondary', data.product_sku);
$('#product_sku').prop('disabled', true);
$('#product_sku,#quantity').prop('disabled', true);
$(`button[type=submit]`).text('Update');
$(`button[type=reset]`).hide();
$('button[type=submit]').text('Update');
$('button[type=reset]').hide();
});
});
});
// Set re
}
});
}
// ── Boot ─────────────────────────────────────────────────────────────
$(async function() {
try {
await retrieve_warehouse('from');
await retrieve_for_edit();
} catch (e) {
} catch(e) {
console.log(e);
}
});
$(`#product_sku`).on('blur', function() {
retrieve_warehouse('from');
});
// ── Change handlers (create mode only) ───────────────────────────────
<?php if(empty($_GET["id"])){ ?>
$("select#warehouse").change(function() {
retrieve_zone('from');
// SKU selected → load lots
$('input#product_sku').on('blur', function() {
var sku = $(this).attr('secondary');
if (!sku) return;
retrieve_active_lot();
retrieve_warehouse();
});
$("select#zone").change(function() {
retrieve_aisle('from');
// Lot selected → reload serials + location
$('#lot_number').on('change', function() {
retrieve_active_serial();
retrieve_warehouse();
});
$("select#aisle").change(function() {
retrieve_rack('from');
// Serial selected → reload location
$('#serial_number').on('change', function() {
retrieve_warehouse();
});
// Warehouse selected → load zone
$('#warehouse').on('change', function() {
retrieve_zone();
});
// Zone selected → load aisle
$('#zone').on('change', function() {
retrieve_aisle();
});
// Aisle selected → load rack
$('#aisle').on('change', function() {
retrieve_rack();
});
<?php } ?>
+239 -163
View File
@@ -2,22 +2,19 @@
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php';?>
<?php require '../include_sidebar.php';?>
<!-- MAIN CONTENT -->
<main id="content" class="content py-15">
<div class="container-fluid">
<div class="row ">
<div class="row">
<div class="col-12">
<div class="mb-6">
<h1 class="fs-3 mb-0">Stock Transfer</h1>
</div>
</div>
</div>
@@ -27,7 +24,8 @@
<div class="card-body p-5">
<h2 class="fs-5 mb-4">Move Stock</h2>
<form class="row gx-5">
<!-- Product Name -->
<!-- Step 1: Product -->
<div class="mb-3 col-lg-3">
<label for="product_sku" class="form-label">SKU</label>
<input type="text" id="product_sku" class="form-control product_search"
@@ -38,67 +36,81 @@
<input type="text" id="product_name" class="form-control" autocomplete="off" disabled>
</div>
<!-- SKU or Product Code -->
<div class="mb-3 col-lg-6">
</div>
<!-- Warehouse -->
<!-- Step 2: Lot Number (active lots for selected SKU) -->
<div class="mb-3 col-lg-3">
<label for="warehouse" class="form-label">From Warehouse</label>
<select name="warehouse" id="warehouse_from" role="from" class="form-select" required>
<option value="">Please select warehouse</option>
<label for="lot_number" class="form-label">Lot Number <span class="text-muted fw-normal">(optional)</span></label>
<select id="lot_number" class="form-select">
<option value="">Select lot...</option>
</select>
</div>
<!-- Step 3: Serial Number (active serials for selected SKU + lot) -->
<div class="mb-3 col-lg-3">
<label for="zone" class="form-label">From Zone</label>
<select name="zone" id="zone_from" role="from" class="form-select" required>
<label for="serial_number" class="form-label">Serial Number <span class="text-muted fw-normal">(optional)</span></label>
<select id="serial_number" class="form-select">
<option value="">Select serial...</option>
</select>
</div>
<div class="col-12"><hr class="my-2"></div>
<!-- From location -->
<div class="col-12 mb-2">
<span class="fw-semibold text-muted">From</span>
</div>
<div class="mb-3 col-lg-3">
<label class="form-label">From Warehouse</label>
<select name="warehouse" id="warehouse_from" role="from" class="form-select" required disabled>
<option value="">Select warehouse...</option>
</select>
</div>
<div class="mb-3 col-lg-3">
<label class="form-label">From Zone</label>
<select name="zone" id="zone_from" role="from" class="form-select" required disabled>
<option value="">Please select zone</option>
</select>
</div>
<div class="mb-3 col-lg-3">
<label for="aisle" class="form-label">From Aisle</label>
<select name="aisle" id="aisle_from" role="from" class="form-select" required>
<label class="form-label">From Aisle</label>
<select name="aisle" id="aisle_from" role="from" class="form-select" required disabled>
<option value="">Please select aisle</option>
</select>
</div>
<div class="mb-3 col-lg-3">
<label for="rack" class="form-label">From Rack</label>
<select name="rack" id="rack_from" role="from" class="form-select" required>
<label class="form-label">From Rack</label>
<select name="rack" id="rack_from" role="from" class="form-select" required disabled>
<option value="">Please select rack</option>
</select>
</div>
<div class="col-12"><hr class="my-2"></div>
<!-- Warehouse -->
<div class="mb-3 col-lg-3">
<label for="warehouse" class="form-label">To Warehouse</label>
<select name="warehouse" id="warehouse_to" role="to" class="form-select" required>
<option value="">Please select warehouse</option>
</select>
<!-- To location -->
<div class="col-12 mb-2">
<span class="fw-semibold text-muted">To</span>
</div>
<div class="mb-3 col-lg-3">
<label for="zone" class="form-label">To Zone</label>
<label class="form-label">To Warehouse</label>
<select name="warehouse" id="warehouse_to" role="to" class="form-select" required>
<option value="">Select warehouse...</option>
</select>
</div>
<div class="mb-3 col-lg-3">
<label class="form-label">To Zone</label>
<select name="zone" id="zone_to" role="to" class="form-select" required>
<option value="">Please select zone</option>
</select>
</div>
<div class="mb-3 col-lg-3">
<label for="aisle" class="form-label">To Aisle</label>
<label class="form-label">To Aisle</label>
<select name="aisle" id="aisle_to" role="to" class="form-select" required>
<option value="">Please select aisle</option>
</select>
</div>
<div class="mb-3 col-lg-3">
<label for="rack" class="form-label">To Rack</label>
<label class="form-label">To Rack</label>
<select name="rack" id="rack_to" role="to" class="form-select" required>
<option value="">Please select rack</option>
</select>
@@ -110,24 +122,21 @@
<input type="text" class="form-control contact_search" id="contact" placeholder="Enter contact name">
</div>
<!-- Notes / Description -->
<div class="mb-3 col-lg-12">
<label for="description" class="form-label">Notes / Description</label>
<textarea class="form-control" id="description" rows="3" placeholder="Enter any notes"></textarea>
</div>
<!-- Submit Button -->
<!-- Submit -->
<div class="mb-3 col-lg-12">
<button type="submit" class="btn btn-primary" onclick="manage_stock_transfer();">Add Stock</button>
<button type="submit" class="btn btn-primary" onclick="manage_stock_transfer();">Transfer Stock</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</main>
@@ -146,135 +155,189 @@
},
action: 'manage',
onSuccess: function(res) {
window.location.href = "<?php echo $server_url?>ics/stock_transfer.php";
}
});
}
// ── Step 1: Product selected → load active lots + to-warehouses ───────
function retrieve_active_lot() {
var sku = $('#product_sku').attr('secondary') || $('#product_sku').val();
if (!sku) return;
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine/retrieve_active_lot.php",
autoPrepare: true,
checkRequired: 0,
noLoading: true,
action: 'read',
data: { "product_sku": sku },
onSuccess: function(res) {
var option = '<option value="">Select lot... (optional)</option>';
$.each(res.output, function(i, item) {
var label = item.lot_number;
if (item.expiry_date) label += ' — exp: ' + item.expiry_date;
option += `<option value="${item.lot_number}">${label}</option>`;
});
$('#lot_number').html(option).prop('disabled', false);
$('#serial_number').html('<option value="">Select serial... (optional)</option>').prop('disabled', false);
reset_from_location();
}
});
}
// ── Step 2: Lot selected → load active serials ────────────────────────
function retrieve_active_serial() {
var sku = $('#product_sku').attr('secondary') || $('#product_sku').val();
if (!sku) return;
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine/retrieve_active_serial.php",
autoPrepare: true,
checkRequired: 0,
noLoading: true,
action: 'read',
data: {
"product_sku": sku,
"lot_number": $('#lot_number').val()
},
onSuccess: function(res) {
var option = '<option value="">Select serial... (optional)</option>';
$.each(res.output, function(i, serial) {
option += `<option value="${serial}">${serial}</option>`;
});
$('#serial_number').html(option).prop('disabled', false);
reset_from_location();
}
});
}
// ── Warehouse loaders ─────────────────────────────────────────────────
function retrieve_warehouse(type) {
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine/retrieve_warehouse.php",
autoPrepare: true,
checkRequired: 0,
data: {
"type": type
},
noLoading: true,
data: { "type": type },
action: 'read',
debugMode: 0,
onSuccess: function(res) {
var option = `<option value="">Please select warehouse</option>`;
var option = '<option value="">Select warehouse...</option>';
$.each(res.output, function(key, item) {
option += `<option value='${item.id}'>${item.warehouse_name}</option>`;
})
$(`select[name='warehouse'][role='${type}']`).html(option);
if(!type) {
$(`select[name='warehouse']`).html(option);
}
});
$(`select[name='warehouse'][role='${type}']`).html(option).prop('disabled', false);
}
});
}
// ── Zone / Aisle / Rack — From (type=from, filtered by sku+lot+serial) ─
function retrieve_zone(type, callback) {
var extra = {};
if (type === 'from') {
extra = {
"product_sku": $('#product_sku').attr('secondary') || $('#product_sku').val(),
"lot_number": $('#lot_number').val(),
"serial_number": $('#serial_number').val()
};
}
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine/retrieve_zone.php",
autoPrepare: true,
checkRequired: 0,
data: {
"type": type
},
noLoading: true,
debugMode: 0,
data: Object.assign({ "type": type }, extra),
action: 'read',
onSuccess: function(res) {
var option = `<option value="">Please select zone</option>`;
var option = '<option value="">Please select zone</option>';
$.each(res.output, function(key, item) {
option += `<option value='${item.zone}'>${item.zone}</option>`;
})
$(`select[name='zone'][role='${type}']`).html(option);
if(!type) {
$(`select[name='zone']`).html(option);
}
if (callback) callback(); // ← add this
});
$(`select[name='zone'][role='${type}']`).html(option).prop('disabled', false);
if (callback) callback();
}
});
}
function retrieve_aisle(type, callback) {
var extra = {};
if (type === 'from') {
extra = {
"product_sku": $('#product_sku').attr('secondary') || $('#product_sku').val(),
"lot_number": $('#lot_number').val(),
"serial_number": $('#serial_number').val()
};
}
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine/retrieve_aisle.php",
autoPrepare: true,
checkRequired: 0,
data: {
"type": type
},
noLoading: true,
data: Object.assign({ "type": type }, extra),
action: 'read',
debugMode: 0,
onSuccess: function(res) {
var option = `<option value="">Please select aisle</option>`;
var option = '<option value="">Please select aisle</option>';
$.each(res.output, function(key, item) {
option += `<option value='${item}'>${item}</option>`;
})
$(`select[name='aisle'][role='${type}']`).html(option);
if(!type) {
$(`select[name='aisle']`).html(option);
}
if (callback) callback(); // ← add this
});
$(`select[name='aisle'][role='${type}']`).html(option).prop('disabled', false);
if (callback) callback();
}
});
}
function retrieve_rack(type, callback) {
var extra = {};
if (type === 'from') {
extra = {
"product_sku": $('#product_sku').attr('secondary') || $('#product_sku').val(),
"lot_number": $('#lot_number').val(),
"serial_number": $('#serial_number').val()
};
}
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine/retrieve_rack.php",
autoPrepare: true,
checkRequired: 0,
data: {
"type": type
},
noLoading: true,
data: Object.assign({ "type": type }, extra),
action: 'read',
onSuccess: function(res) {
var option = `<option value="">Please select rack</option>`;
var option = '<option value="">Please select rack</option>';
$.each(res.output, function(key, item) {
option += `<option value='${item}'>${item}</option>`;
})
$(`select[name='rack'][role='${type}']`).html(option);
if(!type) {
$(`select[name='rack']`).html(option);
}
if (callback) callback(); // ← add this
});
$(`select[name='rack'][role='${type}']`).html(option).prop('disabled', false);
if (callback) callback();
}
});
}
// ── Reset from-location dropdowns ─────────────────────────────────────
function reset_from_location() {
$('#warehouse_from').html('<option value="">Select warehouse...</option>').prop('disabled', true);
$('#zone_from').html('<option value="">Please select zone</option>').prop('disabled', true);
$('#aisle_from').html('<option value="">Please select aisle</option>').prop('disabled', true);
$('#rack_from').html('<option value="">Please select rack</option>').prop('disabled', true);
}
// ── Edit mode ─────────────────────────────────────────────────────────
function retrieve_for_edit() {
<?php if(empty($_GET["id"])){ ?>
@@ -289,54 +352,43 @@
action: 'read',
onSuccess: function(res) {
var type = 'from';
var warehouse = res.output.ref.ref_warehouse;
var data = res.output;
var data = res.output;
var from_wh = data.ref.ref_warehouse;
var to_wh = data.ref_warehouse;
/**
* From warehouse
*/
$(`select[name='warehouse'][role=${type}]`).val(warehouse).attr('disabled', true);
// Lock lot/serial
$('#lot_number').html(`<option value="${data.lot_number || ''}">${data.lot_number || '—'}</option>`)
.val(data.lot_number || '').prop('disabled', true);
$('#serial_number').html(`<option value="${data.serial_number || ''}">${data.serial_number || '—'}</option>`)
.val(data.serial_number || '').prop('disabled', true);
retrieve_zone(type, function() {
$(`select[name='zone'][role=${type}]`).val(data.zone).attr('disabled', true);
// From side
$("select[name='warehouse'][role='from']").val(from_wh).prop('disabled', true);
retrieve_zone('from', function() {
$("select[name='zone'][role='from']").val(data.zone).prop('disabled', true);
retrieve_aisle('from', function() {
$("select[name='aisle'][role='from']").val(data.aisle).prop('disabled', true);
retrieve_rack('from', function() {
$("select[name='rack'][role='from']").val(data.rack).prop('disabled', true);
retrieve_aisle(type, function() {
$(`select[name='aisle'][role=${type}]`).val(data.aisle).attr('disabled', true);
retrieve_rack(type, function() {
$(`select[name='rack'][role=${type}]`).val(data.rack).attr('disabled', true);
type = 'to';
warehouse = res.output.ref_warehouse;
data = res.output.ref;
/**
* To warehouse
*/
$(`select[name='warehouse'][role=${type}]`).val(warehouse).attr('disabled', true);
retrieve_zone(type, function() {
$(`select[name='zone'][role=${type}]`).val(data.zone).attr('disabled', true);
retrieve_aisle(type, function() {
$(`select[name='aisle'][role=${type}]`).val(data.aisle).attr('disabled',true);
retrieve_rack(type, function() {
$(`select[name='rack'][role=${type}]`).val(data.rack).attr('disabled',true);
// To side
$("select[name='warehouse'][role='to']").val(to_wh).prop('disabled', true);
retrieve_zone('to', function() {
$("select[name='zone'][role='to']").val(data.ref.zone).prop('disabled', true);
retrieve_aisle('to', function() {
$("select[name='aisle'][role='to']").val(data.ref.aisle).prop('disabled', true);
retrieve_rack('to', function() {
$("select[name='rack'][role='to']").val(data.ref.rack).prop('disabled', true);
});
});
});
});
});
});
var data = res.output;
// retrieve for edit
// Common fields
$.each(data, function(key, item) {
if (!['warehouse', 'zone', 'aisle', 'rack'].includes(key)) {
if (!['warehouse', 'zone', 'aisle', 'rack', 'lot_number', 'serial_number', 'ref'].includes(key)) {
$(`#${key}`).val(item);
}
});
@@ -346,52 +398,76 @@
.attr('data-id', data.contact_id);
$('#product_name').val(data.product_name);
$('#product_sku').attr('secondary', data.product_sku);
$('#product_sku,#quantity').prop('disabled', true);
$(`button[type=submit]`).text('Update');
$(`button[type=reset]`).hide();
$('#product_sku').attr('secondary', data.product_sku).prop('disabled', true);
$('button[type=submit]').text('Update');
$('button[type=reset]').hide();
}
});
}
// ── Boot ─────────────────────────────────────────────────────────────
$(async function() {
try {
await retrieve_warehouse('from');
await retrieve_warehouse('to');
await retrieve_for_edit();
} catch (e) {
} catch(e) {
console.log(e);
}
});
$(`#product_sku`).on('blur', function() {
$(async function() {
try {
await retrieve_warehouse('from');
await retrieve_warehouse('to');
} catch (e) {
console.log(e);
}
})
});
// ── Change handlers (create mode only) ───────────────────────────────
<?php if(empty($_GET["id"])){ ?>
$("select[name='warehouse']").change(function() {
retrieve_zone($(this).attr('role'));
// SKU selected → load lots + from-warehouse
$('input#product_sku').on('blur', function() {
var sku = $(this).attr('secondary');
if (!sku) return;
retrieve_active_lot();
retrieve_warehouse('from');
});
$("select[name='zone']").change(function() {
retrieve_aisle($(this).attr('role'));
// Lot selected → reload serials + reset from-location
$('#lot_number').on('change', function() {
retrieve_active_serial();
retrieve_warehouse('from');
});
$("select[name='aisle']").change(function() {
retrieve_rack($(this).attr('role'));
// Serial selected → reload from-warehouse
$('#serial_number').on('change', function() {
retrieve_warehouse('from');
});
// From warehouse → from zone
$("select[name='warehouse'][role='from']").on('change', function() {
retrieve_zone('from');
});
// From zone → from aisle
$("select[name='zone'][role='from']").on('change', function() {
retrieve_aisle('from');
});
// From aisle → from rack
$("select[name='aisle'][role='from']").on('change', function() {
retrieve_rack('from');
});
// To warehouse → to zone
$("select[name='warehouse'][role='to']").on('change', function() {
retrieve_zone('to');
});
// To zone → to aisle
$("select[name='zone'][role='to']").on('change', function() {
retrieve_aisle('to');
});
// To aisle → to rack
$("select[name='aisle'][role='to']").on('change', function() {
retrieve_rack('to');
});
<?php } ?>
+4
View File
@@ -48,6 +48,8 @@
<tr>
<th>Date</th>
<th>Product</th>
<th>Lot Number</th>
<th>Serial Number</th>
<th>Quantity</th>
<th>Zone</th>
<th>Aisle</th>
@@ -124,6 +126,8 @@
body += `<tr>
<td class="py-3">${format_date(item["date"])}</td>
<td class="py-3">${item["product_sku"]}: ${item['product_name']}</td>
<td class="py-3">${item['lot_number'] || '<span class="text-muted">—</span>'}</td>
<td class="py-3">${item['serial_number'] || '<span class="text-muted">—</span>'}</td>
<td class="py-3">${item['quantity']}</td>
<td class="py-3">${item['zone']}</td>
<td class="py-3">${item['aisle']}</td>
+4
View File
@@ -48,6 +48,8 @@
<tr>
<th>Date</th>
<th>Product</th>
<th>Lot Number</th>
<th>Serial Number</th>
<th>Quantity</th>
<th>Zone</th>
<th>Aisle</th>
@@ -124,6 +126,8 @@
body += `<tr>
<td class="py-3">${format_date(item["date"])}</td>
<td class="py-3">${item["product_sku"]}: ${item['product_name']}</td>
<td class="py-3">${item['lot_number'] || '<span class="text-muted">—</span>'}</td>
<td class="py-3">${item['serial_number'] || '<span class="text-muted">—</span>'}</td>
<td class="py-3">${item['quantity']}</td>
<td class="py-3">${item['zone']}</td>
<td class="py-3">${item['aisle']}</td>
+199 -357
View File
@@ -2,183 +2,151 @@
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php';?>
<?php require '../include_sidebar.php';?>
<!-- MAIN CONTENT -->
<main id="content" class="content py-15">
<div class="container-fluid">
<div class="row ">
<div class="col-12">
<div class="mb-6">
<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>
<!-- Page header -->
<div class="row">
<div class="col-12">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-6 gap-3">
<div>
<h1 class="fs-3 mb-1">Stock Overview</h1>
<p class="mb-0">Warehouse performance and stock summary</p>
</div>
<div class="d-flex align-items-center gap-3">
<label class="mb-0 text-nowrap fw-semibold">Warehouse</label>
<select id="warehouse" class="form-select" style="min-width:200px;" required>
<option value="">Select warehouse...</option>
</select>
</div>
</div>
</div>
</div>
<div class="row g-5 mb-5">
<div class="col-xl-3 col-md-6 col-12">
<div class="card" id="capacity">
<div class="card-body p-5">
<div class="d-flex justify-content-between">
<div>
<span class="mb-4 d-block">Total Capacity</span>
<h3 class="fw-bold mb-0 text-info">0</h3>
<p class="mb-0 small text-secondary">Across all warehouses</p>
</div>
<div class="">
<!-- Main content -->
<div id="overview_content">
<!-- Stat cards -->
<div class="row g-5 mb-5">
<div class="col-xl-3 col-md-6 col-12">
<div class="card" style="position:relative;">
<div class="card-body p-5">
<div class="d-flex justify-content-between">
<div>
<span class="mb-4 d-block">Total Capacity</span>
<h3 class="fw-bold mb-0 text-info" id="stat_capacity">—</h3>
<p class="mb-0 small text-secondary" id="stat_capacity_sub">Racks in warehouse</p>
</div>
<i class="ti ti-box-seam fs-3 text-muted"></i>
</div>
</div>
<!-- overlay -->
<div class="card-loader-overlay">
<div class="card-loader-overlay" id="overlay_capacity">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-12">
<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">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>
<div class="">
<div class="col-xl-3 col-md-6 col-12">
<div class="card" style="position:relative;">
<div class="card-body p-5">
<div class="d-flex justify-content-between">
<div>
<span class="mb-4 d-block">Total Stock In</span>
<h3 class="fw-bold mb-0 text-success" id="stat_stock_in">—</h3>
<p class="mb-0 small text-secondary">Units added</p>
</div>
<i class="ti ti-trending-up fs-3 text-success"></i>
</div>
</div>
<!-- overlay -->
<div class="card-loader-overlay">
<div class="card-loader-overlay" id="overlay_stock_in">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-12">
<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">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>
<div class="">
<div class="col-xl-3 col-md-6 col-12">
<div class="card" style="position:relative;">
<div class="card-body p-5">
<div class="d-flex justify-content-between">
<div>
<span class="mb-4 d-block">Total Stock Out</span>
<h3 class="fw-bold mb-0 text-danger" id="stat_stock_out">—</h3>
<p class="mb-0 small text-secondary">Units removed</p>
</div>
<i class="ti ti-trending-down fs-3 text-danger"></i>
</div>
</div>
<!-- overlay -->
<div class="card-loader-overlay">
<div class="card-loader-overlay" id="overlay_stock_out">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="card " id="low_stock">
<div class="card-body p-5">
<div class="d-flex justify-content-between">
<div>
<span class="mb-4 d-block">Low Stock Alerts</span>
<h3 class="fw-bold mb-0 text-warning">12</h3>
<p class="mb-0 small text-secondary">Items need attention</p>
</div>
<div class="">
<div class="col-xl-3 col-md-6 col-12">
<div class="card" style="position:relative;">
<div class="card-body p-5">
<div class="d-flex justify-content-between">
<div>
<span class="mb-4 d-block">Low Stock Alerts</span>
<h3 class="fw-bold mb-0 text-warning" id="stat_low_stock">—</h3>
<p class="mb-0 small text-secondary">Items need attention</p>
</div>
<i class="ti ti-alert-triangle fs-3 text-warning"></i>
</div>
</div>
<!-- overlay -->
<div class="card-loader-overlay">
<div class="card-loader-overlay" id="overlay_low_stock">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
</div>
</div>
</div>
</div>
<div class="row g-5 mb-5">
<div class="col-lg-6 col-12">
<div class="card" id="stock_movement">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center">
<!-- Charts -->
<div class="row g-5 mb-5">
<div class="col-lg-6 col-12">
<div class="card" style="position:relative;">
<div class="card-body p-5">
<h5 class="mb-0">Stock Movement</h5>
<div id="ics_stockMovementChart"></div>
</div>
<div>
<div id="stockMovementChart"></div>
</div>
<!-- overlay -->
<div class="card-loader-overlay">
<div class="card-loader-overlay" id="overlay_movement">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
</div>
</div>
</div>
<div class="col-lg-6 col-12">
<div class="card" id="stock_trend">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center">
<div class="col-lg-6 col-12">
<div class="card" style="position:relative;">
<div class="card-body p-5">
<h5 class="mb-0">Stock Level Trend</h5>
<div id="ics_stockLevelTrendChart"></div>
</div>
<div>
<div id="stockLevelTrendChart"></div>
</div>
<!-- overlay -->
<div class="card-loader-overlay">
<div class="card-loader-overlay" id="overlay_trend">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card" id="stock_activity">
<div class="card-body p-5">
<h3 class="fs-5">Recent Stock Activity</h3>
<!-- overlay -->
<div class="card-loader-overlay">
<!-- Recent activity -->
<div class="row">
<div class="col-12">
<div class="card" style="position:relative;">
<div class="card-body p-5">
<h3 class="fs-5 mb-4">Recent Stock Activity</h3>
<div id="activity_list"></div>
</div>
<div class="card-loader-overlay" id="overlay_activity">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
</div>
</div>
</div>
</div>
</div><!-- /overview_content -->
</div>
</main>
@@ -186,233 +154,120 @@
<?php require "../include_ending.php";?>
<script>
var stockMovementChart = null;
var stockLevelTrendChart = null;
function retrieve_warehouse_stats() {
return ajax_request({
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) {
$("#capacity h3").text(format_number(res.total_capacity, 2));
$("#capacity p").text("Accross " + format_number(res.total_warehouse) + " warehouses");
$("#capacity .card-loader-overlay").hide();
}
});
// ── Show all overlays before fetching ────────────────────────────────
function show_overlays() {
$('#overlay_capacity, #overlay_stock_in, #overlay_stock_out, #overlay_low_stock, #overlay_movement, #overlay_trend, #overlay_activity').show();
}
function retrieve_warehouse_balance() {
// ── Load all overview data in a single call ───────────────────────────
function retrieve_overview() {
var warehouse_id = $('#warehouse').val();
if (!warehouse_id) return;
show_overlays();
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine_report/warehouse_balance.php",
url: "<?php echo $server_url?>ics/api/engine_report/warehouse_overview.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) {
$("#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) {
onSuccess: async function(res) {
var d = res.output;
// Clear previous chart safely
if (stockMovementChart && typeof stockMovementChart.destroy === 'function') {
stockMovementChart.destroy();
// ── Stats ──────────────────────────────────────────────────────
$('#stat_capacity').text(format_number(d.total_capacity));
$('#stat_capacity_sub').text(format_number(d.space_used) + ' occupied');
$('#stat_stock_in').text(format_number(d.total_in, 2));
$('#stat_stock_out').text(format_number(d.total_out, 2));
$('#stat_low_stock').text(format_number(d.low_stock));
$('#overlay_capacity, #overlay_stock_in, #overlay_stock_out, #overlay_low_stock').hide();
// ── Stock Movement chart ───────────────────────────────────────
var mv = d.stock_movement;
if (stockMovementChart) {
await stockMovementChart.updateOptions({ xaxis: { categories: mv.labels || [] } }, false, false);
await stockMovementChart.updateSeries([
{ name: 'Stock In', data: mv.stock_in || [] },
{ name: 'Stock Out', data: mv.stock_out || [] }
]);
} else {
// First load: clear any chart rendered by main.js
$("#stockMovementChart").empty();
// Empty container to destroy any instance pre-rendered by main.js
$('#ics_stockMovementChart').empty();
stockMovementChart = new ApexCharts(document.querySelector('#ics_stockMovementChart'), {
series: [
{ name: 'Stock In', data: mv.stock_in || [] },
{ name: 'Stock Out', data: mv.stock_out || [] }
],
chart: { type: 'bar', height: 350, toolbar: { show: false } },
plotOptions: { bar: { horizontal: false, columnWidth: '55%', borderRadius: 5, borderRadiusApplication: 'end' } },
colors: ['#00C951', '#E66239'],
dataLabels: { enabled: false },
stroke: { show: true, width: 2, colors: ['transparent'] },
xaxis: { categories: mv.labels || [] },
yaxis: { title: { text: 'Quantity' } },
fill: { opacity: 1 },
tooltip: { y: { formatter: function(a) { return Number(a).toLocaleString() + ' units'; } } }
});
await stockMovementChart.render();
}
$('#overlay_movement').hide();
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();
// ── Stock Trend chart ──────────────────────────────────────────
var tr = d.stock_trend;
if (stockLevelTrendChart) {
await stockLevelTrendChart.updateOptions({ xaxis: { categories: tr.labels || [] } }, false, false);
await stockLevelTrendChart.updateSeries([
{ name: 'Stock Balance', data: tr.balance || [] }
]);
} else {
$("#stockLevelTrendChart").empty();
// Empty container to destroy any instance pre-rendered by main.js
$('#ics_stockLevelTrendChart').empty();
stockLevelTrendChart = new ApexCharts(document.querySelector('#ics_stockLevelTrendChart'), {
series: [{ name: 'Stock Balance', data: tr.balance || [] }],
chart: { height: 350, type: 'area', toolbar: { show: false } },
colors: ['#E66239'],
dataLabels: { enabled: false },
stroke: { curve: 'smooth', width: 3 },
fill: {
type: 'gradient',
gradient: { shadeIntensity: 1, inverseColors: false, opacityFrom: 0.45, opacityTo: 0.05, stops: [20, 100] }
},
xaxis: { categories: tr.labels || [], axisBorder: { show: false }, axisTicks: { show: false } },
yaxis: { title: { text: 'Quantity' }, labels: { formatter: function(a) { return Number(a).toLocaleString(); } } },
tooltip: { y: { formatter: function(a) { return Number(a).toLocaleString() + ' units'; } } },
grid: { show: true, borderColor: '#e2e8f0' }
});
await stockLevelTrendChart.render();
}
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();
$('#overlay_trend').hide();
}
});
// ── Recent activity ────────────────────────────────────────────
var $list = $('#activity_list');
$list.empty();
}
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();
$list.html('<div class="text-center text-muted py-4">No recent activity</div>');
$('#overlay_activity').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">'
html += '<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-' + colorCls + ' bg-opacity-10 text-' + colorCls + ' rounded-2">'
+ '<i class="ti ' + icon + '"></i>'
@@ -423,87 +278,74 @@
+ '</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>'
+ '<span class="fs-5 text-' + colorCls + '">' + sign + Number(item.qty).toLocaleString() + '</span>'
+ '<span class="small text-muted">' + time_ago(item.date) + '</span>'
+ '</div>'
+ '</div>';
});
$container.append(html);
$("#stock_activity .card-loader-overlay").hide();
$list.html(html);
$('#overlay_activity').hide();
}
});
}
// ── Load warehouse dropdown ───────────────────────────────────────────
function retrieve_warehouse() {
return ajax_request({
url: "<?php echo $server_url?>inventory/api/engine/warehouse.php",
url: "<?php echo $server_url?>ics/api/engine/retrieve_warehouse.php",
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
var option = ``;
var option = '';
$.each(res.output, function(key, item) {
option += `<option value='${item.id}'>${item.warehouse_name}</option>`;
})
$(`select#warehouse`).html(option);
});
$('#warehouse').html(option);
// Auto-select first warehouse on page load
if (res.output && res.output.length > 0) {
$('#warehouse').val(res.output[0].id);
retrieve_overview();
}
}
});
}
// ── Helper: escape HTML ────────────────────────────────────────────
// ── Helpers ───────────────────────────────────────────────────────────
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';
var diff = Math.floor((new Date() - new Date(dateStr.replace(/-/g,'/'))) / 1000);
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();
});
// ── Boot ─────────────────────────────────────────────────────────────
$(async function() {
try {
await retrieve_warehouse();
} catch(e) {
console.log(e);
}
});
$('#warehouse').on('change', function() {
retrieve_overview();
});
</script>
</body>
</html>