Commit 1edf3267 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(accounting): manage the chart of accounts from the screen — move, delete, reclassify

The chart could only be created and edited. Restructuring it — the thing that
actually gets asked for — meant a developer.

Adds move / delete / promote-demote with the guards a ledger needs, each refusing
with the specific reason rather than failing later at month end:

MOVE
- Refuses a move under the account's own descendant, which would detach the
  subtree from the root and make the tree query loop.
- Refuses a parent of a different account_type — that would file an asset under
  liabilities and quietly corrupt the balance sheet.
- Refuses a non-header parent.
- Carries the whole subtree and recomputes every level beneath.

DELETE
- An account with posted history is ARCHIVED, never deleted: removing it would
  leave old journal lines pointing at a name that no longer exists. The button
  relabels itself to "أرشفة" and says why.
- Refuses while children exist, or while anything still points at it — posting
  rules, tax profiles, voucher types, vouchers, bank accounts, treasuries — and
  lists what, so the blocker is actionable.
- System accounts can be deactivated, not removed.

PROMOTE / DEMOTE
- Refuses to promote an account that already carries movement; a header takes no
  entries, so its balance would be stranded.
- Refuses to demote one with children; entries would post at a summary level and
  double-count up the tree.

The management panel loads the usage check before offering anything, so a button
that is going to be refused is disabled with the reason instead of being offered
and failing.

Reference scanning tolerates a missing table or column, so a trimmed or older
install does not break the screen. Descendant walking is iterative and guarded
against a pre-existing cycle rather than recursing into a hang.
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
parent b43c5268
...@@ -9,6 +9,7 @@ use App\Core\App; ...@@ -9,6 +9,7 @@ use App\Core\App;
use App\Core\Response; use App\Core\Response;
use App\Modules\Accounting\Models\Account; use App\Modules\Accounting\Models\Account;
use App\Modules\Accounting\Models\CostCenter; use App\Modules\Accounting\Models\CostCenter;
use App\Modules\Accounting\Services\ChartOfAccountsService;
class ChartOfAccountsController extends Controller class ChartOfAccountsController extends Controller
{ {
...@@ -163,4 +164,107 @@ class ChartOfAccountsController extends Controller ...@@ -163,4 +164,107 @@ class ChartOfAccountsController extends Controller
return $this->json($results); return $this->json($results);
} }
// ────────────────────────────────────────────────────────────
// Structural edits
// ────────────────────────────────────────────────────────────
/** Everything pointing at an account — shown before any destructive action. */
public function usage(Request $request, string $id): Response
{
$this->authorize('accounting.coa.view');
$db = App::getInstance()->db();
$acc = $db->selectOne(
"SELECT id, account_code, name_ar, is_header, is_active, is_system, is_archived
FROM chart_of_accounts WHERE id = ?",
[(int) $id]
);
if (!$acc) {
return $this->json(['success' => false, 'error' => 'الحساب غير موجود']);
}
$usage = ChartOfAccountsService::usage((int) $id);
// Say plainly what each action would do, so the screen never offers a button
// that is going to be refused.
$blockers = [];
if ($usage['children'] > 0) {
$blockers[] = 'له ' . $usage['children'] . ' حساب فرعي';
}
if ($usage['lines'] > 0) {
$blockers[] = 'عليه ' . $usage['lines'] . ' حركة مرحّلة برصيد ' . $usage['balance'];
}
foreach ($usage['refs'] as $r) {
$blockers[] = 'مستخدم في ' . $r;
}
return $this->json([
'success' => true,
'account' => $acc,
'usage' => $usage,
'blockers' => $blockers,
'can_delete' => $usage['children'] === 0 && empty($usage['refs']) && (int) ($acc['is_system'] ?? 0) === 0,
'will_archive'=> $usage['lines'] > 0,
]);
}
public function reparent(Request $request, string $id): Response
{
$this->authorize('accounting.coa.manage');
$newParent = $request->post('parent_id');
$newParent = ($newParent === null || $newParent === '' || (int) $newParent === 0)
? null
: (int) $newParent;
$result = ChartOfAccountsService::reparent((int) $id, $newParent);
return $result['success']
? $this->redirect('/accounting/chart-of-accounts')
->withSuccess('تم نقل الحساب و' . (($result['moved'] ?? 1) - 1) . ' حساب فرعي تحته')
: $this->redirect('/accounting/chart-of-accounts')->withError($result['error'] ?? 'فشل النقل');
}
public function destroy(Request $request, string $id): Response
{
$this->authorize('accounting.coa.manage');
$result = ChartOfAccountsService::remove((int) $id);
if (!$result['success']) {
return $this->redirect('/accounting/chart-of-accounts')->withError($result['error'] ?? 'تعذر الحذف');
}
return !empty($result['archived'])
? $this->redirect('/accounting/chart-of-accounts')
->withWarning('الحساب عليه حركات مرحّلة — تم أرشفته بدل حذفه حتى تظل القيود القديمة مقروءة')
: $this->redirect('/accounting/chart-of-accounts')->withSuccess('تم حذف الحساب');
}
public function toggleHeader(Request $request, string $id): Response
{
$this->authorize('accounting.coa.manage');
$makeHeader = (int) $request->post('is_header', 0) === 1;
$result = ChartOfAccountsService::setHeader((int) $id, $makeHeader);
return $result['success']
? $this->redirect('/accounting/chart-of-accounts')
->withSuccess($makeHeader ? 'تم تحويله لحساب رئيسي' : 'تم تحويله لحساب ترحيل')
: $this->redirect('/accounting/chart-of-accounts')->withError($result['error'] ?? 'تعذر التغيير');
}
/** Next free code under a parent, for the create form. */
public function nextCode(Request $request): Response
{
$this->authorize('accounting.coa.manage');
$parentId = (int) $request->get('parent_id', 0);
if ($parentId <= 0) {
return $this->json(['code' => null]);
}
return $this->json(['code' => ChartOfAccountsService::nextCode($parentId)]);
}
} }
...@@ -19,6 +19,11 @@ return [ ...@@ -19,6 +19,11 @@ return [
['GET', '/accounting/chart-of-accounts/{id:\d+}/edit', 'Accounting\Controllers\ChartOfAccountsController@edit', ['auth'], 'accounting.coa.manage'], ['GET', '/accounting/chart-of-accounts/{id:\d+}/edit', 'Accounting\Controllers\ChartOfAccountsController@edit', ['auth'], 'accounting.coa.manage'],
['POST', '/accounting/chart-of-accounts/{id:\d+}', 'Accounting\Controllers\ChartOfAccountsController@update', ['auth', 'csrf'], 'accounting.coa.manage'], ['POST', '/accounting/chart-of-accounts/{id:\d+}', 'Accounting\Controllers\ChartOfAccountsController@update', ['auth', 'csrf'], 'accounting.coa.manage'],
['GET', '/accounting/chart-of-accounts/search', 'Accounting\Controllers\ChartOfAccountsController@search', ['auth'], 'accounting.coa.view'], ['GET', '/accounting/chart-of-accounts/search', 'Accounting\Controllers\ChartOfAccountsController@search', ['auth'], 'accounting.coa.view'],
['GET', '/accounting/chart-of-accounts/next-code', 'Accounting\Controllers\ChartOfAccountsController@nextCode', ['auth'], 'accounting.coa.manage'],
['GET', '/accounting/chart-of-accounts/{id:\d+}/usage', 'Accounting\Controllers\ChartOfAccountsController@usage', ['auth'], 'accounting.coa.view'],
['POST', '/accounting/chart-of-accounts/{id:\d+}/reparent', 'Accounting\Controllers\ChartOfAccountsController@reparent', ['auth', 'csrf'], 'accounting.coa.manage'],
['POST', '/accounting/chart-of-accounts/{id:\d+}/delete', 'Accounting\Controllers\ChartOfAccountsController@destroy', ['auth', 'csrf'], 'accounting.coa.manage'],
['POST', '/accounting/chart-of-accounts/{id:\d+}/header', 'Accounting\Controllers\ChartOfAccountsController@toggleHeader', ['auth', 'csrf'], 'accounting.coa.manage'],
// ── Journal Types ──────────────────────────────────────── // ── Journal Types ────────────────────────────────────────
['GET', '/accounting/journal-types', 'Accounting\Controllers\JournalTypeController@index', ['auth'], 'accounting.journal_type.view'], ['GET', '/accounting/journal-types', 'Accounting\Controllers\JournalTypeController@index', ['auth'], 'accounting.journal_type.view'],
......
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Services;
use App\Core\App;
/**
* Structural edits to the chart of accounts, with the guards that keep a ledger
* honest.
*
* The chart is not an ordinary tree: every node may carry posted history, and
* several other tables point at it. Moving or removing one carelessly strands a
* balance or breaks a posting rule, and the damage only shows up at month end.
* Every operation here refuses rather than risks it, and says exactly what is in
* the way.
*/
final class ChartOfAccountsService
{
/**
* Everything pointing at an account. Used before any destructive change so the
* refusal can name what is blocking it.
*
* @return array{lines:int, children:int, refs:array<int,string>, balance:string}
*/
public static function usage(int $accountId): array
{
$db = App::getInstance()->db();
$lines = $db->selectOne(
"SELECT COUNT(*) AS n, COALESCE(SUM(debit),0) AS dr, COALESCE(SUM(credit),0) AS cr
FROM journal_entry_lines WHERE account_id = ?",
[$accountId]
);
$children = $db->selectOne(
"SELECT COUNT(*) AS n FROM chart_of_accounts WHERE parent_id = ? AND is_archived = 0",
[$accountId]
);
// Other tables that name this account. Each is optional: a trimmed install
// may not have the table at all, so a missing one is not an error.
$refs = [];
$checks = [
['revenue_posting_rule_lines', 'account_id', 'قاعدة توزيع'],
['revenue_posting_rules', 'debit_account_id', 'حساب مقابل في قاعدة توزيع'],
['revenue_posting_rule_lines', 'recognized_account_id', 'حساب تحقق إيراد مؤجل'],
['revenue_tax_profiles', 'output_tax_account_id', 'ملف ضريبي (مخرجات)'],
['revenue_tax_profiles', 'input_tax_account_id', 'ملف ضريبي (مدخلات)'],
['voucher_types', 'default_counter_account_id', 'نوع سند'],
['voucher_types', 'default_line_account_id', 'نوع سند'],
['vouchers', 'counter_account_id', 'سند'],
['voucher_lines', 'account_id', 'بند سند'],
['bank_accounts', 'gl_account_id', 'حساب بنكي'],
['treasuries', 'account_code', 'خزنة'],
];
foreach ($checks as [$table, $column, $label]) {
try {
$exists = $db->selectOne(
"SELECT 1 AS ok FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = ?",
[$table]
);
if (!$exists) {
continue;
}
// treasuries stores a code, not an id.
if ($column === 'account_code') {
$acc = $db->selectOne("SELECT account_code FROM chart_of_accounts WHERE id = ?", [$accountId]);
if (!$acc) {
continue;
}
$row = $db->selectOne("SELECT COUNT(*) AS n FROM `{$table}` WHERE `{$column}` = ?", [$acc['account_code']]);
} else {
$row = $db->selectOne("SELECT COUNT(*) AS n FROM `{$table}` WHERE `{$column}` = ?", [$accountId]);
}
$n = (int) ($row['n'] ?? 0);
if ($n > 0) {
$refs[] = $label . ' (' . $n . ')';
}
} catch (\Throwable $e) {
// A missing column on an older schema must not break the check.
}
}
$dr = (string) ($lines['dr'] ?? '0.00');
$cr = (string) ($lines['cr'] ?? '0.00');
return [
'lines' => (int) ($lines['n'] ?? 0),
'children' => (int) ($children['n'] ?? 0),
'refs' => array_values(array_unique($refs)),
'balance' => bcsub($dr, $cr, 2),
];
}
/**
* Move an account (and everything under it) to a new parent.
*
* @return array{success:bool, error?:string, moved?:int}
*/
public static function reparent(int $accountId, ?int $newParentId): array
{
$db = App::getInstance()->db();
$account = $db->selectOne("SELECT * FROM chart_of_accounts WHERE id = ? AND is_archived = 0", [$accountId]);
if (!$account) {
return ['success' => false, 'error' => 'الحساب غير موجود'];
}
if ((int) ($account['is_system'] ?? 0) === 1) {
return ['success' => false, 'error' => 'حساب نظامي — لا يمكن نقله'];
}
if ((int) $account['parent_id'] === (int) $newParentId) {
return ['success' => false, 'error' => 'الحساب موجود تحت هذا الأب بالفعل'];
}
$newLevel = 1;
if ($newParentId !== null) {
$parent = $db->selectOne("SELECT * FROM chart_of_accounts WHERE id = ? AND is_archived = 0", [$newParentId]);
if (!$parent) {
return ['success' => false, 'error' => 'الحساب الأب غير موجود'];
}
if ($accountId === $newParentId) {
return ['success' => false, 'error' => 'لا يمكن جعل الحساب أبًا لنفسه'];
}
// A node cannot move under its own descendant — that detaches the subtree
// from the root and the tree query loops forever.
foreach (self::descendantIds($accountId) as $descendantId) {
if ($descendantId === $newParentId) {
return ['success' => false, 'error' => 'لا يمكن نقل الحساب تحت حساب متفرع منه'];
}
}
if ((int) $parent['is_header'] !== 1) {
return ['success' => false, 'error' => 'الحساب الأب المختار ليس حسابًا رئيسيًا'];
}
if ($parent['account_type'] !== $account['account_type']) {
return [
'success' => false,
'error' => 'نوع الحساب (' . $account['account_type'] . ') يختلف عن نوع الأب ('
. $parent['account_type'] . ') — النقل سيخلط الأصول بالالتزامات في التقارير',
];
}
$newLevel = ((int) $parent['level']) + 1;
}
$delta = $newLevel - (int) $account['level'];
$db->beginTransaction();
try {
$db->update('chart_of_accounts', [
'parent_id' => $newParentId,
'level' => $newLevel,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$accountId]);
$moved = 1;
if ($delta !== 0) {
foreach (self::descendantIds($accountId) as $descendantId) {
$db->query(
"UPDATE chart_of_accounts SET level = level + ?, updated_at = ? WHERE id = ?",
[$delta, date('Y-m-d H:i:s'), $descendantId]
);
$moved++;
}
} else {
$moved += count(self::descendantIds($accountId));
}
$db->commit();
return ['success' => true, 'moved' => $moved];
} catch (\Throwable $e) {
$db->rollBack();
return ['success' => false, 'error' => 'فشل النقل: ' . $e->getMessage()];
}
}
/**
* Delete an account, or archive it when history makes deletion wrong.
*
* An account that has ever been posted to is never removed — the ledger would
* lose the name behind old entries. It is archived instead, which hides it from
* pickers while every historic line still resolves.
*
* @return array{success:bool, error?:string, archived?:bool}
*/
public static function remove(int $accountId): array
{
$db = App::getInstance()->db();
$account = $db->selectOne("SELECT * FROM chart_of_accounts WHERE id = ?", [$accountId]);
if (!$account) {
return ['success' => false, 'error' => 'الحساب غير موجود'];
}
if ((int) ($account['is_system'] ?? 0) === 1) {
return ['success' => false, 'error' => 'حساب نظامي — يمكن إيقافه وليس حذفه'];
}
$usage = self::usage($accountId);
if ($usage['children'] > 0) {
return [
'success' => false,
'error' => 'الحساب له ' . $usage['children'] . ' حساب فرعي — انقلها أو احذفها أولًا',
];
}
if (!empty($usage['refs'])) {
return [
'success' => false,
'error' => 'الحساب مستخدم في: ' . implode('، ', $usage['refs']) . ' — عدّلها أولًا',
];
}
// Posted history: archive, never delete.
if ($usage['lines'] > 0) {
$employee = App::getInstance()->currentEmployee();
$db->update('chart_of_accounts', [
'is_archived' => 1,
'is_active' => 0,
'archived_at' => date('Y-m-d H:i:s'),
'archived_by' => $employee ? (int) $employee->id : null,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$accountId]);
return ['success' => true, 'archived' => true];
}
$db->delete('chart_of_accounts', '`id` = ?', [$accountId]);
return ['success' => true, 'archived' => false];
}
/**
* Turn a posting account into a header, or back.
*
* A header cannot carry entries, so promoting one that already has movement
* would orphan that movement; demoting one that has children would let entries
* post at a summary level and double-count in the tree.
*/
public static function setHeader(int $accountId, bool $isHeader): array
{
$db = App::getInstance()->db();
$account = $db->selectOne("SELECT * FROM chart_of_accounts WHERE id = ? AND is_archived = 0", [$accountId]);
if (!$account) {
return ['success' => false, 'error' => 'الحساب غير موجود'];
}
$usage = self::usage($accountId);
if ($isHeader) {
if ($usage['lines'] > 0) {
return [
'success' => false,
'error' => 'الحساب عليه ' . $usage['lines'] . ' حركة مرحّلة — لا يمكن تحويله لحساب رئيسي، رصيده سيصبح معلقًا',
];
}
if (!empty($usage['refs'])) {
return [
'success' => false,
'error' => 'الحساب مستخدم في: ' . implode('، ', $usage['refs']) . ' — الترحيل عليه سيفشل بعد التحويل',
];
}
} else {
if ($usage['children'] > 0) {
return [
'success' => false,
'error' => 'الحساب له ' . $usage['children'] . ' حساب فرعي — لا يمكن جعله حسابًا للترحيل',
];
}
}
$db->update('chart_of_accounts', [
'is_header' => $isHeader ? 1 : 0,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$accountId]);
return ['success' => true];
}
/**
* The next free code under a parent, following the chart's own width convention.
*/
public static function nextCode(int $parentId): ?string
{
$db = App::getInstance()->db();
$parent = $db->selectOne("SELECT account_code FROM chart_of_accounts WHERE id = ?", [$parentId]);
if (!$parent) {
return null;
}
$base = (string) $parent['account_code'];
$width = 2;
$len = \strlen($base) + $width;
$last = $db->selectOne(
"SELECT account_code FROM chart_of_accounts
WHERE account_code LIKE ? AND CHAR_LENGTH(account_code) = ?
ORDER BY account_code DESC LIMIT 1",
[$base . str_repeat('_', $width), $len]
);
$next = $last ? ((int) substr((string) $last['account_code'], -$width)) + 1 : 1;
if ($next > (10 ** $width) - 1) {
return null;
}
return $base . str_pad((string) $next, $width, '0', STR_PAD_LEFT);
}
/** Every id beneath an account. Iterative, and guarded against a cyclic parent. */
public static function descendantIds(int $accountId): array
{
$db = App::getInstance()->db();
$all = [];
$queue = [$accountId];
$visited = [$accountId => true];
$guard = 0;
while ($queue && $guard++ < 10000) {
$current = array_shift($queue);
$rows = $db->select("SELECT id FROM chart_of_accounts WHERE parent_id = ?", [$current]);
foreach ($rows as $r) {
$id = (int) $r['id'];
if (isset($visited[$id])) {
continue; // a pre-existing cycle must not hang the request
}
$visited[$id] = true;
$all[] = $id;
$queue[] = $id;
}
}
return $all;
}
}
...@@ -67,7 +67,13 @@ ...@@ -67,7 +67,13 @@
<span style="flex:1;margin:0 15px;font-weight:<?= $weight ?>;"><?= e($acc['name_ar']) ?></span> <span style="flex:1;margin:0 15px;font-weight:<?= $weight ?>;"><?= e($acc['name_ar']) ?></span>
<span style="width:80px;font-size:12px;color:#6B7280;"><?= $typeLabel ?></span> <span style="width:80px;font-size:12px;color:#6B7280;"><?= $typeLabel ?></span>
<span style="width:100px;direction:ltr;text-align:left;font-size:13px;"><?= !$isHeader ? money($acc['current_balance'] ?? '0.00') : '' ?></span> <span style="width:100px;direction:ltr;text-align:left;font-size:13px;"><?= !$isHeader ? money($acc['current_balance'] ?? '0.00') : '' ?></span>
<?php if (can('accounting.coa.manage')): ?><a href="/accounting/chart-of-accounts/<?= (int)$acc['id'] ?>/edit" style="font-size:12px;color:#0D7377;">تعديل</a><?php endif; ?> <?php if (can('accounting.coa.manage')): ?>
<a href="/accounting/chart-of-accounts/<?= (int)$acc['id'] ?>/edit" style="font-size:12px;color:#0D7377;">تعديل</a>
<button type="button" class="coa-manage" data-id="<?= (int)$acc['id'] ?>"
data-code="<?= e($acc['account_code']) ?>" data-name="<?= e($acc['name_ar']) ?>"
data-header="<?= (int)$acc['is_header'] ?>"
style="background:none;border:none;cursor:pointer;color:#6B7280;font-size:12px;margin-inline-start:10px;">إدارة</button>
<?php endif; ?>
</div> </div>
<?php <?php
if (!empty($acc['children'])) { if (!empty($acc['children'])) {
...@@ -108,4 +114,126 @@ ...@@ -108,4 +114,126 @@
filter(); filter();
})(); })();
</script> </script>
<?php if (can('accounting.coa.manage')): ?>
<div id="coa-modal" style="display:none;position:fixed;inset:0;background:rgba(15,23,42,.55);z-index:900;align-items:center;justify-content:center;padding:16px;">
<div class="card" style="max-width:560px;width:100%;">
<div style="padding:14px 18px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<div>
<h3 style="margin:0;font-size:15px;" id="cm-name"></h3>
<div style="font-size:11px;color:#9CA3AF;direction:ltr;text-align:right;" id="cm-code"></div>
</div>
<button type="button" id="cm-close" class="btn btn-sm btn-ghost">إغلاق</button>
</div>
<div style="padding:18px;">
<div id="cm-usage" style="font-size:12.5px;color:#4B5563;margin-bottom:16px;">جارٍ الفحص…</div>
<div style="border-top:1px solid #F3F4F6;padding-top:14px;margin-bottom:14px;">
<label class="form-label">نقل الحساب تحت حساب رئيسي آخر</label>
<form method="POST" id="cm-move-form" style="display:flex;gap:8px;">
<?= csrf_field() ?>
<select name="parent_id" id="cm-parent" class="form-select" style="flex:1;">
<option value="">— جذر الشجرة —</option>
<?php
$__headers = \App\Core\App::getInstance()->db()->select(
"SELECT id, account_code, name_ar, account_type FROM chart_of_accounts
WHERE is_header = 1 AND is_archived = 0 AND is_active = 1 ORDER BY account_code"
);
foreach ($__headers as $h): ?>
<option value="<?= (int)$h['id'] ?>" data-type="<?= e($h['account_type']) ?>">
<?= e($h['account_code'] . ' — ' . $h['name_ar']) ?>
</option>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-secondary">نقل</button>
</form>
<div class="form-help">النقل بيحرّك الحسابات المتفرعة معاه، وبيرفض لو النوع مختلف أو لو هيعمل دائرة.</div>
</div>
<div style="border-top:1px solid #F3F4F6;padding-top:14px;margin-bottom:14px;display:flex;gap:8px;align-items:center;">
<form method="POST" id="cm-header-form" style="display:inline;">
<?= csrf_field() ?>
<input type="hidden" name="is_header" id="cm-header-val">
<button type="submit" class="btn btn-outline btn-sm" id="cm-header-btn"></button>
</form>
<span style="font-size:11.5px;color:#6B7280;" id="cm-header-help"></span>
</div>
<div style="border-top:1px solid #F3F4F6;padding-top:14px;">
<form method="POST" id="cm-delete-form" onsubmit="return confirm('متأكد؟');">
<?= csrf_field() ?>
<button type="submit" class="btn btn-outline btn-sm" id="cm-delete-btn" style="color:#DC2626;">حذف الحساب</button>
</form>
<div class="form-help" id="cm-delete-help"></div>
</div>
</div>
</div>
</div>
<script>
(function () {
var modal = document.getElementById('coa-modal');
if (!modal) return;
document.querySelectorAll('.coa-manage').forEach(function (b) {
b.addEventListener('click', function () {
var id = b.dataset.id;
document.getElementById('cm-name').textContent = b.dataset.name;
document.getElementById('cm-code').textContent = b.dataset.code;
document.getElementById('cm-move-form').action = '/accounting/chart-of-accounts/' + id + '/reparent';
document.getElementById('cm-header-form').action = '/accounting/chart-of-accounts/' + id + '/header';
document.getElementById('cm-delete-form').action = '/accounting/chart-of-accounts/' + id + '/delete';
var isHeader = b.dataset.header === '1';
document.getElementById('cm-header-val').value = isHeader ? '0' : '1';
document.getElementById('cm-header-btn').textContent = isHeader ? 'حوّله لحساب ترحيل' : 'حوّله لحساب رئيسي';
document.getElementById('cm-header-help').textContent = isHeader
? 'الحساب الرئيسي لا يقبل القيود.'
: 'الحساب الرئيسي تجميعي فقط — لن يقبل قيودًا بعد التحويل.';
var box = document.getElementById('cm-usage');
box.textContent = 'جارٍ الفحص…';
modal.style.display = 'flex';
fetch('/accounting/chart-of-accounts/' + id + '/usage')
.then(function (r) { return r.json(); })
.then(function (d) {
if (!d.success) { box.textContent = d.error || 'تعذر الفحص'; return; }
var h = '';
if (!d.blockers.length) {
h = '<div style="background:#ECFDF5;border-radius:6px;padding:10px;color:#065F46;">حساب فارغ — يمكن حذفه أو نقله بحرية.</div>';
} else {
h = '<div style="background:#FFFBEB;border-radius:6px;padding:10px;color:#92400E;"><strong>مرتبط بـ:</strong><ul style="margin:6px 0 0;padding-inline-start:18px;">';
d.blockers.forEach(function (x) { h += '<li>' + x + '</li>'; });
h += '</ul></div>';
}
box.innerHTML = h;
var delBtn = document.getElementById('cm-delete-btn');
var delHelp = document.getElementById('cm-delete-help');
if (!d.can_delete) {
delBtn.disabled = true;
delBtn.style.opacity = '.5';
delHelp.textContent = 'الحذف غير متاح — عالج الارتباطات أعلاه أولًا.';
} else if (d.will_archive) {
delBtn.disabled = false;
delBtn.style.opacity = '1';
delBtn.textContent = 'أرشفة الحساب';
delHelp.textContent = 'عليه حركات مرحّلة، فسيُؤرشف بدل حذفه حتى تظل القيود القديمة مقروءة.';
} else {
delBtn.disabled = false;
delBtn.style.opacity = '1';
delBtn.textContent = 'حذف الحساب';
delHelp.textContent = 'الحساب فارغ — سيُحذف نهائيًا.';
}
});
});
});
document.getElementById('cm-close').addEventListener('click', function () { modal.style.display = 'none'; });
modal.addEventListener('click', function (e) { if (e.target === modal) modal.style.display = 'none'; });
})();
</script>
<?php endif; ?>
<?php $__template->endSection(); ?> <?php $__template->endSection(); ?>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment