From 20616246413a4cdfaa06a0f4d43d279687627121 Mon Sep 17 00:00:00 2001 From: Thanakorn S Date: Tue, 28 Apr 2026 15:44:57 +0700 Subject: [PATCH] [OOP] contact,inventory,ics,dashboard --- app/assets/utils/classes/ContactManager.php | 188 ++++++ app/assets/utils/classes/ProductManager.php | 236 +++++++ app/assets/utils/classes/ReportManager.php | 271 +++++++- app/assets/utils/classes/StockManager.php | 578 ++++++++++++++++++ app/assets/utils/classes/WarehouseManager.php | 428 ++++++++++++- app/contact/api/engine/contact.php | 20 +- app/contact/api/engine/contact_type.php | 20 +- app/contact/api/engine/manage_contact.php | 85 +-- .../api/engine/manage_contact_type.php | 60 +- app/contact/api/engine/retrieve_contact.php | 29 +- .../api/engine/retrieve_contact_type.php | 25 +- .../api/engine_report/lot_stock_log.php | 57 +- .../api/engine_report/product_lot.php | 63 +- app/dashboard/api/engine_report/rack_log.php | 26 +- .../api/engine_report/rack_occupancy.php | 35 +- app/ics/api/engine/contact_search.php | 25 +- app/ics/api/engine/manage_stock_in.php | 117 +--- app/ics/api/engine/manage_stock_out.php | 138 +---- app/ics/api/engine/manage_stock_transfer.php | 232 +------ app/ics/api/engine/product_search.php | 25 +- app/ics/api/engine/retrieve_active_lot.php | 50 +- app/ics/api/engine/retrieve_active_serial.php | 52 +- app/ics/api/engine/retrieve_aisle.php | 26 +- app/ics/api/engine/retrieve_lot.php | 29 +- app/ics/api/engine/retrieve_rack.php | 26 +- app/ics/api/engine/retrieve_stock_in.php | 55 +- app/ics/api/engine/retrieve_stock_out.php | 51 +- .../api/engine/retrieve_stock_transfer.php | 101 +-- app/ics/api/engine/retrieve_warehouse.php | 50 +- app/ics/api/engine/retrieve_zone.php | 29 +- app/ics/api/engine/stock_in.php | 53 +- app/ics/api/engine/stock_out.php | 53 +- app/ics/api/engine/stock_transfer.php | 54 +- app/ics/api/engine_report/stock_activity.php | 66 +- app/ics/api/engine_report/stock_movement.php | 70 +-- app/ics/api/engine_report/stock_trend.php | 84 +-- .../api/engine_report/warehouse_balance.php | 24 +- app/inventory/api/engine/manage_category.php | 68 +-- app/inventory/api/engine/manage_product.php | 125 +--- app/inventory/api/engine/manage_storage.php | 86 +-- app/inventory/api/engine/manage_warehouse.php | 69 +-- app/inventory/api/engine/product.php | 24 +- app/inventory/api/engine/product_category.php | 24 +- .../api/engine/retrieve_category.php | 25 +- app/inventory/api/engine/retrieve_product.php | 25 +- app/inventory/api/engine/retrieve_storage.php | 25 +- .../api/engine/retrieve_warehouse.php | 25 +- app/inventory/api/engine/storage.php | 29 +- app/inventory/api/engine/warehouse.php | 36 +- 49 files changed, 1946 insertions(+), 2146 deletions(-) create mode 100644 app/assets/utils/classes/StockManager.php diff --git a/app/assets/utils/classes/ContactManager.php b/app/assets/utils/classes/ContactManager.php index e301e2a..eebcfa2 100644 --- a/app/assets/utils/classes/ContactManager.php +++ b/app/assets/utils/classes/ContactManager.php @@ -182,4 +182,192 @@ class ContactManager { ':company_id' => $this->company_id, ]); } + + + // ───────────────────────────────────────────────────────────── + // Contact type queries + // ───────────────────────────────────────────────────────────── + + /** + * Full contact type list. + */ + public function getContactTypeList(): array + { + $sth = $this->pdo->prepare( + "SELECT * FROM md_contact_type + WHERE company_id = :company_id" + ); + $sth->execute([':company_id' => $this->company_id]); + return $sth->fetchAll(PDO::FETCH_ASSOC); + } + + /** + * Fetch a single contact type row by ID. + */ + public function getContactTypeById(int $id): array|false + { + $sth = $this->pdo->prepare( + "SELECT * FROM md_contact_type + WHERE company_id = :company_id AND id = :id" + ); + $sth->execute([':company_id' => $this->company_id, ':id' => $id]); + return $sth->fetch(PDO::FETCH_ASSOC); + } + + /** + * Insert or update a contact type. + * Pass $data['id'] > 0 for update, 0 for insert. + * Must be called inside dbTransaction() by the caller. + */ + public function saveContactType(array $data, array $logging): void + { + $id = (int)($data['id'] ?? 0); + + $sth = $this->pdo->prepare( + "SELECT `log` FROM md_contact_type + 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, + ':contact_type' => $data['contact_type'], + ':description' => $data['description'], + ':status' => (int)$data['status'], + ':log' => json_encode($table_log), + ]; + + if ($id > 0) { + $params[':id'] = $id; + $this->pdo->prepare( + "UPDATE md_contact_type SET + `contact_type` = :contact_type, + `description` = :description, + `status` = :status, + `log` = :log + WHERE id = :id AND company_id = :company_id" + )->execute($params); + } else { + $this->pdo->prepare( + "INSERT INTO md_contact_type + (company_id, contact_type, `description`, `status`, `log`) + VALUES + (:company_id, :contact_type, :description, :status, :log)" + )->execute($params); + } + } + + // ───────────────────────────────────────────────────────────── + // Contact queries + // ───────────────────────────────────────────────────────────── + + /** + * Full contact list. + */ + public function getContactList(): array + { + $sth = $this->pdo->prepare( + "SELECT * FROM md_contact + WHERE company_id = :company_id" + ); + $sth->execute([':company_id' => $this->company_id]); + return $sth->fetchAll(PDO::FETCH_ASSOC); + } + + /** + * Fetch a single contact row by ID. + */ + public function getContactById(int $id): array|false + { + $sth = $this->pdo->prepare( + "SELECT * FROM md_contact + WHERE company_id = :company_id AND id = :id" + ); + $sth->execute([':company_id' => $this->company_id, ':id' => $id]); + return $sth->fetch(PDO::FETCH_ASSOC); + } + + /** + * Search contacts by name keyword — for autocomplete. + */ + public function searchContact(string $keyword): array + { + $sth = $this->pdo->prepare( + "SELECT * FROM md_contact + WHERE company_id = :company_id + AND contact_name LIKE :keyword + LIMIT 50" + ); + $sth->execute([ + ':company_id' => $this->company_id, + ':keyword' => '%' . $keyword . '%', + ]); + return $sth->fetchAll(PDO::FETCH_ASSOC); + } + + + /** + * Insert or update a contact. + * Pass $data['id'] > 0 for update, 0 for insert. + * Must be called inside dbTransaction() by the caller. + */ + public function saveContact(array $data, array $logging, string $contact_image): void + { + $id = (int)($data['id'] ?? 0); + + $sth = $this->pdo->prepare( + "SELECT `log` FROM md_contact + 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, + ':contact_name' => $data['contact_name'], + ':tax_id' => $data['tax_id'], + ':organization' => $data['organization'], + ':branch' => $data['branch'], + ':contact_type' => (int)$data['contact_type'], + ':billing_address' => $data['billing_address'], + ':shipping_location' => $data['shipping_location'], + ':shipping_address' => $data['shipping_address'], + ':remark' => $data['remark'], + ':contact_image' => $contact_image, + ':status' => (int)$data['status'], + ':log' => json_encode($table_log), + ]; + + if ($id > 0) { + $params[':id'] = $id; + $this->pdo->prepare( + "UPDATE md_contact SET + `contact_name` = :contact_name, + `tax_id` = :tax_id, + `organization` = :organization, + `branch` = :branch, + `contact_type` = :contact_type, + `billing_address` = :billing_address, + `shipping_location` = :shipping_location, + `shipping_address` = :shipping_address, + `remark` = :remark, + `contact_image` = :contact_image, + `status` = :status, + `log` = :log + WHERE id = :id AND company_id = :company_id" + )->execute($params); + } else { + $this->pdo->prepare( + "INSERT INTO md_contact + (company_id, contact_name, tax_id, organization, branch, contact_type, + billing_address, shipping_location, shipping_address, remark, contact_image, status, log) + VALUES + (:company_id, :contact_name, :tax_id, :organization, :branch, :contact_type, + :billing_address, :shipping_location, :shipping_address, :remark, :contact_image, :status, :log)" + )->execute($params); + } + } } \ No newline at end of file diff --git a/app/assets/utils/classes/ProductManager.php b/app/assets/utils/classes/ProductManager.php index e6207c8..eca4a3d 100644 --- a/app/assets/utils/classes/ProductManager.php +++ b/app/assets/utils/classes/ProductManager.php @@ -183,4 +183,240 @@ class ProductManager { ':company_id' => $this->company_id, ]); } + + + // ───────────────────────────────────────────────────────────── + // Product queries + // ───────────────────────────────────────────────────────────── + + /** + * Full product list with current warehouse balance. + */ + public function getProductList(): array + { + $sth = $this->pdo->prepare( + "SELECT a.*, IFNULL(SUM(b.total_in - b.total_out), 0) AS product_balance + FROM md_product a + LEFT JOIN warehouse_balance b + ON a.company_id = b.company_id + AND a.sku = b.product_sku + WHERE a.company_id = :company_id + GROUP BY a.id" + ); + $sth->execute([':company_id' => $this->company_id]); + return $sth->fetchAll(PDO::FETCH_ASSOC); + } + + /** + * Fetch a single product row by ID. + */ + public function getProductById(int $id): array|false + { + $sth = $this->pdo->prepare( + "SELECT * FROM md_product + WHERE company_id = :company_id AND id = :id" + ); + $sth->execute([':company_id' => $this->company_id, ':id' => $id]); + return $sth->fetch(PDO::FETCH_ASSOC); + } + + /** + * Search products by SKU keyword — for autocomplete. + */ + public function searchProduct(string $keyword): array + { + $sth = $this->pdo->prepare( + "SELECT * FROM md_product + WHERE company_id = :company_id + AND sku LIKE :keyword + LIMIT 50" + ); + $sth->execute([ + ':company_id' => $this->company_id, + ':keyword' => '%' . $keyword . '%', + ]); + return $sth->fetchAll(PDO::FETCH_ASSOC); + } + + // ───────────────────────────────────────────────────────────── + // Product category queries + // ───────────────────────────────────────────────────────────── + + /** + * Full category list with product count per category. + */ + public function getCategoryList(): array + { + $sth = $this->pdo->prepare( + "SELECT a.*, COUNT(b.id) AS product_count + FROM md_product_category a + LEFT JOIN md_product b + ON a.company_id = b.company_id + AND a.id = b.category + WHERE a.company_id = :company_id + GROUP BY a.id" + ); + $sth->execute([':company_id' => $this->company_id]); + return $sth->fetchAll(PDO::FETCH_ASSOC); + } + + /** + * Fetch a single category row by ID. + */ + public function getCategoryById(int $id): array|false + { + $sth = $this->pdo->prepare( + "SELECT * FROM md_product_category + WHERE company_id = :company_id AND id = :id" + ); + $sth->execute([':company_id' => $this->company_id, ':id' => $id]); + return $sth->fetch(PDO::FETCH_ASSOC); + } + + /** + * Insert or update a product category. + * Pass $data['id'] > 0 for update, 0 for insert. + * Must be called inside dbTransaction() by the caller. + * + * @throws Exception on validation failure + */ + public function saveCategory(array $data, array $logging): void + { + $id = (int)($data['id'] ?? 0); + + $sth = $this->pdo->prepare( + "SELECT `log` FROM md_product_category + 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, + ':category' => $data['category'], + ':slug' => $data['slug'], + ':description' => $data['description'], + ':status' => (int)$data['status'], + ':log' => json_encode($table_log), + ]; + + if ($id > 0) { + $params[':id'] = $id; + $this->pdo->prepare( + "UPDATE md_product_category SET + `category` = :category, + `slug` = :slug, + `description` = :description, + `status` = :status, + `log` = :log + WHERE id = :id AND company_id = :company_id" + )->execute($params); + } else { + $this->pdo->prepare( + "INSERT INTO md_product_category + (company_id, category, slug, `description`, `status`, `log`) + VALUES + (:company_id, :category, :slug, :description, :status, :log)" + )->execute($params); + } + } + + + // ───────────────────────────────────────────────────────────── + // Product write + // ───────────────────────────────────────────────────────────── + + /** + * Insert or update a product. + * Pass $data['id'] > 0 for update, 0 for insert. + * Must be called inside dbTransaction() by the caller. + */ + public function saveProduct(array $data, array $logging, string $product_image): void + { + $id = (int)($data['id'] ?? 0); + + $sth = $this->pdo->prepare( + "SELECT `log` FROM md_product + 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, + ':product_name' => $data['product_name'], + ':sku' => $data['sku'], + ':price' => $data['price'], + ':min_stock' => $data['min_stock'], + ':reorder_point' => $data['reorder_point'], + ':category' => $data['category'], + ':product_image' => $product_image, + ':description' => $data['description'], + ':status' => (int)$data['status'], + ':log' => json_encode($table_log), + ]; + + if ($id > 0) { + $params[':id'] = $id; + $this->pdo->prepare( + "UPDATE md_product SET + product_name = :product_name, + sku = :sku, + price = :price, + min_stock = :min_stock, + reorder_point = :reorder_point, + category = :category, + product_image = :product_image, + `description` = :description, + `status` = :status, + `log` = :log + WHERE id = :id AND company_id = :company_id" + )->execute($params); + } else { + $this->pdo->prepare( + "INSERT INTO md_product + (company_id, product_name, sku, price, min_stock, reorder_point, + category, product_image, `description`, `status`, `log`) + VALUES + (:company_id, :product_name, :sku, :price, :min_stock, :reorder_point, + :category, :product_image, :description, :status, :log)" + )->execute($params); + } + } + + + /** + * 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); + } } \ No newline at end of file diff --git a/app/assets/utils/classes/ReportManager.php b/app/assets/utils/classes/ReportManager.php index 9f6ae37..930aa90 100644 --- a/app/assets/utils/classes/ReportManager.php +++ b/app/assets/utils/classes/ReportManager.php @@ -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); + } } ?> \ No newline at end of file diff --git a/app/assets/utils/classes/StockManager.php b/app/assets/utils/classes/StockManager.php new file mode 100644 index 0000000..d88c36e --- /dev/null +++ b/app/assets/utils/classes/StockManager.php @@ -0,0 +1,578 @@ +pdo = $pdo; + $this->company_id = $company_id; + } + + // ───────────────────────────────────────────────────────────── + // Private helpers + // ───────────────────────────────────────────────────────────── + + /** + * Resolve warehouse name by ID and return the td_stock_ table name. + * Throws if warehouse not found. + */ + private function resolveTable(int $warehouse_id): string + { + $sth = $this->pdo->prepare( + "SELECT warehouse_name FROM md_warehouse + WHERE company_id = :company_id AND id = :id" + ); + $sth->execute([':company_id' => $this->company_id, ':id' => $warehouse_id]); + $name = $sth->fetchColumn(); + + if (!$name) { + throw new Exception("Warehouse not found."); + } + + $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $name); + return "td_stock_{$safe}"; + } + + // ───────────────────────────────────────────────────────────── + // Stock list queries + // ───────────────────────────────────────────────────────────── + + /** + * List all stock records of a given type for a warehouse. + * type: 'in' | 'out' | 'transfer' + */ + public function getStockList(int $warehouse_id, string $type): array + { + $table = $this->resolveTable($warehouse_id); + $column = $type === 'out' ? 'ROUND(a.out, 2)' : 'ROUND(a.in, 2)'; + + // transfer list shows outbound side only (out > 0) + $extra_cond = ($type === 'transfer') ? 'AND a.out > 0' : ''; + + $sth = $this->pdo->prepare( + "SELECT a.*, {$column} AS quantity, 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.type = :type + {$extra_cond} + ORDER BY a.date DESC" + ); + $sth->execute([ + ':company_id' => $this->company_id, + ':type' => $type, + ]); + return $sth->fetchAll(PDO::FETCH_ASSOC); + } + + // ───────────────────────────────────────────────────────────── + // Single record retrieval + // ───────────────────────────────────────────────────────────── + + /** + * Fetch a single stock_in record with product and contact name, + * plus lot expiry date if applicable. + */ + public function getStockInById(int $warehouse_id, int $id): array|false + { + $table = $this->resolveTable($warehouse_id); + + $sth = $this->pdo->prepare( + "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 a.contact_id = b.id + 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" + ); + $sth->execute([':company_id' => $this->company_id, ':id' => $id]); + return $sth->fetch(PDO::FETCH_ASSOC); + } + + /** + * Fetch a single stock_out record with product and contact name. + */ + public function getStockOutById(int $warehouse_id, int $id): array|false + { + $table = $this->resolveTable($warehouse_id); + + $sth = $this->pdo->prepare( + "SELECT a.*, a.out AS quantity, + b.contact_name, + c.product_name + FROM `{$table}` a + LEFT JOIN md_contact b + ON a.company_id = b.company_id AND a.contact_id = b.id + LEFT JOIN md_product c + ON a.company_id = c.company_id AND a.product_sku = c.sku + WHERE a.company_id = :company_id AND a.id = :id" + ); + $sth->execute([':company_id' => $this->company_id, ':id' => $id]); + return $sth->fetch(PDO::FETCH_ASSOC); + } + + /** + * Fetch a transfer record pair — the outbound row plus its paired + * inbound row resolved via ref_warehouse + uuid. + * Returns the outbound row with a 'ref' key containing the inbound row. + */ + public function getTransferById(int $warehouse_id, int $id): array|false + { + $table = $this->resolveTable($warehouse_id); + + // Fetch the outbound (from) row + $sth = $this->pdo->prepare( + "SELECT a.*, a.out AS quantity, + b.contact_name, + c.product_name + FROM `{$table}` a + LEFT JOIN md_contact b + ON a.company_id = b.company_id AND a.contact_id = b.id + LEFT JOIN md_product c + ON a.company_id = c.company_id AND a.product_sku = c.sku + WHERE a.company_id = :company_id AND a.id = :id" + ); + $sth->execute([':company_id' => $this->company_id, ':id' => $id]); + $output = $sth->fetch(PDO::FETCH_ASSOC); + + if (!$output) return false; + + // Resolve the inbound (to) row via ref_warehouse + uuid + $to_warehouse_id = (int)$output['ref_warehouse']; + $to_table = $this->resolveTable($to_warehouse_id); + + $sth = $this->pdo->prepare( + "SELECT a.*, a.in AS quantity, + b.contact_name, + c.product_name + FROM `{$to_table}` a + LEFT JOIN md_contact b + ON a.company_id = b.company_id AND a.contact_id = b.id + LEFT JOIN md_product c + ON a.company_id = b.company_id AND a.product_sku = c.sku + WHERE a.company_id = :company_id AND a.uuid = :uuid" + ); + $sth->execute([':company_id' => $this->company_id, ':uuid' => $output['uuid']]); + $output['ref'] = $sth->fetch(PDO::FETCH_ASSOC); + + return $output; + } + + + // ───────────────────────────────────────────────────────────── + // Stock write operations + // ───────────────────────────────────────────────────────────── + + /** + * Insert or update a stock_in record. + * On insert: upserts md_lot, inserts td_stock row, occupies rack, adjusts balance. + * On update: updates metadata (contact, description, log) only. + * Must be called inside dbTransaction() by the caller. + */ + public function saveStockIn(array $data, array $logging, string $uuid): void + { + $id = (int)$data['id']; + $warehouse_id = (int)$data['warehouse']; + $whMgmt = new WarehouseManager($this->pdo, $this->company_id); + + $ctx = $whMgmt->getStockContext($warehouse_id, $id); + $row = $ctx['row'] ?? []; + $table = $ctx['table']; + $table_log = json_decode($row['log'] ?? '[]', true) ?: []; + $table_log[] = $logging; + + if ($id > 0) { + + $this->pdo->prepare( + "UPDATE `$table` SET + `contact_id` = :contact_id, + `description` = :description, + `log` = :log + WHERE id = :id AND company_id = :company_id" + )->execute([ + ':id' => $id, + ':company_id' => $this->company_id, + ':contact_id' => (int)$data['contact_id'], + ':description' => $data['description'], + ':log' => json_encode($table_log), + ]); + + } else { + + $lot_number = $data['lot_number'] ?: null; + $expiry_date = $data['expiry_date'] ?: null; + + // Upsert md_lot if lot + expiry provided + if ($lot_number && $expiry_date) { + $this->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" + )->execute([ + ':company_id' => $this->company_id, + ':product_sku' => $data['product_sku'], + ':lot_number' => $lot_number, + ':expiry_date' => $expiry_date, + ]); + } + + $this->pdo->prepare( + "INSERT INTO `$table` + (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', :lot_number, :serial_number)" + )->execute([ + ':uuid' => $uuid, + ':company_id' => $this->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, + ]); + + $td_stock_id = (int)$this->pdo->lastInsertId(); + + $whMgmt->occupyRack( + $warehouse_id, + $data['zone'], $data['aisle'], $data['rack'], + $data['product_sku'], + $td_stock_id + ); + + $whMgmt->adjustBalance( + 'in', + $warehouse_id, + $data['product_sku'], + $row['in'] ?? 0, + $data['quantity'] + ); + } + } + + /** + * Insert or update a stock_out record. + * On insert: validates rack, inserts td_stock row, releases rack, adjusts balance. + * On update: updates metadata (contact, description, log) only. + * Must be called inside dbTransaction() by the caller. + */ + public function saveStockOut(array $data, array $logging, string $uuid): void + { + $id = (int)$data['id']; + $warehouse_id = (int)$data['warehouse']; + $whMgmt = new WarehouseManager($this->pdo, $this->company_id); + + $ctx = $whMgmt->getStockContext($warehouse_id, $id); + $row = $ctx['row'] ?? []; + $table = $ctx['table']; + $table_log = json_decode($row['log'] ?? '[]', true) ?: []; + $table_log[] = $logging; + + if ($id > 0) { + + $this->pdo->prepare( + "UPDATE `$table` SET + `contact_id` = :contact_id, + `description` = :description, + `log` = :log + WHERE id = :id AND company_id = :company_id" + )->execute([ + ':id' => $id, + ':company_id' => $this->company_id, + ':contact_id' => (int)$data['contact_id'], + ':description' => $data['description'], + ':log' => json_encode($table_log), + ]); + + } else { + + $source_stock = $whMgmt->getRackStock( + $warehouse_id, + $data['zone'], $data['aisle'], $data['rack'] + ); + + if (!$source_stock) { + throw new Exception( + "Rack {$data['zone']}-{$data['aisle']}-{$data['rack']} is empty — nothing to take out." + ); + } + + if ($source_stock['product_sku'] !== $data['product_sku']) { + throw new Exception( + "Rack holds {$source_stock['product_sku']}, not {$data['product_sku']}." + ); + } + + 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 (!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 = (int)$source_stock['in']; + $ref_id = (int)$source_stock['id']; + $lot_number = $source_stock['lot_number'] ?? null; + $serial_number = $source_stock['serial_number'] ?? null; + + $this->pdo->prepare( + "INSERT INTO `$table` + (uuid, company_id, `date`, product_sku, `out`, zone, aisle, rack, + 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, :lot_number, :serial_number)" + )->execute([ + ':uuid' => $uuid, + ':company_id' => $this->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, + ]); + + $whMgmt->releaseRack( + $warehouse_id, + $data['zone'], $data['aisle'], $data['rack'] + ); + + $whMgmt->adjustBalance( + 'out', + $warehouse_id, + $data['product_sku'], + $row['out'] ?? 0, + $quantity + ); + } + } + + /** + * Insert or update a stock transfer record pair. + * On insert: validates source rack, inserts paired td_stock rows, moves rack state, adjusts both balances. + * On update: updates metadata (contact, description, log) on both rows only. + * Must be called inside dbTransaction() by the caller. + */ + public function saveStockTransfer(array $data, array $logging, string $uuid): void + { + $id = (int)($data['id'] ?? 0); + $whMgmt = new WarehouseManager($this->pdo, $this->company_id); + + $contact_id = (int)($data['contact_id'] ?? 0); + $description = ($data['description'] ?? ''); + + if ($id > 0) { + + $from_warehouse = (int)($data['warehouse_from'] ?? 0); + $to_warehouse = (int)($data['warehouse_to'] ?? 0); + + $from_ctx = $whMgmt->getStockContext($from_warehouse, $id); + $from_row = $from_ctx['row']; + + if (!$from_row) { + throw new Exception("Transfer record not found."); + } + + $to_id = (int)$from_row['ref_id']; + $to_ctx = $whMgmt->getStockContext($to_warehouse, $to_id); + $to_row = $to_ctx['row']; + + if (!$to_row) { + throw new Exception("Paired destination record missing — data integrity issue."); + } + + $from_log = json_decode($from_row['log'] ?? '[]', true) ?: []; + $from_log[] = $logging; + $to_log = json_decode($to_row['log'] ?? '[]', true) ?: []; + $to_log[] = $logging; + + $this->pdo->prepare( + "UPDATE `{$from_ctx['table']}` SET + contact_id = :contact_id, + description = :description, + `log` = :log + WHERE id = :id AND company_id = :company_id" + )->execute([ + ':contact_id' => $contact_id, + ':description' => $description, + ':log' => json_encode($from_log), + ':id' => $id, + ':company_id' => $this->company_id, + ]); + + $this->pdo->prepare( + "UPDATE `{$to_ctx['table']}` SET + contact_id = :contact_id, + description = :description, + `log` = :log + WHERE id = :id AND company_id = :company_id" + )->execute([ + ':contact_id' => $contact_id, + ':description' => $description, + ':log' => json_encode($to_log), + ':id' => $to_id, + ':company_id' => $this->company_id, + ]); + + return; + } + + $from_warehouse = (int)$data['warehouse_from']; + $from_zone = $data['zone_from']; + $from_aisle = $data['aisle_from']; + $from_rack = $data['rack_from']; + $to_warehouse = (int)$data['warehouse_to']; + $to_zone = $data['zone_to']; + $to_aisle = $data['aisle_to']; + $to_rack = $data['rack_to']; + + $source_stock = $whMgmt->getRackStock( + $from_warehouse, $from_zone, $from_aisle, $from_rack + ); + + if (!$source_stock) { + throw new Exception("Source rack {$from_zone}-{$from_aisle}-{$from_rack} is empty."); + } + + if ($source_stock['product_sku'] !== $data['product_sku']) { + throw new Exception( + "Source rack holds {$source_stock['product_sku']}, not {$data['product_sku']}." + ); + } + + 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; + + $from_table = $whMgmt->getStockContext($from_warehouse, 0)['table']; + $to_table = $whMgmt->getStockContext($to_warehouse, 0)['table']; + $table_log = [$logging]; + + $this->pdo->prepare( + "INSERT INTO `$from_table` + (uuid, company_id, `date`, product_sku, `out`, + ref_warehouse, zone, aisle, rack, + 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', :lot_number, :serial_number)" + )->execute([ + ':uuid' => $uuid, + ':company_id' => $this->company_id, + ':date' => date('Y-m-d H:i:s'), + ':product_sku' => $data['product_sku'], + ':quantity' => $quantity, + ':ref_warehouse' => $to_warehouse, + ':zone' => $from_zone, + ':aisle' => $from_aisle, + ':rack' => $from_rack, + ':contact_id' => $contact_id, + ':description' => $description, + ':log' => json_encode($table_log), + ':lot_number' => $lot_number, + ':serial_number' => $serial_number, + ]); + $from_stock_id = (int)$this->pdo->lastInsertId(); + + $this->pdo->prepare( + "INSERT INTO `$to_table` + (uuid, company_id, `date`, product_sku, `in`, + ref_warehouse, ref_id, zone, aisle, rack, + 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', :lot_number, :serial_number)" + )->execute([ + ':uuid' => $uuid, + ':company_id' => $this->company_id, + ':date' => date('Y-m-d H:i:s'), + ':product_sku' => $data['product_sku'], + ':quantity' => $quantity, + ':ref_warehouse' => $from_warehouse, + ':ref_id' => $from_stock_id, + ':zone' => $to_zone, + ':aisle' => $to_aisle, + ':rack' => $to_rack, + ':contact_id' => $contact_id, + ':description' => $description, + ':log' => json_encode($table_log), + ':lot_number' => $lot_number, + ':serial_number' => $serial_number, + ]); + $to_stock_id = (int)$this->pdo->lastInsertId(); + + // Back-fill ref_id on from row so both rows point at each other + $this->pdo->prepare( + "UPDATE `$from_table` SET ref_id = :ref_id + WHERE id = :id AND company_id = :company_id" + )->execute([ + ':ref_id' => $to_stock_id, + ':id' => $from_stock_id, + ':company_id' => $this->company_id, + ]); + + $whMgmt->releaseRack($from_warehouse, $from_zone, $from_aisle, $from_rack); + $whMgmt->occupyRack($to_warehouse, $to_zone, $to_aisle, $to_rack, $data['product_sku'], $to_stock_id); + + $whMgmt->adjustBalance('out', $from_warehouse, $data['product_sku'], 0, $quantity); + $whMgmt->adjustBalance('in', $to_warehouse, $data['product_sku'], 0, $quantity); + } + +} +?> \ No newline at end of file diff --git a/app/assets/utils/classes/WarehouseManager.php b/app/assets/utils/classes/WarehouseManager.php index da640f4..7e48b2e 100644 --- a/app/assets/utils/classes/WarehouseManager.php +++ b/app/assets/utils/classes/WarehouseManager.php @@ -1272,6 +1272,430 @@ class WarehouseManager { ]); } -} -?> \ No newline at end of file + // ───────────────────────────────────────────────────────────── + // 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_ 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'); + } + +} \ No newline at end of file diff --git a/app/contact/api/engine/contact.php b/app/contact/api/engine/contact.php index 63cb544..fa173e6 100644 --- a/app/contact/api/engine/contact.php +++ b/app/contact/api/engine/contact.php @@ -1,20 +1,10 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id - ]); - db_check($sth, $answer); - - $answer["output"] = $sth->fetchAll(PDO::FETCH_ASSOC); - $answer["success"] = 1; + $contact = new ContactManager($pdo2, $company_id); + $answer['output'] = $contact->getContactList(); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/contact/api/engine/contact_type.php b/app/contact/api/engine/contact_type.php index 97293b6..752c150 100644 --- a/app/contact/api/engine/contact_type.php +++ b/app/contact/api/engine/contact_type.php @@ -1,20 +1,10 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id - ]); - db_check($sth, $answer); - - $answer["output"] = $sth->fetchAll(PDO::FETCH_ASSOC); - $answer["success"] = 1; + $contact = new ContactManager($pdo2, $company_id); + $answer['output'] = $contact->getContactTypeList(); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/contact/api/engine/manage_contact.php b/app/contact/api/engine/manage_contact.php index e7098d8..d9dfb25 100644 --- a/app/contact/api/engine/manage_contact.php +++ b/app/contact/api/engine/manage_contact.php @@ -1,106 +1,49 @@ - 0) { $sql = "SELECT `contact_image` FROM md_contact WHERE company_id = :company_id AND id = :id"; $sth = $pdo2->prepare($sql); - $sth->execute([":company_id" => $company_id, ":id" => $id]); + $sth->execute([':company_id' => $company_id, ':id' => $id]); $uploader->cleanup($sth->fetchColumn(), $data['keep_files'] ?? ''); } - // Upload new files $errors = $uploader->upload('contact_image'); if (!empty($errors)) { - $answer["success"] = 0; - $answer["message"] = $errors[0]; + $answer['success'] = 0; + $answer['message'] = $errors[0]; exit(json_encode($answer)); } $db_image_string = $uploader->buildFileString($data['keep_files'] ?? ''); /** 2. DATABASE **/ - dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $id, $db_image_string) { - - $sql = "SELECT `log` FROM md_contact - WHERE company_id = :company_id AND id = :id"; - $sth = $pdo->prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":id" => $id - ]); - - $table_log = json_decode($sth->fetchColumn() ?: '[]', true); - if (!is_array($table_log)) $table_log = []; - $table_log[] = $logging; - - $params = [ - ":company_id" => $company_id, - ":contact_name" => $data["contact_name"], - ":tax_id" => $data["tax_id"], - ":organization" => $data["organization"], - ":branch" => $data["branch"], - ":contact_type" => (int)$data["contact_type"], - ":billing_address" => $data["billing_address"], - ":shipping_location"=> $data["shipping_location"], - ":shipping_address" => $data["shipping_address"], - ":remark" => $data["remark"], - ":contact_image" => $db_image_string, - ":status" => (int)$data["status"], - ":log" => json_encode($table_log), - ]; - - if ($id > 0) { - $sql = "UPDATE md_contact SET - `contact_name` = :contact_name, - `tax_id` = :tax_id, - `organization` = :organization, - `branch` = :branch, - `contact_type` = :contact_type, - `billing_address` = :billing_address, - `shipping_location` = :shipping_location, - `shipping_address` = :shipping_address, - `remark` = :remark, - `contact_image` = :contact_image, - `status` = :status, - `log` = :log - WHERE id = :id AND company_id = :company_id"; - $params[":id"] = $id; - } else { - $sql = "INSERT INTO md_contact - (company_id, contact_name, tax_id, organization, branch, contact_type, - billing_address, shipping_location, shipping_address, remark, contact_image, status, log) - VALUES - (:company_id, :contact_name, :tax_id, :organization, :branch, :contact_type, - :billing_address, :shipping_location, :shipping_address, :remark, :contact_image, :status, :log)"; - } - - $sth = $pdo->prepare($sql); - $sth->execute($params); - + dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $db_image_string) { + $contact = new ContactManager($pdo, $company_id); + $contact->saveContact($data, $logging, $db_image_string); }); - $answer["success"] = 1; + $answer['success'] = 1; } catch (PDOException $e) { - // DB failed — rollback uploaded files if (isset($uploader)) $uploader->rollbackUploads(); - $answer["success"] = 0; - $answer["message"] = "Database error, please try again"; + $answer['success'] = 0; + $answer['message'] = 'Database error, please try again'; http_response_code(500); } catch (Exception $e) { if (isset($uploader)) $uploader->rollbackUploads(); - $answer["success"] = 0; - $answer["message"] = $e->getMessage(); + $answer['success'] = 0; + $answer['message'] = $e->getMessage(); http_response_code(400); } diff --git a/app/contact/api/engine/manage_contact_type.php b/app/contact/api/engine/manage_contact_type.php index 2856325..7b2663e 100644 --- a/app/contact/api/engine/manage_contact_type.php +++ b/app/contact/api/engine/manage_contact_type.php @@ -1,66 +1,24 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":id" => $id - ]); - - $table_log = json_decode($sth->fetchColumn() ?: '[]', true); - if (!is_array($table_log)) $table_log = []; - $table_log[] = $logging; - - $params = [ - ":company_id" => $company_id, - ":contact_type" => $data["contact_type"], - ":description" => $data["description"], - ":status" => (int)$data["status"], - ":log" => json_encode($table_log), - ]; - - if ($id > 0) { - - $sql = "UPDATE md_contact_type SET - `contact_type` = :contact_type, - `description` = :description, - `status` = :status, - `log` = :log - WHERE id = :id AND company_id = :company_id"; - - $params[":id"] = $id; - - } else { - - $sql = "INSERT INTO md_contact_type - (company_id, contact_type, `description`, `status`, `log`) - VALUES - (:company_id, :contact_type, :description, :status, :log)"; - } - - $sth = $pdo->prepare($sql); - $sth->execute($params); - + $contact = new ContactManager($pdo, $company_id); + $contact->saveContactType($data, $logging); }); - $answer["success"] = 1; + $answer['success'] = 1; } catch (PDOException $e) { - $answer["success"] = 0; - $answer["message"] = "Database error, please try again"; + $answer['success'] = 0; + $answer['message'] = 'Database error, please try again'; http_response_code(500); } catch (Exception $e) { - $answer["success"] = 0; - $answer["message"] = $e->getMessage(); + $answer['success'] = 0; + $answer['message'] = $e->getMessage(); http_response_code(400); } diff --git a/app/contact/api/engine/retrieve_contact.php b/app/contact/api/engine/retrieve_contact.php index 2764dcb..1887828 100644 --- a/app/contact/api/engine/retrieve_contact.php +++ b/app/contact/api/engine/retrieve_contact.php @@ -1,28 +1,11 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":id" => $id - ]); - db_check($sth, $answer); - - $target_dir = $include_url."uploads/contact/"; - - $answer["contact_dir"] = $target_dir; - $answer["output"] = $sth->fetch(PDO::FETCH_ASSOC); - $answer["success"] = 1; + $contact = new ContactManager($pdo2, $company_id); + $answer['contact_dir'] = $include_url . 'uploads/contact/'; + $answer['output'] = $contact->getContactById((int)$data['id']); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/contact/api/engine/retrieve_contact_type.php b/app/contact/api/engine/retrieve_contact_type.php index 47997fe..a6d12c2 100644 --- a/app/contact/api/engine/retrieve_contact_type.php +++ b/app/contact/api/engine/retrieve_contact_type.php @@ -1,25 +1,10 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":id" => $id - ]); - db_check($sth, $answer); - - $answer["output"] = $sth->fetch(PDO::FETCH_ASSOC); - $answer["success"] = 1; + $contact = new ContactManager($pdo2, $company_id); + $answer['output'] = $contact->getContactTypeById((int)$data['id']); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/dashboard/api/engine_report/lot_stock_log.php b/app/dashboard/api/engine_report/lot_stock_log.php index 5c6d746..3bee4b4 100644 --- a/app/dashboard/api/engine_report/lot_stock_log.php +++ b/app/dashboard/api/engine_report/lot_stock_log.php @@ -1,60 +1,13 @@ prepare( - "SELECT id, warehouse_name FROM md_warehouse - WHERE company_id = :company_id AND status = 1" + $report = new ReportManager($pdo2, $company_id); + $answer['output'] = $report->getLotStockLog( + $data['product_sku'] ?? '', + $data['lot_number'] ?? '' ); - $sth->execute([':company_id' => $company_id]); - $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 = $pdo2->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' => $company_id, - ':product_sku' => $product_sku, - ':lot_number' => $lot_number, - ]); - $rows = array_merge($rows, $sth->fetchAll(PDO::FETCH_ASSOC)); - } - - // Sort all rows by date DESC - usort($rows, fn($a, $b) => strcmp($b['date'], $a['date'])); - - $answer['output'] = $rows; $answer['success'] = 1; exit(json_encode($answer)); ?> \ No newline at end of file diff --git a/app/dashboard/api/engine_report/product_lot.php b/app/dashboard/api/engine_report/product_lot.php index b6eab06..f831382 100644 --- a/app/dashboard/api/engine_report/product_lot.php +++ b/app/dashboard/api/engine_report/product_lot.php @@ -1,61 +1,16 @@ 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' => $company_id]); - db_check($sth, $answer); + $report = new ReportManager($pdo2, $company_id); + $result = $report->getProductLots(); - $rows = $sth->fetchAll(PDO::FETCH_ASSOC); - $total = count($rows); - $active = 0; - $expired = 0; - $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++; - - // Status - if ($days < 0) $row['status'] = 'expired'; - elseif ($days <= 7) $row['status'] = 'critical'; - elseif ($days <= 30) $row['status'] = 'near'; - else $row['status'] = 'ok'; - } - unset($row); - - $answer['output'] = $rows; - $answer['total'] = $total; - $answer['active'] = $active; - $answer['expired'] = $expired; - $answer['near'] = $near; - $answer['success'] = 1; + $answer['output'] = $result['rows']; + $answer['total'] = $result['total']; + $answer['active'] = $result['active']; + $answer['expired'] = $result['expired']; + $answer['near'] = $result['near']; + $answer['success'] = 1; exit(json_encode($answer)); ?> \ No newline at end of file diff --git a/app/dashboard/api/engine_report/rack_log.php b/app/dashboard/api/engine_report/rack_log.php index 8a48be6..0eb51b5 100644 --- a/app/dashboard/api/engine_report/rack_log.php +++ b/app/dashboard/api/engine_report/rack_log.php @@ -1,6 +1,7 @@ prepare($sql); - $sth->execute([ - ':company_id' => $company_id, - ':rack_id' => $rack_id, - ]); - db_check($sth, $answer); - - $answer['output'] = $sth->fetchAll(PDO::FETCH_ASSOC); + $report = new ReportManager($pdo2, $company_id); + $answer['output'] = $report->getRackLog($rack_id); $answer['success'] = 1; exit(json_encode($answer)); ?> \ No newline at end of file diff --git a/app/dashboard/api/engine_report/rack_occupancy.php b/app/dashboard/api/engine_report/rack_occupancy.php index a6ba0e9..f92a27f 100644 --- a/app/dashboard/api/engine_report/rack_occupancy.php +++ b/app/dashboard/api/engine_report/rack_occupancy.php @@ -3,43 +3,14 @@ require '../../../assets/utils/db_auth.php'; require '../../../assets/utils/classes/ReportManager.php'; - $report = new ReportManager($pdo2, $company_id); - - // Stats + $report = new ReportManager($pdo2, $company_id); $total = $report->getTotalCapacity(); $occupied = $report->getSpaceUsed(); - $empty = $total - $occupied; - // All racks with warehouse name, product info and stock info - $sql = "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 = $pdo2->prepare($sql); - $sth->execute([':company_id' => $company_id]); - db_check($sth, $answer); - - $answer['output'] = $sth->fetchAll(PDO::FETCH_ASSOC); + $answer['output'] = $report->getRackOccupancy(); $answer['total'] = $total; $answer['occupied'] = $occupied; - $answer['empty'] = $empty; + $answer['empty'] = $total - $occupied; $answer['success'] = 1; exit(json_encode($answer)); ?> \ No newline at end of file diff --git a/app/ics/api/engine/contact_search.php b/app/ics/api/engine/contact_search.php index 3ef9a1f..aad19de 100644 --- a/app/ics/api/engine/contact_search.php +++ b/app/ics/api/engine/contact_search.php @@ -1,25 +1,10 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":keyword" => '%'.$data["keyword"].'%' - ]); - db_check($sth, $answer); - - $result = $sth->fetchAll(PDO::FETCH_ASSOC); - $answer["result"] = $result; - $answer["success"] = 1; + $contact = new ContactManager($pdo2, $company_id); + $answer['result'] = $contact->searchContact($data['keyword'] ?? ''); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/ics/api/engine/manage_stock_in.php b/app/ics/api/engine/manage_stock_in.php index 622dc96..4988753 100644 --- a/app/ics/api/engine/manage_stock_in.php +++ b/app/ics/api/engine/manage_stock_in.php @@ -1,122 +1,25 @@ -getStockContext($warehouse_id, $id); - $row = $warehouse['row'] ?? []; - $table = $warehouse['table']; - - // Append this edit/create event to td_stock_.log (stock record history) - $table_log = json_decode($row['log'] ?? '[]', true); - $table_log[] = $logging; - - if ($id > 0) { - - // UPDATE existing stock_in record - $sql = "UPDATE `$table` SET - `contact_id` = :contact_id, - `description` = :description, - `log` = :log - WHERE id = :id AND company_id = :company_id"; - - $sth = $pdo->prepare($sql); - $sth->execute([ - ":id" => $id, - ":company_id" => $company_id, - ":contact_id" => (int)$data["contact_id"], - ":description" => $data["description"], - ":log" => json_encode($table_log), - ]); - - - - } 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`, `lot_number`, `serial_number`) - VALUES - (: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), - ":lot_number" => $lot_number, - ":serial_number" => $data["serial_number"] ?: null, - ]); - - // Grab the newly-generated ID to link the rack - $td_stock_id = (int) $pdo->lastInsertId(); - - // Link the rack to this new stock record - $whMgmt->occupyRack( - $warehouse_id, - $data['zone'], $data['aisle'], $data['rack'], - $data['product_sku'], - $td_stock_id - ); - // Balance adjustment handles both INSERT (delta = full qty) - // and UPDATE (delta = new - old) via the same formula. - $whMgmt->adjustBalance( - 'in', - $warehouse_id, - $data["product_sku"], - $row['in'] ?? 0, - $data["quantity"] - ); - - } - + dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $uuid) { + $stock = new StockManager($pdo, $company_id); + $stock->saveStockIn($data, $logging, $uuid); }); - $answer["success"] = 1; + $answer['success'] = 1; } catch (PDOException $e) { - $answer["success"] = 0; - $answer["message"] = "Database error, please try again"; + $answer['success'] = 0; + $answer['message'] = 'Database error, please try again'; http_response_code(500); } catch (Exception $e) { - $answer["success"] = 0; - $answer["message"] = $e->getMessage(); + $answer['success'] = 0; + $answer['message'] = $e->getMessage(); http_response_code(400); } diff --git a/app/ics/api/engine/manage_stock_out.php b/app/ics/api/engine/manage_stock_out.php index 05484ca..9a9b20a 100644 --- a/app/ics/api/engine/manage_stock_out.php +++ b/app/ics/api/engine/manage_stock_out.php @@ -1,145 +1,25 @@ -getStockContext($warehouse_id, $id); - $row = $warehouse['row'] ?? []; - $table = $warehouse['table']; - - // Append this edit/create event to td_stock_.log (stock record history) - $table_log = json_decode($row['log'] ?? '[]', true); - $table_log[] = $logging; - - if ($id > 0) { - - // UPDATE existing stock_out record - $sql = "UPDATE `$table` SET - `contact_id` = :contact_id, - `description` = :description, - `log` = :log - WHERE id = :id AND company_id = :company_id"; - - $sth = $pdo->prepare($sql); - $sth->execute([ - ":id" => $id, - ":company_id" => $company_id, - ":contact_id" => (int)$data["contact_id"], - ":description" => $data["description"], - ":log" => json_encode($table_log), - ]); - - } else { - - // Look up the source stock record via the rack. - // Under 1:1, the rack points to exactly one td_stock_ row. - $source_stock = $whMgmt->getRackStock( - $warehouse_id, - $data["zone"], - $data["aisle"], - $data["rack"] - ); - - if (!$source_stock) { - throw new Exception( - "Rack {$data['zone']}-{$data['aisle']}-{$data['rack']} is empty — nothing to take out." - ); - } - - // 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']; - $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, lot_number, serial_number) - VALUES - (:uuid, :company_id, :date, :product_sku, :quantity, :zone, :aisle, :rack, - :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, - ":lot_number" => $lot_number, - ":serial_number" => $serial_number, - ]); - - // Release the rack — stock is now gone from this location. - $whMgmt->releaseRack( - $warehouse_id, - $data["zone"], $data["aisle"], $data["rack"] - ); - // Balance: total_out increases by delta. - // INSERT: $row['out'] = 0 → delta = full quantity - // UPDATE: $row['out'] = old → delta = new - old - $whMgmt->adjustBalance( - 'out', - $warehouse_id, - $data["product_sku"], - $row['out'] ?? 0, - $quantity - ); - - } - + $stock = new StockManager($pdo, $company_id); + $stock->saveStockOut($data, $logging, $uuid); }); - $answer["success"] = 1; + $answer['success'] = 1; } catch (PDOException $e) { - $answer["success"] = 0; - $answer["message"] = "Database error, please try again"; + $answer['success'] = 0; + $answer['message'] = 'Database error, please try again'; http_response_code(500); } catch (Exception $e) { - $answer["success"] = 0; - $answer["message"] = $e->getMessage(); + $answer['success'] = 0; + $answer['message'] = $e->getMessage(); http_response_code(400); } diff --git a/app/ics/api/engine/manage_stock_transfer.php b/app/ics/api/engine/manage_stock_transfer.php index f086641..566aae2 100644 --- a/app/ics/api/engine/manage_stock_transfer.php +++ b/app/ics/api/engine/manage_stock_transfer.php @@ -1,239 +1,25 @@ - 0) { - - // Find the "from" row that's being edited. - // We look it up across all td_stock_ via warehouse_from - // since $id alone isn't enough (it's per-table). - $from_warehouse = (int)($data['warehouse_from'] ?? 0); - $to_warehouse = (int)($data['warehouse_to'] ?? 0); - - $from_ctx = $whMgmt->getStockContext($from_warehouse, $id); - $from_table = $from_ctx['table']; - $from_row = $from_ctx['row']; - - if (!$from_row) { - throw new Exception("Transfer record not found."); - } - - // Find the paired "to" row via ref_id stored on the "from" row - $to_id = (int) $from_row['ref_id']; - $to_ctx = $whMgmt->getStockContext($to_warehouse, $to_id); - $to_table = $to_ctx['table']; - $to_row = $to_ctx['row']; - - if (!$to_row) { - throw new Exception("Paired destination record missing — data integrity issue."); - } - - // Append this edit to both rows' logs - $from_log = json_decode($from_row['log'] ?? '[]', true) ?: []; - $from_log[] = $logging; - - $to_log = json_decode($to_row['log'] ?? '[]', true) ?: []; - $to_log[] = $logging; - - // UPDATE "from" row — only metadata fields - $sql = "UPDATE `$from_table` SET - contact_id = :contact_id, - description = :description, - `log` = :log - WHERE id = :id AND company_id = :company_id"; - $pdo->prepare($sql)->execute([ - ":contact_id" => $contact_id, - ":description" => $description, - ":log" => json_encode($from_log), - ":id" => $id, - ":company_id" => $company_id, - ]); - - // UPDATE "to" row — same metadata - $sql = "UPDATE `$to_table` SET - contact_id = :contact_id, - description = :description, - `log` = :log - WHERE id = :id AND company_id = :company_id"; - $pdo->prepare($sql)->execute([ - ":contact_id" => $contact_id, - ":description" => $description, - ":log" => json_encode($to_log), - ":id" => $to_id, - ":company_id" => $company_id, - ]); - - // Done — no rack ops, no balance changes on edit. - return; - } - - // ══════════════════════════════════════════════════════════ - // CREATE FLOW (unchanged from before) - // ══════════════════════════════════════════════════════════ - - // Gather source + destination parameters - $from_warehouse = (int)$data['warehouse_from']; - $from_zone = $data['zone_from']; - $from_aisle = $data['aisle_from']; - $from_rack = $data['rack_from']; - - $to_warehouse = (int)$data['warehouse_to']; - $to_zone = $data['zone_to']; - $to_aisle = $data['aisle_to']; - $to_rack = $data['rack_to']; - - // Validate source rack has stock of the expected SKU. - // Quantity comes from here — NOT user input. - $source_stock = $whMgmt->getRackStock( - $from_warehouse, $from_zone, $from_aisle, $from_rack - ); - - if (!$source_stock) { - throw new Exception( - "Source rack {$from_zone}-{$from_aisle}-{$from_rack} is empty." - ); - } - - if ($source_stock['product_sku'] !== $data["product_sku"]) { - throw new Exception( - "Source rack holds {$source_stock['product_sku']}, not {$data['product_sku']}." - ); - } - - // 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_ tables - $from_ctx = $whMgmt->getStockContext($from_warehouse, 0); - $to_ctx = $whMgmt->getStockContext($to_warehouse, 0); - $from_table = $from_ctx['table']; - $to_table = $to_ctx['table']; - - // Share UUID across both paired rows for easy grouping - $transfer_uuid = $uuid; - - // INSERT "from" row (outbound from source warehouse) - $table_log = [$logging]; - - $sql = "INSERT INTO `$from_table` - (uuid, company_id, `date`, product_sku, `out`, - ref_warehouse, zone, aisle, rack, - 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', :lot_number, :serial_number)"; - $pdo->prepare($sql)->execute([ - ":uuid" => $transfer_uuid, - ":company_id" => $company_id, - ":date" => date('Y-m-d H:i:s'), - ":product_sku" => $data["product_sku"], - ":quantity" => $quantity, - ":ref_warehouse" => $to_warehouse, - ":zone" => $from_zone, - ":aisle" => $from_aisle, - ":rack" => $from_rack, - ":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(); - - // INSERT "to" row (inbound at destination warehouse) - $sql = "INSERT INTO `$to_table` - (uuid, company_id, `date`, product_sku, `in`, - ref_warehouse, ref_id, zone, aisle, rack, - 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', :lot_number, :serial_number)"; - $pdo->prepare($sql)->execute([ - ":uuid" => $transfer_uuid, - ":company_id" => $company_id, - ":date" => date('Y-m-d H:i:s'), - ":product_sku" => $data["product_sku"], - ":quantity" => $quantity, - ":ref_warehouse" => $from_warehouse, - ":ref_id" => $from_stock_id, - ":zone" => $to_zone, - ":aisle" => $to_aisle, - ":rack" => $to_rack, - ":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(); - - // Back-fill the "from" row's ref_id now that we have to_stock_id - // so both rows point at each other - $sql = "UPDATE `$from_table` - SET ref_id = :ref_id - WHERE id = :id AND company_id = :company_id"; - $pdo->prepare($sql)->execute([ - ":ref_id" => $to_stock_id, - ":id" => $from_stock_id, - ":company_id" => $company_id, - ]); - - // Move rack state: release source, occupy destination - // md_rack.log gets 'release' + 'occupy' entries automatically - $whMgmt->releaseRack($from_warehouse, $from_zone, $from_aisle, $from_rack); - $whMgmt->occupyRack( - $to_warehouse, $to_zone, $to_aisle, $to_rack, - $data["product_sku"], $to_stock_id - ); - - // Balance adjustments on both warehouses - $whMgmt->adjustBalance('out', $from_warehouse, $data["product_sku"], 0, $quantity); - $whMgmt->adjustBalance('in', $to_warehouse, $data["product_sku"], 0, $quantity); - + $stock = new StockManager($pdo, $company_id); + $stock->saveStockTransfer($data, $logging, $uuid); }); - $answer["success"] = 1; + $answer['success'] = 1; } catch (PDOException $e) { - $answer["success"] = 0; - $answer["message"] = "Database error, please try again"; + $answer['success'] = 0; + $answer['message'] = 'Database error, please try again'; http_response_code(500); } catch (Exception $e) { - $answer["success"] = 0; - $answer["message"] = $e->getMessage(); + $answer['success'] = 0; + $answer['message'] = $e->getMessage(); http_response_code(400); } diff --git a/app/ics/api/engine/product_search.php b/app/ics/api/engine/product_search.php index a7884ef..008018d 100644 --- a/app/ics/api/engine/product_search.php +++ b/app/ics/api/engine/product_search.php @@ -1,25 +1,10 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":keyword" => '%'.$data["keyword"].'%' - ]); - db_check($sth, $answer); - - $result = $sth->fetchAll(PDO::FETCH_ASSOC); - $answer["result"] = $result; - $answer["success"] = 1; + $product = new ProductManager($pdo2, $company_id); + $answer['result'] = $product->searchProduct($data['keyword'] ?? ''); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/ics/api/engine/retrieve_active_lot.php b/app/ics/api/engine/retrieve_active_lot.php index 2132a0a..98976ff 100644 --- a/app/ics/api/engine/retrieve_active_lot.php +++ b/app/ics/api/engine/retrieve_active_lot.php @@ -3,8 +3,6 @@ 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)) { @@ -13,52 +11,8 @@ 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); + $wh = new WarehouseManager($pdo2, $company_id); + $answer['output'] = $wh->getActiveLots($product_sku); $answer['success'] = 1; exit(json_encode($answer)); ?> \ No newline at end of file diff --git a/app/ics/api/engine/retrieve_active_serial.php b/app/ics/api/engine/retrieve_active_serial.php index 1d47556..9d97c6c 100644 --- a/app/ics/api/engine/retrieve_active_serial.php +++ b/app/ics/api/engine/retrieve_active_serial.php @@ -1,11 +1,9 @@ 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); + $wh = new WarehouseManager($pdo2, $company_id); + $answer['output'] = $wh->getActiveSerials($product_sku, $data['lot_number'] ?? ''); $answer['success'] = 1; exit(json_encode($answer)); ?> \ No newline at end of file diff --git a/app/ics/api/engine/retrieve_aisle.php b/app/ics/api/engine/retrieve_aisle.php index 8390e10..9d175dd 100644 --- a/app/ics/api/engine/retrieve_aisle.php +++ b/app/ics/api/engine/retrieve_aisle.php @@ -6,15 +6,15 @@ $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') { + if (!empty($data['id'])) { + $output = $whMgmt->getAislesAll($warehouse, $zone); + } elseif ($type === 'to') { $output = $whMgmt->getAislesIn($warehouse, $zone); - } elseif ($type === 'from') { $output = $whMgmt->getAislesOut( $warehouse, @@ -23,26 +23,8 @@ $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'); - } - - // 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'); + $output = $whMgmt->getAislesAll($warehouse, $zone); } $answer['output'] = $output; diff --git a/app/ics/api/engine/retrieve_lot.php b/app/ics/api/engine/retrieve_lot.php index b62002c..c343f6d 100644 --- a/app/ics/api/engine/retrieve_lot.php +++ b/app/ics/api/engine/retrieve_lot.php @@ -1,26 +1,13 @@ -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; + $wh = new WarehouseManager($pdo2, $company_id); + $answer['result'] = $wh->getLotList( + $data['product_sku'] ?? '', + $data['keyword'] ?? '' + ); + $answer['success'] = 1; exit(json_encode($answer)); ?> \ No newline at end of file diff --git a/app/ics/api/engine/retrieve_rack.php b/app/ics/api/engine/retrieve_rack.php index d97207f..fc625a0 100644 --- a/app/ics/api/engine/retrieve_rack.php +++ b/app/ics/api/engine/retrieve_rack.php @@ -6,16 +6,16 @@ $type = $data['type'] ?? ''; $whMgmt = new WarehouseManager($pdo2, $company_id); - // 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') { + if (!empty($data['id'])) { + $output = $whMgmt->getRacksAll($warehouse, $zone, $aisle); + } elseif ($type === 'to') { $output = $whMgmt->getRacksIn($warehouse, $zone, $aisle); - } elseif ($type === 'from') { $output = $whMgmt->getRacksOut( $warehouse, @@ -25,26 +25,8 @@ $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'); - } - - // 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'); + $output = $whMgmt->getRacksAll($warehouse, $zone, $aisle); } $answer['output'] = $output; diff --git a/app/ics/api/engine/retrieve_stock_in.php b/app/ics/api/engine/retrieve_stock_in.php index c2a2d87..97c2e3d 100644 --- a/app/ics/api/engine/retrieve_stock_in.php +++ b/app/ics/api/engine/retrieve_stock_in.php @@ -1,55 +1,10 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":warehouse" => $warehouse - ]); - db_check($sth, $answer); - - $warehouse = $sth->fetchColumn(); - - $table = "td_stock_" . $warehouse; - - $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 - a.contact_id = b.id - 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 "; - - $sth = $pdo2->prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":id" => $id - ]); - db_check($sth, $answer); - - $answer["output"] = $sth->fetch(PDO::FETCH_ASSOC); - $answer["success"] = 1; + $stock = new StockManager($pdo2, $company_id); + $answer['output'] = $stock->getStockInById((int)$data['wh'], (int)$data['id']); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/ics/api/engine/retrieve_stock_out.php b/app/ics/api/engine/retrieve_stock_out.php index 68b43b5..12b1e99 100644 --- a/app/ics/api/engine/retrieve_stock_out.php +++ b/app/ics/api/engine/retrieve_stock_out.php @@ -1,51 +1,10 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":warehouse" => $warehouse - ]); - db_check($sth, $answer); - - $warehouse = $sth->fetchColumn(); - - $table = "td_stock_" . $warehouse; - - $sql = "SELECT a.* , a.out as quantity, b.contact_name, c.product_name - FROM `{$table}` a - LEFT JOIN md_contact b ON - a.company_id = b.company_id and - a.contact_id = b.id - LEFT JOIN md_product c ON - a.company_id = c.company_id and - a.product_sku = c.sku - WHERE - a.company_id = :company_id and - a.id = :id "; - - $sth = $pdo2->prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":id" => $id - ]); - db_check($sth, $answer); - - $answer["output"] = $sth->fetch(PDO::FETCH_ASSOC); - $answer["success"] = 1; + $stock = new StockManager($pdo2, $company_id); + $answer['output'] = $stock->getStockOutById((int)$data['wh'], (int)$data['id']); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/ics/api/engine/retrieve_stock_transfer.php b/app/ics/api/engine/retrieve_stock_transfer.php index 9544981..2c37870 100644 --- a/app/ics/api/engine/retrieve_stock_transfer.php +++ b/app/ics/api/engine/retrieve_stock_transfer.php @@ -1,101 +1,10 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":warehouse" => $warehouse - ]); - db_check($sth, $answer); - - $warehouse = $sth->fetchColumn(); - - $table = "td_stock_" . $warehouse; - - $sql = "SELECT a.* , a.out as quantity, b.contact_name, c.product_name - FROM `{$table}` a - LEFT JOIN md_contact b ON - a.company_id = b.company_id and - a.contact_id = b.id - LEFT JOIN md_product c ON - a.company_id = c.company_id and - a.product_sku = c.sku - WHERE - a.company_id = :company_id and - a.id = :id "; - - $sth = $pdo2->prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":id" => $id - ]); - db_check($sth, $answer); - - $output = $sth->fetch(PDO::FETCH_ASSOC); - - /** - * TO warehouse, zone, aisle, rack, product name, quantity, contact name - */ - - // get warehouse name - $sql = "SELECT warehouse_name - FROM md_warehouse - WHERE - company_id = :company_id and - id = :warehouse - "; - - $sth = $pdo2->prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":warehouse" => $output["ref_warehouse"] - ]); - db_check($sth, $answer); - - $warehouse = $sth->fetchColumn(); - - $table = "td_stock_" . $warehouse; - - $sql = "SELECT a.* , a.in as quantity, b.contact_name, c.product_name - FROM `{$table}` a - LEFT JOIN md_contact b ON - a.company_id = b.company_id and - a.contact_id = b.id - LEFT JOIN md_product c ON - a.company_id = b.company_id and - a.product_sku = c.sku - WHERE - a.company_id = :company_id and - a.uuid = :id "; - - $sth = $pdo2->prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":id" => $output["uuid"] - ]); - db_check($sth, $answer); - - $output["ref"] = $sth->fetch(PDO::FETCH_ASSOC); - - $answer["output"] = $output; - $answer["success"] = 1; + $stock = new StockManager($pdo2, $company_id); + $answer['output'] = $stock->getTransferById((int)$data['wh'], (int)$data['id']); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/ics/api/engine/retrieve_warehouse.php b/app/ics/api/engine/retrieve_warehouse.php index ff1d397..fbdb266 100644 --- a/app/ics/api/engine/retrieve_warehouse.php +++ b/app/ics/api/engine/retrieve_warehouse.php @@ -1,46 +1,14 @@ - $company_id]; - - if ($data["type"] == "to") { - $product_sku_filter = "AND r.product_sku IS NULL"; - - } elseif ($data["type"] == "from") { - $product_sku_filter = "AND r.product_sku = :product_sku"; - $params[":product_sku"] = $data["product_sku"]; - } - - if(!empty($data["id"])){ - $product_sku_filter = ""; - unset($params[":product_sku"]); - } - - // Only list warehouses that have racks matching the occupancy filter. - // EXISTS is efficient here — it stops searching as soon as one match is found. - $sql = "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 = $pdo2->prepare($sql); - $sth->execute($params); - db_check($sth, $answer); - - $answer["output"] = $sth->fetchAll(PDO::FETCH_ASSOC); - $answer["success"] = 1; + $wh = new WarehouseManager($pdo2, $company_id); + $answer['output'] = $wh->getWarehouseList( + $data['type'] ?? '', + $data['product_sku'] ?? '', + (int)($data['id'] ?? 0) + ); + $answer['success'] = 1; exit(json_encode($answer)); ?> \ No newline at end of file diff --git a/app/ics/api/engine/retrieve_zone.php b/app/ics/api/engine/retrieve_zone.php index c89f7d6..38b08a6 100644 --- a/app/ics/api/engine/retrieve_zone.php +++ b/app/ics/api/engine/retrieve_zone.php @@ -6,44 +6,23 @@ $type = $data['type'] ?? ''; $whMgmt = new WarehouseManager($pdo2, $company_id); - // 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 + if (!empty($data['id'])) { + $output = $whMgmt->getZonesAll($warehouse); + } elseif ($type === 'to') { $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); - } - - // 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); + $output = $whMgmt->getZonesAll($warehouse); } $answer['output'] = $output; diff --git a/app/ics/api/engine/stock_in.php b/app/ics/api/engine/stock_in.php index f91dffe..2b68f12 100644 --- a/app/ics/api/engine/stock_in.php +++ b/app/ics/api/engine/stock_in.php @@ -1,53 +1,10 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":warehouse" => $warehouse - ]); - db_check($sth, $answer); - - $warehouse = $sth->fetchColumn(); - - if(!$warehouse) { - $answer["message"] = "Warehouse not found"; - exit(json_encode($answer)); - } - - $table = "td_stock_" . $warehouse; - - $sql = "SELECT a.* , ROUND(a.in,2) as quantity , - 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.type = 'in' - ORDER BY a.date DESC"; - $sth = $pdo2->prepare($sql); - $sth->execute([ - ":company_id" => $company_id - ]); - db_check($sth, $answer); - - $answer["output"] = $sth->fetchAll(PDO::FETCH_ASSOC); - $answer["success"] = 1; + $stock = new StockManager($pdo2, $company_id); + $answer['output'] = $stock->getStockList((int)$data['warehouse'], 'in'); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/ics/api/engine/stock_out.php b/app/ics/api/engine/stock_out.php index e877c15..8621e2c 100644 --- a/app/ics/api/engine/stock_out.php +++ b/app/ics/api/engine/stock_out.php @@ -1,53 +1,10 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":warehouse" => $warehouse - ]); - db_check($sth, $answer); - - $warehouse = $sth->fetchColumn(); - - if(!$warehouse) { - $answer["message"] = "Warehouse not found"; - exit(json_encode($answer)); - } - - $table = "td_stock_" . $warehouse; - - $sql = "SELECT a.* , ROUND(a.out,2) as quantity , - 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.type = 'out' - ORDER BY a.date DESC"; - $sth = $pdo2->prepare($sql); - $sth->execute([ - ":company_id" => $company_id - ]); - db_check($sth, $answer); - - $answer["output"] = $sth->fetchAll(PDO::FETCH_ASSOC); - $answer["success"] = 1; + $stock = new StockManager($pdo2, $company_id); + $answer['output'] = $stock->getStockList((int)$data['warehouse'], 'out'); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/ics/api/engine/stock_transfer.php b/app/ics/api/engine/stock_transfer.php index 9dc36f7..a5f31f2 100644 --- a/app/ics/api/engine/stock_transfer.php +++ b/app/ics/api/engine/stock_transfer.php @@ -1,54 +1,10 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":warehouse" => $warehouse - ]); - db_check($sth, $answer); - - $warehouse = $sth->fetchColumn(); - - if(!$warehouse) { - $answer["message"] = "Warehouse not found"; - exit(json_encode($answer)); - } - - $table = "td_stock_" . $warehouse; - - $sql = "SELECT a.* , ROUND(a.in,2) as quantity, - 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.type = 'transfer' and - a.out > 0 - ORDER BY a.date DESC"; - $sth = $pdo2->prepare($sql); - $sth->execute([ - ":company_id" => $company_id - ]); - db_check($sth, $answer); - - $answer["output"] = $sth->fetchAll(PDO::FETCH_ASSOC); - $answer["success"] = 1; + $stock = new StockManager($pdo2, $company_id); + $answer['output'] = $stock->getStockList((int)$data['warehouse'], 'transfer'); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/ics/api/engine_report/stock_activity.php b/app/ics/api/engine_report/stock_activity.php index d8bb3af..b6e33ed 100644 --- a/app/ics/api/engine_report/stock_activity.php +++ b/app/ics/api/engine_report/stock_activity.php @@ -1,66 +1,10 @@ -getStockContext($warehouse_id, $id); - $table = $warehouse['table']; - - - // ── Last 10 stock in/out transactions ──────────────────────────────── - $sql = "SELECT - a.id, - 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 = $pdo2->prepare($sql); - $sth->execute(); - $rows = $sth->fetchAll(PDO::FETCH_ASSOC); - - // ── Build output ───────────────────────────────────────────────────── - $activities = []; - - foreach ($rows 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'], - ]; - } - - // ── Response ───────────────────────────────────────────────────────── - $output = [ - 'activities' => $activities, - ]; - - $answer["output"] = $output; - $answer["success"] = 1; + $report = new ReportManager($pdo2, $company_id); + $answer['output'] = $report->getWarehouseActivity((int)$data['warehouse']); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/ics/api/engine_report/stock_movement.php b/app/ics/api/engine_report/stock_movement.php index 20b12a9..15bc94d 100644 --- a/app/ics/api/engine_report/stock_movement.php +++ b/app/ics/api/engine_report/stock_movement.php @@ -1,70 +1,10 @@ -getStockContext($warehouse_id, $id); - $table = $warehouse['table']; - - - // ── Always last 12 months ──────────────────────────────────────────── - $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'); - - // ── Query: group by month ──────────────────────────────────────────── - $sql = "SELECT - DATE_FORMAT(a.date, '%b') AS `label`, - DATE_FORMAT(a.date, '%Y-%m') AS `sort_key`, - ROUND(SUM(a.`in`), 2) AS `stock_in`, - ROUND(SUM(a.`out`), 2) AS `stock_out` - FROM `{$table}` a - WHERE - a.company_id = {$company_id} and - a.date >= '{$start}' and - a.date <= '{$end}' - GROUP BY `sort_key`, `label` - ORDER BY `sort_key` ASC"; - - $sth = $pdo2->prepare($sql); - $sth->execute(); - $rows = $sth->fetchAll(PDO::FETCH_ASSOC); - - // ── Index DB results by sort_key (YYYY-MM) ────────────────────────── - $dataByMonth = []; - foreach ($rows as $row) { - $dataByMonth[$row['sort_key']] = $row; - } - - // ── Always build 12 months, fill zeros where no data ──────────────── - $labels = []; - $stock_in = []; - $stock_out = []; - - for ($i = 11; $i >= 0; $i--) { - $dt = (clone $now)->modify("-{$i} months"); - $key = $dt->format('Y-m'); // e.g. "2025-04" - $label = $dt->format('M'); // e.g. "Apr" - - $labels[] = $label; - $stock_in[] = isset($dataByMonth[$key]) ? round((float)$dataByMonth[$key]['stock_in'], 2) : 0; - $stock_out[] = isset($dataByMonth[$key]) ? round((float)$dataByMonth[$key]['stock_out'], 2) : 0; - } - - // ── Response ───────────────────────────────────────────────────────── - $output = [ - 'labels' => $labels, - 'stock_in' => $stock_in, - 'stock_out' => $stock_out, - ]; - - $answer["output"] = $output; - $answer["success"] = 1; + $report = new ReportManager($pdo2, $company_id); + $answer['output'] = $report->getWarehouseStockMovement((int)$data['warehouse']); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/ics/api/engine_report/stock_trend.php b/app/ics/api/engine_report/stock_trend.php index 2571776..7add337 100644 --- a/app/ics/api/engine_report/stock_trend.php +++ b/app/ics/api/engine_report/stock_trend.php @@ -1,84 +1,10 @@ -getStockContext($warehouse_id, $id); - $table = $warehouse['table']; - - - // ── Always last 12 months ──────────────────────────────────────────── - $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'); - - // ── Get cumulative balance BEFORE the 12-month window ──────────────── - $sql_prior = "SELECT - ROUND(COALESCE(SUM(a.`in`), 0) - COALESCE(SUM(a.`out`), 0), 2) AS `balance` - FROM `{$table}` a - WHERE - a.company_id = {$company_id} and - a.date < '{$start}'"; - - $sth = $pdo2->prepare($sql_prior); - $sth->execute(); - $prior_balance = (float)$sth->fetchColumn(); - - // ── Get monthly in/out for the 12-month window ────────────────────── - $sql = "SELECT - DATE_FORMAT(a.date, '%Y-%m') AS `sort_key`, - ROUND(SUM(a.`in`), 2) AS `stock_in`, - ROUND(SUM(a.`out`), 2) AS `stock_out` - FROM `{$table}` a - WHERE - a.company_id = {$company_id} and - a.date >= '{$start}' and - a.date <= '{$end}' - GROUP BY `sort_key` - ORDER BY `sort_key` ASC"; - - $sth = $pdo2->prepare($sql); - $sth->execute(); - $rows = $sth->fetchAll(PDO::FETCH_ASSOC); - - // ── Index DB results by sort_key (YYYY-MM) ────────────────────────── - $dataByMonth = []; - foreach ($rows as $row) { - $dataByMonth[$row['sort_key']] = $row; - } - - // ── Always build 12 months, accumulate running balance ────────────── - $labels = []; - $balance = []; - $running = $prior_balance; - - for ($i = 11; $i >= 0; $i--) { - $dt = (clone $now)->modify("-{$i} months"); - $key = $dt->format('Y-m'); - $label = $dt->format('M'); - - $month_in = isset($dataByMonth[$key]) ? (float)$dataByMonth[$key]['stock_in'] : 0; - $month_out = isset($dataByMonth[$key]) ? (float)$dataByMonth[$key]['stock_out'] : 0; - - $running += ($month_in - $month_out); - - $labels[] = $label; - $balance[] = round($running, 2); - } - - // ── Response ───────────────────────────────────────────────────────── - $output = [ - 'labels' => $labels, - 'balance' => $balance, - ]; - - $answer["output"] = $output; - $answer["success"] = 1; + $report = new ReportManager($pdo2, $company_id); + $answer['output'] = $report->getWarehouseStockTrend((int)$data['warehouse']); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/ics/api/engine_report/warehouse_balance.php b/app/ics/api/engine_report/warehouse_balance.php index c4e9abd..33ece80 100644 --- a/app/ics/api/engine_report/warehouse_balance.php +++ b/app/ics/api/engine_report/warehouse_balance.php @@ -1,24 +1,10 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id - ]); - db_check($sth, $answer); - - $output = $sth->fetch(PDO::FETCH_ASSOC); - - $answer["output"] = $output; - $answer["success"] = 1; + $report = new ReportManager($pdo2, $company_id); + $answer['output'] = $report->getWarehouseBalanceSummary(); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/inventory/api/engine/manage_category.php b/app/inventory/api/engine/manage_category.php index 3e412d7..8c243e9 100644 --- a/app/inventory/api/engine/manage_category.php +++ b/app/inventory/api/engine/manage_category.php @@ -1,74 +1,24 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":id" => $id - ]); - - $table_log = json_decode($sth->fetchColumn() ?: '[]', true); - $table_log[] = $logging; - - if ($id > 0) { - - $sql = "UPDATE md_product_category SET - `category` = :category, - `slug` = :slug, - `description` = :description, - `status` = :status, - `log` = :log - WHERE id = :id AND company_id = :company_id"; - - $sth = $pdo->prepare($sql); - $sth->execute([ - ":id" => $id, - ":company_id" => $company_id, - ":category" => $data["category"], - ":slug" => $data["slug"], - ":description" => $data["description"], - ":status" => $data["status"], - ":log" => json_encode($table_log), - ]); - - } else { - - $sql = "INSERT INTO md_product_category - (company_id, category, slug, `description`, `status`, `log`) - VALUES - (:company_id, :category, :slug, :description, :status, :log)"; - - $sth = $pdo->prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":category" => $data["category"], - ":slug" => $data["slug"], - ":description" => $data["description"], - ":status" => $data["status"], - ":log" => json_encode($table_log), - ]); - } - + $product = new ProductManager($pdo, $company_id); + $product->saveCategory($data, $logging); }); - $answer["success"] = 1; + $answer['success'] = 1; } catch (PDOException $e) { - $answer["success"] = 0; - $answer["message"] = "Database error, please try again"; + $answer['success'] = 0; + $answer['message'] = 'Database error, please try again'; http_response_code(500); } catch (Exception $e) { - $answer["success"] = 0; - $answer["message"] = $e->getMessage(); + $answer['success'] = 0; + $answer['message'] = $e->getMessage(); http_response_code(400); } diff --git a/app/inventory/api/engine/manage_product.php b/app/inventory/api/engine/manage_product.php index f2c9efd..8e102dc 100644 --- a/app/inventory/api/engine/manage_product.php +++ b/app/inventory/api/engine/manage_product.php @@ -1,144 +1,69 @@ -= $reorder_point) { - $answer["success"] = 0; - $answer["message"] = "Min stock ({$min_stock}) must be less than reorder point ({$reorder_point})."; + $answer['success'] = 0; + $answer['message'] = "Min stock ({$min_stock}) must be less than reorder point ({$reorder_point})."; http_response_code(400); exit(json_encode($answer)); } try { - /** 1. FILE HANDLING **/ - $uploader = new FileUploader($include_url . "uploads/product/"); + $uploader = new FileUploader($include_url . 'uploads/product/'); if (!empty($_FILES['product_image']['name'])) { - // Cleanup old image when updating if ($id > 0) { $sql = "SELECT `product_image` FROM md_product WHERE company_id = :company_id AND id = :id"; $sth = $pdo2->prepare($sql); - $sth->execute([":company_id" => $company_id, ":id" => $id]); - $existing_files = $sth->fetchColumn(); - - $uploader->cleanup($existing_files, ''); + $sth->execute([':company_id' => $company_id, ':id' => $id]); + $uploader->cleanup($sth->fetchColumn(), ''); } $errors = $uploader->upload('product_image'); if (!empty($errors)) { - $answer["success"] = 0; - $answer["message"] = $errors[0]; + $answer['success'] = 0; + $answer['message'] = $errors[0]; exit(json_encode($answer)); } - // Get the single uploaded filename directly $db_image_string = $uploader->getUploadedFiles()[0] ?? ''; - } else { - // No new file — keep existing, skip cleanup entirely $db_image_string = $data['keep_files'] ?? ''; } - dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $id, $db_image_string) { - - // get existing log - $sql = "SELECT `log` FROM md_product - WHERE company_id = :company_id AND id = :id"; - $sth = $pdo->prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":id" => $id - ]); - - $table_log = json_decode($sth->fetchColumn() ?: '[]', true); - $table_log[] = $logging; - - if ($id > 0) { - - $sql = "UPDATE md_product SET - product_name = :product_name, - sku = :sku, - price = :price, - min_stock = :min_stock, - reorder_point = :reorder_point, - category = :category, - product_image = :product_image, - `description` = :description, - `status` = :status, - `log` = :log - WHERE id = :id AND company_id = :company_id"; - - $sth = $pdo->prepare($sql); - $sth->execute([ - ":id" => $id, - ":company_id" => $company_id, - ":product_name" => $data["product_name"], - ":sku" => $data["sku"], - ":price" => $data["price"], - ":min_stock" => $data["min_stock"], - ":reorder_point" => $data["reorder_point"], - ":category" => $data["category"], - ":product_image"=> $db_image_string, - ":description" => $data["description"], - ":status" => $data["status"], - ":log" => json_encode($table_log), - ]); - - } else { - - $sql = "INSERT INTO md_product - (company_id, product_name, sku, price, min_stock, reorder_point, category, product_image, `description`, `status`, `log`) - VALUES - (:company_id, :product_name, :sku, :price, :min_stock, :reorder_point, :category, :product_image, :description, :status, :log)"; - - $sth = $pdo->prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":product_name" => $data["product_name"], - ":sku" => $data["sku"], - ":price" => $data["price"], - ":min_stock" => $data["min_stock"], - ":reorder_point" => $data["reorder_point"], - ":category" => $data["category"], - ":product_image"=> $db_image_string, - ":description" => $data["description"], - ":status" => $data["status"], - ":log" => json_encode($table_log), - ]); - } - + /** 2. DATABASE **/ + dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $db_image_string) { + $product = new ProductManager($pdo, $company_id); + $product->saveProduct($data, $logging, $db_image_string); }); - $answer["success"] = 1; + $answer['success'] = 1; } catch (PDOException $e) { - $answer["success"] = 0; - $answer["message"] = "Database error, please try again"; + if (isset($uploader)) $uploader->rollbackUploads(); + $answer['success'] = 0; + $answer['message'] = 'Database error, please try again'; http_response_code(500); } catch (Exception $e) { - $answer["success"] = 0; - $answer["message"] = $e->getMessage(); - $answer["error"] = $e->getMessage(); + if (isset($uploader)) $uploader->rollbackUploads(); + $answer['success'] = 0; + $answer['message'] = $e->getMessage(); http_response_code(400); } diff --git a/app/inventory/api/engine/manage_storage.php b/app/inventory/api/engine/manage_storage.php index 75f86ba..b48ba9c 100644 --- a/app/inventory/api/engine/manage_storage.php +++ b/app/inventory/api/engine/manage_storage.php @@ -5,92 +5,20 @@ try { dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging) { - - $id = (int)($data['id'] ?? 0); - - $sql = "SELECT `log` FROM md_storage - WHERE company_id = :company_id AND id = :id"; - $sth = $pdo->prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":id" => $id - ]); - - $table_log = json_decode($sth->fetchColumn() ?: '[]', true); - if (!is_array($table_log)) $table_log = []; - $table_log[] = $logging; - - $params = [ - ":company_id" => $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) { - - $sql = "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"; - - $params[":id"] = $id; - - $sth = $pdo->prepare($sql); - $sth->execute($params); - - $storage_id = $id; - - } else { - - $sql = "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)"; - - $sth = $pdo->prepare($sql); - $sth->execute($params); - - $storage_id = (int) $pdo->lastInsertId(); - } - - // Sync md_rack rows — same $pdo, inside the same transaction - $wm = new WarehouseManager($pdo, $company_id); - $wm->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'], - ]); + $wh = new WarehouseManager($pdo, $company_id); + $wh->saveStorage($data, $logging); }); - $answer["success"] = 1; + $answer['success'] = 1; } catch (PDOException $e) { - $answer["success"] = 0; - $answer["message"] = "Database error, please try again"; + $answer['success'] = 0; + $answer['message'] = 'Database error, please try again'; http_response_code(500); } catch (Exception $e) { - $answer["success"] = 0; - $answer["message"] = $e->getMessage(); + $answer['success'] = 0; + $answer['message'] = $e->getMessage(); http_response_code(400); } diff --git a/app/inventory/api/engine/manage_warehouse.php b/app/inventory/api/engine/manage_warehouse.php index f2bb12d..7bbf114 100644 --- a/app/inventory/api/engine/manage_warehouse.php +++ b/app/inventory/api/engine/manage_warehouse.php @@ -1,77 +1,24 @@ prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":id" => $id - ]); - - $table_log = json_decode($sth->fetchColumn() ?: '[]', true); - if (!is_array($table_log)) $table_log = []; - $table_log[] = $logging; - - $params = [ - ":company_id" => $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) { - - $sql = "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"; - - $params[":id"] = $id; - - } else { - - $sql = "INSERT INTO md_warehouse - (company_id, warehouse_name, location, manager, description, status, `log`) - VALUES - (:company_id, :warehouse_name, :location, :manager, :description, :status, :log)"; - } - - $sth = $pdo->prepare($sql); - $sth->execute($params); - - // Create stock table for new warehouse - if ($id == 0 && !empty($data["warehouse_name"])) { - $safe_name = 'td_stock_' . preg_replace('/[^a-zA-Z0-9_]/', '', $data["warehouse_name"]); - $sql = "CREATE TABLE `$safe_name` LIKE `td_stock`"; - $pdo->exec($sql); - } - + $wh = new WarehouseManager($pdo, $company_id); + $wh->saveWarehouse($data, $logging); }); - $answer["success"] = 1; + $answer['success'] = 1; } catch (PDOException $e) { - $answer["success"] = 0; - $answer["message"] = "Database error, please try again"; + $answer['success'] = 0; + $answer['message'] = 'Database error, please try again'; http_response_code(500); } catch (Exception $e) { - $answer["success"] = 0; - $answer["message"] = $e->getMessage(); + $answer['success'] = 0; + $answer['message'] = $e->getMessage(); http_response_code(400); } diff --git a/app/inventory/api/engine/product.php b/app/inventory/api/engine/product.php index 547b7c4..30d7bf0 100644 --- a/app/inventory/api/engine/product.php +++ b/app/inventory/api/engine/product.php @@ -1,24 +1,10 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id - ]); - db_check($sth, $answer); - - $answer["output"] = $sth->fetchAll(PDO::FETCH_ASSOC); - $answer["success"] = 1; + $product = new ProductManager($pdo2, $company_id); + $answer['output'] = $product->getProductList(); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/inventory/api/engine/product_category.php b/app/inventory/api/engine/product_category.php index 7e90c0e..1978e8b 100644 --- a/app/inventory/api/engine/product_category.php +++ b/app/inventory/api/engine/product_category.php @@ -1,24 +1,10 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id - ]); - db_check($sth, $answer); - - $answer["output"] = $sth->fetchAll(PDO::FETCH_ASSOC); - $answer["success"] = 1; + $product = new ProductManager($pdo2, $company_id); + $answer['output'] = $product->getCategoryList(); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/inventory/api/engine/retrieve_category.php b/app/inventory/api/engine/retrieve_category.php index b53b547..14dd4a0 100644 --- a/app/inventory/api/engine/retrieve_category.php +++ b/app/inventory/api/engine/retrieve_category.php @@ -1,25 +1,10 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":id" => $id - ]); - db_check($sth, $answer); - - $answer["output"] = $sth->fetch(PDO::FETCH_ASSOC); - $answer["success"] = 1; + $product = new ProductManager($pdo2, $company_id); + $answer['output'] = $product->getCategoryById((int)$data['id']); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/inventory/api/engine/retrieve_product.php b/app/inventory/api/engine/retrieve_product.php index 019297b..af23869 100644 --- a/app/inventory/api/engine/retrieve_product.php +++ b/app/inventory/api/engine/retrieve_product.php @@ -1,25 +1,10 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":id" => $id - ]); - db_check($sth, $answer); - - $answer["output"] = $sth->fetch(PDO::FETCH_ASSOC); - $answer["success"] = 1; + $product = new ProductManager($pdo2, $company_id); + $answer['output'] = $product->getProductById((int)$data['id']); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/inventory/api/engine/retrieve_storage.php b/app/inventory/api/engine/retrieve_storage.php index 0f5e8a7..b91cc4e 100644 --- a/app/inventory/api/engine/retrieve_storage.php +++ b/app/inventory/api/engine/retrieve_storage.php @@ -1,25 +1,10 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":id" => $id - ]); - db_check($sth, $answer); - - $answer["output"] = $sth->fetch(PDO::FETCH_ASSOC); - $answer["success"] = 1; + $wh = new WarehouseManager($pdo2, $company_id); + $answer['output'] = $wh->getStorageById((int)$data['id']); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/inventory/api/engine/retrieve_warehouse.php b/app/inventory/api/engine/retrieve_warehouse.php index 50cfa55..3fefb8a 100644 --- a/app/inventory/api/engine/retrieve_warehouse.php +++ b/app/inventory/api/engine/retrieve_warehouse.php @@ -1,25 +1,10 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id, - ":id" => $id - ]); - db_check($sth, $answer); - - $answer["output"] = $sth->fetch(PDO::FETCH_ASSOC); - $answer["success"] = 1; + $wh = new WarehouseManager($pdo2, $company_id); + $answer['output'] = $wh->getWarehouseById((int)$data['id']); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/inventory/api/engine/storage.php b/app/inventory/api/engine/storage.php index 881063e..9beb265 100644 --- a/app/inventory/api/engine/storage.php +++ b/app/inventory/api/engine/storage.php @@ -1,29 +1,10 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id - ]); - db_check($sth, $answer); - - $answer["output"] = $sth->fetchAll(PDO::FETCH_ASSOC); - - // modify output - foreach($answer["output"] as $key => $item){ - - } - - $answer["success"] = 1; + $wh = new WarehouseManager($pdo2, $company_id); + $answer['output'] = $wh->getStorageList(); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file diff --git a/app/inventory/api/engine/warehouse.php b/app/inventory/api/engine/warehouse.php index aa04750..2aed388 100644 --- a/app/inventory/api/engine/warehouse.php +++ b/app/inventory/api/engine/warehouse.php @@ -1,36 +1,10 @@ -prepare($sql); - $sth->execute([ - ":company_id" => $company_id - ]); - db_check($sth, $answer); - - $answer["output"] = $sth->fetchAll(PDO::FETCH_ASSOC); - $answer["success"] = 1; + $wh = new WarehouseManager($pdo2, $company_id); + $answer['output'] = $wh->getWarehouseListAll(); + $answer['success'] = 1; exit(json_encode($answer)); - ?> \ No newline at end of file