pdo = $pdo; $this->companyId = $companyId; } /** * Resolve the td_stock_ table name for a given warehouse_id. * Returns null if warehouse not found. */ private function resolveWarehouseTable(int $warehouse_id): ?string { $sth = $this->pdo->prepare( "SELECT warehouse_name FROM md_warehouse WHERE company_id = :company_id AND id = :id AND status = 1" ); $sth->execute([':company_id' => $this->companyId, ':id' => $warehouse_id]); $name = $sth->fetchColumn(); if (!$name) return null; $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $name); return "td_stock_{$safe}"; } // These helper methods abstract away the common pattern of preparing, executing, and fetching results from the database. private function fetchScalar(string $sql) { $sth = $this->pdo->prepare($sql); $sth->execute([":company_id" => $this->companyId]); return $sth->fetchColumn(); } private function fetchAll(string $sql): array { $sth = $this->pdo->prepare($sql); $sth->execute([":company_id" => $this->companyId]); return $sth->fetchAll(PDO::FETCH_ASSOC); } /** * Master Data Summary */ public function getTotalCategory(): int { $sql = "SELECT COUNT(*) FROM md_product_category WHERE company_id = :company_id"; return (int) $this->fetchScalar($sql); } public function getActiveCategory(): int { $sql = "SELECT COUNT(*) FROM md_product_category WHERE company_id = :company_id AND `status` = 1"; return (int) $this->fetchScalar($sql); } public function getInactiveCategory(): int { $sql = "SELECT COUNT(*) FROM md_product_category WHERE company_id = :company_id AND `status` = 0"; return (int) $this->fetchScalar($sql); } public function getTotalWarehouse(): int { $sql = "SELECT COUNT(*) FROM md_warehouse WHERE company_id = :company_id AND `status` = 1"; return (int) $this->fetchScalar($sql); } public function getTotalLocation(): int { $sql = "SELECT COUNT(DISTINCT `location`) FROM md_warehouse WHERE company_id = :company_id AND `status` = 1"; return (int) $this->fetchScalar($sql); } public function getTotalCapacity(): int { $sql = "SELECT COUNT(*) FROM md_rack WHERE company_id = :company_id"; return (int) $this->fetchScalar($sql); } public function getSpaceUsed(): int { $sql = "SELECT COUNT(*) FROM md_rack WHERE company_id = :company_id AND product_sku IS NOT NULL"; return (int) $this->fetchScalar($sql); } public function getTotalProduct(): int { $sql = "SELECT COUNT(*) FROM md_product WHERE company_id = :company_id"; return (int) $this->fetchScalar($sql); } public function getTotalProductInStock(): int { $sql = "SELECT COUNT(DISTINCT product_sku) FROM warehouse_balance WHERE company_id = :company_id AND (total_in - total_out) > 0"; return (int) $this->fetchScalar($sql); } public function getLowStockCount(): int { $products = $this->getStockBalance(); $count = 0; foreach ($products as $product) { $balance = (float) $product["total_in"] - (float) $product["total_out"]; if ($balance < (float) $product["min_stock"]) { $count++; } } return $count; } public function getStockBalance(): array { $sql = "SELECT a.product_sku, SUM(a.total_in) AS total_in, SUM(a.total_out) AS total_out, b.min_stock FROM warehouse_balance 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 GROUP BY a.product_sku, b.min_stock"; return $this->fetchAll($sql); } public function getLowStockItems(): array { $sql = "SELECT wb.warehouse_id, wb.product_sku, mw.warehouse_name, mp.product_name, mp.min_stock, mp.reorder_point, mp.product_image, ROUND(SUM(wb.total_in) - SUM(wb.total_out), 2) AS balance FROM warehouse_balance wb INNER JOIN md_product mp ON wb.company_id = mp.company_id AND wb.product_sku = mp.sku INNER JOIN md_warehouse mw ON wb.company_id = mw.company_id AND wb.warehouse_id = mw.id WHERE wb.company_id = :company_id AND mp.reorder_point > 0 GROUP BY wb.warehouse_id, wb.product_sku, mw.warehouse_name, mp.product_name, mp.min_stock, mp.reorder_point, mp.product_image HAVING balance <= mp.reorder_point ORDER BY mp.product_name ASC, mw.warehouse_name ASC"; $rows = $this->fetchAll($sql); $items = []; foreach ($rows as $row) { $balance = (float) $row['balance']; $min_stock = (float) $row['min_stock']; $items[] = [ 'warehouse_id' => (int) $row['warehouse_id'], 'warehouse_name' => $row['warehouse_name'], 'product_sku' => $row['product_sku'], 'product_name' => $row['product_name'], 'product_image' => $row['product_image'], 'balance' => max(0, round($balance, 2)), 'min_stock' => (float) $row['min_stock'], 'reorder_point' => (float) $row['reorder_point'], 'status' => ($balance <= $min_stock) ? 'critical' : 'warning', ]; } return $items; } public function getCriticalStockCount(): int { $items = $this->getLowStockItems(); return count(array_filter($items, fn($i) => $i['status'] === 'critical')); } public function getWarningStockCount(): int { $items = $this->getLowStockItems(); return count(array_filter($items, fn($i) => $i['status'] === 'warning')); } // CONTACT SUMMARY public function getTotalContactCategory(): int { $sql = "SELECT COUNT(*) FROM md_contact_type WHERE company_id = :company_id"; return (int) $this->fetchScalar($sql); } public function getActiveContactCategory(): int { $sql = "SELECT COUNT(*) FROM md_contact_type WHERE company_id = :company_id AND `status` = 1"; return (int) $this->fetchScalar($sql); } public function getInactiveContactCategory(): int { $sql = "SELECT COUNT(*) FROM md_contact_type WHERE company_id = :company_id AND `status` = 0"; return (int) $this->fetchScalar($sql); } public function getTotalContact(): int { $sql = "SELECT COUNT(*) FROM md_contact WHERE company_id = :company_id"; return (int) $this->fetchScalar($sql); } // DASHBOARD REPORTS (cross-warehouse, month-filtered via warehouse_balance) public function getDashboardStats(string $month): array { $sth = $this->pdo->prepare( "SELECT ROUND(COALESCE(SUM(total_in), 0), 2) AS total_in, ROUND(COALESCE(SUM(total_out), 0), 2) AS total_out, COUNT(DISTINCT product_sku) AS active_products FROM warehouse_balance WHERE company_id = :company_id AND month = :month" ); $sth->execute([':company_id' => $this->companyId, ':month' => $month]); return $sth->fetch(PDO::FETCH_ASSOC) ?: [ 'total_in' => 0, 'total_out' => 0, 'active_products' => 0 ]; } public function getDashboardLowStockCount(): int { $sth = $this->pdo->prepare( "SELECT wb.product_sku, ROUND(SUM(wb.total_in) - SUM(wb.total_out), 2) AS balance, mp.min_stock FROM warehouse_balance wb INNER JOIN md_product mp ON mp.company_id = wb.company_id AND mp.sku = wb.product_sku WHERE wb.company_id = :company_id GROUP BY wb.product_sku, mp.min_stock" ); $sth->execute([':company_id' => $this->companyId]); $rows = $sth->fetchAll(PDO::FETCH_ASSOC); $count = 0; foreach ($rows as $row) { if ((float)$row['balance'] < (float)$row['min_stock']) $count++; } return $count; } public function getStockMovementChart(int $months = 12): array { $now = new DateTime(); $start = (clone $now)->modify("-" . ($months - 1) . " months")->format('Y-m'); $end = $now->format('Y-m'); $sth = $this->pdo->prepare( "SELECT month, ROUND(SUM(total_in), 2) AS stock_in, ROUND(SUM(total_out), 2) AS stock_out FROM warehouse_balance WHERE company_id = :company_id AND month BETWEEN :start AND :end GROUP BY month ORDER BY month ASC" ); $sth->execute([ ':company_id' => $this->companyId, ':start' => $start, ':end' => $end, ]); $rows = $sth->fetchAll(PDO::FETCH_ASSOC); $dataByMonth = []; foreach ($rows as $row) { $dataByMonth[$row['month']] = $row; } $labels = []; $stock_in = []; $stock_out = []; for ($i = $months - 1; $i >= 0; $i--) { $key = (clone $now)->modify("-{$i} months")->format('Y-m'); $labels[] = (clone $now)->modify("-{$i} months")->format('M Y'); $stock_in[] = isset($dataByMonth[$key]) ? (float)$dataByMonth[$key]['stock_in'] : 0; $stock_out[] = isset($dataByMonth[$key]) ? (float)$dataByMonth[$key]['stock_out'] : 0; } return ['labels' => $labels, 'stock_in' => $stock_in, 'stock_out' => $stock_out]; } public function getMostMovedProducts(string $month, int $limit = 10): array { $limit = (int) $limit; $sth = $this->pdo->prepare( "SELECT wb.product_sku, ROUND(SUM(wb.total_in), 2) AS total_in, ROUND(SUM(wb.total_out), 2) AS total_out, p.product_name, pc.category AS category_name FROM warehouse_balance wb INNER JOIN md_product p ON p.company_id = wb.company_id AND p.sku = wb.product_sku LEFT JOIN md_product_category pc ON pc.company_id = wb.company_id AND pc.id = p.category WHERE wb.company_id = :company_id AND wb.month = :month GROUP BY wb.product_sku, p.product_name, pc.category ORDER BY total_out DESC LIMIT {$limit}" ); $sth->execute([ ':company_id' => $this->companyId, ':month' => $month, ]); return $sth->fetchAll(PDO::FETCH_ASSOC); } public function getAvailableMonths(): array { $sth = $this->pdo->prepare( "SELECT DISTINCT month FROM warehouse_balance WHERE company_id = :company_id ORDER BY month DESC" ); $sth->execute([':company_id' => $this->companyId]); return $sth->fetchAll(PDO::FETCH_COLUMN); } /** * Recent stock activity across all warehouses — last N transactions. */ public function getRecentActivity(int $limit = 10): 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->companyId]); $warehouses = $sth->fetchAll(PDO::FETCH_ASSOC); if (empty($warehouses)) return []; $unions = implode("\nUNION ALL\n", array_map( fn($wh) => "SELECT s.date, s.product_sku, s.type, ROUND(COALESCE(s.`in`, 0), 2) AS stock_in, ROUND(COALESCE(s.`out`, 0), 2) AS stock_out, p.product_name, '{$wh['warehouse_name']}' AS warehouse_name FROM `td_stock_{$wh['warehouse_name']}` s LEFT JOIN md_product p ON p.company_id = s.company_id AND p.sku = s.product_sku WHERE s.company_id = {$this->companyId}", $warehouses )); $limit = (int) $limit; $sth = $this->pdo->query( "SELECT * FROM ({$unions}) AS all_stock ORDER BY date DESC LIMIT {$limit}" ); $items = []; foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $row) { $is_in = (float)$row['stock_in'] > 0; $items[] = [ 'product_name' => $row['product_name'] ?: $row['product_sku'], 'product_sku' => $row['product_sku'], 'warehouse_name' => $row['warehouse_name'], 'direction' => $is_in ? 'in' : 'out', 'qty' => $is_in ? (float)$row['stock_in'] : (float)$row['stock_out'], 'type' => $row['type'], 'date' => $row['date'], ]; } return $items; } // WAREHOUSE OVERVIEW public function getWarehouseCapacity(int $warehouse_id): int { $sth = $this->pdo->prepare( "SELECT COUNT(*) FROM md_rack WHERE company_id = :company_id AND warehouse = :warehouse_id" ); $sth->execute([':company_id' => $this->companyId, ':warehouse_id' => $warehouse_id]); return (int) $sth->fetchColumn(); } public function getWarehouseSpaceUsed(int $warehouse_id): int { $sth = $this->pdo->prepare( "SELECT COUNT(*) FROM md_rack WHERE company_id = :company_id AND warehouse = :warehouse_id AND product_sku IS NOT NULL" ); $sth->execute([':company_id' => $this->companyId, ':warehouse_id' => $warehouse_id]); return (int) $sth->fetchColumn(); } public function getWarehouseBalance(int $warehouse_id): array { $sth = $this->pdo->prepare( "SELECT ROUND(COALESCE(SUM(total_in), 0), 2) AS total_in, ROUND(COALESCE(SUM(total_out), 0), 2) AS total_out FROM warehouse_balance WHERE company_id = :company_id AND warehouse_id = :warehouse_id" ); $sth->execute([':company_id' => $this->companyId, ':warehouse_id' => $warehouse_id]); return $sth->fetch(PDO::FETCH_ASSOC) ?: ['total_in' => 0, 'total_out' => 0]; } public function getWarehouseLowStockCount(int $warehouse_id): int { $sth = $this->pdo->prepare( "SELECT wb.product_sku, ROUND(SUM(wb.total_in) - SUM(wb.total_out), 2) AS balance, mp.min_stock FROM warehouse_balance wb INNER JOIN md_product mp ON mp.company_id = wb.company_id AND mp.sku = wb.product_sku WHERE wb.company_id = :company_id AND wb.warehouse_id = :warehouse_id GROUP BY wb.product_sku, mp.min_stock" ); $sth->execute([':company_id' => $this->companyId, ':warehouse_id' => $warehouse_id]); $rows = $sth->fetchAll(PDO::FETCH_ASSOC); $count = 0; foreach ($rows as $row) { if ((float)$row['balance'] < (float)$row['min_stock']) $count++; } return $count; } public function getWarehouseStockMovement(int $warehouse_id): array { $table = $this->resolveWarehouseTable($warehouse_id); if (!$table) return ['labels' => [], 'stock_in' => [], 'stock_out' => []]; $now = new DateTime(); $start = (clone $now)->modify('-12 months')->format('Y-m-d 00:00:00'); $end = $now->format('Y-m-d 23:59:59'); $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 = :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 = []; for ($i = 11; $i >= 0; $i--) { $dt = (clone $now)->modify("-{$i} months"); $key = $dt->format('Y-m'); $labels[] = $dt->format('M'); $stock_in[] = isset($dataByMonth[$key]) ? (float)$dataByMonth[$key]['stock_in'] : 0; $stock_out[] = isset($dataByMonth[$key]) ? (float)$dataByMonth[$key]['stock_out'] : 0; } return ['labels' => $labels, 'stock_in' => $stock_in, 'stock_out' => $stock_out]; } public function getWarehouseStockTrend(int $warehouse_id): array { $table = $this->resolveWarehouseTable($warehouse_id); if (!$table) return ['labels' => [], 'balance' => []]; $now = new DateTime(); $start = (clone $now)->modify('-12 months')->format('Y-m-d 00:00:00'); $end = $now->format('Y-m-d 23:59:59'); $sth = $this->pdo->prepare( "SELECT ROUND(COALESCE(SUM(`in`) - SUM(`out`), 0), 2) AS balance FROM `{$table}` WHERE company_id = :company_id AND date < :start" ); $sth->execute([':company_id' => $this->companyId, ':start' => $start]); $running = (float)$sth->fetchColumn(); $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 = :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 = []; for ($i = 11; $i >= 0; $i--) { $dt = (clone $now)->modify("-{$i} months"); $key = $dt->format('Y-m'); $running += isset($dataByMonth[$key]) ? (float)$dataByMonth[$key]['stock_in'] - (float)$dataByMonth[$key]['stock_out'] : 0; $labels[] = $dt->format('M'); $balance[] = round($running, 2); } return ['labels' => $labels, 'balance' => $balance]; } public function getWarehouseActivity(int $warehouse_id): array { $table = $this->resolveWarehouseTable($warehouse_id); if (!$table) return []; $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 = :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']; $activities[] = [ 'product_name' => $row['product_name'] ?: $row['product_sku'], 'sku' => $row['product_sku'], 'direction' => $is_in ? 'in' : 'out', 'qty' => round($qty, 2), 'type' => $row['type'], 'description' => $row['description'], 'date' => $row['date'], ]; } return $activities; } // EXPIRED / NEAR EXPIRY STOCK public function getExpiredStock(int $warehouse_id = 0): array { $where_wh = $warehouse_id > 0 ? "AND id = {$warehouse_id}" : ''; $sth = $this->pdo->prepare( "SELECT id, warehouse_name FROM md_warehouse WHERE company_id = :company_id AND status = 1 {$where_wh}" ); $sth->execute([':company_id' => $this->companyId]); $warehouses = $sth->fetchAll(PDO::FETCH_ASSOC); if (empty($warehouses)) return []; $unions = implode("\nUNION ALL\n", array_map( fn($wh) => "SELECT s.id, s.product_sku, s.lot_number, s.zone, s.aisle, s.rack, ROUND(s.`in`, 2) AS quantity, {$wh['id']} AS warehouse_id FROM `td_stock_{$wh['warehouse_name']}` s WHERE s.company_id = {$this->companyId} AND s.type = 'in' AND s.lot_number IS NOT NULL", $warehouses )); $sql = "SELECT stock.id, stock.product_sku, stock.lot_number, stock.zone, stock.aisle, stock.rack, stock.quantity, l.expiry_date, DATEDIFF(l.expiry_date, CURDATE()) AS days_remaining, p.product_name, pc.category AS category_name, mw.warehouse_name FROM ($unions) AS stock INNER JOIN md_lot l ON l.company_id = {$this->companyId} AND l.product_sku = stock.product_sku AND l.lot_number = stock.lot_number INNER JOIN md_product p ON p.company_id = {$this->companyId} AND p.sku = stock.product_sku LEFT JOIN md_product_category pc ON pc.company_id = {$this->companyId} AND pc.id = p.category INNER JOIN md_warehouse mw ON mw.company_id = {$this->companyId} AND mw.id = stock.warehouse_id WHERE l.expiry_date IS NOT NULL ORDER BY l.expiry_date ASC"; $sth = $this->pdo->prepare($sql); $sth->execute([]); $rows = $sth->fetchAll(PDO::FETCH_ASSOC); $items = []; foreach ($rows as $row) { $days = (int) $row['days_remaining']; if ($days < 0) { $status = 'expired'; } elseif ($days <= 7) { $status = 'critical'; } elseif ($days <= 14) { $status = 'warning'; } elseif ($days <= 30) { $status = 'caution'; } else { continue; } $items[] = [ 'id' => (int) $row['id'], 'product_sku' => $row['product_sku'], 'product_name' => $row['product_name'], 'category_name' => $row['category_name'], 'warehouse_name' => $row['warehouse_name'], 'lot_number' => $row['lot_number'], 'zone' => $row['zone'], 'aisle' => $row['aisle'], 'rack' => $row['rack'], 'quantity' => (float) $row['quantity'], 'expiry_date' => $row['expiry_date'], 'days_remaining' => $days, 'status' => $status, ]; } 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); } } ?>