warehouse layers [switchable]

This commit is contained in:
Thanakorn S
2026-05-04 13:43:32 +07:00
parent a90975d9f1
commit 414f1cd81d
26 changed files with 2022 additions and 584 deletions
@@ -5,29 +5,38 @@
* Table: company_setting (company_id, setting_key, value, log, updated_at)
*
* Current keys:
* default_stock_status int 0 = draft (manual approve required)
* 1 = auto-approve on save
* auto_invoice_and_credit_note int 0 = manual
* 1 = auto-generate invoice on confirm order
* + auto-generate credit note on confirm return
* 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
'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)
public function __construct(PDO $pdo, int $companyId, ?PDO $transactionPdo = null)
{
$this->pdo = $pdo;
$this->companyId = $companyId;
$this->pdo = $pdo;
$this->transactionPdo = $transactionPdo;
$this->companyId = $companyId;
}
// ─────────────────────────────────────────────────────────────
@@ -80,6 +89,32 @@ class CompanySettingManager
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.
*
@@ -114,6 +149,144 @@ class CompanySettingManager
]);
}
/**
* 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.
*
@@ -127,14 +300,48 @@ class CompanySettingManager
$action = $data['action'] ?? '';
if ($action === 'read') {
echo json_encode(['success' => 1, 'output' => $this->getAll()]);
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) {
$this->set($key, $data[$key] ?? self::DEFAULTS[$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;
@@ -144,4 +351,4 @@ class CompanySettingManager
echo json_encode(['success' => 0, 'message' => 'Invalid action.']);
exit;
}
}
}