Commit 913bbb73 authored by DevPilot's avatar DevPilot

feat(accounting): bank group in the menu, searchable account lookup, notes receivable closing

١. قايمة المالية كانت ٤٥ بند في ليستة واحدة. اتقسمت لمجموعات:
   المحاسبة والدفتر العام / حسابات البنوك / الإيرادات والتحصيل /
   التقارير المحاسبية / القوائم المالية.
   «حسابات البنوك» جمّعت الحسابات البنكية والشيكات والأوراق التجارية
   وإقفال أوراق الدفع والقبض والودائع والمطابقة والقروض والاعتمادات.

٢. البحث جوه القوايم المنسدلة: مكوّن مشترك بيتفعّل لوحده على أي قائمة
   فيها أكتر من ١٢ خيار (زي دليل الحسابات في قيد اليومية)، وبيدوّر
   بالاسم أو بالرقم. البحث بيتجاهل الهمزات والتشكيل عشان يلاقي بالعربي.

٣. «إقفال أوراق القبض» — المقابل الناقص لإقفال أوراق الدفع: بيأكّد تحصيل
   الشيكات الواردة من كشف حساب البنك.

٤. سجل حركة الشيك بقى بيعمل القيد المحاسبي فعلًا. كان بيسجّل الحركة بس
   من غير ترحيل، وده كان بيخلي الشاشة الجديدة تخالف قاعدة إن أي حركة
   مالية لازم تنعكس على الدفتر. كل حركة دلوقتي متربوطة بقيدها.
parent 31671ce5
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\App;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Modules\Accounting\Services\NotesReceivableClosingService;
/**
* إقفال أوراق القبض الشهري — تأكيد تحصيل الشيكات الواردة من كشف حساب البنك.
*/
class NotesReceivableController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('accounting.notes_receivable.view');
$db = App::getInstance()->db();
$bankAccounts = $db->select(
"SELECT id, account_name_ar FROM bank_accounts WHERE is_active = 1 ORDER BY account_name_ar"
);
return $this->view('Accounting.Views.notes_receivable.index', [
'open' => NotesReceivableClosingService::open(),
'bankAccounts' => $bankAccounts,
]);
}
public function close(Request $request): Response
{
$this->authorize('accounting.notes_receivable.manage');
$result = NotesReceivableClosingService::close(
(array) $request->post('instrument_ids', []),
(int) $request->post('bank_account_id', 0),
$request->postDate('entry_date')
);
if (!$result['success']) {
return $this->redirect('/accounting/notes-receivable')->withError($result['error']);
}
$response = $this->redirect('/accounting/notes-receivable')
->withSuccess('اتحصّل واتقفل ' . $result['closed'] . ' شيك بإجمالي ' . money($result['total']));
if (!empty($result['warnings'])) {
$response = $response->withWarning(implode(' — ', array_slice($result['warnings'], 0, 3)));
}
return $response;
}
}
......@@ -219,6 +219,8 @@ return [
['POST', '/accounting/branch-fees/cash-ban', 'Accounting\Controllers\BranchFeeController@toggleCashBan', ['auth', 'csrf'], 'accounting.branch_fees.manage'],
// ── Notes payable monthly closing ─────────────────────────
['GET', '/accounting/notes-receivable', 'Accounting\Controllers\NotesReceivableController@index', ['auth'], 'accounting.notes_receivable.view'],
['POST', '/accounting/notes-receivable', 'Accounting\Controllers\NotesReceivableController@close', ['auth', 'csrf'], 'accounting.notes_receivable.manage'],
['GET', '/accounting/notes-payable', 'Accounting\Controllers\NotesPayableController@index', ['auth'], 'accounting.notes_payable.view'],
['POST', '/accounting/notes-payable/close', 'Accounting\Controllers\NotesPayableController@close', ['auth', 'csrf'], 'accounting.notes_payable.manage'],
......
......@@ -4,7 +4,9 @@ declare(strict_types=1);
namespace App\Modules\Accounting\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
use App\Modules\Accounting\Services\Revenue\InstrumentPostingService;
/**
* دورة حياة الشيك — الحالات المسموحة لكل اتجاه، وتسجيل كل إجراء كحركة دائمة.
......@@ -204,7 +206,7 @@ final class InstrumentLifecycleService
$db->update('negotiable_instruments', $update, 'id = ?', [$instrumentId]);
$db->insert('instrument_movements', [
$movementId = $db->insert('instrument_movements', [
'instrument_id' => $instrumentId,
'action' => $opts['action'] ?? (self::STATUS_ACTION[$toStatus] ?? 'note'),
'from_status' => $from,
......@@ -225,7 +227,46 @@ final class InstrumentLifecycleService
return ['success' => false, 'error' => 'فشل تنفيذ الإجراء: ' . $e->getMessage()];
}
return ['success' => true];
EventBus::dispatch('instrument.status_changed', [
'instrument_id' => $instrumentId,
'from_status' => $from,
'to_status' => $toStatus,
'instrument' => $ins,
]);
// ── القيد المحاسبي ──────────────────────────────────────────
// بعد الـ commit بقصد: نقل الشيك حركة حقيقية لازم تتسجّل حتى لو
// الحسابات لسه مش مربوطة. لو القيد فشل بنرجّع تحذير مش خطأ، عشان
// ما نرجّعش حالة الشيك ويبقى الورق في إيد حد والنظام بيقول حاجة تانية.
$warning = null;
try {
$posting = InstrumentPostingService::onStatusChange(
array_merge($ins, $update),
$toStatus,
$opts + ['date' => $actionDate]
);
if (!empty($posting['posted']) && !empty($posting['journal_entry_id'])) {
$db->update(
'negotiable_instruments',
['journal_entry_id' => (int) $posting['journal_entry_id']],
'id = ?',
[$instrumentId]
);
$db->update(
'instrument_movements',
['journal_entry_id' => (int) $posting['journal_entry_id']],
'id = ?',
[$movementId]
);
}
$warning = $posting['error'] ?? null;
} catch (\Throwable $e) {
Logger::error('Instrument posting failed: ' . $e->getMessage());
$warning = 'الحركة اتسجّلت بس القيد المحاسبي ما اتعملش: ' . $e->getMessage();
}
return ['success' => true, 'warning' => $warning];
}
/** ملاحظة من غير تغيير حالة — بتتسجّل كحركة برضه. */
......
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Services;
use App\Core\App;
/**
* إقفال أوراق القبض الشهري — المقابل لإقفال أوراق الدفع.
*
* الشيك الوارد بيقعد في «شيكات تحت التحصيل» من ساعة ما يتودع في البنك لحد
* ما كشف حساب البنك يأكّد إنه اتحصّل فعلًا. الشاشة دي هي المكان اللي
* المحاسب بيأكّد فيه اللي اتحصّل، وبتاخد الشيك لآخر دورة حياته:
* تحصيل (وبيتولد القيد: من ح/ البنك إلى ح/ شيكات تحت التحصيل) ثم إقفال.
*
* ملحوظة مهمة: الخدمة دي ما بتكتبش قيد بإيدها. بتمشّي الشيك على
* InstrumentLifecycleService اللي بيسجّل الحركة في السجل الدائم وبيستدعي
* محرّك الترحيل. كده مفيش قيد بيتكرر ومفيش حركة بتضيع من الـ Audit Trail.
*/
final class NotesReceivableClosingService
{
/** الحالات اللي الشيك الوارد بيكون فيها لسه مستنّي تأكيد البنك. */
private const PENDING = ['deposited', 'under_collection'];
/** الشيكات الواردة المودعة اللي لسه ما اتأكدش تحصيلها. */
public static function open(): array
{
try {
$in = implode(',', array_fill(0, count(self::PENDING), '?'));
return App::getInstance()->db()->select(
"SELECT ni.id, ni.instrument_number, ni.amount, ni.due_date, ni.status,
ni.drawer_name, ni.drawer_bank,
b.account_name_ar AS bank_name
FROM negotiable_instruments ni
LEFT JOIN bank_accounts b ON b.id = ni.bank_account_id
WHERE ni.direction = 'receivable'
AND ni.status IN ({$in})
AND ni.closed_at IS NULL
AND ni.is_archived = 0
ORDER BY ni.due_date ASC",
self::PENDING
);
} catch (\Throwable) {
return [];
}
}
/**
* تأكيد تحصيل الشيكات المختارة من كشف حساب البنك.
*
* @param int[] $instrumentIds
* @return array{success:bool, error:?string, closed:int, total:string, warnings:string[]}
*/
public static function close(array $instrumentIds, int $bankAccountId, ?string $entryDate): array
{
$ids = array_values(array_unique(array_filter(
array_map('intval', $instrumentIds),
static fn(int $i): bool => $i > 0
)));
if (!$ids) {
return ['success' => false, 'error' => 'ما اخترتش أي شيك', 'closed' => 0, 'total' => '0.00', 'warnings' => []];
}
if ($bankAccountId <= 0) {
return ['success' => false, 'error' => 'اختار الحساب البنكي', 'closed' => 0, 'total' => '0.00', 'warnings' => []];
}
$db = App::getInstance()->db();
$bank = $db->selectOne(
"SELECT id, gl_account_id FROM bank_accounts WHERE id = ? AND is_active = 1",
[$bankAccountId]
);
if (!$bank || empty($bank['gl_account_id'])) {
return ['success' => false, 'error' => 'الحساب البنكي مش مربوط بحساب في الدليل', 'closed' => 0, 'total' => '0.00', 'warnings' => []];
}
$date = $entryDate && preg_match('/^\d{4}-\d{2}-\d{2}$/', $entryDate) ? $entryDate : date('Y-m-d');
$closed = 0;
$total = '0.00';
$warnings = [];
foreach ($ids as $id) {
$ins = $db->selectOne(
"SELECT id, instrument_number, amount, status, direction
FROM negotiable_instruments
WHERE id = ? AND direction = 'receivable' AND closed_at IS NULL",
[$id]
);
if (!$ins || !in_array((string) $ins['status'], self::PENDING, true)) {
$warnings[] = 'الشيك رقم ' . ($ins['instrument_number'] ?? $id) . ' مش في حالة تسمح بالتحصيل';
continue;
}
// خطوة ١: التحصيل — دي اللي بيتولد معاها القيد
$collect = InstrumentLifecycleService::act($id, 'collected', [
'action_date' => $date,
'bank_account_id' => $bankAccountId,
'notes' => 'تأكيد التحصيل من كشف حساب البنك',
]);
if (empty($collect['success'])) {
$warnings[] = 'الشيك ' . $ins['instrument_number'] . ': ' . ($collect['error'] ?? 'فشل التحصيل');
continue;
}
if (!empty($collect['warning'])) {
$warnings[] = 'الشيك ' . $ins['instrument_number'] . ': ' . $collect['warning'];
}
// خطوة ٢: الإقفال — الشيك خلّص دورته
$close = InstrumentLifecycleService::act($id, 'closed', [
'action_date' => $date,
'notes' => 'إقفال أوراق قبض شهري',
]);
if (empty($close['success'])) {
$warnings[] = 'الشيك ' . $ins['instrument_number'] . ' اتحصّل بس ما اتقفلش: ' . ($close['error'] ?? '');
}
$closed++;
$total = bcadd($total, (string) $ins['amount'], 2);
}
if ($closed === 0) {
return [
'success' => false,
'error' => $warnings ? implode(' — ', array_slice($warnings, 0, 3)) : 'ما اتقفلش أي شيك',
'closed' => 0,
'total' => '0.00',
'warnings' => $warnings,
];
}
return ['success' => true, 'error' => null, 'closed' => $closed, 'total' => $total, 'warnings' => $warnings];
}
}
......@@ -131,9 +131,16 @@ function addLine() {
const tbody = document.getElementById('linesBody');
const firstRow = tbody.querySelector('tr');
const newRow = firstRow.cloneNode(true);
// الصف المنسوخ بيجي شايل خانة البحث بتاعة الحساب، فبنرجّعه select عادي
// الأول وبعدين نفعّل البحث من جديد على النسخة.
if (window.resetSearchableSelects) window.resetSearchableSelects(newRow);
newRow.querySelectorAll('input').forEach(i => { if(i.type === 'number') i.value = '0.00'; else i.value = ''; });
newRow.querySelectorAll('select').forEach(s => s.selectedIndex = 0);
tbody.appendChild(newRow);
if (window.enhanceSearchableSelects) window.enhanceSearchableSelects(newRow);
}
function removeLine(btn) {
......
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إقفال أوراق القبض<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/accounting/instruments/register?direction=receivable" class="btn btn-outline">سجل الشيكات الواردة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
use App\Modules\Accounting\Services\InstrumentLifecycleService as LC;
$total = '0.00';
foreach ($open as $o) { $total = bcadd($total, (string) $o['amount'], 2); }
?>
<div class="card" style="margin-bottom:18px;">
<div style="padding:14px 18px;">
<p style="margin:0;font-size:13px;color:#374151;line-height:1.8;">
الشيكات الواردة اللي اتودعت في البنك بتقعد في حساب <strong>«شيكات تحت التحصيل»</strong>
لحد ما كشف حساب البنك يأكّد إنها اتحصّلت. اختار من الليستة اللي ظهرت في الكشف واضغط تأكيد —
النظام هيعمل قيد التحصيل (من ح/ البنك إلى ح/ شيكات تحت التحصيل) ويقفل الشيك،
وكل ده هيتسجّل في سجل حركة كل شيك.
</p>
</div>
</div>
<div class="card">
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<strong style="font-size:14px;">شيكات مودعة بانتظار تأكيد التحصيل (<?= count($open) ?>)</strong>
<span style="font-size:13px;font-weight:700;direction:ltr;"><?= money($total) ?></span>
</div>
<?php if (!empty($open)): ?>
<form method="POST" action="/accounting/notes-receivable">
<?= csrf_field() ?>
<div style="padding:14px 16px;display:grid;grid-template-columns:repeat(3,1fr);gap:12px;border-bottom:1px solid #E5E7EB;">
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">الحساب البنكي <span style="color:#DC2626;">*</span></label>
<select name="bank_account_id" class="form-select" required>
<option value="">— اختار البنك —</option>
<?php foreach ($bankAccounts as $b): ?>
<option value="<?= (int) $b['id'] ?>"><?= e($b['account_name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">تاريخ القيد</label>
<input type="date" name="entry_date" class="form-input" value="<?= e(date('Y-m-d')) ?>">
</div>
<div class="form-group" style="margin:0;display:flex;align-items:end;">
<button type="submit" class="btn btn-primary" style="width:100%;">تأكيد التحصيل والإقفال</button>
</div>
</div>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th style="width:40px;"><input type="checkbox" onclick="document.querySelectorAll('.nr-chk').forEach(c => c.checked = this.checked)"></th>
<th>رقم الشيك</th>
<th>الساحب</th>
<th>البنك</th>
<th>الاستحقاق</th>
<th>الحالة</th>
<th>المبلغ</th>
</tr>
</thead>
<tbody>
<?php foreach ($open as $o): ?>
<?php $c = LC::STATUS_COLORS[$o['status']] ?? ['#F3F4F6', '#374151']; ?>
<tr>
<td><input type="checkbox" class="nr-chk" name="instrument_ids[]" value="<?= (int) $o['id'] ?>"></td>
<td><code style="font-size:12.5px;"><?= e($o['instrument_number']) ?></code></td>
<td><?= e($o['drawer_name'] ?: '—') ?></td>
<td style="font-size:12.5px;color:#6B7280;"><?= e($o['bank_name'] ?: ($o['drawer_bank'] ?: '—')) ?></td>
<td style="white-space:nowrap;"><?= e($o['due_date']) ?></td>
<td>
<span style="background:<?= $c[0] ?>;color:<?= $c[1] ?>;padding:2px 10px;border-radius:10px;font-size:12px;">
<?= e(LC::statusLabel($o['status'])) ?>
</span>
</td>
<td style="direction:ltr;text-align:left;font-weight:700;"><?= money($o['amount']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</form>
<?php else: ?>
<div style="padding:30px 20px;text-align:center;color:#6B7280;">
مفيش شيكات واردة مستنية تأكيد تحصيل. الشيكات بتوصل هنا بعد ما تتودع في البنك من سجل الشيكات.
</div>
<?php endif; ?>
</div>
<?php $__template->endSection(); ?>
......@@ -128,6 +128,8 @@ PermissionRegistry::register('accounting', [
// Notes payable monthly closing
'accounting.notes_payable.view' => ['ar' => 'عرض أوراق الدفع', 'en' => 'View Notes Payable'],
'accounting.notes_payable.manage' => ['ar' => 'إقفال أوراق الدفع الشهري', 'en' => 'Close Notes Payable'],
'accounting.notes_receivable.view' => ['ar' => 'عرض أوراق القبض', 'en' => 'View Notes Receivable'],
'accounting.notes_receivable.manage' => ['ar' => 'إقفال أوراق القبض الشهري', 'en' => 'Close Notes Receivable'],
// Vouchers
'accounting.voucher.view' => ['ar' => 'عرض السندات', 'en' => 'View Vouchers'],
......@@ -155,48 +157,100 @@ MenuRegistry::register('accounting', [
'parent' => null,
'order' => 330,
'children' => [
['label_ar' => 'لوحة التحكم', 'label_en' => 'Dashboard', 'route' => '/accounting', 'permission' => 'accounting.reports.view', 'order' => 1],
['label_ar' => 'دليل الحسابات', 'label_en' => 'Chart of Accounts', 'route' => '/accounting/chart-of-accounts', 'permission' => 'accounting.coa.view', 'order' => 2],
['label_ar' => 'توزيع الإيرادات', 'label_en' => 'Revenue Mapping', 'route' => '/accounting/revenue-mapping', 'permission' => 'accounting.revenue_mapping.view', 'order' => 2],
['label_ar' => 'مركز التوصيل', 'label_en' => 'Connection Centre', 'route' => '/accounting/revenue-mapping/connections', 'permission' => 'accounting.revenue_mapping.view', 'order' => 2],
['label_ar' => 'مسار الفلوس', 'label_en' => 'Posting Chains', 'route' => '/accounting/posting-chains', 'permission' => 'accounting.chains.view', 'order' => 2],
['label_ar' => 'فين الفلوس دلوقتي', 'label_en' => 'Money in Transit', 'route' => '/accounting/posting-chains/parked', 'permission' => 'accounting.chains.view', 'order' => 2],
['label_ar' => 'الاستحقاقات', 'label_en' => 'Accruals', 'route' => '/accounting/accruals', 'permission' => 'accounting.accruals.view', 'order' => 2],
['label_ar' => 'سد الفجوات', 'label_en' => 'Accounting Gaps', 'route' => '/accounting/gaps', 'permission' => 'accounting.gaps.view', 'order' => 2],
['label_ar' => 'رسوم الفروع', 'label_en' => 'Branch Fees', 'route' => '/accounting/branch-fees', 'permission' => 'accounting.branch_fees.view', 'order' => 2],
['label_ar' => 'إقفال أوراق الدفع', 'label_en' => 'Notes Payable Closing', 'route' => '/accounting/notes-payable', 'permission' => 'accounting.notes_payable.view', 'order' => 23],
['label_ar' => 'المطالبات والتحصيل', 'label_en' => 'Billing & Collection', 'route' => '/accounting/billing', 'permission' => 'accounting.billing.view', 'order' => 2],
['label_ar' => 'سندات الصرف والقبض', 'label_en' => 'Vouchers', 'route' => '/accounting/vouchers', 'permission' => 'accounting.voucher.view', 'order' => 3],
['label_ar' => 'الإيراد المؤجل', 'label_en' => 'Deferred Revenue', 'route' => '/accounting/revenue-mapping/recognition', 'permission' => 'accounting.revenue_mapping.view', 'order' => 2],
['label_ar' => 'قيود اليومية', 'label_en' => 'Journal Entries', 'route' => '/accounting/journal-entries', 'permission' => 'accounting.journal.view', 'order' => 3],
['label_ar' => 'أنواع اليومية', 'label_en' => 'Journal Types', 'route' => '/accounting/journal-types', 'permission' => 'accounting.journal_type.view', 'order' => 4],
['label_ar' => 'السنوات المالية', 'label_en' => 'Fiscal Years', 'route' => '/accounting/fiscal-years', 'permission' => 'accounting.fiscal_year.view', 'order' => 5],
['label_ar' => 'مراكز التكلفة', 'label_en' => 'Cost Centers', 'route' => '/accounting/cost-centers', 'permission' => 'accounting.cost_center.view', 'order' => 5],
['label_ar' => 'الموازنات التقديرية', 'label_en' => 'Budgets', 'route' => '/accounting/budgets', 'permission' => 'accounting.budget.view', 'order' => 6],
['label_ar' => 'الحسابات البنكية', 'label_en' => 'Bank Accounts', 'route' => '/accounting/bank-accounts', 'permission' => 'accounting.bank_account.view', 'order' => 7],
['label_ar' => 'الشيكات المرتدة', 'label_en' => 'Bounced Cheques', 'route' => '/accounting/instruments/bounced', 'permission' => 'accounting.instruments.view', 'order' => 33],
['label_ar' => 'صادر ووارد شيكات بنكية', 'label_en' => 'Cheque Register', 'route' => '/accounting/instruments/register', 'permission' => 'accounting.instruments.view', 'order' => 8],
['label_ar' => 'الأوراق التجارية', 'label_en' => 'Instruments', 'route' => '/accounting/instruments', 'permission' => 'accounting.instruments.view', 'order' => 8],
['label_ar' => 'الأبعاد المحاسبية', 'label_en' => 'Dimensions', 'route' => '/accounting/dimensions', 'permission' => 'accounting.dimensions.view', 'order' => 9],
['label_ar' => 'المطابقة البنكية', 'label_en' => 'Bank Reconciliation', 'route' => '/accounting/bank-reconciliation', 'permission' => 'accounting.bank_recon.view', 'order' => 10],
['label_ar' => 'الحركات اليومية', 'label_en' => 'Daily Transactions', 'route' => '/accounting/daily-transactions', 'permission' => 'accounting.daily_tx.view', 'order' => 11],
['label_ar' => 'إعادة تبويب الحسابات', 'label_en' => 'Reclassification', 'route' => '/accounting/reclassification', 'permission' => 'accounting.journal.view', 'order' => 26],
['label_ar' => 'القيود الافتتاحية', 'label_en' => 'Opening Entries', 'route' => '/accounting/opening-entries', 'permission' => 'accounting.opening_entry.view', 'order' => 12],
['label_ar' => 'إقفال الفترات', 'label_en' => 'Period Closing', 'route' => '/accounting/period-closing', 'permission' => 'accounting.period.view', 'order' => 12],
['label_ar' => 'ميزان المراجعة', 'label_en' => 'Trial Balance', 'route' => '/accounting/reports/trial-balance', 'permission' => 'accounting.reports.trial_balance', 'order' => 9],
['label_ar' => 'دفتر الأستاذ', 'label_en' => 'General Ledger', 'route' => '/accounting/reports/general-ledger', 'permission' => 'accounting.reports.general_ledger', 'order' => 10],
['label_ar' => 'المدينون (AR)', 'label_en' => 'Accounts Receivable', 'route' => '/accounting/reports/accounts-receivable', 'permission' => 'accounting.reports.ar', 'order' => 14],
['label_ar' => 'الدائنون (AP)', 'label_en' => 'Accounts Payable', 'route' => '/accounting/reports/accounts-payable', 'permission' => 'accounting.reports.ap', 'order' => 15],
['label_ar' => 'كشف حساب عضو', 'label_en' => 'Member Statement', 'route' => '/accounting/reports/member-statement', 'permission' => 'accounting.reports.member_statement','order' => 16],
['label_ar' => 'الخزينة والمدفوعات', 'label_en' => 'Treasury & Payments', 'route' => '/accounting/reports/treasury', 'permission' => 'accounting.reports.treasury', 'order' => 17],
['label_ar' => 'تحليل الإيرادات', 'label_en' => 'Revenue Analysis', 'route' => '/accounting/reports/revenue-analysis', 'permission' => 'accounting.reports.revenue_analysis','order' => 18],
['label_ar' => 'كشف حساب عميل', 'label_en' => 'Customer Statement', 'route' => '/accounting/statements/customer', 'permission' => 'accounting.statements.view', 'order' => 19],
['label_ar' => 'كشف حساب مورد', 'label_en' => 'Supplier Statement', 'route' => '/accounting/statements/supplier', 'permission' => 'accounting.statements.view', 'order' => 20],
['label_ar' => 'حركة النقدية اليومية', 'label_en' => 'Daily Cash', 'route' => '/accounting/statements/daily-cash', 'permission' => 'accounting.cash.view', 'order' => 21],
['label_ar' => 'القروض البنكية', 'label_en' => 'Bank Loans', 'route' => '/accounting/loans', 'permission' => 'accounting.loans.view', 'order' => 22],
['label_ar' => 'التسويات', 'label_en' => 'Settlements', 'route' => '/accounting/settlements', 'permission' => 'accounting.settlements.view', 'order' => 23],
['label_ar' => 'الاعتمادات المستندية', 'label_en' => 'Documentary Credits', 'route' => '/accounting/documentary-credits', 'permission' => 'accounting.lc.view', 'order' => 24],
['label_ar' => 'خطابات الضمان', 'label_en' => 'Guarantees', 'route' => '/accounting/guarantees', 'permission' => 'accounting.guarantee.view', 'order' => 25],
['label_ar' => 'لوحة التحكم', 'label_en' => 'Dashboard', 'route' => '/accounting', 'permission' => 'accounting.reports.view', 'order' => 1],
['label_ar' => 'دليل الحسابات', 'label_en' => 'Chart of Accounts', 'route' => '/accounting/chart-of-accounts', 'permission' => 'accounting.coa.view', 'order' => 2],
['label_ar' => 'قيود اليومية', 'label_en' => 'Journal Entries', 'route' => '/accounting/journal-entries', 'permission' => 'accounting.journal.view', 'order' => 3],
['label_ar' => 'أنواع اليومية', 'label_en' => 'Journal Types', 'route' => '/accounting/journal-types', 'permission' => 'accounting.journal_type.view', 'order' => 4],
['label_ar' => 'سندات الصرف والقبض', 'label_en' => 'Vouchers', 'route' => '/accounting/vouchers', 'permission' => 'accounting.voucher.view', 'order' => 5],
['label_ar' => 'الحركات اليومية', 'label_en' => 'Daily Transactions', 'route' => '/accounting/daily-transactions', 'permission' => 'accounting.daily_tx.view', 'order' => 6],
['label_ar' => 'مراكز التكلفة', 'label_en' => 'Cost Centers', 'route' => '/accounting/cost-centers', 'permission' => 'accounting.cost_center.view', 'order' => 7],
['label_ar' => 'الأبعاد المحاسبية', 'label_en' => 'Dimensions', 'route' => '/accounting/dimensions', 'permission' => 'accounting.dimensions.view', 'order' => 8],
['label_ar' => 'الموازنات التقديرية', 'label_en' => 'Budgets', 'route' => '/accounting/budgets', 'permission' => 'accounting.budget.view', 'order' => 9],
['label_ar' => 'السنوات المالية', 'label_en' => 'Fiscal Years', 'route' => '/accounting/fiscal-years', 'permission' => 'accounting.fiscal_year.view', 'order' => 10],
['label_ar' => 'القيود الافتتاحية', 'label_en' => 'Opening Entries', 'route' => '/accounting/opening-entries', 'permission' => 'accounting.opening_entry.view', 'order' => 11],
['label_ar' => 'إعادة تبويب الحسابات', 'label_en' => 'Reclassification', 'route' => '/accounting/reclassification', 'permission' => 'accounting.journal.view', 'order' => 12],
['label_ar' => 'إقفال الفترات', 'label_en' => 'Period Closing', 'route' => '/accounting/period-closing', 'permission' => 'accounting.period.view', 'order' => 13],
],
]);
// ────────────────────────────────────────────────────────────
// حسابات البنوك — كل حاجة ليها علاقة بالبنك والشيكات في مكان واحد
// ────────────────────────────────────────────────────────────
// قبل كده كانت متفرّقة وسط ٤٥ بند في قائمة المحاسبة، فالمحاسب بيدوّر
// على «المطابقة البنكية» بين «الموازنات» و«الحركات اليومية».
MenuRegistry::register('bank_accounts', [
'label_ar' => 'حسابات البنوك',
'label_en' => 'Bank Accounts',
'icon' => 'landmark',
'route' => '/accounting/bank-accounts',
'permission' => 'accounting.bank_account.view',
'parent' => null,
'order' => 332,
'children' => [
['label_ar' => 'الحسابات البنكية', 'label_en' => 'Bank Accounts', 'route' => '/accounting/bank-accounts', 'permission' => 'accounting.bank_account.view', 'order' => 1],
['label_ar' => 'صادر ووارد شيكات بنكية', 'label_en' => 'Cheque Register', 'route' => '/accounting/instruments/register', 'permission' => 'accounting.instruments.view', 'order' => 2],
['label_ar' => 'الشيكات المرتدة', 'label_en' => 'Bounced Cheques', 'route' => '/accounting/instruments/bounced', 'permission' => 'accounting.instruments.view', 'order' => 3],
['label_ar' => 'الأوراق التجارية', 'label_en' => 'Instruments', 'route' => '/accounting/instruments', 'permission' => 'accounting.instruments.view', 'order' => 4],
['label_ar' => 'إقفال أوراق القبض', 'label_en' => 'Notes Receivable', 'route' => '/accounting/notes-receivable', 'permission' => 'accounting.notes_receivable.view', 'order' => 5],
['label_ar' => 'إقفال أوراق الدفع', 'label_en' => 'Notes Payable', 'route' => '/accounting/notes-payable', 'permission' => 'accounting.notes_payable.view', 'order' => 6],
['label_ar' => 'الودائع البنكية', 'label_en' => 'Bank Deposits', 'route' => '/cashier/deposits', 'permission' => 'cashier.manage_deposits', 'order' => 7],
['label_ar' => 'المطابقة البنكية', 'label_en' => 'Bank Reconciliation', 'route' => '/accounting/bank-reconciliation', 'permission' => 'accounting.bank_recon.view', 'order' => 8],
['label_ar' => 'القروض البنكية', 'label_en' => 'Bank Loans', 'route' => '/accounting/loans', 'permission' => 'accounting.loans.view', 'order' => 9],
['label_ar' => 'الاعتمادات المستندية', 'label_en' => 'Documentary Credits', 'route' => '/accounting/documentary-credits', 'permission' => 'accounting.lc.view', 'order' => 10],
['label_ar' => 'خطابات الضمان', 'label_en' => 'Guarantees', 'route' => '/accounting/guarantees', 'permission' => 'accounting.guarantee.view', 'order' => 11],
],
]);
// ────────────────────────────────────────────────────────────
// الإيرادات والتحصيل — من إيه اللي اتباع لحد ما يوصل الدفتر
// ────────────────────────────────────────────────────────────
MenuRegistry::register('revenue_flow', [
'label_ar' => 'الإيرادات والتحصيل',
'label_en' => 'Revenue & Collection',
'icon' => 'trending-up',
'route' => '/accounting/revenue-mapping',
'permission' => 'accounting.revenue_mapping.view',
'parent' => null,
'order' => 333,
'children' => [
['label_ar' => 'توزيع الإيرادات', 'label_en' => 'Revenue Mapping', 'route' => '/accounting/revenue-mapping', 'permission' => 'accounting.revenue_mapping.view', 'order' => 1],
['label_ar' => 'مركز التوصيل', 'label_en' => 'Connection Centre', 'route' => '/accounting/revenue-mapping/connections', 'permission' => 'accounting.revenue_mapping.view', 'order' => 2],
['label_ar' => 'مسار الفلوس', 'label_en' => 'Posting Chains', 'route' => '/accounting/posting-chains', 'permission' => 'accounting.chains.view', 'order' => 3],
['label_ar' => 'فين الفلوس دلوقتي', 'label_en' => 'Money in Transit', 'route' => '/accounting/posting-chains/parked', 'permission' => 'accounting.chains.view', 'order' => 4],
['label_ar' => 'الإيراد المؤجل', 'label_en' => 'Deferred Revenue', 'route' => '/accounting/revenue-mapping/recognition', 'permission' => 'accounting.revenue_mapping.view', 'order' => 5],
['label_ar' => 'الاستحقاقات', 'label_en' => 'Accruals', 'route' => '/accounting/accruals', 'permission' => 'accounting.accruals.view', 'order' => 6],
['label_ar' => 'المطالبات والتحصيل', 'label_en' => 'Billing', 'route' => '/accounting/billing', 'permission' => 'accounting.billing.view', 'order' => 7],
['label_ar' => 'رسوم الفروع', 'label_en' => 'Branch Fees', 'route' => '/accounting/branch-fees', 'permission' => 'accounting.branch_fees.view', 'order' => 8],
['label_ar' => 'سد الفجوات', 'label_en' => 'Accounting Gaps', 'route' => '/accounting/gaps', 'permission' => 'accounting.gaps.view', 'order' => 9],
['label_ar' => 'التسويات', 'label_en' => 'Settlements', 'route' => '/accounting/settlements', 'permission' => 'accounting.settlements.view', 'order' => 10],
],
]);
// ────────────────────────────────────────────────────────────
// التقارير المحاسبية — الكشوف وأرصدة الحسابات
// ────────────────────────────────────────────────────────────
MenuRegistry::register('accounting_reports', [
'label_ar' => 'التقارير المحاسبية',
'label_en' => 'Accounting Reports',
'icon' => 'bar-chart',
'route' => '/accounting/reports/trial-balance',
'permission' => 'accounting.reports.view',
'parent' => null,
'order' => 334,
'children' => [
['label_ar' => 'ميزان المراجعة', 'label_en' => 'Trial Balance', 'route' => '/accounting/reports/trial-balance', 'permission' => 'accounting.reports.trial_balance', 'order' => 1],
['label_ar' => 'دفتر الأستاذ', 'label_en' => 'General Ledger', 'route' => '/accounting/reports/general-ledger', 'permission' => 'accounting.reports.general_ledger', 'order' => 2],
['label_ar' => 'المدينون (AR)', 'label_en' => 'Accounts Receivable', 'route' => '/accounting/reports/accounts-receivable', 'permission' => 'accounting.reports.ar', 'order' => 3],
['label_ar' => 'الدائنون (AP)', 'label_en' => 'Accounts Payable', 'route' => '/accounting/reports/accounts-payable', 'permission' => 'accounting.reports.ap', 'order' => 4],
['label_ar' => 'كشف حساب عضو', 'label_en' => 'Member Statement', 'route' => '/accounting/reports/member-statement', 'permission' => 'accounting.reports.member_statement', 'order' => 5],
['label_ar' => 'كشف حساب عميل', 'label_en' => 'Customer Statement', 'route' => '/accounting/statements/customer', 'permission' => 'accounting.statements.view', 'order' => 6],
['label_ar' => 'كشف حساب مورد', 'label_en' => 'Supplier Statement', 'route' => '/accounting/statements/supplier', 'permission' => 'accounting.statements.view', 'order' => 7],
['label_ar' => 'الخزينة والمدفوعات', 'label_en' => 'Treasury & Payments', 'route' => '/accounting/reports/treasury', 'permission' => 'accounting.reports.treasury', 'order' => 8],
['label_ar' => 'حركة النقدية اليومية', 'label_en' => 'Daily Cash', 'route' => '/accounting/statements/daily-cash', 'permission' => 'accounting.cash.view', 'order' => 9],
['label_ar' => 'تحليل الإيرادات', 'label_en' => 'Revenue Analysis', 'route' => '/accounting/reports/revenue-analysis', 'permission' => 'accounting.reports.revenue_analysis','order' => 10],
],
]);
......
<?php
/**
* بحث جوه أي قائمة منسدلة طويلة.
*
* المشكلة: قوايم زي دليل الحسابات فيها مئات الخيارات، والمستخدم مضطر يدوّر
* بعينه. المكوّن ده بيحوّل أي <select> طويل لقائمة بيتكتب فيها فيتفلتر
* المحتوى بالاسم أو بالرقم.
*
* بيشتغل لوحده على أي select فيه خيارات أكتر من الحد، من غير ما الشاشة
* تعمل أي حاجة. ولو عايز تفعّله على قائمة قصيرة حط data-searchable عليها،
* ولو عايز تمنعه حط data-no-search.
*/
?>
<style>
.ss-wrap { position: relative; }
.ss-wrap .ss-native { position: absolute; opacity: 0; pointer-events: none; height: 0; width: 0; }
.ss-input { width: 100%; cursor: text; }
.ss-input.ss-empty { color: #9CA3AF; }
.ss-menu {
position: absolute; z-index: 1200; inset-inline-start: 0; inset-inline-end: 0; top: 100%;
background: #fff; border: 1px solid #D1D5DB; border-radius: 6px; margin-top: 2px;
max-height: 260px; overflow-y: auto; box-shadow: 0 8px 20px rgba(0,0,0,.12); display: none;
}
.ss-menu.open { display: block; }
.ss-opt { padding: 7px 11px; font-size: 13px; cursor: pointer; white-space: nowrap;
overflow: hidden; text-overflow: ellipsis; }
.ss-opt:hover, .ss-opt.ss-active { background: #F0FDFA; }
.ss-opt.ss-chosen { font-weight: 700; }
.ss-empty-msg { padding: 12px; text-align: center; color: #9CA3AF; font-size: 12.5px; }
</style>
<script>
(function () {
'use strict';
var MIN_OPTIONS = 12; // أقل من كده القائمة قصيرة وما تحتاجش بحث
// بنشيل التشكيل والهمزات عشان البحث بالعربي ما يتعلقش على شكل الحرف
function norm(s) {
return (s || '').toString().toLowerCase()
.replace(/[ً-ْـ]/g, '')
.replace(/[أإآ]/g, 'ا')
.replace(/ى/g, 'ي')
.replace(/ة/g, 'ه')
.trim();
}
function labelOf(select) {
var o = select.options[select.selectedIndex];
return o && o.value !== '' ? o.textContent.trim() : '';
}
function enhance(select) {
if (select.dataset.ssDone === '1') return;
if (select.multiple || select.disabled) return;
if (select.hasAttribute('data-no-search')) return;
if (select.options.length < MIN_OPTIONS && !select.hasAttribute('data-searchable')) return;
select.dataset.ssDone = '1';
var wrap = document.createElement('div');
wrap.className = 'ss-wrap';
select.parentNode.insertBefore(wrap, select);
wrap.appendChild(select);
select.classList.add('ss-native');
var input = document.createElement('input');
input.type = 'text';
input.className = 'form-input ss-input';
input.autocomplete = 'off';
input.placeholder = select.dataset.searchPlaceholder || 'اكتب للبحث…';
// الـ required بتفضل على الـ select الأصلي عشان التحقق يشتغل زي ما هو
wrap.appendChild(input);
var menu = document.createElement('div');
menu.className = 'ss-menu';
wrap.appendChild(menu);
var items = [];
for (var i = 0; i < select.options.length; i++) {
var o = select.options[i];
items.push({ value: o.value, text: o.textContent.trim(), key: norm(o.textContent) });
}
var active = -1;
function syncInput() {
var l = labelOf(select);
input.value = l;
input.classList.toggle('ss-empty', l === '');
}
function render(filter) {
var q = norm(filter);
menu.innerHTML = '';
active = -1;
var shown = 0;
items.forEach(function (it) {
if (q && it.key.indexOf(q) === -1) return;
var d = document.createElement('div');
d.className = 'ss-opt' + (it.value === select.value && it.value !== '' ? ' ss-chosen' : '');
d.textContent = it.text;
d.dataset.value = it.value;
d.addEventListener('mousedown', function (e) {
e.preventDefault(); // قبل الـ blur عشان الاختيار ما يضيعش
pick(it.value);
});
menu.appendChild(d);
shown++;
});
if (!shown) {
var m = document.createElement('div');
m.className = 'ss-empty-msg';
m.textContent = 'مفيش نتيجة مطابقة';
menu.appendChild(m);
}
}
function pick(value) {
select.value = value;
select.dispatchEvent(new Event('change', { bubbles: true }));
syncInput();
close();
}
function open() {
render('');
menu.classList.add('open');
}
function close() {
menu.classList.remove('open');
}
function move(step) {
var opts = menu.querySelectorAll('.ss-opt');
if (!opts.length) return;
if (active >= 0) opts[active].classList.remove('ss-active');
active = (active + step + opts.length) % opts.length;
opts[active].classList.add('ss-active');
opts[active].scrollIntoView({ block: 'nearest' });
}
input.addEventListener('focus', function () { input.select(); open(); });
input.addEventListener('input', function () { render(input.value); menu.classList.add('open'); });
input.addEventListener('blur', function () { setTimeout(function () { close(); syncInput(); }, 120); });
input.addEventListener('keydown', function (e) {
if (e.key === 'ArrowDown') { e.preventDefault(); if (!menu.classList.contains('open')) open(); move(1); }
else if (e.key === 'ArrowUp') { e.preventDefault(); move(-1); }
else if (e.key === 'Enter') {
var opts = menu.querySelectorAll('.ss-opt');
if (menu.classList.contains('open') && active >= 0 && opts[active]) {
e.preventDefault();
pick(opts[active].dataset.value);
}
} else if (e.key === 'Escape') { close(); syncInput(); }
});
// لو حد غيّر الـ select من كود تاني، الخانة تتحدّث معاه
select.addEventListener('change', syncInput);
syncInput();
}
/** بيفعّل البحث على أي select جديد جوه العنصر ده (أو الصفحة كلها). */
function enhanceAll(root) {
(root || document).querySelectorAll('select').forEach(enhance);
}
/**
* بعد نسخ صف فيه select متفعّل عليه البحث، لازم ننضّف النسخة الأول
* وإلا هتفضل شايلة خانة البحث القديمة بقيمتها.
*/
function resetSearchable(root) {
(root || document).querySelectorAll('.ss-wrap').forEach(function (wrap) {
var select = wrap.querySelector('select');
if (!select) { wrap.remove(); return; }
select.classList.remove('ss-native');
delete select.dataset.ssDone;
wrap.parentNode.insertBefore(select, wrap);
wrap.remove();
});
}
window.enhanceSearchableSelects = enhanceAll;
window.resetSearchableSelects = resetSearchable;
document.addEventListener('DOMContentLoaded', function () { enhanceAll(document); });
})();
</script>
......@@ -196,6 +196,7 @@ window.addEventListener('load', function() {
}
});
</script>
<?php $__template->include('Shared.Components.searchable_select'); ?>
<?= $__template->yield('scripts', '') ?>
<?php if (str_starts_with($_SERVER['REQUEST_URI'] ?? '', '/tutorials/')): ?>
<script src="/assets/js/tutorial-screenshots.js"></script>
......
<?php
declare(strict_types=1);
/**
* ربط كل حركة على الشيك بالقيد المحاسبي اللي اتعمل بسببها.
*
* من غير العمود ده، سجل حركة الشيك بيقول «اتحصّل» من غير ما تعرف القيد
* اللي اتولد، فالمحاسب مضطر يدوّر عليه بإيده في دفتر اليومية.
*/
return [
'up' => "
ALTER TABLE `instrument_movements`
ADD COLUMN `journal_entry_id` BIGINT UNSIGNED NULL DEFAULT NULL AFTER `bank_account_id`,
ADD KEY `idx_im_journal` (`journal_entry_id`);
",
'down' => "
ALTER TABLE `instrument_movements`
DROP KEY `idx_im_journal`,
DROP COLUMN `journal_entry_id`;
",
];
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