Files
wms-app/app/assets/utils/classes/CompanySettingManager.php
T
2026-05-27 17:14:53 +07:00

361 lines
13 KiB
PHP

<?php
/**
* CompanySettingManager — per-company key/value configuration store.
*
* Table: company_setting (company_id, setting_key, value, log, updated_at)
*
* Current keys:
* default_stock_status int 0 = draft, 1 = auto-approve on save
* auto_invoice_and_credit_note int 0 = manual, 1 = auto-generate
* auto_complete_on_ship int 0 = manual, 1 = auto-complete on shipped
* advanced_location int 0 = simple (single location), 1 = zone/aisle/bin
* location_label_bin string label for bin / single location field
* location_label_zone string label for zone (advanced only)
* location_label_aisle string label for aisle (advanced only)
*
* Designed to accept new keys without schema changes.
*/
class CompanySettingManager
{
private PDO $pdo;
private ?PDO $transactionPdo;
private int $companyId;
// ── Known keys with their defaults ───────────────────────────────────────
private const DEFAULTS = [
'default_stock_status' => 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_bin' => '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) {
if (array_key_exists($key, $data)) {
$incoming[$key] = $data[$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;
}
}