diff --git a/app.zip b/app.zip new file mode 100644 index 0000000..4d695bc Binary files /dev/null and b/app.zip differ diff --git a/app/assets/js/custom.js b/app/assets/js/custom.js index 10cb9ec..e62a374 100644 --- a/app/assets/js/custom.js +++ b/app/assets/js/custom.js @@ -1,3 +1,27 @@ +/** ========================= + * UTILITIES + * ========================= */ + +/** + * Returns a debounced version of fn that delays invoking until + * after `wait` ms have elapsed since the last call. + */ +function debounce(fn, wait) { + wait = wait || 300; + var timer; + return function (event) { + var ctx = this; + var args = arguments; + // Persist the jQuery event so it's accessible after the timeout + if (event && event.persist) event.persist(); + clearTimeout(timer); + timer = setTimeout(function () { + fn.apply(ctx, args); + }, wait); + }; +} + + /** ========================= * DEFAULT ON LOAD FUNCTION * ========================= */ @@ -355,9 +379,11 @@ function prepare_form_data(check_required, raw_data) { q[key] = value; }); - // Collect form inputs - $(".form-control, .form-select").each(function () { + // Collect form inputs (exclude search inputs — UI-only filters, not API data) + $(".form-control:not([type=search]), .form-select").each(function () { if (!$(this).attr("id")) return true; + var el = $(this).get(0); + if (!el || !el.nodeName) return true; q[$(this).attr("id")] = $(this).val(); }); diff --git a/app/assets/utils/classes/CompanySettingManager.php b/app/assets/utils/classes/CompanySettingManager.php new file mode 100644 index 0000000..9f270d0 --- /dev/null +++ b/app/assets/utils/classes/CompanySettingManager.php @@ -0,0 +1,143 @@ + 1, // auto-approve by default + ]; + + public function __construct(PDO $pdo, int $companyId) + { + $this->pdo = $pdo; + $this->companyId = $companyId; + } + + // ───────────────────────────────────────────────────────────── + // Public API + // ───────────────────────────────────────────────────────────── + + /** + * Return the value for a single key, or the default if not configured. + * + * @param string $key Setting key. + * @return mixed Stored value cast to int, or the default. + */ + public function get(string $key): mixed + { + $sth = $this->pdo->prepare( + "SELECT value FROM company_setting + WHERE company_id = :company_id AND setting_key = :setting_key + LIMIT 1" + ); + $sth->execute([':company_id' => $this->companyId, ':setting_key' => $key]); + $row = $sth->fetch(PDO::FETCH_ASSOC); + + if ($row === false) { + return self::DEFAULTS[$key] ?? null; + } + + // Cast numeric-looking values to int for clean comparisons + return is_numeric($row['value']) ? (int)$row['value'] : $row['value']; + } + + /** + * Return all settings for this company, merged with defaults. + * + * @return array Associative array of key => value. + */ + public function getAll(): array + { + $sth = $this->pdo->prepare( + "SELECT setting_key, value FROM company_setting + WHERE company_id = :company_id" + ); + $sth->execute([':company_id' => $this->companyId]); + $rows = $sth->fetchAll(PDO::FETCH_KEY_PAIR); + + // Merge stored values over defaults, cast numeric values + $result = self::DEFAULTS; + foreach ($rows as $k => $v) { + $result[$k] = is_numeric($v) ? (int)$v : $v; + } + return $result; + } + + /** + * Upsert a single setting value. + * + * @param string $key + * @param mixed $value + */ + public function set(string $key, mixed $value): void + { + // Fetch existing log to append to it + $sth = $this->pdo->prepare( + "SELECT log FROM company_setting + WHERE company_id = :company_id AND setting_key = :setting_key + LIMIT 1" + ); + $sth->execute([':company_id' => $this->companyId, ':setting_key' => $key]); + $existing_log = json_decode($sth->fetchColumn() ?: '[]', true) ?: []; + $existing_log[] = [ + 'user_id' => $_SESSION['login_user_id'] ?? null, + 'dt' => date('Y-m-d H:i:s'), + 'value' => $value, + ]; + + $this->pdo->prepare( + "INSERT INTO company_setting (company_id, setting_key, value, log, updated_at) + VALUES (:company_id, :setting_key, :value, :log, NOW()) + ON DUPLICATE KEY UPDATE value = VALUES(value), log = VALUES(log), updated_at = NOW()" + )->execute([ + ':company_id' => $this->companyId, + ':setting_key' => $key, + ':value' => $value, + ':log' => json_encode($existing_log), + ]); + } + + /** + * HTTP handler — read or update settings via AJAX. + * + * action: 'read' → return all settings + * action: 'update' → upsert one or more keys from $data + * + * @param array $data Request data array. + */ + public function handle(array $data): void + { + $action = $data['action'] ?? ''; + + if ($action === 'read') { + echo json_encode(['success' => 1, 'output' => $this->getAll()]); + exit; + } + + if ($action === 'update') { + $allowed = array_keys(self::DEFAULTS); + foreach ($allowed as $key) { + $this->set($key, $data[$key] ?? self::DEFAULTS[$key]); + } + echo json_encode(['success' => 1, 'message' => 'Settings saved.']); + exit; + } + + http_response_code(400); + echo json_encode(['success' => 0, 'message' => 'Invalid action.']); + exit; + } +} \ No newline at end of file diff --git a/app/assets/utils/classes/PasswordManager.php b/app/assets/utils/classes/PasswordManager.php index 9a837b0..8d723de 100644 --- a/app/assets/utils/classes/PasswordManager.php +++ b/app/assets/utils/classes/PasswordManager.php @@ -11,7 +11,7 @@ * * Method order: * Core public API → checkStrength, change, forceSet - * HTTP handlers → handleCheck, handleChange (thin wrappers for AJAX endpoints) + * HTTP handler → handleChange (thin wrapper for AJAX endpoint) * Private helpers → loadZxcvbn, fetchUser, enforceStrength, persist * * Usage: @@ -49,7 +49,7 @@ class PasswordManager { */ public function __construct($pdo, string $include_url) { $this->pdo = $pdo; - $this->zxcvbn_path = rtrim($include_url, '/') . '/assets/zxcvbn-php-master/vendor/autoload.php'; + $this->zxcvbn_path = rtrim($include_url, '/') . '/../lib/zxcvbn-php-master/vendor/autoload.php'; } // ───────────────────────────────────────────────────────────── @@ -166,44 +166,9 @@ class PasswordManager { } // ───────────────────────────────────────────────────────────── - // HTTP handlers (thin AJAX endpoint wrappers) + // HTTP handler (thin AJAX endpoint wrapper) // ───────────────────────────────────────────────────────────── - /** - * Handle an AJAX strength-check request and echo a JSON response. - * - * Called by: setting/api/engine/check_password.php - * - * Expected $data keys: - * password (string) — the password string to analyse - * - * Response JSON keys: - * success (int 1), score (int -1 if empty, else 0–4), feedback (string) - * - * Outputs JSON and calls exit. Safe against XSS — all output via json_encode. - * - * @param array $data Request data array (typically from $_POST or decoded JSON body). - */ - public function handleCheck(array $data): void { - - $password = $data['password'] ?? ''; - - if (empty($password)) { - echo json_encode(['success' => 1, 'score' => -1, 'feedback' => '']); - exit; - } - - $result = $this->checkStrength($password); - $feedback = $result['warning'] ?: ($result['suggestions'][0] ?? ''); - - echo json_encode([ - 'success' => 1, - 'score' => $result['score'], - 'feedback' => $feedback, - ]); - exit; - } - /** * Handle an AJAX change-password request and echo a JSON response. * diff --git a/app/assets/utils/classes/ReportManager.php b/app/assets/utils/classes/ReportManager.php index 4a7f366..e693264 100644 --- a/app/assets/utils/classes/ReportManager.php +++ b/app/assets/utils/classes/ReportManager.php @@ -25,18 +25,20 @@ * - getRecentActivity: warehouse_name in SQL now uses $safe (was unescaped) * - getExpiredStock: warehouse_id cast to (int) before SQL fragment injection; * warehouse_name in UNION now uses $safe (was unescaped) - * - getRackLog: DB name from SELECT DATABASE() bound via PDO (was raw interpolation) + * - getRackLog: cross-DB join uses $mainDb injected at construction time * - getRackOccupancy: (already safe — no dynamic identifiers) */ class ReportManager { - private PDO $pdo; - private int $companyId; + private PDO $pdo; + private int $companyId; + private string $mainDb; - public function __construct(PDO $pdo, int $companyId) + public function __construct(PDO $pdo, int $companyId, string $mainDb = '') { - $this->pdo = $pdo; + $this->pdo = $pdo; $this->companyId = $companyId; + $this->mainDb = $mainDb; } // ───────────────────────────────────────────────────────────── @@ -600,7 +602,8 @@ class ReportManager LEFT JOIN md_product p ON p.company_id = s.company_id AND p.sku = s.product_sku - WHERE s.company_id = {$cid}"; + WHERE s.company_id = {$cid} + AND s.status = 1"; }, $warehouses )); @@ -740,7 +743,8 @@ class ReportManager ROUND(SUM(`out`), 2) AS stock_out FROM `{$table}` WHERE company_id = :company_id - AND date >= :start AND date <= :end + AND status = 1 + AND date >= :start AND date <= :end GROUP BY sort_key, label ORDER BY sort_key ASC" ); @@ -790,7 +794,7 @@ class ReportManager $sth = $this->pdo->prepare( "SELECT ROUND(COALESCE(SUM(`in`) - SUM(`out`), 0), 2) AS balance FROM `{$table}` - WHERE company_id = :company_id AND date < :start" + WHERE company_id = :company_id AND status = 1 AND date < :start" ); $sth->execute([':company_id' => $this->companyId, ':start' => $start]); $running = (float)$sth->fetchColumn(); @@ -801,7 +805,8 @@ class ReportManager ROUND(SUM(`out`), 2) AS stock_out FROM `{$table}` WHERE company_id = :company_id - AND date >= :start AND date <= :end + AND status = 1 + AND date >= :start AND date <= :end GROUP BY sort_key ORDER BY sort_key ASC" ); @@ -939,7 +944,8 @@ class ReportManager FROM `td_stock_{$safe}` s WHERE s.company_id = {$cid} AND s.type = 'in' - AND s.lot_number IS NOT NULL"; + AND s.lot_number IS NOT NULL + AND s.status = 1"; }, $warehouses )); @@ -1058,6 +1064,7 @@ class ReportManager WHERE s.company_id = :company_id AND s.product_sku = :product_sku AND s.lot_number = :lot_number + AND s.status = 1 ORDER BY s.date DESC" ); $sth->execute([ @@ -1089,6 +1096,37 @@ class ReportManager */ public function getProductLots(): array { + $warehouses = $this->pdo->prepare( + "SELECT id, warehouse_name FROM md_warehouse + WHERE company_id = :company_id AND status = 1" + ); + $warehouses->execute([':company_id' => $this->companyId]); + $wh_list = $warehouses->fetchAll(PDO::FETCH_ASSOC); + + // Build per-lot balance by summing approved td_stock rows across all warehouses. + // warehouse_balance is per-product, not per-lot, so we query td_stock directly. + $lot_balance = []; + $cid = (int) $this->companyId; + + foreach ($wh_list as $wh) { + $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $wh['warehouse_name']); + $table = "td_stock_{$safe}"; + + $sth = $this->pdo->query( + "SELECT lot_number, + ROUND(SUM(COALESCE(`in`, 0)) - SUM(COALESCE(`out`, 0)), 2) AS lot_balance + FROM `{$table}` + WHERE company_id = {$cid} + AND status = 1 + AND lot_number IS NOT NULL + GROUP BY lot_number" + ); + foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $lb) { + $key = $lb['lot_number']; + $lot_balance[$key] = ($lot_balance[$key] ?? 0) + (float)$lb['lot_balance']; + } + } + $sth = $this->pdo->prepare( "SELECT l.id, @@ -1097,14 +1135,7 @@ class ReportManager 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 + DATEDIFF(l.expiry_date, CURDATE()) AS days_remaining FROM md_lot l INNER JOIN md_product p ON p.company_id = l.company_id @@ -1117,18 +1148,24 @@ class ReportManager $active = $expired = $near = 0; + // Exclude lots that have no td_stock record at all (e.g. all rows were soft-deleted) + $rows = array_values(array_filter($rows, fn($r) => array_key_exists($r['lot_number'], $lot_balance))); + foreach ($rows as &$row) { $days = (int)$row['days_remaining']; - $balance = (float)$row['balance']; + $balance = round($lot_balance[$row['lot_number']] ?? 0, 2); + + $row['balance'] = $balance; + $row['is_active'] = $balance > 0 ? 1 : 0; 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'; + if ($days < 0) $row['status'] = 'expired'; + elseif ($days <= 7) $row['status'] = 'critical'; + elseif ($days <= 30) $row['status'] = 'near'; + else $row['status'] = 'ok'; } unset($row); @@ -1158,9 +1195,7 @@ class ReportManager { if (!$rack_id) return []; - // Fetch DB name safely — used as a backtick-quoted identifier, not user input - $db_name = $this->pdo->query("SELECT DATABASE()")->fetchColumn(); - $db_name = preg_replace('/[^a-zA-Z0-9_]/', '', (string)$db_name); + $main_db = preg_replace('/[^a-zA-Z0-9_]/', '', $this->mainDb); $sth = $this->pdo->prepare( "SELECT @@ -1172,7 +1207,7 @@ class ReportManager rl.login, u.name AS user_name FROM md_rack_log rl - LEFT JOIN `{$db_name}`.user u + LEFT JOIN `{$main_db}`.user u ON u.user_id = rl.user_id WHERE rl.company_id = :company_id AND rl.md_rack_id = :rack_id @@ -1299,6 +1334,7 @@ class ReportManager "SELECT DISTINCT product_sku FROM `{$table}` WHERE company_id = :company_id + AND status = 1 AND date BETWEEN :date_from AND :date_to ORDER BY product_sku ASC" ); @@ -1313,6 +1349,7 @@ class ReportManager ROUND(SUM(COALESCE(`out`, 0)), 2) AS total_out FROM `{$table}` WHERE company_id = :company_id + AND status = 1 AND date BETWEEN :date_from AND :date_to" ); $sth->execute([':company_id' => $this->companyId, ':date_from' => $date_from, ':date_to' => $date_to]); @@ -1366,6 +1403,7 @@ class ReportManager FROM `{$table}` WHERE company_id = :company_id AND product_sku = :sku + AND status = 1 AND date < :date_from" ); $sth->execute([':company_id' => $this->companyId, ':sku' => $product_sku, ':date_from' => $date_from]); @@ -1404,6 +1442,7 @@ class ReportManager AND c.id = s.contact_id WHERE s.company_id = :company_id AND s.product_sku = :sku + AND s.status = 1 AND s.date BETWEEN :date_from AND :date_to ORDER BY s.date ASC, s.id ASC" ); diff --git a/app/assets/utils/classes/StockManager.php b/app/assets/utils/classes/StockManager.php index 285da12..1dbcddc 100644 --- a/app/assets/utils/classes/StockManager.php +++ b/app/assets/utils/classes/StockManager.php @@ -252,7 +252,7 @@ class StockManager { * @param array $logging Audit entry to append to the log column. * @param string $uuid UUID for this transaction (shared across transfer pairs). */ - public function saveStockIn(array $data, array $logging, string $uuid): void + public function saveStockIn(array $data, array $logging, string $uuid): int { $id = (int)($data['id'] ?? 0); $warehouse_id = (int)($data['warehouse'] ?? 0); @@ -282,6 +282,8 @@ class StockManager { ':log' => json_encode($table_log), ]); + return 0; // update — no new row + } else { $lot_number = $data['lot_number'] ?: null; @@ -304,10 +306,10 @@ class StockManager { $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) + contact_id, `description`, `log`, `type`, lot_number, serial_number, status) VALUES (:uuid, :company_id, :date, :product_sku, :quantity, :zone, :aisle, :rack, - :contact_id, :description, :log, 'in', :lot_number, :serial_number)" + :contact_id, :description, :log, 'in', :lot_number, :serial_number, 0)" )->execute([ ':uuid' => $uuid, ':company_id' => $this->company_id, @@ -326,22 +328,8 @@ class StockManager { $td_stock_id = (int)$this->pdo->lastInsertId(); - // Mark rack as occupied and link it to this stock row - $whMgmt->occupyRack( - $warehouse_id, - $data['zone'], $data['aisle'], $data['rack'], - $data['product_sku'], - $td_stock_id - ); - - // Update running balance (+quantity in this warehouse) - $whMgmt->adjustBalance( - 'in', - $warehouse_id, - $data['product_sku'], - $row['in'] ?? 0, - $data['quantity'] - ); + // occupyRack and adjustBalance deferred — called from approveStock() only. + return $td_stock_id; } } @@ -365,7 +353,7 @@ class StockManager { * @param string $uuid UUID for this transaction. * @throws Exception If the rack is empty, holds a different SKU/lot/serial. */ - public function saveStockOut(array $data, array $logging, string $uuid): void + public function saveStockOut(array $data, array $logging, string $uuid): int { $id = (int)($data['id'] ?? 0); $warehouse_id = (int)($data['warehouse'] ?? 0); @@ -395,6 +383,8 @@ class StockManager { ':log' => json_encode($table_log), ]); + return 0; // update — no new row + } else { // Validate rack holds the expected product / lot / serial @@ -436,10 +426,10 @@ class StockManager { $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) + contact_id, `description`, `log`, `type`, ref_id, lot_number, serial_number, status) VALUES (:uuid, :company_id, :date, :product_sku, :quantity, :zone, :aisle, :rack, - :contact_id, :description, :log, 'out', :ref_id, :lot_number, :serial_number)" + :contact_id, :description, :log, 'out', :ref_id, :lot_number, :serial_number, 0)" )->execute([ ':uuid' => $uuid, ':company_id' => $this->company_id, @@ -457,19 +447,10 @@ class StockManager { ':serial_number' => $serial_number, ]); - // Release the source rack and reverse balance - $whMgmt->releaseRack( - $warehouse_id, - $data['zone'], $data['aisle'], $data['rack'] - ); + $out_stock_id = (int)$this->pdo->lastInsertId(); - $whMgmt->adjustBalance( - 'out', - $warehouse_id, - $data['product_sku'], - $row['out'] ?? 0, - $quantity - ); + // releaseRack and adjustBalance deferred — called from approveStock() only. + return $out_stock_id; } } @@ -500,7 +481,7 @@ class StockManager { * @param string $uuid UUID shared by both the from and to rows. * @throws Exception If source rack validation fails or paired record is missing on update. */ - public function saveStockTransfer(array $data, array $logging, string $uuid): void + public function saveStockTransfer(array $data, array $logging, string $uuid): int { $id = (int)($data['id'] ?? 0); $whMgmt = new WarehouseManager($this->pdo, $this->company_id); @@ -564,7 +545,7 @@ class StockManager { ':company_id' => $this->company_id, ]); - return; + return 0; // update — no new row } // Insert: validate source rack, then create paired rows @@ -617,18 +598,18 @@ class StockManager { "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) + contact_id, `description`, `log`, `type`, lot_number, serial_number, status) VALUES (:uuid, :company_id, :date, :product_sku, :quantity, :ref_warehouse, :zone, :aisle, :rack, - :contact_id, :description, :log, 'transfer', :lot_number, :serial_number)" + :contact_id, :description, :log, 'transfer', :lot_number, :serial_number, 0)" )->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, + ':ref_warehouse' => $whMgmt->getWarehouseName($to_warehouse), ':zone' => $from_zone, ':aisle' => $from_aisle, ':rack' => $from_rack, @@ -645,18 +626,18 @@ class StockManager { "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) + contact_id, `description`, `log`, `type`, lot_number, serial_number, status) VALUES (:uuid, :company_id, :date, :product_sku, :quantity, :ref_warehouse, :ref_id, :zone, :aisle, :rack, - :contact_id, :description, :log, 'transfer', :lot_number, :serial_number)" + :contact_id, :description, :log, 'transfer', :lot_number, :serial_number, 0)" )->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_warehouse' => $whMgmt->getWarehouseName($from_warehouse), ':ref_id' => $from_stock_id, ':zone' => $to_zone, ':aisle' => $to_aisle, @@ -679,13 +660,160 @@ class StockManager { ':company_id' => $this->company_id, ]); - // Rack state: release source, occupy destination - $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); + // releaseRack, occupyRack and adjustBalance deferred — called from approveStock() only. + return $from_stock_id; + } - // Balance: deduct from source, add to destination - $whMgmt->adjustBalance('out', $from_warehouse, $data['product_sku'], 0, $quantity); - $whMgmt->adjustBalance('in', $to_warehouse, $data['product_sku'], 0, $quantity); + // ───────────────────────────────────────────────────────────── + // Approve + // ───────────────────────────────────────────────────────────── + + /** + * Approve a draft td_stock row (status 0 → 1) and update warehouse_balance. + * + * This is the single entry point for balance updates — both auto-approve + * (called immediately after save) and manual approve go through here. + * adjustBalance() is never called from anywhere else. + * + * For transfer rows, both the outbound (from) and inbound (to) rows share + * the same uuid. We approve both in one call so the pair is always consistent. + * + * @param int $id The td_stock row id. + * @param int $warehouse_id The warehouse the row belongs to. + * @param string $type 'in' | 'out' | 'transfer' + * @param WarehouseManager $whMgmt Injected to keep balance logic centralised. + * @throws Exception If the row is not found, already approved, or wrong company. + */ + public function approveStock(int $id, int $warehouse_id, string $type, WarehouseManager $whMgmt): void + { + $table = $this->resolveTable($warehouse_id); + + // Load the row and validate ownership / status + $sth = $this->pdo->prepare( + "SELECT * FROM `{$table}` + WHERE id = :id AND company_id = :company_id + LIMIT 1" + ); + $sth->execute([':id' => $id, ':company_id' => $this->company_id]); + $row = $sth->fetch(PDO::FETCH_ASSOC); + + if (!$row) { + throw new Exception('Stock record not found.'); + } + + if ((int)$row['status'] === 1) { + throw new Exception('Already approved.'); + } + + // ── Approve this row ────────────────────────────────────────────── + $this->pdo->prepare( + "UPDATE `{$table}` SET status = 1 WHERE id = :id AND company_id = :company_id" + )->execute([':id' => $id, ':company_id' => $this->company_id]); + + // ── Rack state + balance ────────────────────────────────────────── + if ($type === 'in') { + + // Occupy rack now that stock is approved + $whMgmt->occupyRack( + $warehouse_id, + $row['zone'], $row['aisle'], $row['rack'], + $row['product_sku'], + $id + ); + $whMgmt->adjustBalance('in', $warehouse_id, $row['product_sku'], 0, (float)$row['in']); + + } elseif ($type === 'out') { + + // Release rack now that stock-out is approved + $whMgmt->releaseRack($warehouse_id, $row['zone'], $row['aisle'], $row['rack']); + $whMgmt->adjustBalance('out', $warehouse_id, $row['product_sku'], 0, (float)$row['out']); + + } elseif ($type === 'transfer') { + + // Transfer always has two rows in two different tables: + // outbound row → td_stock_ (out > 0, ref_warehouse = to_name) + // inbound row → td_stock_ (in > 0, ref_warehouse = from_name) + // Both rows share the same uuid. We always approve both atomically. + // Identify which side we were given and derive the other. + + $is_outbound = (float)$row['out'] > 0; + + // The outbound row lives in the from-warehouse table (already loaded as $row/$table). + // The inbound row lives in td_stock_. + $from_row = $is_outbound ? $row : null; + $from_table = $is_outbound ? $table : null; + $from_wh_id = $is_outbound ? $warehouse_id : null; + + // Resolve the paired table from ref_warehouse + $paired_wh_name = $row['ref_warehouse']; + $paired_safe = preg_replace('/[^a-zA-Z0-9_]/', '', $paired_wh_name); + $paired_table = "td_stock_{$paired_safe}"; + $paired_wh_id = $whMgmt->getWarehouseIdByName($paired_wh_name); + + // If we received the inbound side, swap so $from_* is always outbound + if (!$is_outbound) { + $from_table = $paired_table; + $from_wh_id = $paired_wh_id; + $paired_table = $table; + $paired_wh_id = $warehouse_id; + } + + // Load the inbound row from the paired table using uuid + $sth = $this->pdo->prepare( + "SELECT * FROM `{$paired_table}` + WHERE uuid = :uuid AND company_id = :company_id + LIMIT 1" + ); + $sth->execute([':uuid' => $row['uuid'], ':company_id' => $this->company_id]); + $inbound_row = $sth->fetch(PDO::FETCH_ASSOC); + + // Load outbound row if we were given the inbound side + if (!$is_outbound) { + $sth = $this->pdo->prepare( + "SELECT * FROM `{$from_table}` + WHERE uuid = :uuid AND company_id = :company_id + LIMIT 1" + ); + $sth->execute([':uuid' => $row['uuid'], ':company_id' => $this->company_id]); + $from_row = $sth->fetch(PDO::FETCH_ASSOC); + } else { + $from_row = $row; + } + + // Approve outbound row + if ($from_row && (int)$from_row['status'] === 0) { + $this->pdo->prepare( + "UPDATE `{$from_table}` SET status = 1 + WHERE id = :id AND company_id = :company_id" + )->execute([':id' => $from_row['id'], ':company_id' => $this->company_id]); + } + + // Approve inbound row + if ($inbound_row && (int)$inbound_row['status'] === 0) { + $this->pdo->prepare( + "UPDATE `{$paired_table}` SET status = 1 + WHERE id = :id AND company_id = :company_id" + )->execute([':id' => $inbound_row['id'], ':company_id' => $this->company_id]); + } + + // Rack state: release source, occupy destination + if ($from_row && $from_wh_id) { + $whMgmt->releaseRack( + $from_wh_id, + $from_row['zone'], $from_row['aisle'], $from_row['rack'] + ); + $whMgmt->adjustBalance('out', $from_wh_id, $from_row['product_sku'], 0, (float)$from_row['out']); + } + if ($inbound_row && $paired_wh_id) { + $whMgmt->occupyRack( + $paired_wh_id, + $inbound_row['zone'], $inbound_row['aisle'], $inbound_row['rack'], + $inbound_row['product_sku'], + $inbound_row['id'] + ); + $whMgmt->adjustBalance('in', $paired_wh_id, $inbound_row['product_sku'], 0, (float)$inbound_row['in']); + } + } } } \ No newline at end of file diff --git a/app/assets/utils/classes/WarehouseManager.php b/app/assets/utils/classes/WarehouseManager.php index 9794c15..73bf1bb 100644 --- a/app/assets/utils/classes/WarehouseManager.php +++ b/app/assets/utils/classes/WarehouseManager.php @@ -216,7 +216,8 @@ class WarehouseManager { $unions = implode(' UNION ALL ', array_map( fn($t) => "SELECT `type`, `date` FROM `$t` WHERE company_id = :company_id - AND product_sku = :product_sku", + AND product_sku = :product_sku + AND status = 1", $tables )); @@ -428,6 +429,25 @@ class WarehouseManager { return $sth->fetchColumn(); } + /** + * Reverse lookup — return warehouse_id for a given warehouse_name. + * Used by approveStock() to resolve the destination warehouse on transfers. + * + * @param string $name The warehouse_name stored in td_stock.ref_warehouse. + * @return int|null The warehouse id, or null if not found. + */ + public function getWarehouseIdByName(string $name): ?int + { + $sth = $this->pdo->prepare( + "SELECT id FROM md_warehouse + WHERE company_id = :company_id AND warehouse_name = :name + LIMIT 1" + ); + $sth->execute([':company_id' => $this->company_id, ':name' => $name]); + $id = $sth->fetchColumn(); + return $id !== false ? (int)$id : null; + } + /** * Insert a new warehouse or update an existing one. * @@ -853,14 +873,35 @@ class WarehouseManager { */ public function getLotList(string $product_sku, string $keyword): array { + // Only return lots that have at least one active td_stock row. + // Lots whose stock was fully soft-deleted (company_id negated) are excluded. + $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); + + if (empty($warehouses)) return []; + + $cid = (int)$this->company_id; + + // Build UNION EXISTS subquery across all td_stock_* tables + $unions = implode(' UNION ALL ', array_map(function($wh) use ($cid) { + $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $wh['warehouse_name']); + return "SELECT lot_number FROM `td_stock_{$safe}` + WHERE company_id = {$cid} AND lot_number IS NOT NULL"; + }, $warehouses)); + $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" + FROM md_lot + WHERE company_id = :company_id + AND product_sku = :product_sku + AND lot_number LIKE :keyword + AND lot_number IN (SELECT lot_number FROM ({$unions}) AS all_lots) + ORDER BY lot_number ASC + LIMIT 50" ); $sth->execute([ ':company_id' => $this->company_id, @@ -906,10 +947,12 @@ class WarehouseManager { WHERE s.company_id = :company_id AND s.product_sku = :product_sku AND s.type = 'in' + AND s.status = 1 AND s.lot_number IS NOT NULL AND EXISTS ( SELECT 1 FROM md_rack r WHERE r.company_id = s.company_id + AND r.warehouse = :warehouse_id AND r.td_stock_id = s.id AND r.product_sku IS NOT NULL )" @@ -917,6 +960,7 @@ class WarehouseManager { $sth->execute([ ':company_id' => $this->company_id, ':product_sku' => $product_sku, + ':warehouse_id' => $wh['id'], ]); foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $row) { @@ -953,15 +997,18 @@ class WarehouseManager { $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $wh['warehouse_name']); $table = "td_stock_{$safe}"; + $wh_id = $wh['id']; $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.status = 1 AND s.serial_number IS NOT NULL AND EXISTS ( SELECT 1 FROM md_rack r WHERE r.company_id = s.company_id + AND r.warehouse = {$wh_id} AND r.td_stock_id = s.id AND r.product_sku IS NOT NULL )"; @@ -1765,11 +1812,11 @@ class WarehouseManager { WHERE id = :id AND company_id = :company_id" )->execute([':id' => $stock_id, ':company_id' => $this->company_id]); - // Release the rack back to empty - $this->releaseRack($warehouse_id, $row['zone'], $row['aisle'], $row['rack']); - - // Reverse the balance (subtract the previously added quantity) - $this->adjustBalance('in', $warehouse_id, $product_sku, (int)$row['in'], 0); + // Only reverse side effects if the row was approved — draft rows never had them applied + if ((int)$row['status'] === 1) { + $this->releaseRack($warehouse_id, $row['zone'], $row['aisle'], $row['rack']); + $this->adjustBalance('in', $warehouse_id, $product_sku, (int)$row['in'], 0); + } } /** @@ -1809,16 +1856,17 @@ class WarehouseManager { WHERE id = :id AND company_id = :company_id" )->execute([':id' => $stock_id, ':company_id' => $this->company_id]); - // Re-occupy the rack with the original stock_in batch that was consumed - $this->occupyRack( - $warehouse_id, - $row['zone'], $row['aisle'], $row['rack'], - $product_sku, - (int)$row['ref_id'] - ); - - // Reverse the balance (subtract the previously deducted quantity) - $this->adjustBalance('out', $warehouse_id, $product_sku, (int)$row['out'], 0); + // Only reverse side effects if the row was approved — draft rows never had them applied + if ((int)$row['status'] === 1) { + // Re-occupy the rack with the original stock_in batch that was consumed + $this->occupyRack( + $warehouse_id, + $row['zone'], $row['aisle'], $row['rack'], + $product_sku, + (int)$row['ref_id'] + ); + $this->adjustBalance('out', $warehouse_id, $product_sku, (int)$row['out'], 0); + } } /** @@ -1877,30 +1925,92 @@ class WarehouseManager { WHERE id = :id AND company_id = :company_id" )->execute([':id' => $ref_id, ':company_id' => $this->company_id]); - // Destination rack was occupied by transfer_in — release it - $this->releaseRack( - $to_warehouse, - $to_row['zone'], $to_row['aisle'], $to_row['rack'] - ); + // Only reverse side effects if approved — draft rows never had them applied + if ((int)$from_row['status'] === 1) { + // Destination rack was occupied by transfer_in — release it + $this->releaseRack( + $to_warehouse, + $to_row['zone'], $to_row['aisle'], $to_row['rack'] + ); - // Source rack was released by transfer_out — re-occupy with original batch - $this->occupyRack( - $from_warehouse_id, - $from_row['zone'], $from_row['aisle'], $from_row['rack'], - $product_sku, - $from_stock_id - ); + // Source rack was released by transfer_out — re-occupy with original batch + $this->occupyRack( + $from_warehouse_id, + $from_row['zone'], $from_row['aisle'], $from_row['rack'], + $product_sku, + $from_stock_id + ); - // Reverse balances on both sides - $quantity = (int)$from_row['out']; - $this->adjustBalance('out', $from_warehouse_id, $product_sku, $quantity, 0); - $this->adjustBalance('in', $to_warehouse, $product_sku, $quantity, 0); + // Reverse balances on both sides + $quantity = (int)$from_row['out']; + $this->adjustBalance('out', $from_warehouse_id, $product_sku, $quantity, 0); + $this->adjustBalance('in', $to_warehouse, $product_sku, $quantity, 0); + } } // ───────────────────────────────────────────────────────────── // REPORT BASIS — Warehouse list queries // ───────────────────────────────────────────────────────────── + /** + * Return warehouse ids that currently hold an approved, rack-occupied stock-in + * row for a given product_sku + lot_number combination. + * + * Used to filter the warehouse dropdown in stock-out/transfer when a lot is selected, + * so only warehouses actually holding that lot are shown. + * + * @param string $product_sku + * @param string $lot_number + * @return int[] Array of warehouse ids. + */ + public function getWarehousesByLot(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); + + $result = []; + $cid = (int)$this->company_id; + + foreach ($warehouses as $wh) { + $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $wh['warehouse_name']); + $table = "td_stock_{$safe}"; + $wh_id = (int)$wh['id']; + + $sth = $this->pdo->prepare( + "SELECT 1 FROM `{$table}` s + WHERE s.company_id = :company_id + AND s.product_sku = :product_sku + AND s.lot_number = :lot_number + AND s.type = 'in' + AND s.status = 1 + AND EXISTS ( + SELECT 1 FROM md_rack r + WHERE r.company_id = s.company_id + AND r.warehouse = :warehouse_id + AND r.td_stock_id = s.id + AND r.product_sku IS NOT NULL + ) + LIMIT 1" + ); + $sth->execute([ + ':company_id' => $cid, + ':product_sku' => $product_sku, + ':lot_number' => $lot_number, + ':warehouse_id' => $wh_id, + ]); + + if ($sth->fetchColumn() !== false) { + $result[] = ['id' => $wh_id, 'warehouse_name' => $wh['warehouse_name']]; + } + } + + return $result; + } + /** * Return warehouses filtered by rack availability — for stock movement warehouse selectors. * diff --git a/app/ics/api/engine/approve_stock.php b/app/ics/api/engine/approve_stock.php new file mode 100644 index 0000000..5077cf5 --- /dev/null +++ b/app/ics/api/engine/approve_stock.php @@ -0,0 +1,46 @@ + 0, 'message' => 'Invalid parameters.'])); +} + +$stock = new StockManager($pdo2, $company_id); +$whMgmt = new WarehouseManager($pdo2, $company_id); + +try { + $pdo2->beginTransaction(); + + $result = $stock->approveStock($id, $warehouse_id, $type, $whMgmt); + + $pdo2->commit(); + + $answer['success'] = 1; + $answer['message'] = 'Stock approved.'; + exit(json_encode($answer)); + +} catch (Exception $e) { + $pdo2->rollBack(); + http_response_code(400); + exit(json_encode(['success' => 0, 'message' => $e->getMessage()])); +} \ 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 4988753..82c4ac5 100644 --- a/app/ics/api/engine/manage_stock_in.php +++ b/app/ics/api/engine/manage_stock_in.php @@ -3,14 +3,26 @@ require '../../../assets/utils/db_auth.php'; require '../../../assets/utils/classes/StockManager.php'; require '../../../assets/utils/classes/WarehouseManager.php'; + require '../../../assets/utils/classes/CompanySettingManager.php'; + + $csm = new CompanySettingManager($pdo1, $company_id); + $auto_approve = (int)$csm->get('default_stock_status') === 1; try { - dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $uuid) { - $stock = new StockManager($pdo, $company_id); - $stock->saveStockIn($data, $logging, $uuid); + $new_id = null; + + dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $uuid, $auto_approve, &$new_id) { + $stock = new StockManager($pdo, $company_id); + $whMgmt = new WarehouseManager($pdo, $company_id); + $new_id = $stock->saveStockIn($data, $logging, $uuid); + + if ($auto_approve && $new_id > 0) { + $stock->approveStock($new_id, (int)$data['warehouse'], 'in', $whMgmt); + } }); - $answer['success'] = 1; + $answer['success'] = 1; + $answer['auto_approved'] = $auto_approve; } catch (PDOException $e) { $answer['success'] = 0; diff --git a/app/ics/api/engine/manage_stock_out.php b/app/ics/api/engine/manage_stock_out.php index 9a9b20a..450a4f4 100644 --- a/app/ics/api/engine/manage_stock_out.php +++ b/app/ics/api/engine/manage_stock_out.php @@ -3,14 +3,26 @@ require '../../../assets/utils/db_auth.php'; require '../../../assets/utils/classes/StockManager.php'; require '../../../assets/utils/classes/WarehouseManager.php'; + require '../../../assets/utils/classes/CompanySettingManager.php'; + + $csm = new CompanySettingManager($pdo1, $company_id); + $auto_approve = (int)$csm->get('default_stock_status') === 1; try { - dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $uuid) { - $stock = new StockManager($pdo, $company_id); - $stock->saveStockOut($data, $logging, $uuid); + $new_id = null; + + dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $uuid, $auto_approve, &$new_id) { + $stock = new StockManager($pdo, $company_id); + $whMgmt = new WarehouseManager($pdo, $company_id); + $new_id = $stock->saveStockOut($data, $logging, $uuid); + + if ($auto_approve && $new_id > 0) { + $stock->approveStock($new_id, (int)$data['warehouse'], 'out', $whMgmt); + } }); - $answer['success'] = 1; + $answer['success'] = 1; + $answer['auto_approved'] = $auto_approve; } catch (PDOException $e) { $answer['success'] = 0; diff --git a/app/ics/api/engine/manage_stock_transfer.php b/app/ics/api/engine/manage_stock_transfer.php index 566aae2..a116223 100644 --- a/app/ics/api/engine/manage_stock_transfer.php +++ b/app/ics/api/engine/manage_stock_transfer.php @@ -3,14 +3,26 @@ require '../../../assets/utils/db_auth.php'; require '../../../assets/utils/classes/StockManager.php'; require '../../../assets/utils/classes/WarehouseManager.php'; + require '../../../assets/utils/classes/CompanySettingManager.php'; + + $csm = new CompanySettingManager($pdo1, $company_id); + $auto_approve = (int)$csm->get('default_stock_status') === 1; try { - dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $uuid) { - $stock = new StockManager($pdo, $company_id); - $stock->saveStockTransfer($data, $logging, $uuid); + $new_id = null; + + dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $uuid, $auto_approve, &$new_id) { + $stock = new StockManager($pdo, $company_id); + $whMgmt = new WarehouseManager($pdo, $company_id); + $new_id = $stock->saveStockTransfer($data, $logging, $uuid); + + if ($auto_approve && $new_id > 0) { + $stock->approveStock($new_id, (int)$data['warehouse_from'], 'transfer', $whMgmt); + } }); - $answer['success'] = 1; + $answer['success'] = 1; + $answer['auto_approved'] = $auto_approve; } catch (PDOException $e) { $answer['success'] = 0; diff --git a/app/ics/api/engine/retrieve_warehouse.php b/app/ics/api/engine/retrieve_warehouse.php index fbdb266..f18e514 100644 --- a/app/ics/api/engine/retrieve_warehouse.php +++ b/app/ics/api/engine/retrieve_warehouse.php @@ -3,12 +3,20 @@ require '../../../assets/utils/db_auth.php'; require '../../../assets/utils/classes/WarehouseManager.php'; - $wh = new WarehouseManager($pdo2, $company_id); - $answer['output'] = $wh->getWarehouseList( - $data['type'] ?? '', - $data['product_sku'] ?? '', - (int)($data['id'] ?? 0) - ); + $wh = new WarehouseManager($pdo2, $company_id); + $type = $data['type'] ?? ''; + $sku = $data['product_sku'] ?? ''; + $lot = $data['lot_number'] ?? ''; + $id = (int)($data['id'] ?? 0); + + // When type=from and a lot is selected, filter warehouses to only those + // holding that specific lot in an approved, rack-occupied stock-in row. + if ($type === 'from' && $sku && $lot) { + $answer['output'] = $wh->getWarehousesByLot($sku, $lot); + } else { + $answer['output'] = $wh->getWarehouseList($type, $sku, $id); + } + $answer['success'] = 1; exit(json_encode($answer)); ?> \ No newline at end of file diff --git a/app/ics/manage_stock_in.php b/app/ics/manage_stock_in.php index 5ec0f25..95a9644 100644 --- a/app/ics/manage_stock_in.php +++ b/app/ics/manage_stock_in.php @@ -289,8 +289,8 @@ .attr('data-id', data.contact_id); $('#product_name').val(data.product_name); - $('#lot_number').val(data.lot_number || ''); - $('#serial_number').val(data.serial_number || ''); + $('#lot_number').val(data.lot_number || '').prop('disabled', true); + $('#serial_number').val(data.serial_number || '').prop('disabled', true); if (data.lot_number) { if (expiryPicker) { expiryPicker.setDate(data.expiry_date || '', true, 'Y-m-d'); } $('#expiry_date').prop('disabled', true).attr('title', 'Expiry date is locked for existing lots') diff --git a/app/ics/manage_stock_out.php b/app/ics/manage_stock_out.php index 4754859..88b766e 100644 --- a/app/ics/manage_stock_out.php +++ b/app/ics/manage_stock_out.php @@ -198,7 +198,7 @@ autoPrepare: true, checkRequired: 0, noLoading: true, - data: { "type": 'from' }, + data: { "type": 'from', "lot_number": $('#lot_number').val() }, action: 'read', onSuccess: function(res) { var option = ''; diff --git a/app/ics/manage_stock_transfer.php b/app/ics/manage_stock_transfer.php index 31c45fd..e7f9840 100644 --- a/app/ics/manage_stock_transfer.php +++ b/app/ics/manage_stock_transfer.php @@ -225,7 +225,7 @@ autoPrepare: true, checkRequired: 0, noLoading: true, - data: { "type": type }, + data: { "type": type, "lot_number": type === 'from' ? $('#lot_number').val() : '' }, action: 'read', debugMode: 0, onSuccess: function(res) { diff --git a/app/ics/stock_in.php b/app/ics/stock_in.php index 3b3728d..c130110 100644 --- a/app/ics/stock_in.php +++ b/app/ics/stock_in.php @@ -133,10 +133,16 @@ ${item['aisle']} ${item['rack']} - ${(item['status'])?'Active':'Inactive'} + ${item['status'] == 1 ? 'Approved' : 'Draft'} + ${item['status'] == 0 + ? (auto_approve + ? '' + : '' + ) + : ''} @@ -189,13 +195,52 @@ $(async function() { try { + await load_company_setting(); await retrieve_warehouse(); await retrieve_stock_in(); } catch (e) { console.log(e); } }); - + + // ── Stock approval ──────────────────────────────────────────────────────── + var auto_approve = false; // set from company_setting on load + + function load_company_setting() { + return ajax_request({ + url: server_url + 'setting/api/engine/company_setting.php', + autoPrepare: true, + checkRequired: 0, + action: 'read', + onSuccess: function(res) { + auto_approve = res.output.default_stock_status == 1; + } + }); + } + + function approve_stock(id, warehouse_id, type) { + bootbox.confirm({ + message: 'Approve this stock record?', + buttons: { confirm: { label: 'Approve', className: 'btn-success' }, cancel: { label: 'Cancel', className: 'btn-secondary' } }, + callback: function(result) { + if (!result) return; + ajax_request({ + url: server_url + 'ics/api/engine/approve_stock.php', + autoPrepare: true, + checkRequired: 0, + action: 'update', + id: id, + warehouse: warehouse_id, + type: type, + onSuccess: function(res) { + retrieve_stock_list(); + } + }); + } + }); + } + + diff --git a/app/ics/stock_out.php b/app/ics/stock_out.php index 4ffa0b3..3566e3a 100644 --- a/app/ics/stock_out.php +++ b/app/ics/stock_out.php @@ -133,10 +133,16 @@ ${item['aisle']} ${item['rack']} - ${(item['status'])?'Active':'Inactive'} + ${item['status'] == 1 ? 'Approved' : 'Draft'} + ${item['status'] == 0 + ? (auto_approve + ? '' + : '' + ) + : ''} @@ -189,13 +195,52 @@ $(async function() { try { + await load_company_setting(); await retrieve_warehouse(); await retrieve_stock_out(); } catch (e) { console.log(e); } }); - + + // ── Stock approval ──────────────────────────────────────────────────────── + var auto_approve = false; // set from company_setting on load + + function load_company_setting() { + return ajax_request({ + url: server_url + 'setting/api/engine/company_setting.php', + autoPrepare: true, + checkRequired: 0, + action: 'read', + onSuccess: function(res) { + auto_approve = res.output.default_stock_status == 1; + } + }); + } + + function approve_stock(id, warehouse_id, type) { + bootbox.confirm({ + message: 'Approve this stock record?', + buttons: { confirm: { label: 'Approve', className: 'btn-success' }, cancel: { label: 'Cancel', className: 'btn-secondary' } }, + callback: function(result) { + if (!result) return; + ajax_request({ + url: server_url + 'ics/api/engine/approve_stock.php', + autoPrepare: true, + checkRequired: 0, + action: 'update', + id: id, + warehouse: warehouse_id, + type: type, + onSuccess: function(res) { + retrieve_stock_list(); + } + }); + } + }); + } + + diff --git a/app/ics/stock_transfer.php b/app/ics/stock_transfer.php index c1b3458..0bd649b 100644 --- a/app/ics/stock_transfer.php +++ b/app/ics/stock_transfer.php @@ -129,10 +129,16 @@ ${item['aisle']} ${item['rack']} - ${(item['status'])?'Active':'Inactive'} + ${item['status'] == 1 ? 'Approved' : 'Draft'} + ${item['status'] == 0 + ? (auto_approve + ? '' + : '' + ) + : ''} @@ -187,13 +193,52 @@ $(async function() { try { + await load_company_setting(); await retrieve_warehouse(); await retrieve_stock_transfer(); } catch (e) { console.log(e); } }); - + + // ── Stock approval ──────────────────────────────────────────────────────── + var auto_approve = false; // set from company_setting on load + + function load_company_setting() { + return ajax_request({ + url: server_url + 'setting/api/engine/company_setting.php', + autoPrepare: true, + checkRequired: 0, + action: 'read', + onSuccess: function(res) { + auto_approve = res.output.default_stock_status == 1; + } + }); + } + + function approve_stock(id, warehouse_id, type) { + bootbox.confirm({ + message: 'Approve this stock record?', + buttons: { confirm: { label: 'Approve', className: 'btn-success' }, cancel: { label: 'Cancel', className: 'btn-secondary' } }, + callback: function(result) { + if (!result) return; + ajax_request({ + url: server_url + 'ics/api/engine/approve_stock.php', + autoPrepare: true, + checkRequired: 0, + action: 'update', + id: id, + warehouse: warehouse_id, + type: type, + onSuccess: function(res) { + retrieve_stock_list(); + } + }); + } + }); + } + + diff --git a/app/include_header.php b/app/include_header.php index cef468c..b7a2d5d 100644 --- a/app/include_header.php +++ b/app/include_header.php @@ -48,6 +48,8 @@ + + diff --git a/app/include_setting_sidebar.php b/app/include_setting_sidebar.php index b04d504..4daf24b 100644 --- a/app/include_setting_sidebar.php +++ b/app/include_setting_sidebar.php @@ -56,6 +56,14 @@ +
  • + + + System Configuration + +
  • +