1, // auto-approve by default 'auto_invoice_and_credit_note' => 0, // manual by default 'auto_complete_on_ship' => 1, // auto-complete by default 'advanced_location' => 0, // simple mode by default 'location_label_rack' => 'Location', // single label default 'location_label_zone' => 'Zone', // advanced level 1 default 'location_label_aisle' => 'Aisle', // advanced level 2 default 'posting_open_from' => '', // YYYY-MM-DD; empty = no lower bound 'posting_open_to' => '', // YYYY-MM-DD; empty = no upper bound 'gl_open_from' => '', // legacy alias; use posting_open_from 'gl_open_to' => '', // legacy alias; use posting_open_to ]; public function __construct(PDO $pdo, int $company_id, ?PDO $transactionPdo = null) { $this->pdo = $pdo; $this->transactionPdo = $transactionPdo; $this->companyId = $company_id; } // ───────────────────────────────────────────────────────────── // 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; } /** * Return settings that are locked by existing transaction data. * * @return array Locked key => reason pairs. */ public function getTransactionLocks(): array { $locked = []; if (!$this->transactionPdo || $this->isLocalhostRequest()) { return $locked; } if ($this->hasStockTransactions()) { $locked['default_stock_status'] = 'stock transactions already exist'; $locked['advanced_location'] = 'stock transactions already exist'; } if ($this->hasOrderTransactions()) { $locked['auto_complete_on_ship'] = 'order, invoice, or return transactions already exist'; $locked['auto_invoice_and_credit_note'] = 'order, invoice, or return transactions already exist'; } return $locked; } /** * 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), ]); } /** * Return keys whose value cannot be changed because related transactions exist. * * @param array $incoming Requested key => value pairs. * @return array Blocked key => reason pairs. */ private function blockedChanges(array $incoming): array { if (!$this->transactionPdo || $this->isLocalhostRequest()) { return []; } $current = $this->getAll(); $blocked = []; $stock_keys = [ 'default_stock_status', 'advanced_location', ]; $order_keys = [ 'auto_complete_on_ship', 'auto_invoice_and_credit_note', ]; $needs_stock_check = false; foreach ($stock_keys as $key) { if (array_key_exists($key, $incoming) && (string)$current[$key] !== (string)$incoming[$key]) { $needs_stock_check = true; break; } } $needs_order_check = false; foreach ($order_keys as $key) { if (array_key_exists($key, $incoming) && (string)$current[$key] !== (string)$incoming[$key]) { $needs_order_check = true; break; } } $has_stock_transactions = $needs_stock_check ? $this->hasStockTransactions() : false; $has_order_transactions = $needs_order_check ? $this->hasOrderTransactions() : false; if ($has_stock_transactions) { foreach ($stock_keys as $key) { if (array_key_exists($key, $incoming) && (string)$current[$key] !== (string)$incoming[$key]) { $blocked[$key] = 'stock transactions already exist'; } } } if ($has_order_transactions) { foreach ($order_keys as $key) { if (array_key_exists($key, $incoming) && (string)$current[$key] !== (string)$incoming[$key]) { $blocked[$key] = 'order, invoice, or return transactions already exist'; } } } return $blocked; } private function hasOrderTransactions(): bool { foreach (['td_order', 'td_invoice', 'td_return'] as $table) { if ($this->tableHasCompanyRows($table)) { return true; } } return false; } private function hasStockTransactions(): bool { if ($this->tableHasCompanyRows('etl_stock_summary')) { return true; } if ($this->tableHasCompanyRows('md_lot')) { return true; } $sth = $this->transactionPdo->prepare( "SELECT id FROM md_warehouse WHERE company_id = :company_id" ); $sth->execute([':company_id' => $this->companyId]); $warehouse_ids = $sth->fetchAll(PDO::FETCH_COLUMN); foreach ($warehouse_ids as $warehouse_id) { $warehouse_id = (int)$warehouse_id; if ($warehouse_id > 0 && $this->tableHasCompanyRows('td_stock_' . $warehouse_id)) { return true; } } return false; } private function tableHasCompanyRows(string $table): bool { if (!$this->tableExists($table)) { return false; } $safe_table = str_replace('`', '``', $table); $sth = $this->transactionPdo->prepare( "SELECT COUNT(*) FROM `{$safe_table}` WHERE company_id = :company_id LIMIT 1" ); $sth->execute([':company_id' => $this->companyId]); return (int)$sth->fetchColumn() > 0; } private function tableExists(string $table): bool { $sth = $this->transactionPdo->prepare( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table_name" ); $sth->execute([':table_name' => $table]); return (int)$sth->fetchColumn() > 0; } private function isLocalhostRequest(): bool { $host = strtolower($_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? ''); $host = preg_replace('/:\d+$/', '', $host); return in_array($host, ['localhost', '127.0.0.1', '::1'], true) || ($_SERVER['REMOTE_ADDR'] ?? '') === '127.0.0.1' || ($_SERVER['REMOTE_ADDR'] ?? '') === '::1'; } /** * 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(), 'locked' => $this->getTransactionLocks(), ]); exit; } if ($action === 'update') { $allowed = array_keys(self::DEFAULTS); $incoming = []; foreach ($allowed as $key) { $incoming[$key] = $data[$key] ?? self::DEFAULTS[$key]; } $blocked = $this->blockedChanges($incoming); if (!empty($blocked)) { $labels = [ 'default_stock_status' => 'Default Stock Status', 'advanced_location' => 'Advanced Location', 'auto_invoice_and_credit_note' => 'Invoice & Credit Note Generation', 'auto_complete_on_ship' => 'Auto-Complete on Shipped', ]; $names = array_map( fn($key) => $labels[$key] ?? $key, array_keys($blocked) ); http_response_code(409); echo json_encode([ 'success' => 0, 'message' => 'Cannot change ' . implode(', ', $names) . ' because relevant transactions already exist.', 'blocked' => $blocked, ]); exit; } foreach ($incoming as $key => $value) { if ((string)$this->get($key) !== (string)$value) { $this->set($key, $value); } } echo json_encode(['success' => 1, 'message' => 'Settings saved.']); exit; } http_response_code(400); echo json_encode(['success' => 0, 'message' => 'Invalid action.']); exit; } }