[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
+243 -28
View File
@@ -488,23 +488,29 @@ class ReportManager
$start = (clone $now)->modify('-12 months')->format('Y-m-d 00:00:00');
$end = $now->format('Y-m-d 23:59:59');
$sth = $this->pdo->query(
$sth = $this->pdo->prepare(
"SELECT DATE_FORMAT(date, '%b') AS label,
DATE_FORMAT(date, '%Y-%m') AS sort_key,
ROUND(SUM(`in`), 2) AS stock_in,
ROUND(SUM(`out`), 2) AS stock_out
FROM `{$table}`
WHERE company_id = {$this->companyId}
AND date >= '{$start}' AND date <= '{$end}'
GROUP BY sort_key, label
ORDER BY sort_key ASC"
FROM `{$table}`
WHERE company_id = :company_id
AND date >= :start AND date <= :end
GROUP BY sort_key, label
ORDER BY sort_key ASC"
);
$sth->execute([
':company_id' => $this->companyId,
':start' => $start,
':end' => $end,
]);
$dataByMonth = [];
foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $row) {
$dataByMonth[$row['sort_key']] = $row;
}
$labels = []; $stock_in = []; $stock_out = [];
$labels = $stock_in = $stock_out = [];
for ($i = 11; $i >= 0; $i--) {
$dt = (clone $now)->modify("-{$i} months");
$key = $dt->format('Y-m');
@@ -525,29 +531,36 @@ class ReportManager
$start = (clone $now)->modify('-12 months')->format('Y-m-d 00:00:00');
$end = $now->format('Y-m-d 23:59:59');
$sth = $this->pdo->query(
$sth = $this->pdo->prepare(
"SELECT ROUND(COALESCE(SUM(`in`) - SUM(`out`), 0), 2) AS balance
FROM `{$table}`
WHERE company_id = {$this->companyId} AND date < '{$start}'"
FROM `{$table}`
WHERE company_id = :company_id AND date < :start"
);
$running = (float) $sth->fetchColumn();
$sth->execute([':company_id' => $this->companyId, ':start' => $start]);
$running = (float)$sth->fetchColumn();
$sth = $this->pdo->query(
$sth = $this->pdo->prepare(
"SELECT DATE_FORMAT(date, '%Y-%m') AS sort_key,
ROUND(SUM(`in`), 2) AS stock_in,
ROUND(SUM(`out`), 2) AS stock_out
FROM `{$table}`
WHERE company_id = {$this->companyId}
AND date >= '{$start}' AND date <= '{$end}'
GROUP BY sort_key
ORDER BY sort_key ASC"
FROM `{$table}`
WHERE company_id = :company_id
AND date >= :start AND date <= :end
GROUP BY sort_key
ORDER BY sort_key ASC"
);
$sth->execute([
':company_id' => $this->companyId,
':start' => $start,
':end' => $end,
]);
$dataByMonth = [];
foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $row) {
$dataByMonth[$row['sort_key']] = $row;
}
$labels = []; $balance = [];
$labels = $balance = [];
for ($i = 11; $i >= 0; $i--) {
$dt = (clone $now)->modify("-{$i} months");
$key = $dt->format('Y-m');
@@ -566,23 +579,24 @@ class ReportManager
$table = $this->resolveWarehouseTable($warehouse_id);
if (!$table) return [];
$sth = $this->pdo->query(
$sth = $this->pdo->prepare(
"SELECT a.date, a.product_sku,
a.`in` AS stock_in, a.`out` AS stock_out,
a.type, a.description, b.product_name
FROM `{$table}` a
LEFT JOIN md_product b
ON a.company_id = b.company_id AND a.product_sku = b.sku
WHERE a.company_id = {$this->companyId}
AND (a.`in` > 0 OR a.`out` > 0)
ORDER BY a.date DESC, a.id DESC
LIMIT 10"
FROM `{$table}` a
LEFT JOIN md_product b
ON a.company_id = b.company_id AND a.product_sku = b.sku
WHERE a.company_id = :company_id
AND (a.`in` > 0 OR a.`out` > 0)
ORDER BY a.date DESC, a.id DESC
LIMIT 10"
);
$sth->execute([':company_id' => $this->companyId]);
$activities = [];
foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $row) {
$is_in = (float)$row['stock_in'] > 0;
$qty = $is_in ? (float)$row['stock_in'] : (float)$row['stock_out'];
$is_in = (float)$row['stock_in'] > 0;
$qty = $is_in ? (float)$row['stock_in'] : (float)$row['stock_out'];
$activities[] = [
'product_name' => $row['product_name'] ?: $row['product_sku'],
'sku' => $row['product_sku'],
@@ -694,6 +708,207 @@ class ReportManager
return $items;
}
// ─────────────────────────────────────────────────────────────
// Warehouse balance summary
// ─────────────────────────────────────────────────────────────
/**
* Aggregate total_in, total_out and warehouse count across all warehouses.
*/
public function getWarehouseBalanceSummary(): array
{
$sth = $this->pdo->prepare(
"SELECT
SUM(total_in) AS total_in,
SUM(total_out) AS total_out,
COUNT(DISTINCT warehouse_id) AS total_warehouse
FROM warehouse_balance
WHERE company_id = :company_id"
);
$sth->execute([':company_id' => $this->companyId]);
return $sth->fetch(PDO::FETCH_ASSOC) ?: [];
}
// ─────────────────────────────────────────────────────────────
// Dashboard report queries
// ─────────────────────────────────────────────────────────────
/**
* All stock movements for a given product_sku + lot_number across all warehouses,
* sorted by date DESC.
*/
public function getLotStockLog(string $product_sku, string $lot_number): array
{
if (!$product_sku || !$lot_number) return [];
$sth = $this->pdo->prepare(
"SELECT id, warehouse_name FROM md_warehouse
WHERE company_id = :company_id AND status = 1"
);
$sth->execute([':company_id' => $this->companyId]);
$warehouses = $sth->fetchAll(PDO::FETCH_ASSOC);
$rows = [];
foreach ($warehouses as $wh) {
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $wh['warehouse_name']);
$table = "td_stock_{$safe}";
$sth = $this->pdo->prepare(
"SELECT
s.id,
s.date,
s.type,
s.zone,
s.aisle,
s.rack,
ROUND(COALESCE(s.`in`, 0), 2) AS stock_in,
ROUND(COALESCE(s.`out`, 0), 2) AS stock_out,
s.serial_number,
s.description,
'{$wh['warehouse_name']}' AS warehouse_name
FROM `{$table}` s
WHERE s.company_id = :company_id
AND s.product_sku = :product_sku
AND s.lot_number = :lot_number
ORDER BY s.date DESC"
);
$sth->execute([
':company_id' => $this->companyId,
':product_sku' => $product_sku,
':lot_number' => $lot_number,
]);
$rows = array_merge($rows, $sth->fetchAll(PDO::FETCH_ASSOC));
}
usort($rows, fn($a, $b) => strcmp($b['date'], $a['date']));
return $rows;
}
/**
* All lots with running balance, expiry status, and days remaining.
* Returns rows plus summary counts (total, active, expired, near).
*/
public function getProductLots(): array
{
$sth = $this->pdo->prepare(
"SELECT
l.id,
l.product_sku,
l.lot_number,
l.expiry_date,
l.description,
p.product_name,
DATEDIFF(l.expiry_date, CURDATE()) AS days_remaining,
COALESCE(
(SELECT ROUND(SUM(wb.total_in) - SUM(wb.total_out), 2)
FROM warehouse_balance wb
WHERE wb.company_id = l.company_id
AND wb.product_sku = l.product_sku),
0
) AS balance
FROM md_lot l
INNER JOIN md_product p
ON p.company_id = l.company_id
AND p.sku = l.product_sku
WHERE l.company_id = :company_id
ORDER BY l.product_sku, l.expiry_date ASC"
);
$sth->execute([':company_id' => $this->companyId]);
$rows = $sth->fetchAll(PDO::FETCH_ASSOC);
$active = $expired = $near = 0;
foreach ($rows as &$row) {
$days = (int)$row['days_remaining'];
$balance = (float)$row['balance'];
if ($balance > 0) $active++;
if ($days < 0) $expired++;
if ($days >= 0 && $days <= 30) $near++;
if ($days < 0) $row['status'] = 'expired';
elseif ($days <= 7) $row['status'] = 'critical';
elseif ($days <= 30) $row['status'] = 'near';
else $row['status'] = 'ok';
}
unset($row);
return [
'rows' => $rows,
'total' => count($rows),
'active' => $active,
'expired' => $expired,
'near' => $near,
];
}
/**
* Rack activity log for a given rack_id, with user name.
*/
public function getRackLog(int $rack_id): array
{
if (!$rack_id) return [];
$db = $this->pdo->query("SELECT DATABASE()")->fetchColumn();
$sth = $this->pdo->prepare(
"SELECT
rl.id,
rl.dt,
rl.action,
rl.product_sku,
rl.td_stock_id,
rl.login,
u.name AS user_name
FROM md_rack_log rl
LEFT JOIN {$db}.user u
ON u.user_id = rl.user_id
WHERE rl.company_id = :company_id
AND rl.md_rack_id = :rack_id
ORDER BY rl.dt DESC"
);
$sth->execute([
':company_id' => $this->companyId,
':rack_id' => $rack_id,
]);
return $sth->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Full rack list with warehouse name, product info and occupancy status.
*/
public function getRackOccupancy(): array
{
$sth = $this->pdo->prepare(
"SELECT
r.id,
r.zone,
r.aisle,
r.rack,
r.product_sku,
r.td_stock_id,
mw.warehouse_name,
mw.id AS warehouse_id,
p.product_name,
CASE WHEN r.product_sku IS NOT NULL THEN 'occupied' ELSE 'empty' END AS status
FROM md_rack r
INNER JOIN md_warehouse mw
ON mw.company_id = r.company_id
AND mw.id = r.warehouse
LEFT JOIN md_product p
ON p.company_id = r.company_id
AND p.sku = r.product_sku
WHERE r.company_id = :company_id
ORDER BY mw.warehouse_name, r.zone,
CAST(r.aisle AS UNSIGNED), r.aisle,
CAST(r.rack AS UNSIGNED), r.rack"
);
$sth->execute([':company_id' => $this->companyId]);
return $sth->fetchAll(PDO::FETCH_ASSOC);
}
}
?>