From 4f884177a5ca7844e3f66868609044ec48c17e37 Mon Sep 17 00:00:00 2001 From: Thanakorn S Date: Mon, 25 May 2026 17:02:52 +0700 Subject: [PATCH] Seal live dashboard event gaps --- .../api/engine/save_manual_journal.php | 3 ++ app/assets/utils/classes/StockManager.php | 8 +++++ app/assets/utils/db_helpers.php | 31 ++++++++++++++-- app/assets/utils/notify_node.php | 20 +++++++++++ .../engine/delete_supplier_credit_note.php | 35 +++++++++++++++++++ app/finance/api/engine/manage_payment.php | 2 +- app/finance/api/engine/manage_receipt.php | 2 +- app/ics/api/engine/approve_stock.php | 17 +++------ app/order/api/engine/confirm_return.php | 3 ++ app/order/api/engine/issue_invoice.php | 2 +- app/po/api/engine/confirm_supplier_return.php | 4 +++ app/revenue/api/engine/delete_credit_note.php | 35 +++++++++++++++++++ app/revenue/api/engine/proceed_to_invoice.php | 2 ++ 13 files changed, 147 insertions(+), 17 deletions(-) create mode 100644 app/expense/api/engine/delete_supplier_credit_note.php create mode 100644 app/revenue/api/engine/delete_credit_note.php diff --git a/app/accounting/api/engine/save_manual_journal.php b/app/accounting/api/engine/save_manual_journal.php index 0f7497e..5183216 100644 --- a/app/accounting/api/engine/save_manual_journal.php +++ b/app/accounting/api/engine/save_manual_journal.php @@ -3,10 +3,12 @@ session_start(); require_once '../../../assets/utils/db_auth.php'; require_once '../../../assets/utils/classes_ac/PostingWindowGuard.php'; require_once '../../../assets/utils/classes_ac/GlManager.php'; +require_once '../../../assets/utils/notify_node.php'; require_role($user_role, ['owner', 'admin']); $gl_id = (int)($data['gl_id'] ?? 0); +$event_action = $gl_id > 0 ? 'replaced' : 'posted'; $reference = trim((string)($data['reference'] ?? '')); $description = trim((string)($data['description'] ?? '')); $journal_date = trim((string)($data['journal_date'] ?? '')); @@ -73,6 +75,7 @@ try { $pdo2->commit(); $answer['success'] = 1; $answer['gl_id'] = $gl_id; + notify_node('gl_posted', gl_posted_payload('manual', (int)$gl_id, $event_action, $lines), $company_id); } catch (Exception $e) { if ($pdo2->inTransaction()) $pdo2->rollBack(); $answer['message'] = $e->getMessage(); diff --git a/app/assets/utils/classes/StockManager.php b/app/assets/utils/classes/StockManager.php index 5f9f227..006e790 100644 --- a/app/assets/utils/classes/StockManager.php +++ b/app/assets/utils/classes/StockManager.php @@ -1,5 +1,8 @@ company_id; + db_after_commit(function() use ($warehouse_id, $type, $notify_company_id) { + notify_node("stock_updated", ["warehouse_id" => $warehouse_id, "type" => $type], $notify_company_id); + }); } } diff --git a/app/assets/utils/db_helpers.php b/app/assets/utils/db_helpers.php index 3a058b0..44257d8 100644 --- a/app/assets/utils/db_helpers.php +++ b/app/assets/utils/db_helpers.php @@ -69,17 +69,43 @@ function db_check($sth, &$answer, $options = []) { +// Run a callback after the surrounding dbTransaction() commits. If no managed +// transaction is active, run immediately. Used for non-authoritative side effects +// such as live dashboard notifications. +function db_after_commit(callable $callback): void { + if (!isset($GLOBALS['_db_after_commit_stack']) || empty($GLOBALS['_db_after_commit_stack'])) { + $callback(); + return; + } + + $idx = count($GLOBALS['_db_after_commit_stack']) - 1; + $GLOBALS['_db_after_commit_stack'][$idx][] = $callback; +} + // transaction wrapper with retry logic for deadlocks function dbTransaction($pdo, $callback, $maxRetries = 3) { for ($attempt = 0; $attempt < $maxRetries; $attempt++) { + $GLOBALS['_db_after_commit_stack'][] = []; + try { $pdo->beginTransaction(); $result = $callback($pdo); $pdo->commit(); + + $callbacks = array_pop($GLOBALS['_db_after_commit_stack']); + foreach ($callbacks as $after_commit) { + try { + $after_commit(); + } catch (Throwable $e) { + error_log("[AFTER COMMIT ERROR] " . $e->getMessage() . " | " . $e->getFile() . ":" . $e->getLine()); + } + } + return $result; } catch (PDOException $e) { - $pdo->rollBack(); + if ($pdo->inTransaction()) $pdo->rollBack(); + array_pop($GLOBALS['_db_after_commit_stack']); if ($e->getCode() == '40001' && $attempt < $maxRetries - 1) { usleep(600000 * ($attempt + 1)); @@ -90,7 +116,8 @@ function dbTransaction($pdo, $callback, $maxRetries = 3) { throw $e; } catch (Exception $e) { - $pdo->rollBack(); + if ($pdo->inTransaction()) $pdo->rollBack(); + array_pop($GLOBALS['_db_after_commit_stack']); error_log("[ERROR] " . $e->getMessage() . " | " . $e->getFile() . ":" . $e->getLine()); throw $e; } diff --git a/app/assets/utils/notify_node.php b/app/assets/utils/notify_node.php index 7fc3716..39a3fc2 100644 --- a/app/assets/utils/notify_node.php +++ b/app/assets/utils/notify_node.php @@ -34,3 +34,23 @@ function notify_node(string $event, array $data, int $company_id): void curl_exec($ch); curl_close($ch); } + +function gl_posted_payload(string $doc_type, int $id, string $action, array $lines): array +{ + $has_revenue = false; + $has_expense = false; + + foreach ($lines as $line) { + $code = (int)($line['account_code'] ?? 0); + if ($code >= 4000 && $code < 5000) $has_revenue = true; + if ($code >= 5000) $has_expense = true; + } + + return [ + 'doc_type' => $doc_type, + 'id' => $id, + 'action' => $action, + 'has_revenue' => $has_revenue, + 'has_expense' => $has_expense, + ]; +} diff --git a/app/expense/api/engine/delete_supplier_credit_note.php b/app/expense/api/engine/delete_supplier_credit_note.php new file mode 100644 index 0000000..b65a1e4 --- /dev/null +++ b/app/expense/api/engine/delete_supplier_credit_note.php @@ -0,0 +1,35 @@ +softDelete($id); + }); + + $answer['success'] = 1; + $answer['message'] = 'Supplier credit note deleted.'; + notify_node('invoice_updated', [], $company_id); + +} catch (PDOException $e) { + $answer['message'] = 'Database error, please try again.'; + http_response_code(500); + +} catch (Exception $e) { + $answer['message'] = $e->getMessage(); + http_response_code(400); +} + +exit(json_encode($answer)); diff --git a/app/finance/api/engine/manage_payment.php b/app/finance/api/engine/manage_payment.php index 38d5d81..5aff3bc 100644 --- a/app/finance/api/engine/manage_payment.php +++ b/app/finance/api/engine/manage_payment.php @@ -69,7 +69,7 @@ $pdo2->beginTransaction(); $gl->post('payment', $new_id, $built['formula_id'], $built['period'], $built['lines'], ['journal_date' => $built['doc_date']]); $pdo2->commit(); - notify_node('gl_posted', ['doc_type' => 'payment', 'id' => $new_id, 'action' => 'posted'], $company_id); + notify_node('gl_posted', gl_posted_payload('payment', (int)$new_id, 'posted', $built['lines'] ?? []), $company_id); $answer['gl_posted'] = true; } catch (Exception $e) { if ($pdo2->inTransaction()) $pdo2->rollBack(); diff --git a/app/finance/api/engine/manage_receipt.php b/app/finance/api/engine/manage_receipt.php index 6f65d8d..3fc6a3a 100644 --- a/app/finance/api/engine/manage_receipt.php +++ b/app/finance/api/engine/manage_receipt.php @@ -69,7 +69,7 @@ $pdo2->beginTransaction(); $gl->post('receipt', $new_id, $built['formula_id'], $built['period'], $built['lines'], ['journal_date' => $built['doc_date']]); $pdo2->commit(); - notify_node('gl_posted', ['doc_type' => 'receipt', 'id' => $new_id, 'action' => 'posted'], $company_id); + notify_node('gl_posted', gl_posted_payload('receipt', (int)$new_id, 'posted', $built['lines'] ?? []), $company_id); $answer['gl_posted'] = true; } catch (Exception $e) { if ($pdo2->inTransaction()) $pdo2->rollBack(); diff --git a/app/ics/api/engine/approve_stock.php b/app/ics/api/engine/approve_stock.php index 4c46546..e83f6f0 100644 --- a/app/ics/api/engine/approve_stock.php +++ b/app/ics/api/engine/approve_stock.php @@ -15,7 +15,6 @@ session_start(); require_once '../../../assets/utils/db_auth.php'; require_once '../../../assets/utils/classes/StockManager.php'; require_once '../../../assets/utils/classes/WarehouseManager.php'; -require_once '../../../assets/utils/notify_node.php'; require_role($user_role, ['owner', 'admin']); @@ -29,24 +28,18 @@ if (!$id || !$warehouse_id || !in_array($type, ['in', 'out', 'transfer'])) { exit(json_encode($answer)); } -$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(); - - notify_node('stock_updated', ['warehouse_id' => $warehouse_id, 'type' => $type], $company_id); + dbTransaction($pdo2, function($pdo) use ($id, $warehouse_id, $type, $company_id) { + $stock = new StockManager($pdo, $company_id); + $whMgmt = new WarehouseManager($pdo, $company_id); + $stock->approveStock($id, $warehouse_id, $type, $whMgmt); + }); $answer['success'] = 1; $answer['message'] = 'Stock approved.'; exit(json_encode($answer)); } catch (Exception $e) { - $pdo2->rollBack(); http_response_code(400); $answer['message'] = $e->getMessage(); exit(json_encode($answer)); diff --git a/app/order/api/engine/confirm_return.php b/app/order/api/engine/confirm_return.php index 0b8c565..bde481c 100644 --- a/app/order/api/engine/confirm_return.php +++ b/app/order/api/engine/confirm_return.php @@ -32,6 +32,9 @@ $answer['message'] = 'Return confirmed.'; $answer['auto_cn'] = $auto_cn; notify_node('return_updated', [], $company_id); + if ($auto_approve) { + notify_node('stock_updated', ['warehouse_id' => 0, 'type' => 'in'], $company_id); + } if ($auto_cn) { notify_node('invoice_updated', [], $company_id); } diff --git a/app/order/api/engine/issue_invoice.php b/app/order/api/engine/issue_invoice.php index 717d5bb..1580f42 100644 --- a/app/order/api/engine/issue_invoice.php +++ b/app/order/api/engine/issue_invoice.php @@ -57,7 +57,7 @@ } $pdo2->commit(); - notify_node('gl_posted', ['doc_type' => $doc['doc_type'], 'id' => $id, 'action' => 'posted'], $company_id); + notify_node('gl_posted', gl_posted_payload((string)$doc['doc_type'], (int)$id, $existing ? 'replaced' : 'posted', $built['lines'] ?? []), $company_id); $answer['gl_posted'] = true; } catch (Exception $e) { if ($pdo2->inTransaction()) $pdo2->rollBack(); diff --git a/app/po/api/engine/confirm_supplier_return.php b/app/po/api/engine/confirm_supplier_return.php index 8dd54e7..ee09570 100644 --- a/app/po/api/engine/confirm_supplier_return.php +++ b/app/po/api/engine/confirm_supplier_return.php @@ -6,6 +6,7 @@ require_once '../../../assets/utils/classes/WarehouseManager.php'; require_once '../../../assets/utils/classes/InvoiceManager.php'; require_once '../../../assets/utils/classes/CompanySettingManager.php'; + require_once '../../../assets/utils/notify_node.php'; $return_id = (int)($data['id'] ?? 0); @@ -29,6 +30,9 @@ $answer['success'] = 1; $answer['message'] = 'Supplier return confirmed.'; + if ($auto_approve) { + notify_node('stock_updated', ['warehouse_id' => 0, 'type' => 'out'], $company_id); + } } catch (PDOException $e) { $answer['message'] = 'Database error, please try again.'; diff --git a/app/revenue/api/engine/delete_credit_note.php b/app/revenue/api/engine/delete_credit_note.php new file mode 100644 index 0000000..c355848 --- /dev/null +++ b/app/revenue/api/engine/delete_credit_note.php @@ -0,0 +1,35 @@ +softDelete($id); + }); + + $answer['success'] = 1; + $answer['message'] = 'Credit note deleted.'; + notify_node('invoice_updated', [], $company_id); + +} catch (PDOException $e) { + $answer['message'] = 'Database error, please try again.'; + http_response_code(500); + +} catch (Exception $e) { + $answer['message'] = $e->getMessage(); + http_response_code(400); +} + +exit(json_encode($answer)); diff --git a/app/revenue/api/engine/proceed_to_invoice.php b/app/revenue/api/engine/proceed_to_invoice.php index 7e32d39..cb92042 100644 --- a/app/revenue/api/engine/proceed_to_invoice.php +++ b/app/revenue/api/engine/proceed_to_invoice.php @@ -4,6 +4,7 @@ require_role($user_role, ['owner', 'admin', 'staff']); require_once '../../../assets/utils/classes/OrderManager.php'; require_once '../../../assets/utils/classes/InvoiceManager.php'; + require_once '../../../assets/utils/notify_node.php'; $order_id = (int)($data['order_id'] ?? 0); @@ -26,6 +27,7 @@ $answer['success'] = 1; $answer['new_id'] = $new_id; + notify_node('invoice_updated', [], $company_id); } catch (PDOException $e) { $answer['message'] = 'Database error, please try again.';