[OOP] contact,inventory,ics,dashboard

This commit is contained in:
Thanakorn S
2026-04-28 15:44:57 +07:00
parent 3d3b497c08
commit 2061624641
49 changed files with 1946 additions and 2146 deletions
+426 -2
View File
@@ -1272,6 +1272,430 @@ class WarehouseManager {
]);
}
}
?>
// ─────────────────────────────────────────────────────────────
// Warehouse list queries
// ─────────────────────────────────────────────────────────────
/**
* List warehouses filtered by rack occupancy type.
* type='to' → only warehouses with at least one empty rack
* type='from' → only warehouses holding the given product_sku
* other → all warehouses
* Pass $id > 0 to bypass the filter (edit flows).
*/
public function getWarehouseList(string $type, string $product_sku = '', int $id = 0): array
{
$product_sku_filter = '';
$params = [':company_id' => $this->company_id];
if (!$id) {
if ($type === 'to') {
$product_sku_filter = 'AND r.product_sku IS NULL';
} elseif ($type === 'from') {
$product_sku_filter = 'AND r.product_sku = :product_sku';
$params[':product_sku'] = $product_sku;
}
}
$sth = $this->pdo->prepare(
"SELECT w.id, w.warehouse_name
FROM md_warehouse w
WHERE w.company_id = :company_id
AND EXISTS (
SELECT 1 FROM md_rack r
WHERE r.company_id = w.company_id
AND r.warehouse = w.id
$product_sku_filter
)
ORDER BY w.warehouse_name"
);
$sth->execute($params);
return $sth->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Full warehouse list with rack stats and manager name — for inventory listing.
*/
public function getWarehouseListAll(): array
{
$sth = $this->pdo->prepare(
"SELECT
a.*,
COALESCE(r.total_racks, 0) AS capacity,
COALESCE(r.occupied_racks, 0) AS space_used,
COALESCE(r.unique_product, 0) AS unique_product,
c.name AS manager_name,
c.surname AS manager_surname
FROM md_warehouse a
LEFT JOIN (
SELECT
company_id, warehouse,
COUNT(*) AS total_racks,
COUNT(product_sku) AS occupied_racks,
COUNT(DISTINCT product_sku) AS unique_product
FROM md_rack
GROUP BY company_id, warehouse
) r ON a.company_id = r.company_id AND a.id = r.warehouse
LEFT JOIN wms.user c ON a.manager = c.user_id
WHERE a.company_id = :company_id"
);
$sth->execute([':company_id' => $this->company_id]);
return $sth->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Fetch a single warehouse row by ID.
*/
public function getWarehouseById(int $id): array|false
{
$sth = $this->pdo->prepare(
"SELECT * FROM md_warehouse
WHERE company_id = :company_id AND id = :id"
);
$sth->execute([':company_id' => $this->company_id, ':id' => $id]);
return $sth->fetch(PDO::FETCH_ASSOC);
}
// ─────────────────────────────────────────────────────────────
// Lot / Serial queries
// ─────────────────────────────────────────────────────────────
/**
* Search md_lot by product_sku + keyword — for lot number autocomplete.
*/
public function getLotList(string $product_sku, string $keyword): array
{
$sth = $this->pdo->prepare(
"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->execute([
':company_id' => $this->company_id,
':product_sku' => $product_sku,
':keyword' => '%' . $keyword . '%',
]);
return $sth->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Return distinct active lots for a product_sku across all warehouses.
* "Active" = rack is still occupied (product_sku IS NOT NULL in md_rack).
*/
public function getActiveLots(string $product_sku): array
{
$sth = $this->pdo->prepare(
"SELECT id, warehouse_name FROM md_warehouse
WHERE company_id = :company_id AND status = 1"
);
$sth->execute([':company_id' => $this->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 = $this->pdo->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' => $this->company_id,
':product_sku' => $product_sku,
]);
foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $row) {
$active_lots[$row['lot_number']] ??= $row;
}
}
return array_values($active_lots);
}
/**
* Return distinct active serial numbers for a product_sku (+ optional lot)
* across all warehouses.
* "Active" = rack is still occupied.
*/
public function getActiveSerials(string $product_sku, string $lot_number = ''): array
{
$sth = $this->pdo->prepare(
"SELECT id, warehouse_name FROM md_warehouse
WHERE company_id = :company_id AND status = 1"
);
$sth->execute([':company_id' => $this->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' => $this->company_id,
':product_sku' => $product_sku,
];
if (!empty($lot_number)) {
$sql .= ' AND s.lot_number = :lot_number';
$params[':lot_number'] = $lot_number;
}
$sth = $this->pdo->prepare($sql);
$sth->execute($params);
foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $row) {
$active_serials[$row['serial_number']] = $row['serial_number'];
}
}
return array_values($active_serials);
}
// ─────────────────────────────────────────────────────────────
// Storage queries
// ─────────────────────────────────────────────────────────────
/**
* Full storage list with warehouse name.
*/
public function getStorageList(): array
{
$sth = $this->pdo->prepare(
"SELECT a.*, b.warehouse_name
FROM md_storage a
LEFT JOIN md_warehouse b
ON a.company_id = b.company_id
AND a.warehouse = b.id
WHERE a.company_id = :company_id"
);
$sth->execute([':company_id' => $this->company_id]);
return $sth->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Fetch a single storage row by ID.
*/
public function getStorageById(int $id): array|false
{
$sth = $this->pdo->prepare(
"SELECT * FROM md_storage
WHERE company_id = :company_id AND id = :id"
);
$sth->execute([':company_id' => $this->company_id, ':id' => $id]);
return $sth->fetch(PDO::FETCH_ASSOC);
}
// ─────────────────────────────────────────────────────────────
// Warehouse write
// ─────────────────────────────────────────────────────────────
/**
* Insert or update a warehouse.
* Creates td_stock_<name> table on insert.
* Pass $data['id'] > 0 for update, 0 for insert.
* Must be called inside dbTransaction() by the caller.
*/
public function saveWarehouse(array $data, array $logging): void
{
$id = (int)($data['id'] ?? 0);
$sth = $this->pdo->prepare(
"SELECT `log` FROM md_warehouse
WHERE company_id = :company_id AND id = :id"
);
$sth->execute([':company_id' => $this->company_id, ':id' => $id]);
$table_log = json_decode($sth->fetchColumn() ?: '[]', true) ?: [];
$table_log[] = $logging;
$params = [
':company_id' => $this->company_id,
':warehouse_name' => $data['warehouse_name'] ?? '',
':location' => $data['location'] ?? '',
':manager' => (int)($data['manager'] ?? 0),
':description' => $data['description'] ?? '',
':status' => (int)($data['status'] ?? 1),
':log' => json_encode($table_log, JSON_UNESCAPED_UNICODE),
];
if ($id > 0) {
$params[':id'] = $id;
$this->pdo->prepare(
"UPDATE md_warehouse SET
`warehouse_name` = :warehouse_name,
`location` = :location,
`manager` = :manager,
`description` = :description,
`status` = :status,
`log` = :log
WHERE id = :id AND company_id = :company_id"
)->execute($params);
} else {
$this->pdo->prepare(
"INSERT INTO md_warehouse
(company_id, warehouse_name, location, manager, description, status, `log`)
VALUES
(:company_id, :warehouse_name, :location, :manager, :description, :status, :log)"
)->execute($params);
// Create the per-warehouse stock table on insert
if (!empty($data['warehouse_name'])) {
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $data['warehouse_name']);
$this->pdo->exec("CREATE TABLE `td_stock_{$safe}` LIKE `td_stock`");
}
}
}
/**
* Insert or update a storage record.
* Pass $data['id'] > 0 for update, 0 for insert.
* Must be called inside dbTransaction() by the caller.
* Automatically syncs md_rack rows after save.
*/
public function saveStorage(array $data, array $logging): void
{
$id = (int)($data['id'] ?? 0);
$sth = $this->pdo->prepare(
"SELECT `log` FROM md_storage
WHERE company_id = :company_id AND id = :id"
);
$sth->execute([':company_id' => $this->company_id, ':id' => $id]);
$table_log = json_decode($sth->fetchColumn() ?: '[]', true) ?: [];
$table_log[] = $logging;
$params = [
':company_id' => $this->company_id,
':warehouse' => $data['warehouse'] ?? '',
':zone' => $data['zone'] ?? '',
':aisle_from' => $data['aisle_from'] ?? '',
':aisle_to' => $data['aisle_to'] ?? '',
':rack_from' => $data['rack_from'] ?? '',
':rack_to' => $data['rack_to'] ?? '',
':description' => $data['description'] ?? '',
':status' => (int)($data['status'] ?? 1),
':log' => json_encode($table_log, JSON_UNESCAPED_UNICODE),
];
if ($id > 0) {
$params[':id'] = $id;
$this->pdo->prepare(
"UPDATE md_storage SET
warehouse = :warehouse,
zone = :zone,
aisle_from = :aisle_from,
aisle_to = :aisle_to,
rack_from = :rack_from,
rack_to = :rack_to,
description = :description,
status = :status,
`log` = :log
WHERE id = :id AND company_id = :company_id"
)->execute($params);
$storage_id = $id;
} else {
$this->pdo->prepare(
"INSERT INTO md_storage
(company_id, warehouse, zone, aisle_from, aisle_to,
rack_from, rack_to, description, status, `log`)
VALUES
(:company_id, :warehouse, :zone, :aisle_from, :aisle_to,
:rack_from, :rack_to, :description, :status, :log)"
)->execute($params);
$storage_id = (int)$this->pdo->lastInsertId();
}
// Sync md_rack rows to match the new range
$this->syncRacks($storage_id, [
'warehouse' => $data['warehouse'],
'zone' => $data['zone'],
'aisle_from' => $data['aisle_from'],
'aisle_to' => $data['aisle_to'],
'rack_from' => $data['rack_from'],
'rack_to' => $data['rack_to'],
]);
}
/**
* All distinct zones in a warehouse — for admin/read-only listing.
*/
public function getZonesAll(int $warehouse_id): array
{
$sth = $this->pdo->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' => $this->company_id, ':warehouse' => $warehouse_id]);
return $sth->fetchAll(PDO::FETCH_ASSOC);
}
/**
* All distinct aisles in a zone — for admin/read-only listing.
*/
public function getAislesAll(int $warehouse_id, string $zone): array
{
$sth = $this->pdo->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' => $this->company_id, ':warehouse' => $warehouse_id, ':zone' => $zone]);
return array_column($sth->fetchAll(PDO::FETCH_ASSOC), 'aisle');
}
/**
* All distinct racks in an aisle — for admin/read-only listing.
*/
public function getRacksAll(int $warehouse_id, string $zone, string $aisle): array
{
$sth = $this->pdo->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' => $this->company_id, ':warehouse' => $warehouse_id, ':zone' => $zone, ':aisle' => $aisle]);
return array_column($sth->fetchAll(PDO::FETCH_ASSOC), 'rack');
}
}