Commit 927edd62 authored by DevPilot's avatar DevPilot

feat(accounting): unified cheque register with full lifecycle history

The cheque data existed but the screen was a per-direction list and the
status was overwritten in place — there was no record of who did what,
when, or what the previous state was. Adds:

- صادر ووارد شيكات بنكية: one screen for both directions, with the
  full filter set (date range on either the cheque date or the movement
  date, direction, number, party, bank, branch, status, amount range),
  a reset, and per-direction summary cards whose totals are clickable
  and drive the filters.
- instrument_movements: every action is appended as an immutable row
  (action, from/to status, date, user, bank, reference, notes). Nothing
  is ever deleted, so each cheque carries a complete audit trail.
  Existing cheques get an opening "register" movement on migrate so the
  history starts from a known point.
- The complete status sets for both directions — registered, ready,
  delivered, deposited, under collection, pending, collected, paid,
  bounced, endorsed, returned, replaced, cancelled, closed — with a
  direction-specific transition map that refuses illogical moves such
  as collecting a cancelled cheque.
- Cheques with movements cannot be deleted; corrections are new
  actions, not edits.

The older screens now funnel through the same service, and the bounce
and resolve paths log movements too, so the trail stays complete no
matter which screen the action came from.
parent ec7ca512
<?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\InstrumentLifecycleService;
use App\Modules\Accounting\Services\InstrumentRegisterService;
/**
* صادر ووارد شيكات بنكية — شاشة واحدة لمتابعة دورة حياة كل شيك.
*/
class InstrumentRegisterController extends Controller
{
private function filters(Request $request): array
{
return [
'direction' => (string) $request->get('direction', ''),
'date_from' => (string) $request->get('date_from', ''),
'date_to' => (string) $request->get('date_to', ''),
'date_basis' => (string) $request->get('date_basis', 'instrument'),
'number' => trim((string) $request->get('number', '')),
'party' => trim((string) $request->get('party', '')),
'bank' => trim((string) $request->get('bank', '')),
'branch_id' => (string) $request->get('branch_id', ''),
'status' => (string) $request->get('status', ''),
'amount_from' => (string) $request->get('amount_from', ''),
'amount_to' => (string) $request->get('amount_to', ''),
];
}
public function index(Request $request): Response
{
$this->authorize('accounting.instruments.view');
$db = App::getInstance()->db();
$f = $this->filters($request);
return $this->view('Accounting.Views.instruments.register', [
'filters' => $f,
'rows' => InstrumentRegisterService::search($f),
'summary' => InstrumentRegisterService::summary($f),
'branches' => $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1 ORDER BY name_ar"),
'bankAccounts' => $db->select("SELECT id, account_name_ar FROM bank_accounts WHERE is_active = 1 ORDER BY account_name_ar"),
'statusesIn' => InstrumentLifecycleService::statusesFor('receivable'),
'statusesOut' => InstrumentLifecycleService::statusesFor('payable'),
]);
}
public function show(Request $request, string $id): Response
{
$this->authorize('accounting.instruments.view');
$db = App::getInstance()->db();
$ins = $db->selectOne(
"SELECT ni.*, b.account_name_ar AS bank_name, br.name_ar AS branch_name,
rep.instrument_number AS replaced_by_number
FROM negotiable_instruments ni
LEFT JOIN bank_accounts b ON b.id = ni.bank_account_id
LEFT JOIN branches br ON br.id = ni.branch_id
LEFT JOIN negotiable_instruments rep ON rep.id = ni.replaced_by_id
WHERE ni.id = ?",
[(int) $id]
);
if (!$ins) {
return $this->redirect('/accounting/instruments/register')->withError('الشيك غير موجود');
}
$allowed = InstrumentLifecycleService::allowedNext((string) $ins['direction'], (string) $ins['status']);
return $this->view('Accounting.Views.instruments.register_show', [
'ins' => $ins,
'history' => InstrumentLifecycleService::history((int) $id),
'allowed' => $allowed,
'bankAccounts' => $db->select("SELECT id, account_name_ar FROM bank_accounts WHERE is_active = 1 ORDER BY account_name_ar"),
'canDelete' => InstrumentLifecycleService::canDelete((int) $id),
]);
}
public function act(Request $request, string $id): Response
{
$this->authorize('accounting.instruments.manage');
$result = InstrumentLifecycleService::act((int) $id, (string) $request->post('to_status', ''), [
'action_date' => $request->postDate('action_date'),
'bank_account_id' => $request->post('bank_account_id'),
'endorsee_name' => $request->post('endorsee_name'),
'reference' => $request->post('reference'),
'notes' => $request->post('notes'),
]);
if (!$result['success']) {
return $this->redirect('/accounting/instruments/register/' . $id)->withError($result['error']);
}
return $this->redirect('/accounting/instruments/register/' . $id)->withSuccess('تم تسجيل الإجراء في سجل حركة الشيك');
}
public function note(Request $request, string $id): Response
{
$this->authorize('accounting.instruments.manage');
$result = InstrumentLifecycleService::addNote(
(int) $id,
(string) $request->post('notes', ''),
$request->postDate('action_date')
);
if (!$result['success']) {
return $this->redirect('/accounting/instruments/register/' . $id)->withError($result['error']);
}
return $this->redirect('/accounting/instruments/register/' . $id)->withSuccess('تمت إضافة الملاحظة للسجل');
}
}
......@@ -8,6 +8,7 @@ use App\Core\Request;
use App\Core\App;
use App\Core\Response;
use App\Modules\Accounting\Models\NegotiableInstrument;
use App\Modules\Accounting\Services\InstrumentLifecycleService;
class NegotiableInstrumentController extends Controller
{
......@@ -53,7 +54,7 @@ class NegotiableInstrumentController extends Controller
$session = App::getInstance()->session();
NegotiableInstrument::create([
$created = NegotiableInstrument::create([
'instrument_type' => $data['instrument_type'],
'direction' => $data['direction'],
'instrument_number' => $data['instrument_number'],
......@@ -73,8 +74,23 @@ class NegotiableInstrumentController extends Controller
'created_by' => (int)($session->get('employee_id') ?? 0) ?: null,
]);
return $this->redirect('/accounting/instruments?direction=' . $data['direction'])
->withSuccess('تم تسجيل الورقة التجارية بنجاح');
// أول حركة في سجل الشيك — من غيرها السجل يبدأ ناقص
if ($created && !empty($created->id)) {
App::getInstance()->db()->insert('instrument_movements', [
'instrument_id' => (int) $created->id,
'action' => 'register',
'from_status' => null,
'to_status' => 'in_hand',
'action_date' => $data['issue_date'],
'amount' => (float) $data['amount'],
'notes' => $request->post('notes'),
'performed_by' => (int) ($session->get('employee_id') ?? 0) ?: null,
'created_at' => date('Y-m-d H:i:s'),
]);
}
return $this->redirect('/accounting/instruments/register/' . ($created->id ?? ''))
->withSuccess('تم تسجيل الشيك وبدأ سجل حركته');
}
public function show(Request $request, string $id): Response
......@@ -92,26 +108,20 @@ class NegotiableInstrumentController extends Controller
{
$this->authorize('accounting.instruments.manage');
$instrument = NegotiableInstrument::findOrFail((int)$id);
$newStatus = $request->post('new_status');
if (!$instrument->canTransitionTo($newStatus)) {
return $this->redirect('/accounting/instruments/' . $id)
->withError('لا يمكن تغيير الحالة إلى: ' . (NegotiableInstrument::$statusLabels[$newStatus] ?? $newStatus));
}
// كل تغيير حالة بيمر من نفس الخدمة عشان يتسجّل في سجل الحركة —
// مفيش تغيير حالة من غير حركة مقابلة.
$result = InstrumentLifecycleService::act((int) $id, (string) $request->post('new_status', ''), [
'bank_account_id' => $request->post('bank_account_id'),
'endorsee_name' => $request->post('endorsee_name'),
'notes' => $request->post('notes'),
]);
$extra = [];
if ($request->post('bank_account_id')) {
$extra['bank_account_id'] = (int)$request->post('bank_account_id');
if (!$result['success']) {
return $this->redirect('/accounting/instruments/' . $id)->withError($result['error']);
}
if ($request->post('endorsee_name')) {
$extra['endorsee_name'] = $request->post('endorsee_name');
}
$instrument->transitionTo($newStatus, $extra ?: null);
return $this->redirect('/accounting/instruments/' . $id)
->withSuccess('تم تغيير الحالة إلى: ' . (NegotiableInstrument::$statusLabels[$newStatus] ?? $newStatus));
return $this->redirect('/accounting/instruments/register/' . $id)
->withSuccess('تم تنفيذ الإجراء وتسجيله في سجل حركة الشيك');
}
public function dueSoon(Request $request): Response
......
......@@ -73,6 +73,10 @@ return [
// ── Negotiable Instruments ──────────────────────────────
['GET', '/accounting/instruments', 'Accounting\Controllers\NegotiableInstrumentController@index', ['auth'], 'accounting.instruments.view'],
['POST', '/accounting/instruments', 'Accounting\Controllers\NegotiableInstrumentController@store', ['auth', 'csrf'], 'accounting.instruments.manage'],
['GET', '/accounting/instruments/register', 'Accounting\\Controllers\\InstrumentRegisterController@index', ['auth'], 'accounting.instruments.view'],
['GET', '/accounting/instruments/register/{id:\\d+}', 'Accounting\\Controllers\\InstrumentRegisterController@show', ['auth'], 'accounting.instruments.view'],
['POST', '/accounting/instruments/register/{id:\\d+}/act', 'Accounting\\Controllers\\InstrumentRegisterController@act', ['auth', 'csrf'], 'accounting.instruments.manage'],
['POST', '/accounting/instruments/register/{id:\\d+}/note', 'Accounting\\Controllers\\InstrumentRegisterController@note', ['auth', 'csrf'], 'accounting.instruments.manage'],
['GET', '/accounting/instruments/due-soon', 'Accounting\Controllers\NegotiableInstrumentController@dueSoon', ['auth'], 'accounting.instruments.view'],
// الشيكات المرتدة والمتأخرة — declared before {id} so the word is not read as an id
['GET', '/accounting/instruments/bounced', 'Accounting\Controllers\BouncedChequeController@index', ['auth'], 'accounting.instruments.view'],
......
......@@ -123,6 +123,13 @@ final class BouncedChequeService
return $result;
}
self::logMovement($instrumentId, 'bounce', (string) $instrument['status'], 'bounced', [
'date' => $date ?? date('Y-m-d'),
'amount' => $instrument['amount'] ?? null,
'reference' => $opts['protest_number'] ?? null,
'notes' => 'ارتداد الشيك' . (!empty($code) ? ' — كود ' . $code : ''),
]);
$db->update('negotiable_instruments', [
'bounce_code' => $code,
'bounce_count' => (int) $instrument['bounce_count'] + 1,
......@@ -239,6 +246,11 @@ final class BouncedChequeService
}
}
self::logMovement($instrumentId, 'note', (string) ($instrument['status'] ?? ''), (string) ($instrument['status'] ?? ''), [
'date' => $date,
'notes' => 'معالجة ارتداد: ' . $resolution . (trim($notes) !== '' ? ' — ' . mb_substr($notes, 0, 200) : ''),
]);
$db->update('negotiable_instruments', [
'resolution' => $resolution,
'resolved_date' => $date,
......@@ -462,4 +474,29 @@ final class BouncedChequeService
{
return number_format((float) $v, self::SCALE, '.', '');
}
/**
* كل إجراء ارتداد أو معالجة بيتسجّل في سجل حركة الشيك كمان — عشان السجل
* يفضل كامل مهما كانت الشاشة اللي اتعمل منها الإجراء.
*/
private static function logMovement(int $instrumentId, string $action, ?string $from, ?string $to, array $opts = []): void
{
try {
$employee = App::getInstance()->currentEmployee();
App::getInstance()->db()->insert('instrument_movements', [
'instrument_id' => $instrumentId,
'action' => $action,
'from_status' => $from,
'to_status' => $to,
'action_date' => $opts['date'] ?? date('Y-m-d'),
'amount' => $opts['amount'] ?? null,
'reference' => $opts['reference'] ?? null,
'notes' => $opts['notes'] ?? null,
'performed_by' => $employee ? (int) $employee->id : null,
'created_at' => date('Y-m-d H:i:s'),
]);
} catch (\Throwable $e) {
Logger::error('Instrument movement log failed: ' . $e->getMessage());
}
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Services;
use App\Core\App;
use App\Core\Logger;
/**
* دورة حياة الشيك — الحالات المسموحة لكل اتجاه، وتسجيل كل إجراء كحركة دائمة.
*
* القاعدة الحاكمة: الحالة ما بتتغيّرش من غير ما تتسجّل حركة معاها. الحركة
* بتقول: إيه اللي حصل، إمتى، مين عمله، من أي حالة لأي حالة، وأي ملاحظات.
* الحركات لا تتحذف أبدًا — حتى لو الشيك اتلغى أو اتقفل.
*/
final class InstrumentLifecycleService
{
/** كل الحالات بأسمائها العربية. */
public const STATUS_LABELS = [
'in_hand' => 'مسجل',
'ready' => 'جاهز للصرف',
'delivered' => 'تم تسليمه للمستفيد',
'deposited' => 'تم إيداعه بالبنك',
'under_collection' => 'تحت التحصيل',
'pending_clearance' => 'تحت الانتظار',
'collected' => 'محصل',
'paid' => 'تم صرفه',
'bounced' => 'مرتد',
'endorsed' => 'مظهّر',
'returned' => 'مرتجع',
'replaced' => 'مستبدل',
'cancelled' => 'ملغي',
'closed' => 'مغلق',
];
/** ألوان الحالات في الشاشة. */
public const STATUS_COLORS = [
'in_hand' => ['#F3F4F6', '#374151'],
'ready' => ['#EFF6FF', '#2563EB'],
'delivered' => ['#EFF6FF', '#1D4ED8'],
'deposited' => ['#FFF7ED', '#C2410C'],
'under_collection' => ['#FFF7ED', '#D97706'],
'pending_clearance' => ['#FFF7ED', '#B45309'],
'collected' => ['#ECFDF5', '#059669'],
'paid' => ['#ECFDF5', '#047857'],
'bounced' => ['#FEE2E2', '#DC2626'],
'endorsed' => ['#F5F3FF', '#7C3AED'],
'returned' => ['#FEF3C7', '#92400E'],
'replaced' => ['#F5F3FF', '#6D28D9'],
'cancelled' => ['#F3F4F6', '#9CA3AF'],
'closed' => ['#E5E7EB', '#1F2937'],
];
/** أسماء الإجراءات. */
public const ACTION_LABELS = [
'register' => 'تسجيل الشيك',
'ready' => 'تجهيز للصرف',
'deliver' => 'تسليم للمستفيد',
'deposit' => 'إيداع بالبنك',
'collect' => 'تحصيل',
'pay' => 'صرف',
'bounce' => 'ارتداد',
'endorse' => 'تظهير',
'return' => 'ارتجاع',
'replace' => 'استبدال',
'cancel' => 'إلغاء',
'close' => 'إغلاق',
'note' => 'ملاحظة',
];
/**
* الانتقالات المسموحة لكل اتجاه.
*
* الوارد (receivable) بيتحصّل، والصادر (payable) بيتصرف — فالمسارين مختلفين،
* وده اللي بيمنع إجراءات غير منطقية زي «تحصيل» شيك صادر أو شيك ملغي.
*/
private const TRANSITIONS = [
'receivable' => [
'in_hand' => ['deposited', 'under_collection', 'endorsed', 'replaced', 'cancelled', 'returned'],
'deposited' => ['under_collection', 'collected', 'bounced', 'returned'],
'under_collection' => ['collected', 'bounced', 'returned'],
'collected' => ['closed'],
'bounced' => ['in_hand', 'deposited', 'replaced', 'cancelled', 'returned'],
'endorsed' => ['returned', 'closed'],
'returned' => ['in_hand', 'closed'],
'replaced' => ['closed'],
'cancelled' => ['closed'],
'closed' => [],
],
'payable' => [
'in_hand' => ['ready', 'replaced', 'cancelled'],
'ready' => ['delivered', 'replaced', 'cancelled'],
'delivered' => ['pending_clearance', 'paid', 'bounced', 'returned'],
'pending_clearance' => ['paid', 'bounced', 'returned'],
'paid' => ['closed'],
'bounced' => ['ready', 'replaced', 'cancelled'],
'returned' => ['ready', 'cancelled', 'closed'],
'replaced' => ['closed'],
'cancelled' => ['closed'],
'closed' => [],
],
];
/** الإجراء اللي بيوصل لكل حالة — عشان نسمّي الحركة صح. */
private const STATUS_ACTION = [
'in_hand' => 'register',
'ready' => 'ready',
'delivered' => 'deliver',
'deposited' => 'deposit',
'under_collection' => 'deposit',
'pending_clearance' => 'deliver',
'collected' => 'collect',
'paid' => 'pay',
'bounced' => 'bounce',
'endorsed' => 'endorse',
'returned' => 'return',
'replaced' => 'replace',
'cancelled' => 'cancel',
'closed' => 'close',
];
public static function statusLabel(?string $s): string
{
return self::STATUS_LABELS[$s ?? ''] ?? (string) $s;
}
public static function actionLabel(?string $a): string
{
return self::ACTION_LABELS[$a ?? ''] ?? (string) $a;
}
/** الحالات المسموح الانتقال ليها من الحالة الحالية. */
public static function allowedNext(string $direction, string $status): array
{
return self::TRANSITIONS[$direction][$status] ?? [];
}
/** الحالات المتاحة للاتجاه ده (للفلاتر). */
public static function statusesFor(string $direction): array
{
$keys = array_keys(self::TRANSITIONS[$direction] ?? []);
$out = [];
foreach ($keys as $k) {
$out[$k] = self::STATUS_LABELS[$k] ?? $k;
}
return $out;
}
/**
* تنفيذ إجراء على الشيك: بيتحقق من المنطق، بيحدّث الحالة، وبيسجّل الحركة.
*
* @return array{success:bool, error?:string}
*/
public static function act(int $instrumentId, string $toStatus, array $opts = []): array
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$ins = $db->selectOne("SELECT * FROM negotiable_instruments WHERE id = ?", [$instrumentId]);
if (!$ins) {
return ['success' => false, 'error' => 'الشيك غير موجود'];
}
$direction = (string) $ins['direction'];
$from = (string) $ins['status'];
if (!in_array($toStatus, self::allowedNext($direction, $from), true)) {
return [
'success' => false,
'error' => 'إجراء غير مسموح: لا يمكن الانتقال من «' . self::statusLabel($from)
. '» إلى «' . self::statusLabel($toStatus) . '»',
];
}
$actionDate = !empty($opts['action_date']) ? $opts['action_date'] : date('Y-m-d');
$db->beginTransaction();
try {
$update = [
'status' => $toStatus,
'updated_at' => date('Y-m-d H:i:s'),
];
// مكان الشيك بيتغيّر مع حالته
$update['current_location'] = match ($toStatus) {
'deposited', 'under_collection', 'collected' => 'bank',
'endorsed' => 'endorsed_to',
'returned' => 'returned_to_drawer',
default => $ins['current_location'] ?: 'safe',
};
if (!empty($opts['bank_account_id'])) {
$update['bank_account_id'] = (int) $opts['bank_account_id'];
}
if (!empty($opts['endorsee_name'])) {
$update['endorsee_name'] = $opts['endorsee_name'];
}
if (!empty($opts['replaced_by_id'])) {
$update['replaced_by_id'] = (int) $opts['replaced_by_id'];
}
if ($toStatus === 'closed') {
$update['closed_at'] = date('Y-m-d H:i:s');
}
$db->update('negotiable_instruments', $update, 'id = ?', [$instrumentId]);
$db->insert('instrument_movements', [
'instrument_id' => $instrumentId,
'action' => $opts['action'] ?? (self::STATUS_ACTION[$toStatus] ?? 'note'),
'from_status' => $from,
'to_status' => $toStatus,
'action_date' => $actionDate,
'amount' => $opts['amount'] ?? $ins['amount'],
'bank_account_id' => !empty($opts['bank_account_id']) ? (int) $opts['bank_account_id'] : null,
'reference' => $opts['reference'] ?? null,
'notes' => $opts['notes'] ?? null,
'performed_by' => $employee ? (int) $employee->id : null,
'created_at' => date('Y-m-d H:i:s'),
]);
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
Logger::error('Instrument action failed: ' . $e->getMessage());
return ['success' => false, 'error' => 'فشل تنفيذ الإجراء: ' . $e->getMessage()];
}
return ['success' => true];
}
/** ملاحظة من غير تغيير حالة — بتتسجّل كحركة برضه. */
public static function addNote(int $instrumentId, string $note, ?string $date = null): array
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$ins = $db->selectOne("SELECT status FROM negotiable_instruments WHERE id = ?", [$instrumentId]);
if (!$ins) {
return ['success' => false, 'error' => 'الشيك غير موجود'];
}
if (trim($note) === '') {
return ['success' => false, 'error' => 'اكتب الملاحظة'];
}
$db->insert('instrument_movements', [
'instrument_id' => $instrumentId,
'action' => 'note',
'from_status' => $ins['status'],
'to_status' => $ins['status'],
'action_date' => $date ?: date('Y-m-d'),
'notes' => $note,
'performed_by' => $employee ? (int) $employee->id : null,
'created_at' => date('Y-m-d H:i:s'),
]);
return ['success' => true];
}
/** سجل الحركة الكامل للشيك — الأقدم أولًا عشان يتقري كقصة. */
public static function history(int $instrumentId): array
{
return App::getInstance()->db()->select(
"SELECT m.*, e.full_name_ar AS performed_by_name, b.account_name_ar AS bank_name
FROM instrument_movements m
LEFT JOIN employees e ON e.id = m.performed_by
LEFT JOIN bank_accounts b ON b.id = m.bank_account_id
WHERE m.instrument_id = ?
ORDER BY m.action_date ASC, m.id ASC",
[$instrumentId]
);
}
/** آخر إجراء على الشيك — بيظهر في الشاشة الرئيسية. */
public static function lastActionMap(array $instrumentIds): array
{
if (empty($instrumentIds)) {
return [];
}
$in = implode(',', array_map('intval', $instrumentIds));
$rows = App::getInstance()->db()->select(
"SELECT m.instrument_id, m.action, m.action_date
FROM instrument_movements m
JOIN (
SELECT instrument_id, MAX(id) AS max_id
FROM instrument_movements
WHERE instrument_id IN ({$in})
GROUP BY instrument_id
) last ON last.max_id = m.id"
);
$map = [];
foreach ($rows as $r) {
$map[(int) $r['instrument_id']] = $r;
}
return $map;
}
/** هل يمكن حذف الشيك؟ ممنوع لو عليه أي حركة غير حركة التسجيل. */
public static function canDelete(int $instrumentId): bool
{
$row = App::getInstance()->db()->selectOne(
"SELECT COUNT(*) AS n FROM instrument_movements
WHERE instrument_id = ? AND action <> 'register'",
[$instrumentId]
);
return (int) ($row['n'] ?? 0) === 0;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Services;
use App\Core\App;
/**
* سجل الشيكات الصادرة والواردة — البحث والفلترة والملخص المالي.
*/
final class InstrumentRegisterService
{
/**
* @param array $f من تاريخ/إلى تاريخ، الاتجاه، رقم الشيك، الطرف، البنك،
* الفرع، الحالة، المبلغ من/إلى، وأساس التاريخ (شيك/حركة)
*/
public static function search(array $f): array
{
$db = App::getInstance()->db();
[$where, $params] = self::buildWhere($f);
$rows = $db->select(
"SELECT ni.*,
b.account_name_ar AS bank_name,
br.name_ar AS branch_name
FROM negotiable_instruments ni
LEFT JOIN bank_accounts b ON b.id = ni.bank_account_id
LEFT JOIN branches br ON br.id = ni.branch_id
WHERE {$where}
ORDER BY ni.due_date ASC, ni.id DESC
LIMIT 500",
$params
);
$ids = array_map(fn($r) => (int) $r['id'], $rows);
$last = InstrumentLifecycleService::lastActionMap($ids);
foreach ($rows as &$r) {
$r['last_action'] = $last[(int) $r['id']]['action'] ?? null;
$r['last_action_date'] = $last[(int) $r['id']]['action_date'] ?? null;
}
return $rows;
}
/** ملخص مالي لكل اتجاه — عدد وقيمة لكل مجموعة حالات. */
public static function summary(array $f): array
{
$db = App::getInstance()->db();
$out = [];
foreach (['receivable', 'payable'] as $dir) {
$scoped = $f;
$scoped['direction'] = $dir;
[$where, $params] = self::buildWhere($scoped);
$row = $db->selectOne(
"SELECT COUNT(*) AS n, COALESCE(SUM(ni.amount),0) AS total
FROM negotiable_instruments ni
WHERE {$where}",
$params
);
$byStatus = $db->select(
"SELECT ni.status, COUNT(*) AS n, COALESCE(SUM(ni.amount),0) AS total
FROM negotiable_instruments ni
WHERE {$where}
GROUP BY ni.status",
$params
);
$buckets = [];
foreach ($byStatus as $b) {
$buckets[$b['status']] = ['n' => (int) $b['n'], 'total' => (string) $b['total']];
}
$pick = function (array $statuses) use ($buckets) {
$n = 0; $t = '0.00';
foreach ($statuses as $s) {
if (isset($buckets[$s])) {
$n += $buckets[$s]['n'];
$t = bcadd($t, $buckets[$s]['total'], 2);
}
}
return ['n' => $n, 'total' => $t];
};
$out[$dir] = [
'all' => ['n' => (int) ($row['n'] ?? 0), 'total' => (string) ($row['total'] ?? '0.00')],
'settled' => $pick($dir === 'receivable' ? ['collected'] : ['paid']),
'bounced' => $pick(['bounced']),
'cancelled' => $pick(['cancelled']),
'open' => $pick($dir === 'receivable'
? ['in_hand', 'deposited', 'under_collection', 'endorsed', 'returned']
: ['in_hand', 'ready', 'delivered', 'pending_clearance', 'returned']),
'by_status' => $buckets,
];
}
return $out;
}
/** @return array{0:string,1:array} */
private static function buildWhere(array $f): array
{
$where = ['ni.is_archived = 0'];
$params = [];
if (!empty($f['direction']) && in_array($f['direction'], ['receivable', 'payable'], true)) {
$where[] = 'ni.direction = ?';
$params[] = $f['direction'];
}
// أساس التاريخ: تاريخ الشيك نفسه أم تاريخ آخر حركة عليه
$basis = ($f['date_basis'] ?? 'instrument') === 'movement' ? 'movement' : 'instrument';
if (!empty($f['date_from'])) {
if ($basis === 'movement') {
$where[] = 'EXISTS (SELECT 1 FROM instrument_movements m WHERE m.instrument_id = ni.id AND m.action_date >= ?)';
} else {
$where[] = 'ni.due_date >= ?';
}
$params[] = $f['date_from'];
}
if (!empty($f['date_to'])) {
if ($basis === 'movement') {
$where[] = 'EXISTS (SELECT 1 FROM instrument_movements m WHERE m.instrument_id = ni.id AND m.action_date <= ?)';
} else {
$where[] = 'ni.due_date <= ?';
}
$params[] = $f['date_to'];
}
if (!empty($f['number'])) {
$where[] = 'ni.instrument_number LIKE ?';
$params[] = '%' . $f['number'] . '%';
}
if (!empty($f['party'])) {
$where[] = '(ni.drawer_name LIKE ? OR ni.beneficiary_name LIKE ? OR ni.endorsee_name LIKE ?)';
$like = '%' . $f['party'] . '%';
$params[] = $like; $params[] = $like; $params[] = $like;
}
if (!empty($f['bank'])) {
$where[] = '(ni.drawer_bank LIKE ? OR EXISTS (SELECT 1 FROM bank_accounts b WHERE b.id = ni.bank_account_id AND b.account_name_ar LIKE ?))';
$like = '%' . $f['bank'] . '%';
$params[] = $like; $params[] = $like;
}
if (!empty($f['branch_id'])) {
$where[] = 'ni.branch_id = ?';
$params[] = (int) $f['branch_id'];
}
if (!empty($f['status'])) {
$where[] = 'ni.status = ?';
$params[] = $f['status'];
}
if (($f['amount_from'] ?? '') !== '') {
$where[] = 'ni.amount >= ?';
$params[] = $f['amount_from'];
}
if (($f['amount_to'] ?? '') !== '') {
$where[] = 'ni.amount <= ?';
$params[] = $f['amount_to'];
}
return [implode(' AND ', $where), $params];
}
}
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>صادر ووارد شيكات بنكية<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/accounting/instruments" class="btn btn-outline">تسجيل شيك جديد</a>
<a href="/accounting/instruments/bounced" class="btn btn-outline">الشيكات المرتدة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
use App\Modules\Accounting\Services\InstrumentLifecycleService as LC;
$f = $filters;
$chip = function (?string $status) {
$c = LC::STATUS_COLORS[$status ?? ''] ?? ['#F3F4F6', '#374151'];
return 'background:' . $c[0] . ';color:' . $c[1] . ';padding:2px 10px;border-radius:10px;font-size:12px;font-weight:600;white-space:nowrap;';
};
$qs = function (array $over) use ($f) {
return '/accounting/instruments/register?' . http_build_query(array_merge($f, $over));
};
?>
<!-- ملخص مالي -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:18px;">
<?php foreach ([['receivable', 'الشيكات الواردة', '#059669'], ['payable', 'الشيكات الصادرة', '#2563EB']] as [$dir, $label, $color]): ?>
<?php $s = $summary[$dir]; ?>
<div class="card" style="border-top:3px solid <?= $color ?>;">
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<strong style="color:<?= $color ?>;font-size:15px;"><?= e($label) ?></strong>
<a href="<?= e($qs(['direction' => $dir, 'status' => ''])) ?>" style="font-size:12px;">عرض الكل</a>
</div>
<div style="padding:12px 16px;display:grid;grid-template-columns:repeat(2,1fr);gap:10px;font-size:13px;">
<?php
$cells = [
['الإجمالي', $s['all'], ''],
[$dir === 'receivable' ? 'المحصّل' : 'المصروف', $s['settled'], $dir === 'receivable' ? 'collected' : 'paid'],
['القائم', $s['open'], ''],
['المرتد', $s['bounced'], 'bounced'],
['الملغي', $s['cancelled'], 'cancelled'],
];
foreach ($cells as [$t, $v, $st]):
?>
<a href="<?= e($qs(['direction' => $dir, 'status' => $st])) ?>"
style="display:block;padding:8px 10px;border:1px solid #E5E7EB;border-radius:6px;text-decoration:none;color:inherit;">
<div style="color:#6B7280;font-size:11px;"><?= e($t) ?> (<?= (int) $v['n'] ?>)</div>
<div style="font-weight:700;direction:ltr;text-align:left;"><?= money($v['total']) ?></div>
</a>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- الفلاتر -->
<div class="card" style="margin-bottom:18px;">
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB;"><strong style="font-size:14px;">بحث وفلترة</strong></div>
<div style="padding:14px 16px;">
<form method="GET" action="/accounting/instruments/register">
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;">
<div class="form-group">
<label class="form-label" style="font-size:12px;">نوع الشيك</label>
<select name="direction" class="form-select">
<option value="">الكل</option>
<option value="receivable" <?= $f['direction'] === 'receivable' ? 'selected' : '' ?>>وارد</option>
<option value="payable" <?= $f['direction'] === 'payable' ? 'selected' : '' ?>>صادر</option>
</select>
</div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">أساس التاريخ</label>
<select name="date_basis" class="form-select">
<option value="instrument" <?= $f['date_basis'] !== 'movement' ? 'selected' : '' ?>>تاريخ استحقاق الشيك</option>
<option value="movement" <?= $f['date_basis'] === 'movement' ? 'selected' : '' ?>>تاريخ الحركة</option>
</select>
</div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">من تاريخ</label>
<input type="date" name="date_from" class="form-input" value="<?= e($f['date_from']) ?>">
</div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">إلى تاريخ</label>
<input type="date" name="date_to" class="form-input" value="<?= e($f['date_to']) ?>">
</div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">رقم الشيك</label>
<input type="text" name="number" class="form-input" value="<?= e($f['number']) ?>">
</div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">المستفيد / الساحب</label>
<input type="text" name="party" class="form-input" value="<?= e($f['party']) ?>">
</div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">البنك</label>
<input type="text" name="bank" class="form-input" value="<?= e($f['bank']) ?>">
</div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">الفرع</label>
<select name="branch_id" class="form-select">
<option value="">الكل</option>
<?php foreach ($branches as $b): ?>
<option value="<?= (int) $b['id'] ?>" <?= (string) $f['branch_id'] === (string) $b['id'] ? 'selected' : '' ?>><?= e($b['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-select">
<option value="">الكل</option>
<optgroup label="وارد">
<?php foreach ($statusesIn as $k => $v): ?>
<option value="<?= e($k) ?>" <?= $f['status'] === $k ? 'selected' : '' ?>><?= e($v) ?></option>
<?php endforeach; ?>
</optgroup>
<optgroup label="صادر">
<?php foreach ($statusesOut as $k => $v): ?>
<option value="<?= e($k) ?>" <?= $f['status'] === $k ? 'selected' : '' ?>><?= e($v) ?></option>
<?php endforeach; ?>
</optgroup>
</select>
</div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">مبلغ من</label>
<input type="number" name="amount_from" class="form-input" step="0.01" value="<?= e($f['amount_from']) ?>" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">مبلغ إلى</label>
<input type="number" name="amount_to" class="form-input" step="0.01" value="<?= e($f['amount_to']) ?>" style="direction:ltr;text-align:left;">
</div>
<div class="form-group" style="display:flex;align-items:end;gap:8px;">
<button type="submit" class="btn btn-primary" style="flex:1;">بحث</button>
<a href="/accounting/instruments/register" class="btn btn-outline">إعادة تعيين</a>
</div>
</div>
</form>
</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($rows) ?>)</strong>
<?php if (count($rows) >= 500): ?>
<span style="font-size:12px;color:#D97706;">معروض أول 500 نتيجة — ضيّق الفلاتر لعرض الباقي</span>
<?php endif; ?>
</div>
<?php if (!empty($rows)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>النوع</th>
<th>رقم الشيك</th>
<th>المستفيد / الساحب</th>
<th>المبلغ</th>
<th>الإصدار</th>
<th>الاستحقاق</th>
<th>البنك</th>
<th>الفرع</th>
<th>الحالة</th>
<th>آخر إجراء</th>
<th>تاريخه</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td>
<span style="<?= $r['direction'] === 'receivable' ? 'color:#059669' : 'color:#2563EB' ?>;font-weight:600;font-size:12px;">
<?= $r['direction'] === 'receivable' ? 'وارد' : 'صادر' ?>
</span>
</td>
<td>
<a href="/accounting/instruments/register/<?= (int) $r['id'] ?>">
<code style="font-size:12px;"><?= e($r['instrument_number']) ?></code>
</a>
</td>
<td><?= e($r['direction'] === 'receivable' ? ($r['drawer_name'] ?: '—') : ($r['beneficiary_name'] ?: '—')) ?></td>
<td style="direction:ltr;text-align:left;font-weight:700;"><?= money($r['amount']) ?></td>
<td><?= e($r['issue_date']) ?></td>
<td><?= e($r['due_date']) ?></td>
<td style="font-size:12px;"><?= e($r['bank_name'] ?: ($r['drawer_bank'] ?: '—')) ?></td>
<td style="font-size:12px;"><?= e($r['branch_name'] ?: '—') ?></td>
<td><span style="<?= $chip($r['status']) ?>"><?= e(LC::statusLabel($r['status'])) ?></span></td>
<td style="font-size:12px;"><?= e($r['last_action'] ? LC::actionLabel($r['last_action']) : '—') ?></td>
<td style="font-size:12px;"><?= e($r['last_action_date'] ?: '—') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;color:#6B7280;">لا توجد شيكات مطابقة للفلاتر المختارة.</div>
<?php endif; ?>
</div>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>شيك <?= e($ins['instrument_number']) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/accounting/instruments/register" class="btn btn-outline">
<i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> سجل الشيكات
</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
use App\Modules\Accounting\Services\InstrumentLifecycleService as LC;
$c = LC::STATUS_COLORS[$ins['status']] ?? ['#F3F4F6', '#374151'];
$isIn = $ins['direction'] === 'receivable';
?>
<!-- بيانات الشيك -->
<div class="card" style="margin-bottom:18px;">
<div style="padding:14px 18px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;">
<div>
<h3 style="margin:0;font-size:16px;">
شيك <?= $isIn ? 'وارد' : 'صادر' ?> رقم
<code style="font-size:14px;"><?= e($ins['instrument_number']) ?></code>
</h3>
<p style="margin:4px 0 0;color:#6B7280;font-size:13px;">
<?= e($isIn ? 'الساحب: ' . ($ins['drawer_name'] ?: '—') : 'المستفيد: ' . ($ins['beneficiary_name'] ?: '—')) ?>
<?= e($ins['bank_name'] ?: ($ins['drawer_bank'] ?: 'بنك غير محدد')) ?>
</p>
</div>
<div style="text-align:left;">
<div style="font-size:22px;font-weight:800;direction:ltr;"><?= money($ins['amount']) ?></div>
<span style="background:<?= $c[0] ?>;color:<?= $c[1] ?>;padding:3px 12px;border-radius:10px;font-size:13px;font-weight:700;">
<?= e(LC::statusLabel($ins['status'])) ?>
</span>
</div>
</div>
<div style="padding:16px 18px;display:grid;grid-template-columns:repeat(4,1fr);gap:14px;font-size:13px;">
<div><div style="color:#6B7280;font-size:11px;">تاريخ الإصدار</div><strong><?= e($ins['issue_date']) ?></strong></div>
<div><div style="color:#6B7280;font-size:11px;">تاريخ الاستحقاق</div><strong><?= e($ins['due_date']) ?></strong></div>
<div><div style="color:#6B7280;font-size:11px;">الفرع</div><strong><?= e($ins['branch_name'] ?: '—') ?></strong></div>
<div><div style="color:#6B7280;font-size:11px;">مكان الشيك</div><strong><?= e($ins['current_location'] ?: '—') ?></strong></div>
<?php if (!empty($ins['replaced_by_number'])): ?>
<div><div style="color:#6B7280;font-size:11px;">استُبدل بالشيك</div><strong><?= e($ins['replaced_by_number']) ?></strong></div>
<?php endif; ?>
<?php if (!empty($ins['closed_at'])): ?>
<div><div style="color:#6B7280;font-size:11px;">تاريخ الإغلاق</div><strong><?= e($ins['closed_at']) ?></strong></div>
<?php endif; ?>
</div>
<?php if (!$canDelete): ?>
<div style="padding:10px 18px;background:#FFFBEB;border-top:1px solid #FDE68A;font-size:12.5px;color:#92400E;">
على الشيك ده حركات مسجّلة — فمش ممكن يتحذف. أي تصحيح بيتعمل بإجراء جديد يتسجّل في السجل، مش بحذف.
</div>
<?php endif; ?>
</div>
<!-- تنفيذ إجراء -->
<?php if (can('accounting.instruments.manage')): ?>
<div class="card" style="margin-bottom:18px;">
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB;"><strong style="font-size:14px;">تسجيل إجراء جديد</strong></div>
<div style="padding:14px 16px;">
<?php if (!empty($allowed)): ?>
<form method="POST" action="/accounting/instruments/register/<?= (int) $ins['id'] ?>/act">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;">
<div class="form-group">
<label class="form-label" style="font-size:12px;">الإجراء <span style="color:#DC2626;">*</span></label>
<select name="to_status" class="form-select" required>
<?php foreach ($allowed as $st): ?>
<option value="<?= e($st) ?>"><?= e(LC::statusLabel($st)) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">تاريخ الإجراء</label>
<input type="date" name="action_date" class="form-input" value="<?= e(date('Y-m-d')) ?>">
</div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">البنك (للإيداع)</label>
<select name="bank_account_id" class="form-select">
<option value=""></option>
<?php foreach ($bankAccounts as $b): ?>
<option value="<?= (int) $b['id'] ?>" <?= (int) ($ins['bank_account_id'] ?? 0) === (int) $b['id'] ? 'selected' : '' ?>>
<?= e($b['account_name_ar']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">مرجع</label>
<input type="text" name="reference" class="form-input" placeholder="رقم إشعار أو مستند">
</div>
</div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">ملاحظات</label>
<input type="text" name="notes" class="form-input" placeholder="سبب الإجراء أو أي تفاصيل">
</div>
<button type="submit" class="btn btn-primary">تنفيذ وتسجيل في السجل</button>
</form>
<?php else: ?>
<p style="margin:0;color:#6B7280;font-size:13px;">
الشيك في حالة «<?= e(LC::statusLabel($ins['status'])) ?>» ومفيش إجراءات تانية متاحة عليه — دي نهاية دورة حياته.
</p>
<?php endif; ?>
<hr style="margin:16px 0;border:none;border-top:1px solid #E5E7EB;">
<form method="POST" action="/accounting/instruments/register/<?= (int) $ins['id'] ?>/note" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
<?= csrf_field() ?>
<div class="form-group" style="flex:1;margin:0;min-width:260px;">
<label class="form-label" style="font-size:12px;">إضافة ملاحظة (من غير تغيير الحالة)</label>
<input type="text" name="notes" class="form-input" required>
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">التاريخ</label>
<input type="date" name="action_date" class="form-input" value="<?= e(date('Y-m-d')) ?>">
</div>
<button type="submit" class="btn btn-outline">أضف للسجل</button>
</form>
</div>
</div>
<?php endif; ?>
<!-- سجل الحركة -->
<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($history) ?>)</strong>
<span style="font-size:12px;color:#6B7280;">لا تُحذف أي حركة — السجل كامل من لحظة التسجيل</span>
</div>
<?php if (!empty($history)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>التاريخ</th>
<th>الإجراء</th>
<th>من حالة</th>
<th>إلى حالة</th>
<th>المستخدم</th>
<th>البنك / المرجع</th>
<th>ملاحظات</th>
</tr>
</thead>
<tbody>
<?php foreach ($history as $h): ?>
<?php
$fc = LC::STATUS_COLORS[$h['from_status'] ?? ''] ?? ['#F3F4F6', '#6B7280'];
$tc = LC::STATUS_COLORS[$h['to_status'] ?? ''] ?? ['#F3F4F6', '#6B7280'];
?>
<tr>
<td style="white-space:nowrap;"><?= e($h['action_date']) ?></td>
<td style="font-weight:600;"><?= e(LC::actionLabel($h['action'])) ?></td>
<td>
<?php if (!empty($h['from_status'])): ?>
<span style="background:<?= $fc[0] ?>;color:<?= $fc[1] ?>;padding:2px 8px;border-radius:9px;font-size:11.5px;"><?= e(LC::statusLabel($h['from_status'])) ?></span>
<?php else: ?><?php endif; ?>
</td>
<td>
<?php if (!empty($h['to_status'])): ?>
<span style="background:<?= $tc[0] ?>;color:<?= $tc[1] ?>;padding:2px 8px;border-radius:9px;font-size:11.5px;"><?= e(LC::statusLabel($h['to_status'])) ?></span>
<?php else: ?><?php endif; ?>
</td>
<td style="font-size:12.5px;"><?= e($h['performed_by_name'] ?: '—') ?></td>
<td style="font-size:12px;color:#6B7280;">
<?= e($h['bank_name'] ?: '') ?><?= !empty($h['bank_name']) && !empty($h['reference']) ? ' — ' : '' ?><?= e($h['reference'] ?: '') ?>
<?= empty($h['bank_name']) && empty($h['reference']) ? '—' : '' ?>
</td>
<td style="font-size:12.5px;"><?= e($h['notes'] ?: '—') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:30px 20px;text-align:center;color:#6B7280;">لا توجد حركات مسجّلة بعد.</div>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () { if (typeof lucide !== 'undefined') lucide.createIcons(); });
</script>
<?php $__template->endSection(); ?>
......@@ -175,6 +175,7 @@ MenuRegistry::register('accounting', [
['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],
......
<?php
declare(strict_types=1);
/**
* دورة حياة الشيكات — الحالات الكاملة وسجل الحركة.
*
* قبل كده الحالة كانت بتتكتب فوق القديمة، فمفيش أي أثر لمين عمل إيه وإمتى.
* هنا بنضيف:
* - حالات ناقصة للصادر والوارد (إيداع، تسليم، جاهز للصرف، استبدال، إغلاق…)
* - عمود الفرع، والشيك البديل، وتاريخ الإغلاق
* - جدول instrument_movements: كل إجراء بيتسجّل كحركة جديدة ولا تتحذف أبدًا
* - حركة «تسجيل» أولى لكل شيك موجود، عشان السجل يبدأ من نقطة معروفة
*/
return [
'up' => "
ALTER TABLE `negotiable_instruments`
MODIFY COLUMN `status` ENUM(
'in_hand','under_collection','collected','bounced','endorsed',
'cancelled','paid','returned',
'ready','delivered','deposited','pending_clearance','replaced','closed'
) NOT NULL DEFAULT 'in_hand';
ALTER TABLE `negotiable_instruments`
ADD COLUMN `branch_id` BIGINT UNSIGNED NULL AFTER `bank_account_id`,
ADD COLUMN `replaced_by_id` BIGINT UNSIGNED NULL COMMENT 'الشيك البديل' AFTER `branch_id`,
ADD COLUMN `closed_at` DATETIME NULL AFTER `replaced_by_id`,
ADD INDEX `idx_ni_branch` (`branch_id`);
CREATE TABLE IF NOT EXISTS `instrument_movements` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`instrument_id` BIGINT UNSIGNED NOT NULL,
`action` VARCHAR(40) NOT NULL COMMENT 'register, deposit, collect, bounce, deliver, pay, cancel, replace, close, note',
`from_status` VARCHAR(30) NULL,
`to_status` VARCHAR(30) NULL,
`action_date` DATE NOT NULL,
`amount` DECIMAL(18,2) NULL COMMENT 'المبلغ المرتبط بالحركة إن وجد',
`bank_account_id` BIGINT UNSIGNED NULL,
`reference` VARCHAR(100) NULL,
`notes` TEXT NULL,
`performed_by` BIGINT UNSIGNED NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_im_instrument` (`instrument_id`, `id`),
INDEX `idx_im_action_date` (`action_date`),
CONSTRAINT `fk_im_instrument` FOREIGN KEY (`instrument_id`)
REFERENCES `negotiable_instruments`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `instrument_movements`
(`instrument_id`, `action`, `from_status`, `to_status`, `action_date`, `amount`, `notes`, `performed_by`, `created_at`)
SELECT ni.`id`, 'register', NULL, ni.`status`, ni.`issue_date`, ni.`amount`,
'حركة افتتاحية — الشيك كان مسجّلًا قبل تفعيل سجل الحركة',
ni.`created_by`, ni.`created_at`
FROM `negotiable_instruments` ni
WHERE NOT EXISTS (
SELECT 1 FROM `instrument_movements` m WHERE m.`instrument_id` = ni.`id`
);
",
'down' => "
DROP TABLE IF EXISTS `instrument_movements`;
ALTER TABLE `negotiable_instruments`
DROP INDEX `idx_ni_branch`,
DROP COLUMN `branch_id`,
DROP COLUMN `replaced_by_id`,
DROP COLUMN `closed_at`;
",
];
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