355 lines
12 KiB
PHP
355 lines
12 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/rack
|
|
* location_label_rack string label for rack / 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_rack' => 'Location', // single label default
|
|
'location_label_zone' => 'Zone', // advanced level 1 default
|
|
'location_label_aisle' => 'Aisle', // advanced level 2 default
|
|
];
|
|
|
|
public function __construct(PDO $pdo, int $companyId, ?PDO $transactionPdo = null)
|
|
{
|
|
$this->pdo = $pdo;
|
|
$this->transactionPdo = $transactionPdo;
|
|
$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;
|
|
}
|
|
|
|
/**
|
|
* 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('warehouse_balance')) {
|
|
return true;
|
|
}
|
|
|
|
if ($this->tableHasCompanyRows('md_lot')) {
|
|
return true;
|
|
}
|
|
|
|
$sth = $this->transactionPdo->prepare(
|
|
"SELECT warehouse_name FROM md_warehouse WHERE company_id = :company_id"
|
|
);
|
|
$sth->execute([':company_id' => $this->companyId]);
|
|
$warehouses = $sth->fetchAll(PDO::FETCH_COLUMN);
|
|
|
|
foreach ($warehouses as $warehouse_name) {
|
|
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', (string)$warehouse_name);
|
|
if ($safe !== '' && $this->tableHasCompanyRows('td_stock_' . $safe)) {
|
|
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;
|
|
}
|
|
}
|