Commit 06e0ae29 authored by Mahmoud Aglan's avatar Mahmoud Aglan

the TUtorial Update

parent 9e075ef2
use the NewServer.pem File
the new server ip "3.68.63.185"
username "ubuntu"
The server has caprover setup password "Alarcade123#" manage the # in the password well plesase
The database application on caprover is
Your app is internally available as srv-captain--mysql-db to other apps. In case of web-app, it is accessible via http://srv-captain--mysql-db from other apps
\ No newline at end of file
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAwNRFitb/O8fS9TaUoCZ/VJZUEehvdyb1EgCjPrQbTeT6TlZh
rvkzHYcGIKsgarI6wuP9aK4+rLW8SL9VP6Ey3G4CgY/Hx9ZxhoSN5N2ffZkJE1Ji
hvgkDXzSN+l4P3e422ICxuVQqozba8/o8pZo46EzgRry760i9RcR8Q8JsXysjeQ1
Q68F8JhUYt1GNQlc5/A1EHEHyv1XIMDkYQ0eart1iUf9uvU/tp6pFTNUq/UtL/BT
RaJdnShbstS7bsfZwkyRtzXUlu15z/xdCsoXbbz+GC4oV7thzZQ+eRS8sZBGTsHF
6AaNqvd3QQnbFEpUSDzK3xupVEvLw3BbwYFvxwIDAQABAoIBAB4Gr9F/yvynD/1p
A1mwxPEJ+4tSU1ENeunTuZfA+eN2PVfHcayKV2BIrzaVDxYuLKI+WC5du5qvLeNy
D7c5xa63XqKIHgbLKKBWsbWqoPQwyU397SOxLgP/pMhaDYRsgxd+Oop4GMiF6IDw
PgjQTQLtDhUTejLCFghuEDgmLE87oi4oV3m8y36Yl1gHSHLzHivk8tiJoFdd9Jw3
FdM9wPS2FcafGaT7CDhbmo8XtHgynxbjCAX6D8tOpsbhVuClseRLXMfhkai0UuO4
JhgJ4tvDoxW3G/3qZkSvL/jUr5gybUCjVAcBfE7HIvfcYKQxU+iX9R8Q7cWzFO/d
RjooSWECgYEA9DDSYxBXuOkVHq9KDWnRBWUslLk2i9nvPEL+JVPqzR6Q+uRnZzl/
j54XBd25lAcCkTDmjZTHKFroX5uvgBzHGfGZFGtoZuObfVkzK9eicupPqTe7rcN6
fTJWeJnNJsYbkGzv0jrqvBJOG+/9zGVsn7UQZvWHiS9lgKYrDz2Vx5cCgYEAyieS
3xFK+lytLgJ6pqNx6RuvEAKgouZi3sgYIxwyMSA9Yap/5po6Osj8w9X18Bvm6YYF
gok+Zx63pEB7296RrmGDxkOw5Hl/gH07Yx2hvM4et3RyvK3udOXCdcXgWN0ue/Uf
H75UZ4CLAmNALEUa9lOcB2uydVHOhXCmgPveH1ECgYAtShzLKM3MStaS8VnfsP+G
a6RgFRXrzEjVuWsfizfiQUgMcG5JM93Xyi9k9CGmNcKhIRuxqKVjc7DjgqGDNlMr
GacVpXIgmxhMoE2gVQcZHyIVNXQGn1nJfJuTFJt7FIUqPTohmLHOneqEvfcpgKor
2M4o+mLf6718pdUYp4hvEwKBgBSJhLBIz3cz5xwfgFphjHcEKvrTaYJjKXQ8m8cl
XCwFfHbpnWjODlBejt9OY1frXcAnr3Odgct0IW/8ZRjnOaGfooWH5vavKTbigiAF
qKLHxfMZT3a/rNQPa3wPiEU+4zQQqQLOkUCanIS3lJNqydxwjg9q74xfrT19Pk0o
SV6hAoGBAKfidUGqWGH3FugbgG2cm7rK54nh978brZLKglekR1RlRWKG7QpRP33v
D13y3BD1rRM3vguD2aABhwqbYVt1hjHA+mv+yDzJps08FtZIasiTRpm2mFanOD84
yKf+0/HMD2G45HzoMYdG6BdZ5HP1y4WFNfRoxjwCTnwyrDMNJhOl
-----END RSA PRIVATE KEY-----
...@@ -21,6 +21,7 @@ final class AccountCodes ...@@ -21,6 +21,7 @@ final class AccountCodes
// Cash (under 1206 = النقدية ومافي حكمها) // Cash (under 1206 = النقدية ومافي حكمها)
const CASH_ON_HAND = '12060101'; // الصندوق بالجنيه المصري const CASH_ON_HAND = '12060101'; // الصندوق بالجنيه المصري
const SUB_TREASURY_CASH = '12060102'; // صندوق الخزنة الفرعية - الأنشطة الرياضية
const CASH_AT_BANK = '12060201'; // البنك الرئيسي - جنيه مصري const CASH_AT_BANK = '12060201'; // البنك الرئيسي - جنيه مصري
// Receivables (under 1203/1204) // Receivables (under 1203/1204)
...@@ -138,6 +139,22 @@ final class AccountCodes ...@@ -138,6 +139,22 @@ final class AccountCodes
}; };
} }
public static function debitAccountForTreasury(string $method, ?int $treasuryId = null): string
{
if ($method !== 'cash' || $treasuryId === null) {
return self::debitAccountForMethod($method);
}
$db = \App\Core\App::getInstance()->db();
$treasury = $db->selectOne("SELECT account_code, type FROM treasuries WHERE id = ?", [$treasuryId]);
if ($treasury && $treasury['type'] === 'sub') {
return $treasury['account_code'] ?: self::SUB_TREASURY_CASH;
}
return self::CASH_ON_HAND;
}
public static function creditAccountForPaymentType(string $type): string public static function creditAccountForPaymentType(string $type): string
{ {
return match ($type) { return match ($type) {
......
...@@ -40,8 +40,13 @@ final class AccountingIntegrationService ...@@ -40,8 +40,13 @@ final class AccountingIntegrationService
return; return;
} }
// Determine debit account (where money goes) // Determine debit account (where money goes) — checks if payment was at a sub-treasury
$debitAccountCode = AccountCodes::debitAccountForMethod($method); $treasuryId = isset($data['treasury_id']) ? (int) $data['treasury_id'] : null;
if ($treasuryId === null && $paymentId > 0) {
$paymentRow = $db->selectOne("SELECT treasury_id FROM payments WHERE id = ?", [$paymentId]);
$treasuryId = $paymentRow && $paymentRow['treasury_id'] ? (int) $paymentRow['treasury_id'] : null;
}
$debitAccountCode = AccountCodes::debitAccountForTreasury($method, $treasuryId);
// Determine credit account (revenue type) // Determine credit account (revenue type)
$creditAccountCode = AccountCodes::creditAccountForPaymentType($type); $creditAccountCode = AccountCodes::creditAccountForPaymentType($type);
...@@ -1391,4 +1396,141 @@ final class AccountingIntegrationService ...@@ -1391,4 +1396,141 @@ final class AccountingIntegrationService
default => $type, default => $type,
}; };
} }
// ────────────────────────────────────────────────────────────
// TREASURY MODULE
// ────────────────────────────────────────────────────────────
/**
* Settlement received by main treasury.
* Dr. 12060101 (Main Cash) | Cr. 12060102 (Sub-Treasury Cash)
*/
public static function onTreasurySettlementReceived(array $data): void
{
$db = App::getInstance()->db();
$amount = (string) ($data['amount'] ?? '0.00');
$settlementId = (int) ($data['settlement_id'] ?? 0);
if (bccomp($amount, '0.00', 2) <= 0 || $settlementId <= 0) {
return;
}
$debitAccount = self::getAccountByCode(AccountCodes::CASH_ON_HAND);
$creditAccount = self::getAccountByCode(AccountCodes::SUB_TREASURY_CASH);
if (!$debitAccount || !$creditAccount) {
Logger::error("Treasury settlement auto-post failed: accounts not found", ['settlement_id' => $settlementId]);
return;
}
$settlement = $db->selectOne("SELECT settlement_number FROM treasury_settlements WHERE id = ?", [$settlementId]);
$refNumber = $settlement['settlement_number'] ?? '';
$description = 'تسوية من الخزنة الفرعية — ' . $refNumber;
$lines = [
[
'account_id' => (int) $debitAccount['id'],
'debit' => $amount,
'credit' => '0.00',
'description_ar' => $description,
],
[
'account_id' => (int) $creditAccount['id'],
'debit' => '0.00',
'credit' => $amount,
'description_ar' => $description,
],
];
$result = JournalService::createEntry([
'entry_date' => date('Y-m-d'),
'description_ar' => $description,
'description_en' => 'Sub-treasury settlement — ' . $refNumber,
'reference_type' => 'treasury_settlement',
'reference_id' => $settlementId,
'reference_number' => $refNumber,
'source_module' => 'treasury',
'is_auto_generated' => 1,
], $lines, true);
if ($result['success'] && !empty($result['entry_id'])) {
$db->update('treasury_settlements', ['journal_entry_id' => (int) $result['entry_id'], 'updated_at' => date('Y-m-d H:i:s')], '`id` = ?', [$settlementId]);
} elseif (!$result['success']) {
Logger::error("Treasury settlement journal entry failed", ['settlement_id' => $settlementId, 'error' => $result['error'] ?? 'unknown']);
}
}
/**
* Bank deposit confirmed by accounting manager.
* Dr. 12060201 (Cash at Bank) | Cr. 12060101 (Main Cash)
*/
public static function onTreasuryDepositConfirmed(array $data): void
{
$db = App::getInstance()->db();
$amount = (string) ($data['amount'] ?? '0.00');
$depositId = (int) ($data['deposit_id'] ?? 0);
$bankAccountId = $data['bank_account_id'] ?? null;
if (bccomp($amount, '0.00', 2) <= 0 || $depositId <= 0) {
return;
}
$debitAccountCode = AccountCodes::CASH_AT_BANK;
if ($bankAccountId) {
$bankAccount = $db->selectOne("SELECT gl_account_id FROM bank_accounts WHERE id = ?", [(int) $bankAccountId]);
if ($bankAccount && $bankAccount['gl_account_id']) {
$glAccount = $db->selectOne("SELECT account_code FROM chart_of_accounts WHERE id = ?", [(int) $bankAccount['gl_account_id']]);
if ($glAccount) {
$debitAccountCode = $glAccount['account_code'];
}
}
}
$debitAccount = self::getAccountByCode($debitAccountCode);
$creditAccount = self::getAccountByCode(AccountCodes::CASH_ON_HAND);
if (!$debitAccount || !$creditAccount) {
Logger::error("Treasury deposit auto-post failed: accounts not found", ['deposit_id' => $depositId]);
return;
}
$deposit = $db->selectOne("SELECT deposit_number, deposit_date, bank_receipt_serial FROM treasury_deposits WHERE id = ?", [$depositId]);
$refNumber = $deposit['deposit_number'] ?? '';
$description = 'إيداع بنكي — ' . $refNumber;
if (!empty($deposit['bank_receipt_serial'])) {
$description .= ' — سيريال: ' . $deposit['bank_receipt_serial'];
}
$lines = [
[
'account_id' => (int) $debitAccount['id'],
'debit' => $amount,
'credit' => '0.00',
'description_ar' => $description,
],
[
'account_id' => (int) $creditAccount['id'],
'debit' => '0.00',
'credit' => $amount,
'description_ar' => $description,
],
];
$result = JournalService::createEntry([
'entry_date' => $deposit['deposit_date'] ?? date('Y-m-d'),
'description_ar' => $description,
'description_en' => 'Bank deposit — ' . $refNumber,
'reference_type' => 'treasury_deposit',
'reference_id' => $depositId,
'reference_number' => $refNumber,
'source_module' => 'treasury',
'is_auto_generated' => 1,
], $lines, true);
if ($result['success'] && !empty($result['entry_id'])) {
$db->update('treasury_deposits', ['journal_entry_id' => (int) $result['entry_id'], 'updated_at' => date('Y-m-d H:i:s')], '`id` = ?', [$depositId]);
} elseif (!$result['success']) {
Logger::error("Treasury deposit journal entry failed", ['deposit_id' => $depositId, 'error' => $result['error'] ?? 'unknown']);
}
}
} }
...@@ -16,8 +16,8 @@ ...@@ -16,8 +16,8 @@
<input type="text" name="account_name_ar" class="form-input" required> <input type="text" name="account_name_ar" class="form-input" required>
</div> </div>
<div> <div>
<label class="form-label">اسم الحساب (إنجليزي)</label> <label class="form-label">اسم الحساب (إنجليزي) <span style="color:#DC2626;">*</span></label>
<input type="text" name="account_name_en" class="form-input" dir="ltr"> <input type="text" name="account_name_en" class="form-input" dir="ltr" required>
</div> </div>
<div> <div>
<label class="form-label">اسم البنك (عربي) <span style="color:#DC2626;">*</span></label> <label class="form-label">اسم البنك (عربي) <span style="color:#DC2626;">*</span></label>
......
...@@ -408,6 +408,25 @@ EventBus::listen('member.dropped', function (array $data): void { ...@@ -408,6 +408,25 @@ EventBus::listen('member.dropped', function (array $data): void {
} }
}, 50); }, 50);
// ── Treasury Module ─────────────────────────────────────────
// Settlement received by main treasury: Dr. Main Cash, Cr. Sub Cash
EventBus::listen('treasury.settlement.received', function (array $data): void {
try {
AccountingIntegrationService::onTreasurySettlementReceived($data);
} catch (\Throwable $e) {
\App\Core\Logger::error('Accounting auto-post failed (treasury.settlement.received): ' . $e->getMessage());
}
}, 50);
// Bank deposit confirmed by accounting manager: Dr. Bank, Cr. Main Cash
EventBus::listen('treasury.deposit.confirmed', function (array $data): void {
try {
AccountingIntegrationService::onTreasuryDepositConfirmed($data);
} catch (\Throwable $e) {
\App\Core\Logger::error('Accounting auto-post failed (treasury.deposit.confirmed): ' . $e->getMessage());
}
}, 50);
// ── Statement Integration ─────────────────────────────────── // ── Statement Integration ───────────────────────────────────
// Auto-records customer/supplier transactions for account statements and credit limits // Auto-records customer/supplier transactions for account statements and credit limits
StatementIntegrationService::registerListeners(); StatementIntegrationService::registerListeners();
......
<?php
declare(strict_types=1);
namespace App\Modules\Cashier\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Cashier\Services\DepositService;
use App\Modules\Treasury\Services\CustodyService;
use App\Modules\Treasury\Services\TreasuryService;
class DepositController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('cashier.manage_deposits');
$filters = [
'status' => trim((string) $request->get('status', '')),
];
$deposits = DepositService::getDeposits($filters);
return $this->view('Cashier.Views.deposits.index', [
'deposits' => $deposits,
'filters' => $filters,
]);
}
public function create(Request $request): Response
{
$this->authorize('cashier.manage_deposits');
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$mainTreasury = TreasuryService::getMainTreasury();
$custodyBalance = '0.00';
if ($mainTreasury && $employee) {
$custodyBalance = CustodyService::getCurrentBalance((int) $mainTreasury['id'], (int) $employee->id);
}
$bankAccounts = $db->select(
"SELECT id, account_name_ar, bank_name_ar, account_number FROM bank_accounts WHERE is_active = 1 ORDER BY is_default DESC, account_name_ar"
);
return $this->view('Cashier.Views.deposits.create', [
'bankAccounts' => $bankAccounts,
'custodyBalance' => $custodyBalance,
]);
}
public function store(Request $request): Response
{
$this->authorize('cashier.manage_deposits');
$amount = trim((string) $request->post('amount', '0'));
$bankAccountId = (int) $request->post('bank_account_id', 0);
$depositDate = trim((string) $request->post('deposit_date', date('Y-m-d')));
$bankReceiptSerial = trim((string) $request->post('bank_receipt_serial', ''));
$notes = trim((string) $request->post('notes', ''));
if (bccomp($amount, '0.01', 2) < 0) {
return $this->redirect('/cashier/deposits/create')->withError('المبلغ يجب أن يكون أكبر من صفر');
}
if ($bankAccountId <= 0) {
return $this->redirect('/cashier/deposits/create')->withError('يجب اختيار حساب بنكي');
}
if ($bankReceiptSerial === '') {
return $this->redirect('/cashier/deposits/create')->withError('رقم إيصال البنك مطلوب');
}
// Handle file upload
$bankReceiptPath = null;
$uploadedFile = $_FILES['bank_receipt_image'] ?? null;
if ($uploadedFile && $uploadedFile['error'] === UPLOAD_ERR_OK && $uploadedFile['size'] > 0) {
$allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
if (!in_array($uploadedFile['type'], $allowedTypes, true)) {
return $this->redirect('/cashier/deposits/create')->withError('نوع الملف غير مسموح (JPG, PNG, WebP, PDF فقط)');
}
if ($uploadedFile['size'] > 5 * 1024 * 1024) {
return $this->redirect('/cashier/deposits/create')->withError('حجم الملف يجب ألا يتجاوز 5 ميجابايت');
}
$uploadDir = App::getInstance()->basePath() . '/public/uploads/deposits/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$ext = pathinfo($uploadedFile['name'], PATHINFO_EXTENSION);
$filename = 'dep_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
move_uploaded_file($uploadedFile['tmp_name'], $uploadDir . $filename);
$bankReceiptPath = 'uploads/deposits/' . $filename;
}
$result = DepositService::createDeposit([
'amount' => $amount,
'bank_account_id' => $bankAccountId,
'deposit_date' => $depositDate,
'bank_receipt_serial' => $bankReceiptSerial,
'bank_receipt_path' => $bankReceiptPath,
'notes' => $notes !== '' ? $notes : null,
]);
if (!$result['success']) {
return $this->redirect('/cashier/deposits/create')->withError($result['error']);
}
return $this->redirect('/cashier/deposits')->withSuccess(
'تم تسجيل الإيداع — رقم: ' . $result['deposit_number'] . '. في انتظار تأكيد مدير الحسابات.'
);
}
public function show(Request $request, string $id): Response
{
$this->authorize('cashier.manage_deposits');
$deposit = DepositService::find((int) $id);
if (!$deposit) {
return $this->redirect('/cashier/deposits')->withError('الإيداع غير موجود');
}
return $this->view('Cashier.Views.deposits.show', [
'deposit' => $deposit,
]);
}
public function confirm(Request $request, string $id): Response
{
$this->authorize('cashier.confirm_deposit');
$employee = App::getInstance()->currentEmployee();
$result = DepositService::confirmDeposit((int) $id, (int) $employee->id);
if (!$result['success']) {
return $this->redirect('/cashier/deposits/' . $id)->withError($result['error']);
}
return $this->redirect('/cashier/deposits')->withSuccess(
'تم تأكيد الإيداع — المبلغ: ' . money($result['amount']) . '. تم إخلاء العهدة.'
);
}
public function reject(Request $request, string $id): Response
{
$this->authorize('cashier.confirm_deposit');
$employee = App::getInstance()->currentEmployee();
$reason = trim((string) $request->post('reason', ''));
if ($reason === '') {
return $this->redirect('/cashier/deposits/' . $id)->withError('يجب إدخال سبب الرفض');
}
$result = DepositService::rejectDeposit((int) $id, (int) $employee->id, $reason);
if (!$result['success']) {
return $this->redirect('/cashier/deposits/' . $id)->withError($result['error']);
}
return $this->redirect('/cashier/deposits')->withSuccess('تم رفض الإيداع — المبلغ عاد لعهدة الخزنة');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Cashier\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Treasury\Services\SettlementService;
class SettlementReceiveController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('cashier.receive_settlement');
$settlements = SettlementService::getPendingSettlements();
return $this->view('Cashier.Views.settlements.index', [
'settlements' => $settlements,
]);
}
public function receive(Request $request, string $id): Response
{
$this->authorize('cashier.receive_settlement');
$employee = App::getInstance()->currentEmployee();
$result = SettlementService::receiveSettlement((int) $id, (int) $employee->id);
if (!$result['success']) {
return $this->redirect('/cashier/settlements')->withError($result['error']);
}
return $this->redirect('/cashier/settlements')->withSuccess(
'تم استلام التسوية — المبلغ: ' . money($result['amount'])
);
}
public function reject(Request $request, string $id): Response
{
$this->authorize('cashier.receive_settlement');
$employee = App::getInstance()->currentEmployee();
$reason = trim((string) $request->post('reason', ''));
if ($reason === '') {
return $this->redirect('/cashier/settlements')->withError('يجب إدخال سبب الرفض');
}
$result = SettlementService::rejectSettlement((int) $id, (int) $employee->id, $reason);
if (!$result['success']) {
return $this->redirect('/cashier/settlements')->withError($result['error']);
}
return $this->redirect('/cashier/settlements')->withSuccess('تم رفض التسوية');
}
}
...@@ -2,9 +2,23 @@ ...@@ -2,9 +2,23 @@
declare(strict_types=1); declare(strict_types=1);
return [ return [
// Payment queue
['GET', '/cashier', 'Cashier\Controllers\CashierController@queue', ['auth'], 'cashier.view_queue'], ['GET', '/cashier', 'Cashier\Controllers\CashierController@queue', ['auth'], 'cashier.view_queue'],
['GET', '/cashier/{id}', 'Cashier\Controllers\CashierController@process', ['auth'], 'cashier.process_payment'], ['GET', '/cashier/{id:\d+}', 'Cashier\Controllers\CashierController@process', ['auth'], 'cashier.process_payment'],
['POST', '/cashier/{id}/complete', 'Cashier\Controllers\CashierController@complete', ['auth', 'csrf'], 'cashier.process_payment'], ['POST', '/cashier/{id:\d+}/complete', 'Cashier\Controllers\CashierController@complete', ['auth', 'csrf'], 'cashier.process_payment'],
['POST', '/cashier/{id}/cancel', 'Cashier\Controllers\CashierController@cancel', ['auth', 'csrf'], 'cashier.cancel_request'], ['POST', '/cashier/{id:\d+}/cancel', 'Cashier\Controllers\CashierController@cancel', ['auth', 'csrf'], 'cashier.cancel_request'],
['POST', '/cashier/{id}/requeue', 'Cashier\Controllers\CashierController@requeue', ['auth', 'csrf'], 'cashier.cancel_request'], ['POST', '/cashier/{id:\d+}/requeue', 'Cashier\Controllers\CashierController@requeue', ['auth', 'csrf'], 'cashier.cancel_request'],
// Settlement receiving (from sub-treasuries)
['GET', '/cashier/settlements', 'Cashier\Controllers\SettlementReceiveController@index', ['auth'], 'cashier.receive_settlement'],
['POST', '/cashier/settlements/{id:\d+}/receive', 'Cashier\Controllers\SettlementReceiveController@receive', ['auth', 'csrf'], 'cashier.receive_settlement'],
['POST', '/cashier/settlements/{id:\d+}/reject', 'Cashier\Controllers\SettlementReceiveController@reject', ['auth', 'csrf'], 'cashier.receive_settlement'],
// Bank deposits
['GET', '/cashier/deposits', 'Cashier\Controllers\DepositController@index', ['auth'], 'cashier.manage_deposits'],
['GET', '/cashier/deposits/create', 'Cashier\Controllers\DepositController@create', ['auth'], 'cashier.manage_deposits'],
['POST', '/cashier/deposits', 'Cashier\Controllers\DepositController@store', ['auth', 'csrf'], 'cashier.manage_deposits'],
['GET', '/cashier/deposits/{id:\d+}', 'Cashier\Controllers\DepositController@show', ['auth'], 'cashier.manage_deposits'],
['POST', '/cashier/deposits/{id:\d+}/confirm', 'Cashier\Controllers\DepositController@confirm', ['auth', 'csrf'], 'cashier.confirm_deposit'],
['POST', '/cashier/deposits/{id:\d+}/reject', 'Cashier\Controllers\DepositController@reject', ['auth', 'csrf'], 'cashier.confirm_deposit'],
]; ];
<?php
declare(strict_types=1);
namespace App\Modules\Cashier\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
use App\Modules\Treasury\Services\CustodyService;
use App\Modules\Treasury\Services\TreasuryService;
final class DepositService
{
public static function createDeposit(array $data): array
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$amount = $data['amount'] ?? '0.00';
$bankAccountId = (int) ($data['bank_account_id'] ?? 0);
$depositDate = $data['deposit_date'] ?? date('Y-m-d');
$bankReceiptSerial = $data['bank_receipt_serial'] ?? null;
$bankReceiptPath = $data['bank_receipt_path'] ?? null;
$notes = $data['notes'] ?? null;
if (bccomp((string) $amount, '0.01', 2) < 0) {
return ['success' => false, 'error' => 'المبلغ يجب أن يكون أكبر من صفر'];
}
$mainTreasury = TreasuryService::getMainTreasury();
if (!$mainTreasury) {
return ['success' => false, 'error' => 'لم يتم تعريف الخزنة الرئيسية'];
}
$depositNumber = self::generateDepositNumber($db);
$now = date('Y-m-d H:i:s');
$id = $db->insert('treasury_deposits', [
'deposit_number' => $depositNumber,
'treasury_id' => (int) $mainTreasury['id'],
'bank_account_id' => $bankAccountId > 0 ? $bankAccountId : null,
'amount' => $amount,
'deposit_date' => $depositDate,
'bank_receipt_serial' => $bankReceiptSerial,
'bank_receipt_path' => $bankReceiptPath,
'deposited_by' => $employee ? (int) $employee->id : 0,
'deposited_at' => $now,
'status' => 'deposited',
'notes' => $notes,
'created_at' => $now,
'updated_at' => $now,
]);
CustodyService::recordDeposit(
(int) $mainTreasury['id'],
$employee ? (int) $employee->id : 0,
(string) $amount,
$id
);
EventBus::dispatch('treasury.deposit.created', [
'deposit_id' => $id,
'treasury_id' => (int) $mainTreasury['id'],
'amount' => (string) $amount,
'deposited_by' => $employee ? (int) $employee->id : 0,
]);
Logger::info("Treasury deposit created", ['id' => $id, 'amount' => $amount, 'serial' => $bankReceiptSerial]);
return [
'success' => true,
'deposit_id' => $id,
'deposit_number' => $depositNumber,
];
}
public static function confirmDeposit(int $depositId, int $confirmedBy): array
{
$db = App::getInstance()->db();
$deposit = $db->selectOne(
"SELECT * FROM treasury_deposits WHERE id = ? AND status = 'deposited'",
[$depositId]
);
if (!$deposit) {
return ['success' => false, 'error' => 'الإيداع غير موجود أو تمت معالجته'];
}
$now = date('Y-m-d H:i:s');
$db->update('treasury_deposits', [
'confirmed_by' => $confirmedBy,
'confirmed_at' => $now,
'status' => 'confirmed',
'updated_at' => $now,
], '`id` = ?', [$depositId]);
CustodyService::recordDepositConfirmed(
(int) $deposit['treasury_id'],
(int) $deposit['deposited_by'],
(string) $deposit['amount'],
$depositId
);
EventBus::dispatch('treasury.deposit.confirmed', [
'deposit_id' => $depositId,
'treasury_id' => (int) $deposit['treasury_id'],
'amount' => (string) $deposit['amount'],
'confirmed_by' => $confirmedBy,
'bank_account_id' => $deposit['bank_account_id'] ? (int) $deposit['bank_account_id'] : null,
]);
Logger::info("Treasury deposit confirmed", ['id' => $depositId, 'amount' => $deposit['amount']]);
return ['success' => true, 'amount' => (string) $deposit['amount']];
}
public static function rejectDeposit(int $depositId, int $rejectedBy, string $reason): array
{
$db = App::getInstance()->db();
$deposit = $db->selectOne(
"SELECT * FROM treasury_deposits WHERE id = ? AND status = 'deposited'",
[$depositId]
);
if (!$deposit) {
return ['success' => false, 'error' => 'الإيداع غير موجود أو تمت معالجته'];
}
$now = date('Y-m-d H:i:s');
$db->update('treasury_deposits', [
'confirmed_by' => $rejectedBy,
'confirmed_at' => $now,
'status' => 'rejected',
'rejection_reason' => $reason,
'updated_at' => $now,
], '`id` = ?', [$depositId]);
// Reverse custody deduction — amount goes back to main treasury manager
CustodyService::recordSettlementIn(
(int) $deposit['treasury_id'],
(int) $deposit['deposited_by'],
(string) $deposit['amount'],
$depositId
);
EventBus::dispatch('treasury.deposit.rejected', [
'deposit_id' => $depositId,
'reason' => $reason,
'rejected_by' => $rejectedBy,
]);
Logger::info("Treasury deposit rejected", ['id' => $depositId, 'reason' => $reason]);
return ['success' => true];
}
public static function getDeposits(array $filters = []): array
{
$db = App::getInstance()->db();
$where = '1=1';
$params = [];
$status = $filters['status'] ?? '';
if ($status !== '') {
$where .= ' AND d.status = ?';
$params[] = $status;
}
return $db->select(
"SELECT d.*, e_dep.full_name_ar as deposited_by_name, e_conf.full_name_ar as confirmed_by_name,
ba.account_name_ar as bank_name
FROM treasury_deposits d
LEFT JOIN employees e_dep ON e_dep.id = d.deposited_by
LEFT JOIN employees e_conf ON e_conf.id = d.confirmed_by
LEFT JOIN bank_accounts ba ON ba.id = d.bank_account_id
WHERE {$where}
ORDER BY d.id DESC
LIMIT 100",
$params
);
}
public static function getPendingDeposits(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT d.*, e.full_name_ar as deposited_by_name, ba.account_name_ar as bank_name
FROM treasury_deposits d
LEFT JOIN employees e ON e.id = d.deposited_by
LEFT JOIN bank_accounts ba ON ba.id = d.bank_account_id
WHERE d.status = 'deposited'
ORDER BY d.deposited_at ASC"
);
}
public static function find(int $depositId): ?array
{
$db = App::getInstance()->db();
return $db->selectOne(
"SELECT d.*, e_dep.full_name_ar as deposited_by_name, e_conf.full_name_ar as confirmed_by_name,
ba.account_name_ar as bank_name, ba.bank_name_ar as bank_institution
FROM treasury_deposits d
LEFT JOIN employees e_dep ON e_dep.id = d.deposited_by
LEFT JOIN employees e_conf ON e_conf.id = d.confirmed_by
LEFT JOIN bank_accounts ba ON ba.id = d.bank_account_id
WHERE d.id = ?",
[$depositId]
);
}
private static function generateDepositNumber($db): string
{
$year = date('Y');
$row = $db->selectOne(
"SELECT MAX(CAST(SUBSTRING(deposit_number, 10) AS UNSIGNED)) as max_num
FROM treasury_deposits WHERE deposit_number LIKE ?",
["DEP-{$year}-%"]
);
$next = ((int) ($row['max_num'] ?? 0)) + 1;
return sprintf('DEP-%s-%06d', $year, $next);
}
}
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إيداع بنكي جديد<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="max-width:600px;margin:0 auto;">
<!-- Custody Warning -->
<?php if (bccomp($custodyBalance, '0', 2) > 0): ?>
<div style="background:#FEF3C7;border:1px solid #FCD34D;border-radius:8px;padding:15px;margin-bottom:15px;">
<div style="font-size:13px;color:#92400E;">
<strong>رصيد العهدة الحالي:</strong>
<span style="font-size:18px;font-weight:700;"><?= money($custodyBalance) ?></span>
<br><span style="font-size:12px;">يجب إيداع هذا المبلغ في البنك</span>
</div>
</div>
<?php endif; ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;font-weight:600;">تسجيل إيداع بنكي</div>
<div style="padding:20px;">
<form method="POST" action="/cashier/deposits" enctype="multipart/form-data">
<?= csrf_field() ?>
<div style="margin-bottom:15px;">
<label class="form-label">المبلغ <span style="color:red;">*</span></label>
<input type="number" name="amount" class="form-input" step="0.01" min="0.01" required
value="<?= e($custodyBalance !== '0.00' ? $custodyBalance : '') ?>" placeholder="0.00">
</div>
<div style="margin-bottom:15px;">
<label class="form-label">الحساب البنكي <span style="color:red;">*</span></label>
<select name="bank_account_id" class="form-select" required>
<option value="">اختر الحساب...</option>
<?php foreach ($bankAccounts as $ba): ?>
<option value="<?= (int) $ba['id'] ?>">
<?= e($ba['bank_name_ar'] ?? '') ?><?= e($ba['account_name_ar']) ?> (<?= e($ba['account_number']) ?>)
</option>
<?php endforeach; ?>
</select>
</div>
<div style="margin-bottom:15px;">
<label class="form-label">تاريخ الإيداع <span style="color:red;">*</span></label>
<input type="date" name="deposit_date" class="form-input" required value="<?= date('Y-m-d') ?>">
</div>
<div style="margin-bottom:15px;">
<label class="form-label">رقم إيصال البنك / السيريال <span style="color:red;">*</span></label>
<input type="text" name="bank_receipt_serial" class="form-input" required placeholder="رقم الإيصال من البنك...">
</div>
<div style="margin-bottom:15px;">
<label class="form-label">صورة إيصال البنك</label>
<input type="file" name="bank_receipt_image" class="form-input" accept="image/*,.pdf">
<div style="font-size:11px;color:#9CA3AF;margin-top:3px;">JPG, PNG, WebP, PDF — حد أقصى 5 ميجابايت</div>
</div>
<div style="margin-bottom:15px;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-input" rows="2" placeholder="ملاحظات إضافية..."></textarea>
</div>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary" style="flex:1;">تسجيل الإيداع</button>
<a href="/cashier/deposits" class="btn btn-outline">رجوع</a>
</div>
</form>
</div>
</div>
</div>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إيداعات البنك<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<span style="font-weight:600;">سجل الإيداعات البنكية</span>
<a href="/cashier/deposits/create" class="btn btn-primary" style="font-size:13px;">إيداع جديد</a>
</div>
<!-- Filters -->
<div style="padding:10px 20px;border-bottom:1px solid #F3F4F6;">
<form method="GET" action="/cashier/deposits" style="display:flex;gap:10px;align-items:center;">
<select name="status" class="form-select" style="width:auto;">
<option value="">الكل</option>
<option value="deposited" <?= ($filters['status'] ?? '') === 'deposited' ? 'selected' : '' ?>>في انتظار التأكيد</option>
<option value="confirmed" <?= ($filters['status'] ?? '') === 'confirmed' ? 'selected' : '' ?>>مؤكد</option>
<option value="rejected" <?= ($filters['status'] ?? '') === 'rejected' ? 'selected' : '' ?>>مرفوض</option>
</select>
<button type="submit" class="btn btn-outline" style="font-size:12px;">تصفية</button>
</form>
</div>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>رقم الإيداع</th>
<th>البنك</th>
<th>المبلغ</th>
<th>التاريخ</th>
<th>رقم الإيصال</th>
<th>أودع بواسطة</th>
<th>الحالة</th>
<th></th>
</tr>
</thead>
<tbody>
<?php if (empty($deposits)): ?>
<tr><td colspan="8" style="text-align:center;padding:40px;color:#9CA3AF;">لا توجد إيداعات</td></tr>
<?php else: ?>
<?php foreach ($deposits as $d):
$statusColor = match($d['status']) {
'pending_deposit' => '#9CA3AF',
'deposited' => '#F59E0B',
'confirmed' => '#059669',
'rejected' => '#DC2626',
default => '#6B7280',
};
$statusLabel = match($d['status']) {
'pending_deposit' => 'لم يُودع',
'deposited' => 'في انتظار التأكيد',
'confirmed' => 'مؤكد',
'rejected' => 'مرفوض',
default => $d['status'],
};
?>
<tr>
<td>
<a href="/cashier/deposits/<?= (int) $d['id'] ?>" style="font-family:monospace;font-size:12px;color:#3B82F6;">
<?= e($d['deposit_number']) ?>
</a>
</td>
<td style="font-size:12px;"><?= e($d['bank_name'] ?? '—') ?></td>
<td style="font-weight:600;"><?= money($d['amount']) ?></td>
<td style="font-size:12px;"><?= e($d['deposit_date']) ?></td>
<td style="font-family:monospace;font-size:12px;"><?= e($d['bank_receipt_serial'] ?? '—') ?></td>
<td style="font-size:12px;"><?= e($d['deposited_by_name'] ?? '—') ?></td>
<td><span style="color:<?= $statusColor ?>;font-weight:600;font-size:12px;"><?= e($statusLabel) ?></span></td>
<td>
<?php if ($d['status'] === 'deposited'): ?>
<a href="/cashier/deposits/<?= (int) $d['id'] ?>" class="btn btn-outline" style="font-size:11px;padding:3px 8px;">تفاصيل</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تفاصيل الإيداع<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="max-width:700px;margin:0 auto;">
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;font-weight:600;">
إيداع: <?= e($deposit['deposit_number']) ?>
</div>
<div style="padding:20px;">
<?php
$statusColor = match($deposit['status']) {
'pending_deposit' => '#9CA3AF',
'deposited' => '#F59E0B',
'confirmed' => '#059669',
'rejected' => '#DC2626',
default => '#6B7280',
};
$statusLabel = match($deposit['status']) {
'pending_deposit' => 'لم يُودع',
'deposited' => 'في انتظار تأكيد مدير الحسابات',
'confirmed' => 'مؤكد — تم إخلاء العهدة',
'rejected' => 'مرفوض',
default => $deposit['status'],
};
?>
<div style="background:#F9FAFB;border-radius:8px;padding:15px;margin-bottom:15px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;font-size:14px;">
<div><span style="color:#6B7280;">الحالة:</span></div>
<div><span style="color:<?= $statusColor ?>;font-weight:700;"><?= e($statusLabel) ?></span></div>
<div><span style="color:#6B7280;">المبلغ:</span></div>
<div style="font-size:20px;font-weight:700;color:#059669;"><?= money($deposit['amount']) ?></div>
<div><span style="color:#6B7280;">البنك:</span></div>
<div><?= e($deposit['bank_name'] ?? '—') ?> <?= $deposit['bank_institution'] ? '(' . e($deposit['bank_institution']) . ')' : '' ?></div>
<div><span style="color:#6B7280;">تاريخ الإيداع:</span></div>
<div><?= e($deposit['deposit_date']) ?></div>
<div><span style="color:#6B7280;">رقم إيصال البنك:</span></div>
<div style="font-family:monospace;font-weight:600;"><?= e($deposit['bank_receipt_serial'] ?? '—') ?></div>
<div><span style="color:#6B7280;">أودع بواسطة:</span></div>
<div><?= e($deposit['deposited_by_name'] ?? '—') ?><?= e($deposit['deposited_at'] ?? '') ?></div>
<?php if ($deposit['confirmed_by_name']): ?>
<div><span style="color:#6B7280;"><?= $deposit['status'] === 'rejected' ? 'رفض بواسطة' : 'أكد بواسطة' ?>:</span></div>
<div><?= e($deposit['confirmed_by_name']) ?><?= e($deposit['confirmed_at'] ?? '') ?></div>
<?php endif; ?>
<?php if ($deposit['rejection_reason']): ?>
<div><span style="color:#6B7280;">سبب الرفض:</span></div>
<div style="color:#DC2626;"><?= e($deposit['rejection_reason']) ?></div>
<?php endif; ?>
</div>
</div>
<?php if ($deposit['bank_receipt_path']): ?>
<div style="margin-bottom:15px;">
<label class="form-label">صورة الإيصال:</label>
<?php if (str_ends_with($deposit['bank_receipt_path'], '.pdf')): ?>
<a href="/<?= e($deposit['bank_receipt_path']) ?>" target="_blank" class="btn btn-outline" style="font-size:12px;">فتح PDF</a>
<?php else: ?>
<img src="/<?= e($deposit['bank_receipt_path']) ?>" style="max-width:100%;border-radius:8px;border:1px solid #E5E7EB;margin-top:5px;" alt="إيصال البنك">
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Actions -->
<?php if ($deposit['status'] === 'deposited'): ?>
<div style="display:flex;gap:10px;margin-top:20px;padding-top:15px;border-top:1px solid #E5E7EB;">
<form method="POST" action="/cashier/deposits/<?= (int) $deposit['id'] ?>/confirm" style="flex:1;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-primary" style="width:100%;"
onclick="return confirm('تأكيد أن الإيداع وصل فعلاً للبنك؟ سيتم إخلاء عهدة الخزنة.')">
تأكيد الإيداع
</button>
</form>
<div style="flex:1;">
<button type="button" class="btn btn-outline" style="width:100%;color:#DC2626;border-color:#DC2626;"
onclick="document.getElementById('reject-form').style.display='block'">
رفض
</button>
</div>
</div>
<form id="reject-form" method="POST" action="/cashier/deposits/<?= (int) $deposit['id'] ?>/reject" style="display:none;margin-top:10px;">
<?= csrf_field() ?>
<input type="text" name="reason" class="form-input" placeholder="سبب الرفض..." required style="margin-bottom:8px;">
<button type="submit" class="btn btn-outline" style="color:#DC2626;border-color:#DC2626;">تأكيد الرفض</button>
</form>
<?php endif; ?>
<div style="margin-top:15px;">
<a href="/cashier/deposits" class="btn btn-outline">العودة للقائمة</a>
</div>
</div>
</div>
</div>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>استلام التسويات — الخزنة الرئيسية<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;font-weight:600;">
تسويات واردة من الخزنة الفرعية
</div>
<?php if (empty($settlements)): ?>
<div style="padding:40px;text-align:center;color:#9CA3AF;">
لا توجد تسويات معلقة
</div>
<?php else: ?>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>رقم التسوية</th>
<th>من</th>
<th>المبلغ</th>
<th>عدد الإيصالات</th>
<th>بواسطة</th>
<th>التاريخ</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($settlements as $s): ?>
<tr>
<td style="font-family:monospace;font-size:12px;"><?= e($s['settlement_number']) ?></td>
<td><?= e($s['from_treasury_name'] ?? '—') ?></td>
<td style="font-weight:700;font-size:16px;color:#059669;"><?= money($s['amount']) ?></td>
<td><?= (int) $s['receipt_count'] ?></td>
<td style="font-size:12px;"><?= e($s['settled_by_name'] ?? '—') ?></td>
<td style="font-size:12px;"><?= e($s['settled_at'] ?? '') ?></td>
<td>
<div style="display:flex;gap:5px;">
<form method="POST" action="/cashier/settlements/<?= (int) $s['id'] ?>/receive" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-primary" style="font-size:12px;padding:4px 12px;"
onclick="return confirm('تأكيد استلام <?= money($s['amount']) ?>؟')">
استلام
</button>
</form>
<button type="button" class="btn btn-outline" style="font-size:12px;padding:4px 8px;color:#DC2626;border-color:#DC2626;"
onclick="document.getElementById('reject-<?= (int) $s['id'] ?>').style.display='block'">
رفض
</button>
</div>
<form id="reject-<?= (int) $s['id'] ?>" method="POST" action="/cashier/settlements/<?= (int) $s['id'] ?>/reject" style="display:none;margin-top:8px;">
<?= csrf_field() ?>
<input type="text" name="reason" class="form-input" placeholder="سبب الرفض..." required style="font-size:12px;margin-bottom:5px;">
<button type="submit" class="btn btn-outline" style="font-size:11px;padding:3px 8px;color:#DC2626;border-color:#DC2626;">تأكيد الرفض</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php $__template->endSection(); ?>
...@@ -6,22 +6,28 @@ use App\Core\Registries\PermissionRegistry; ...@@ -6,22 +6,28 @@ use App\Core\Registries\PermissionRegistry;
use App\Core\EventBus; use App\Core\EventBus;
MenuRegistry::register('cashier', [ MenuRegistry::register('cashier', [
'label_ar' => 'الخزينة', 'label_ar' => 'الخزينة الرئيسية',
'label_en' => 'Cashier', 'label_en' => 'Main Treasury',
'icon' => 'credit-card', 'icon' => 'credit-card',
'route' => '/cashier', 'route' => '/cashier',
'permission' => 'cashier.view_queue', 'permission' => 'cashier.view_queue',
'parent' => null, 'parent' => null,
'order' => 250, 'order' => 250,
'children' => [ 'children' => [
['label_ar' => 'طابور الدفع', 'label_en' => 'Payment Queue', 'route' => '/cashier', 'permission' => 'cashier.view_queue', 'order' => 1], ['label_ar' => 'طابور الدفع', 'label_en' => 'Payment Queue', 'route' => '/cashier', 'permission' => 'cashier.view_queue', 'order' => 1],
['label_ar' => 'استلام تسويات', 'label_en' => 'Receive Settlements','route' => '/cashier/settlements', 'permission' => 'cashier.receive_settlement', 'order' => 2],
['label_ar' => 'إيداعات البنك', 'label_en' => 'Bank Deposits', 'route' => '/cashier/deposits', 'permission' => 'cashier.manage_deposits', 'order' => 3],
], ],
]); ]);
PermissionRegistry::register('cashier', [ PermissionRegistry::register('cashier', [
'cashier.view_queue' => ['ar' => 'عرض طابور الخزينة', 'en' => 'View Cashier Queue'], 'cashier.view_queue' => ['ar' => 'عرض طابور الخزينة', 'en' => 'View Cashier Queue'],
'cashier.process_payment' => ['ar' => 'معالجة طلب دفع', 'en' => 'Process Payment Request'], 'cashier.process_payment' => ['ar' => 'معالجة طلب دفع', 'en' => 'Process Payment Request'],
'cashier.cancel_request' => ['ar' => 'إلغاء طلب دفع', 'en' => 'Cancel Payment Request'], 'cashier.cancel_request' => ['ar' => 'إلغاء طلب دفع', 'en' => 'Cancel Payment Request'],
'cashier.receive_settlement' => ['ar' => 'استلام تسوية من خزنة فرعية', 'en' => 'Receive Sub-Treasury Settlement'],
'cashier.manage_deposits' => ['ar' => 'إدارة إيداعات البنك', 'en' => 'Manage Bank Deposits'],
'cashier.confirm_deposit' => ['ar' => 'تأكيد إيداع بنكي', 'en' => 'Confirm Bank Deposit'],
'cashier.view_custody' => ['ar' => 'عرض رصيد عهدة الخزنة الرئيسية', 'en' => 'View Main Treasury Custody'],
]); ]);
// When a payment is voided, delegate to PaymentLifecycleService for status reversion // When a payment is voided, delegate to PaymentLifecycleService for status reversion
......
...@@ -10,6 +10,7 @@ use App\Core\App; ...@@ -10,6 +10,7 @@ use App\Core\App;
use App\Modules\Coaches\Models\Coach; use App\Modules\Coaches\Models\Coach;
use App\Modules\Coaches\Services\CoachService; use App\Modules\Coaches\Services\CoachService;
use App\Modules\Coaches\Services\CoachSchedulingService; use App\Modules\Coaches\Services\CoachSchedulingService;
use App\Modules\Members\Services\NationalIdParser;
class CoachController extends Controller class CoachController extends Controller
{ {
...@@ -49,15 +50,27 @@ class CoachController extends Controller ...@@ -49,15 +50,27 @@ class CoachController extends Controller
public function store(Request $request): Response public function store(Request $request): Response
{ {
$nationalId = trim((string) $request->post('national_id', '')) ?: null;
$dateOfBirth = trim((string) $request->post('date_of_birth', '')) ?: null;
$gender = trim((string) $request->post('gender', '')) ?: null;
if ($nationalId !== null && strlen($nationalId) === 14) {
$parsed = NationalIdParser::parse($nationalId);
if ($parsed['is_valid']) {
$dateOfBirth = $parsed['dob'];
$gender = $parsed['gender'];
}
}
$data = [ $data = [
'full_name_ar' => trim((string) $request->post('full_name_ar', '')), 'full_name_ar' => trim((string) $request->post('full_name_ar', '')),
'full_name_en' => trim((string) $request->post('full_name_en', '')) ?: null, 'full_name_en' => trim((string) $request->post('full_name_en', '')) ?: null,
'code' => trim((string) $request->post('code', '')), 'code' => trim((string) $request->post('code', '')),
'national_id' => trim((string) $request->post('national_id', '')) ?: null, 'national_id' => $nationalId,
'phone' => trim((string) $request->post('phone', '')) ?: null, 'phone' => trim((string) $request->post('phone', '')) ?: null,
'email' => trim((string) $request->post('email', '')) ?: null, 'email' => trim((string) $request->post('email', '')) ?: null,
'date_of_birth' => trim((string) $request->post('date_of_birth', '')) ?: null, 'date_of_birth' => $dateOfBirth,
'gender' => trim((string) $request->post('gender', '')) ?: null, 'gender' => $gender,
'bio_ar' => trim((string) $request->post('bio_ar', '')) ?: null, 'bio_ar' => trim((string) $request->post('bio_ar', '')) ?: null,
'employment_type' => trim((string) $request->post('employment_type', 'contract')), 'employment_type' => trim((string) $request->post('employment_type', 'contract')),
'max_players_default'=> (int) $request->post('max_players_default', 20), 'max_players_default'=> (int) $request->post('max_players_default', 20),
...@@ -165,15 +178,27 @@ class CoachController extends Controller ...@@ -165,15 +178,27 @@ class CoachController extends Controller
return $this->redirect('/coaches')->withError('المدرب غير موجود'); return $this->redirect('/coaches')->withError('المدرب غير موجود');
} }
$nationalId = trim((string) $request->post('national_id', '')) ?: null;
$dateOfBirth = trim((string) $request->post('date_of_birth', '')) ?: null;
$gender = trim((string) $request->post('gender', '')) ?: null;
if ($nationalId !== null && strlen($nationalId) === 14) {
$parsed = NationalIdParser::parse($nationalId);
if ($parsed['is_valid']) {
$dateOfBirth = $parsed['dob'];
$gender = $parsed['gender'];
}
}
$data = [ $data = [
'full_name_ar' => trim((string) $request->post('full_name_ar', '')), 'full_name_ar' => trim((string) $request->post('full_name_ar', '')),
'full_name_en' => trim((string) $request->post('full_name_en', '')) ?: null, 'full_name_en' => trim((string) $request->post('full_name_en', '')) ?: null,
'code' => trim((string) $request->post('code', '')), 'code' => trim((string) $request->post('code', '')),
'national_id' => trim((string) $request->post('national_id', '')) ?: null, 'national_id' => $nationalId,
'phone' => trim((string) $request->post('phone', '')) ?: null, 'phone' => trim((string) $request->post('phone', '')) ?: null,
'email' => trim((string) $request->post('email', '')) ?: null, 'email' => trim((string) $request->post('email', '')) ?: null,
'date_of_birth' => trim((string) $request->post('date_of_birth', '')) ?: null, 'date_of_birth' => $dateOfBirth,
'gender' => trim((string) $request->post('gender', '')) ?: null, 'gender' => $gender,
'bio_ar' => trim((string) $request->post('bio_ar', '')) ?: null, 'bio_ar' => trim((string) $request->post('bio_ar', '')) ?: null,
'employment_type' => trim((string) $request->post('employment_type', 'contract')), 'employment_type' => trim((string) $request->post('employment_type', 'contract')),
'max_players_default'=> (int) $request->post('max_players_default', 20), 'max_players_default'=> (int) $request->post('max_players_default', 20),
......
...@@ -62,15 +62,17 @@ ...@@ -62,15 +62,17 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">تاريخ الميلاد</label> <label class="form-label">تاريخ الميلاد</label>
<input type="date" name="date_of_birth" value="<?= e(old('date_of_birth') ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;" id="dobInput"> <input type="date" name="date_of_birth" value="<?= e(old('date_of_birth') ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;" id="dobInput" readonly>
<small id="nidStatus" style="display:none;margin-top:4px;font-size:11px;"></small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">النوع</label> <label class="form-label">النوع</label>
<select name="gender" class="form-select"> <select name="gender" class="form-select" id="genderSelect" disabled>
<option value="">-- اختر --</option> <option value="">-- اختر --</option>
<option value="male" <?= old('gender') === 'male' ? 'selected' : '' ?>>ذكر</option> <option value="male" <?= old('gender') === 'male' ? 'selected' : '' ?>>ذكر</option>
<option value="female" <?= old('gender') === 'female' ? 'selected' : '' ?>>أنثى</option> <option value="female" <?= old('gender') === 'female' ? 'selected' : '' ?>>أنثى</option>
</select> </select>
<input type="hidden" name="gender" id="genderHidden" value="<?= e(old('gender') ?? '') ?>">
</div> </div>
</div> </div>
</div> </div>
...@@ -193,20 +195,46 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -193,20 +195,46 @@ document.addEventListener('DOMContentLoaded', function() {
} }
var nidInput = document.getElementById('nidInput'); var nidInput = document.getElementById('nidInput');
var nidStatus = document.getElementById('nidStatus');
var dobInput = document.getElementById('dobInput');
var genderSelect = document.getElementById('genderSelect');
var genderHidden = document.getElementById('genderHidden');
if (nidInput) { if (nidInput) {
nidInput.addEventListener('input', function() { nidInput.addEventListener('input', function() {
var v = this.value.replace(/\D/g, ''); var v = this.value.replace(/\D/g, '');
this.value = v;
if (v.length === 14) { if (v.length === 14) {
var century = v[0] === '2' ? '19' : '20'; fetch('/api/members/parse-nid', {
var year = century + v.substring(1,3); method: 'POST',
var month = v.substring(3,5); headers: {'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},
var day = v.substring(5,7); body: JSON.stringify({national_id: v})
var dob = year + '-' + month + '-' + day; })
var dobInput = document.getElementById('dobInput'); .then(function(r) { return r.json(); })
if (dobInput) dobInput.value = dob; .then(function(data) {
var genderDigit = parseInt(v[12]); if (data.parsed && data.parsed.is_valid) {
var genderSelect = document.querySelector('[name=gender]'); dobInput.value = data.parsed.dob;
if (genderSelect) genderSelect.value = (genderDigit % 2 === 1) ? 'male' : 'female'; dobInput.style.background = '#ECFDF5';
genderSelect.value = data.parsed.gender;
genderHidden.value = data.parsed.gender;
genderSelect.style.background = '#ECFDF5';
nidStatus.style.display = 'block';
nidStatus.style.color = '#059669';
nidStatus.textContent = '✓ ' + data.parsed.governorate_name_ar + ' — ' + data.parsed.age_years + ' سنة';
} else {
nidStatus.style.display = 'block';
nidStatus.style.color = '#DC2626';
nidStatus.textContent = (data.parsed && data.parsed.errors) ? data.parsed.errors[0] : 'رقم قومي غير صالح';
}
});
} else {
nidStatus.style.display = 'none';
if (v.length === 0) {
dobInput.value = '';
dobInput.style.background = '';
dobInput.removeAttribute('readonly');
genderSelect.disabled = false;
genderSelect.style.background = '';
}
} }
}); });
} }
......
...@@ -55,7 +55,8 @@ ...@@ -55,7 +55,8 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">الرقم القومي</label> <label class="form-label">الرقم القومي</label>
<input type="text" name="national_id" value="<?= e(old('national_id') ?: ($coach->national_id ?? '')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;"> <input type="text" name="national_id" id="nidInput" value="<?= e(old('national_id') ?: ($coach->national_id ?? '')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;">
<small id="nidStatus" style="display:none;margin-top:4px;font-size:11px;"></small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">الهاتف</label> <label class="form-label">الهاتف</label>
...@@ -69,15 +70,16 @@ ...@@ -69,15 +70,16 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">تاريخ الميلاد</label> <label class="form-label">تاريخ الميلاد</label>
<input type="date" name="date_of_birth" value="<?= e(old('date_of_birth') ?: ($coach->date_of_birth ?? '')) ?>" class="form-input" style="direction:ltr;text-align:left;"> <input type="date" name="date_of_birth" id="dobInput" value="<?= e(old('date_of_birth') ?: ($coach->date_of_birth ?? '')) ?>" class="form-input" readonly style="background:#F9FAFB;direction:ltr;text-align:left;">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">النوع</label> <label class="form-label">النوع</label>
<select name="gender" class="form-select"> <select name="gender" id="genderSelect" class="form-select" disabled>
<option value="">-- اختر --</option> <option value="">-- اختر --</option>
<option value="male" <?= (old('gender') ?: ($coach->gender ?? '')) === 'male' ? 'selected' : '' ?>>ذكر</option> <option value="male" <?= (old('gender') ?: ($coach->gender ?? '')) === 'male' ? 'selected' : '' ?>>ذكر</option>
<option value="female" <?= (old('gender') ?: ($coach->gender ?? '')) === 'female' ? 'selected' : '' ?>>أنثى</option> <option value="female" <?= (old('gender') ?: ($coach->gender ?? '')) === 'female' ? 'selected' : '' ?>>أنثى</option>
</select> </select>
<input type="hidden" name="gender" id="genderHidden" value="<?= e(old('gender') ?: ($coach->gender ?? '')) ?>">
</div> </div>
</div> </div>
</div> </div>
...@@ -220,4 +222,47 @@ function addCertRow() { ...@@ -220,4 +222,47 @@ function addCertRow() {
} }
</script> </script>
<script>
(function(){
var nid = document.getElementById('nidInput');
var dob = document.getElementById('dobInput');
var genderSelect = document.getElementById('genderSelect');
var genderHidden = document.getElementById('genderHidden');
var status = document.getElementById('nidStatus');
if (!nid) return;
function parseNid(val) {
if (val.length !== 14) { status.style.display = 'none'; dob.readOnly = false; dob.style.background = ''; genderSelect.disabled = false; return; }
fetch('/api/members/parse-nid', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'},
body: JSON.stringify({national_id: val})
})
.then(function(r){ return r.json(); })
.then(function(d){
if (d.success && d.data) {
dob.value = d.data.dob || '';
dob.readOnly = true;
dob.style.background = '#F9FAFB';
var g = d.data.gender || '';
genderSelect.value = g;
genderSelect.disabled = true;
genderHidden.value = g;
status.style.display = 'block';
status.style.color = '#059669';
status.textContent = (d.data.governorate || '') + ' — ' + (d.data.age || '') + ' سنة';
} else {
status.style.display = 'block';
status.style.color = '#DC2626';
status.textContent = d.message || 'رقم قومي غير صحيح';
}
}).catch(function(){});
}
nid.addEventListener('input', function(){ parseNid(this.value.trim()); });
genderSelect.addEventListener('change', function(){ genderHidden.value = this.value; });
if (nid.value.trim().length === 14) parseNid(nid.value.trim());
})();
</script>
<?php $__template->endSection(); ?> <?php $__template->endSection(); ?>
...@@ -13,6 +13,7 @@ use App\Modules\Archive\Services\ArchiveService; ...@@ -13,6 +13,7 @@ use App\Modules\Archive\Services\ArchiveService;
use App\Modules\Rules\Services\RuleEngine; use App\Modules\Rules\Services\RuleEngine;
use App\Modules\Payments\Services\PaymentService; use App\Modules\Payments\Services\PaymentService;
use App\Modules\Cashier\Services\PaymentRequestService; use App\Modules\Cashier\Services\PaymentRequestService;
use App\Modules\Members\Services\NationalIdParser;
use App\Modules\ServiceCatalog\Models\ServicePrice; use App\Modules\ServiceCatalog\Models\ServicePrice;
use App\Modules\Members\Models\Member; use App\Modules\Members\Models\Member;
...@@ -273,6 +274,16 @@ class DeathController extends Controller ...@@ -273,6 +274,16 @@ class DeathController extends Controller
$data = $request->all(); $data = $request->all();
unset($data['_csrf_token']); unset($data['_csrf_token']);
// Auto-extract DOB and gender from national ID
$nid = $data['national_id'] ?? '';
if ($nid !== '' && strlen($nid) === 14) {
$parsed = NationalIdParser::parse($nid);
if ($parsed['is_valid']) {
$data['date_of_birth'] = $parsed['dob'];
$data['gender'] = $parsed['gender'];
}
}
$requiredFields = ['full_name_ar', 'national_id', 'date_of_birth', 'gender', 'qualification_id']; $requiredFields = ['full_name_ar', 'national_id', 'date_of_birth', 'gender', 'qualification_id'];
foreach ($requiredFields as $field) { foreach ($requiredFields as $field) {
if (empty($data[$field])) { if (empty($data[$field])) {
......
...@@ -24,18 +24,20 @@ ...@@ -24,18 +24,20 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">الرقم القومي <span style="color:#DC2626;">*</span></label> <label class="form-label">الرقم القومي <span style="color:#DC2626;">*</span></label>
<input type="text" name="national_id" value="<?= e($spouse['national_id'] ?? '') ?>" class="form-input" required maxlength="14"> <input type="text" name="national_id" value="<?= e($spouse['national_id'] ?? '') ?>" class="form-input" required maxlength="14" id="nidInput" style="direction:ltr;text-align:left;">
<small id="nidStatus" style="display:none;margin-top:4px;font-size:11px;"></small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">تاريخ الميلاد <span style="color:#DC2626;">*</span></label> <label class="form-label">تاريخ الميلاد <span style="color:#DC2626;">*</span></label>
<input type="date" name="date_of_birth" value="<?= e($spouse['date_of_birth'] ?? '') ?>" class="form-input" required> <input type="date" name="date_of_birth" value="<?= e($spouse['date_of_birth'] ?? '') ?>" class="form-input" required id="dobInput" readonly style="background:#F9FAFB;">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">النوع <span style="color:#DC2626;">*</span></label> <label class="form-label">النوع <span style="color:#DC2626;">*</span></label>
<select name="gender" class="form-select" required> <select name="gender" class="form-select" id="genderDisplay" disabled style="background:#F9FAFB;">
<option value="male" <?= ($spouse['gender'] ?? '') === 'male' ? 'selected' : '' ?>>ذكر</option> <option value="male" <?= ($spouse['gender'] ?? '') === 'male' ? 'selected' : '' ?>>ذكر</option>
<option value="female" <?= ($spouse['gender'] ?? '') === 'female' ? 'selected' : '' ?>>أنثى</option> <option value="female" <?= ($spouse['gender'] ?? '') === 'female' ? 'selected' : '' ?>>أنثى</option>
</select> </select>
<input type="hidden" name="gender" id="genderHidden" value="<?= e($spouse['gender'] ?? 'female') ?>">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">الجنسية</label> <label class="form-label">الجنسية</label>
...@@ -139,4 +141,60 @@ ...@@ -139,4 +141,60 @@
<a href="/death/<?= (int) $case['id'] ?>" class="btn btn-outline">← العودة</a> <a href="/death/<?= (int) $case['id'] ?>" class="btn btn-outline">← العودة</a>
</div> </div>
</form> </form>
<script>
document.addEventListener('DOMContentLoaded', function() {
var nidInput = document.getElementById('nidInput');
var nidStatus = document.getElementById('nidStatus');
var dobInput = document.getElementById('dobInput');
var genderDisplay = document.getElementById('genderDisplay');
var genderHidden = document.getElementById('genderHidden');
var govSelect = document.querySelector('[name=governorate]');
function parseNid(v) {
if (v.length !== 14) return;
fetch('/api/members/parse-nid', {
method: 'POST',
headers: {'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},
body: JSON.stringify({national_id: v})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.parsed && data.parsed.is_valid) {
dobInput.value = data.parsed.dob;
dobInput.style.background = '#ECFDF5';
genderDisplay.value = data.parsed.gender;
genderHidden.value = data.parsed.gender;
genderDisplay.style.background = '#ECFDF5';
if (govSelect && data.parsed.governorate_name_ar) {
for (var i = 0; i < govSelect.options.length; i++) {
if (govSelect.options[i].value === data.parsed.governorate_name_ar) {
govSelect.selectedIndex = i;
govSelect.style.background = '#ECFDF5';
break;
}
}
}
nidStatus.style.display = 'block';
nidStatus.style.color = '#059669';
nidStatus.textContent = '✓ ' + data.parsed.governorate_name_ar + ' — ' + data.parsed.age_years + ' سنة';
} else {
nidStatus.style.display = 'block';
nidStatus.style.color = '#DC2626';
nidStatus.textContent = (data.parsed && data.parsed.errors) ? data.parsed.errors[0] : 'رقم قومي غير صالح';
}
});
}
if (nidInput) {
nidInput.addEventListener('input', function() {
var v = this.value.replace(/\D/g, '');
this.value = v;
if (v.length === 14) parseNid(v);
else nidStatus.style.display = 'none';
});
if (nidInput.value.length === 14) parseNid(nidInput.value);
}
});
</script>
<?php $__template->endSection(); ?> <?php $__template->endSection(); ?>
...@@ -10,6 +10,7 @@ use App\Core\App; ...@@ -10,6 +10,7 @@ use App\Core\App;
use App\Core\EventBus; use App\Core\EventBus;
use App\Core\Logger; use App\Core\Logger;
use App\Modules\HR\Models\HrEmployeeProfile; use App\Modules\HR\Models\HrEmployeeProfile;
use App\Modules\Members\Services\NationalIdParser;
use App\Modules\HR\Models\HrDepartment; use App\Modules\HR\Models\HrDepartment;
use App\Modules\HR\Models\HrJobTitle; use App\Modules\HR\Models\HrJobTitle;
use App\Modules\HR\Models\HrSalaryStructure; use App\Modules\HR\Models\HrSalaryStructure;
...@@ -334,15 +335,27 @@ class EmployeeProfileController extends Controller ...@@ -334,15 +335,27 @@ class EmployeeProfileController extends Controller
private function extractData(Request $request): array private function extractData(Request $request): array
{ {
$nationalId = trim((string) $request->post('national_id', ''));
$dateOfBirth = trim((string) $request->post('date_of_birth', '')) ?: null;
$gender = trim((string) $request->post('gender', 'male'));
if ($nationalId !== '' && strlen($nationalId) === 14) {
$parsed = NationalIdParser::parse($nationalId);
if ($parsed['is_valid']) {
$dateOfBirth = $parsed['dob'];
$gender = $parsed['gender'];
}
}
return [ return [
'employee_id' => ((int) $request->post('employee_id', 0)) ?: null, 'employee_id' => ((int) $request->post('employee_id', 0)) ?: null,
'first_name_ar' => trim((string) $request->post('first_name_ar', '')), 'first_name_ar' => trim((string) $request->post('first_name_ar', '')),
'last_name_ar' => trim((string) $request->post('last_name_ar', '')), 'last_name_ar' => trim((string) $request->post('last_name_ar', '')),
'first_name_en' => trim((string) $request->post('first_name_en', '')) ?: null, 'first_name_en' => trim((string) $request->post('first_name_en', '')) ?: null,
'last_name_en' => trim((string) $request->post('last_name_en', '')) ?: null, 'last_name_en' => trim((string) $request->post('last_name_en', '')) ?: null,
'national_id' => trim((string) $request->post('national_id', '')), 'national_id' => $nationalId,
'date_of_birth' => trim((string) $request->post('date_of_birth', '')) ?: null, 'date_of_birth' => $dateOfBirth,
'gender' => trim((string) $request->post('gender', 'male')), 'gender' => $gender,
'marital_status' => trim((string) $request->post('marital_status', 'single')), 'marital_status' => trim((string) $request->post('marital_status', 'single')),
'religion' => trim((string) $request->post('religion', 'muslim')), 'religion' => trim((string) $request->post('religion', 'muslim')),
'phone' => trim((string) $request->post('phone', '')) ?: null, 'phone' => trim((string) $request->post('phone', '')) ?: null,
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
<?php endif; ?> <?php endif; ?>
<div> <div>
<label style="display:block;margin-bottom:4px;font-size:13px;">نوع العقد <span style="color:#DC2626;">*</span></label> <label style="display:block;margin-bottom:4px;font-size:13px;">نوع العقد <span style="color:#DC2626;">*</span></label>
<select name="contract_type" class="form-control"> <select name="contract_type" class="form-control" required>
<option value="definite" <?= (old('contract_type') ?? ($isEdit ? $contract->contract_type : '')) === 'definite' ? 'selected' : '' ?>>محدد المدة</option> <option value="definite" <?= (old('contract_type') ?? ($isEdit ? $contract->contract_type : '')) === 'definite' ? 'selected' : '' ?>>محدد المدة</option>
<option value="indefinite" <?= (old('contract_type') ?? ($isEdit ? $contract->contract_type : '')) === 'indefinite' ? 'selected' : '' ?>>غير محدد المدة</option> <option value="indefinite" <?= (old('contract_type') ?? ($isEdit ? $contract->contract_type : '')) === 'indefinite' ? 'selected' : '' ?>>غير محدد المدة</option>
</select> </select>
...@@ -48,12 +48,12 @@ ...@@ -48,12 +48,12 @@
<input type="number" name="notice_period_months" value="<?= e(old('notice_period_months') ?? ($isEdit ? $contract->notice_period_months : '2')) ?>" class="form-control" min="0"> <input type="number" name="notice_period_months" value="<?= e(old('notice_period_months') ?? ($isEdit ? $contract->notice_period_months : '2')) ?>" class="form-control" min="0">
</div> </div>
<div> <div>
<label style="display:block;margin-bottom:4px;font-size:13px;">الراتب الأساسي</label> <label style="display:block;margin-bottom:4px;font-size:13px;">الراتب الأساسي <span style="color:#DC2626;">*</span></label>
<input type="text" name="basic_salary" value="<?= e(old('basic_salary') ?? ($isEdit ? $contract->basic_salary : ($profile ? $profile->basic_salary : '0.00'))) ?>" class="form-control"> <input type="number" name="basic_salary" value="<?= e(old('basic_salary') ?? ($isEdit ? $contract->basic_salary : ($profile ? $profile->basic_salary : '0.00'))) ?>" class="form-control" step="0.01" min="0" required>
</div> </div>
<div> <div>
<label style="display:block;margin-bottom:4px;font-size:13px;">إجمالي الحزمة</label> <label style="display:block;margin-bottom:4px;font-size:13px;">إجمالي الحزمة <span style="color:#DC2626;">*</span></label>
<input type="text" name="total_package" value="<?= e(old('total_package') ?? ($isEdit ? $contract->total_package : ($profile ? $profile->basic_salary : '0.00'))) ?>" class="form-control"> <input type="number" name="total_package" value="<?= e(old('total_package') ?? ($isEdit ? $contract->total_package : ($profile ? $profile->basic_salary : '0.00'))) ?>" class="form-control" step="0.01" min="0" required>
<small style="color:#6B7280;">الراتب الأساسي + البدلات</small> <small style="color:#6B7280;">الراتب الأساسي + البدلات</small>
</div> </div>
<div style="grid-column:1/-1;"> <div style="grid-column:1/-1;">
......
...@@ -49,19 +49,21 @@ ...@@ -49,19 +49,21 @@
</div> </div>
<div> <div>
<label style="display:block;margin-bottom:4px;font-size:13px;">الرقم القومي <span style="color:#DC2626;">*</span></label> <label style="display:block;margin-bottom:4px;font-size:13px;">الرقم القومي <span style="color:#DC2626;">*</span></label>
<input type="text" name="national_id" value="<?= e(old('national_id') ?? ($isEdit ? $profile->national_id : '')) ?>" class="form-control" maxlength="14" required> <input type="text" name="national_id" value="<?= e(old('national_id') ?? ($isEdit ? $profile->national_id : '')) ?>" class="form-control" maxlength="14" required id="nidInput" style="direction:ltr;text-align:left;">
<small id="nidStatus" style="display:none;margin-top:4px;font-size:11px;"></small>
</div> </div>
<div> <div>
<label style="display:block;margin-bottom:4px;font-size:13px;">تاريخ الميلاد</label> <label style="display:block;margin-bottom:4px;font-size:13px;">تاريخ الميلاد</label>
<input type="date" name="date_of_birth" value="<?= e(old('date_of_birth') ?? ($isEdit ? $profile->date_of_birth : '')) ?>" class="form-control"> <input type="date" name="date_of_birth" value="<?= e(old('date_of_birth') ?? ($isEdit ? $profile->date_of_birth : '')) ?>" class="form-control" id="dobInput" readonly style="background:#F9FAFB;">
</div> </div>
<div> <div>
<label style="display:block;margin-bottom:4px;font-size:13px;">الجنس</label> <label style="display:block;margin-bottom:4px;font-size:13px;">الجنس</label>
<select name="gender" class="form-control"> <select name="gender" class="form-control" id="genderDisplay" disabled style="background:#F9FAFB;">
<?php foreach ($genders as $k => $v): ?> <?php foreach ($genders as $k => $v): ?>
<option value="<?= e($k) ?>" <?= (old('gender') ?? ($isEdit ? $profile->gender : 'male')) === $k ? 'selected' : '' ?>><?= e($v) ?></option> <option value="<?= e($k) ?>" <?= (old('gender') ?? ($isEdit ? $profile->gender : 'male')) === $k ? 'selected' : '' ?>><?= e($v) ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<input type="hidden" name="gender" id="genderHidden" value="<?= e(old('gender') ?? ($isEdit ? $profile->gender : 'male')) ?>">
</div> </div>
<div> <div>
<label style="display:block;margin-bottom:4px;font-size:13px;">الحالة الاجتماعية</label> <label style="display:block;margin-bottom:4px;font-size:13px;">الحالة الاجتماعية</label>
...@@ -147,7 +149,7 @@ ...@@ -147,7 +149,7 @@
<div style="padding:16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;"> <div style="padding:16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;">
<div> <div>
<label style="display:block;margin-bottom:4px;font-size:13px;">الراتب الأساسي <span style="color:#DC2626;">*</span></label> <label style="display:block;margin-bottom:4px;font-size:13px;">الراتب الأساسي <span style="color:#DC2626;">*</span></label>
<input type="text" name="basic_salary" value="<?= e(old('basic_salary') ?? ($isEdit ? $profile->basic_salary : '0.00')) ?>" class="form-control" required> <input type="number" name="basic_salary" value="<?= e(old('basic_salary') ?? ($isEdit ? $profile->basic_salary : '0.00')) ?>" class="form-control" step="0.01" min="0" required>
</div> </div>
<div> <div>
<label style="display:block;margin-bottom:4px;font-size:13px;">هيكل الرواتب</label> <label style="display:block;margin-bottom:4px;font-size:13px;">هيكل الرواتب</label>
...@@ -213,4 +215,50 @@ ...@@ -213,4 +215,50 @@
<button type="submit" class="btn btn-primary"><?= $isEdit ? 'تحديث' : 'حفظ' ?></button> <button type="submit" class="btn btn-primary"><?= $isEdit ? 'تحديث' : 'حفظ' ?></button>
</div> </div>
</form> </form>
<script>
document.addEventListener('DOMContentLoaded', function() {
var nidInput = document.getElementById('nidInput');
var nidStatus = document.getElementById('nidStatus');
var dobInput = document.getElementById('dobInput');
var genderDisplay = document.getElementById('genderDisplay');
var genderHidden = document.getElementById('genderHidden');
function parseNid(v) {
if (v.length !== 14) return;
fetch('/api/members/parse-nid', {
method: 'POST',
headers: {'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},
body: JSON.stringify({national_id: v})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.parsed && data.parsed.is_valid) {
dobInput.value = data.parsed.dob;
dobInput.style.background = '#ECFDF5';
genderDisplay.value = data.parsed.gender;
genderHidden.value = data.parsed.gender;
genderDisplay.style.background = '#ECFDF5';
nidStatus.style.display = 'block';
nidStatus.style.color = '#059669';
nidStatus.textContent = '✓ ' + data.parsed.governorate_name_ar + ' — ' + data.parsed.age_years + ' سنة';
} else {
nidStatus.style.display = 'block';
nidStatus.style.color = '#DC2626';
nidStatus.textContent = (data.parsed && data.parsed.errors) ? data.parsed.errors[0] : 'رقم قومي غير صالح';
}
});
}
if (nidInput) {
nidInput.addEventListener('input', function() {
var v = this.value.replace(/\D/g, '');
this.value = v;
if (v.length === 14) parseNid(v);
else nidStatus.style.display = 'none';
});
if (nidInput.value.length === 14) parseNid(nidInput.value);
}
});
</script>
<?php $__template->endSection(); ?> <?php $__template->endSection(); ?>
...@@ -11,17 +11,17 @@ ...@@ -11,17 +11,17 @@
<?php foreach ($employees as $emp): ?><option value="<?= (int) $emp['id'] ?>" <?= old('employee_profile_id') == $emp['id'] ? 'selected' : '' ?>><?= e($emp['first_name_ar'] . ' ' . $emp['last_name_ar']) ?></option><?php endforeach; ?> <?php foreach ($employees as $emp): ?><option value="<?= (int) $emp['id'] ?>" <?= old('employee_profile_id') == $emp['id'] ? 'selected' : '' ?>><?= e($emp['first_name_ar'] . ' ' . $emp['last_name_ar']) ?></option><?php endforeach; ?>
</select> </select>
</div> </div>
<div><label style="display:block;margin-bottom:4px;font-size:13px;">نوع السلفة</label> <div><label style="display:block;margin-bottom:4px;font-size:13px;">نوع السلفة <span style="color:#DC2626;">*</span></label>
<select name="loan_type" class="form-control"> <select name="loan_type" class="form-control" required>
<option value="salary_advance" <?= old('loan_type') === 'salary_advance' ? 'selected' : '' ?>>سلفة راتب</option> <option value="salary_advance" <?= old('loan_type') === 'salary_advance' ? 'selected' : '' ?>>سلفة راتب</option>
<option value="personal_loan" <?= old('loan_type') === 'personal_loan' ? 'selected' : '' ?>>قرض شخصي</option> <option value="personal_loan" <?= old('loan_type') === 'personal_loan' ? 'selected' : '' ?>>قرض شخصي</option>
<option value="emergency_loan" <?= old('loan_type') === 'emergency_loan' ? 'selected' : '' ?>>قرض طوارئ</option> <option value="emergency_loan" <?= old('loan_type') === 'emergency_loan' ? 'selected' : '' ?>>قرض طوارئ</option>
</select> </select>
</div> </div>
<div><label style="display:block;margin-bottom:4px;font-size:13px;">المبلغ <span style="color:#DC2626;">*</span></label><input type="text" name="loan_amount" value="<?= e(old('loan_amount') ?? '') ?>" class="form-control" placeholder="0.00" required></div> <div><label style="display:block;margin-bottom:4px;font-size:13px;">المبلغ <span style="color:#DC2626;">*</span></label><input type="number" name="loan_amount" value="<?= e(old('loan_amount') ?? '') ?>" class="form-control" step="0.01" min="0.01" placeholder="0.00" required></div>
<div><label style="display:block;margin-bottom:4px;font-size:13px;">عدد الأقساط <span style="color:#DC2626;">*</span></label><input type="number" name="number_of_installments" value="<?= e(old('number_of_installments') ?? '1') ?>" class="form-control" min="1" max="60" required></div> <div><label style="display:block;margin-bottom:4px;font-size:13px;">عدد الأقساط <span style="color:#DC2626;">*</span></label><input type="number" name="number_of_installments" value="<?= e(old('number_of_installments') ?? '1') ?>" class="form-control" min="1" max="60" required></div>
<div><label style="display:block;margin-bottom:4px;font-size:13px;">تاريخ الطلب</label><input type="date" name="request_date" value="<?= e(old('request_date') ?? date('Y-m-d')) ?>" class="form-control"></div> <div><label style="display:block;margin-bottom:4px;font-size:13px;">تاريخ الطلب</label><input type="date" name="request_date" value="<?= e(old('request_date') ?? date('Y-m-d')) ?>" class="form-control"></div>
<div><label style="display:block;margin-bottom:4px;font-size:13px;">بداية الخصم</label><input type="date" name="start_deduction_date" value="<?= e(old('start_deduction_date') ?? date('Y-m-01', strtotime('+1 month'))) ?>" class="form-control"></div> <div><label style="display:block;margin-bottom:4px;font-size:13px;">بداية الخصم <span style="color:#DC2626;">*</span></label><input type="date" name="start_deduction_date" value="<?= e(old('start_deduction_date') ?? date('Y-m-01', strtotime('+1 month'))) ?>" class="form-control" required></div>
<div style="grid-column:1/-1;"><label style="display:block;margin-bottom:4px;font-size:13px;">السبب</label><textarea name="reason" class="form-control" rows="2"><?= e(old('reason') ?? '') ?></textarea></div> <div style="grid-column:1/-1;"><label style="display:block;margin-bottom:4px;font-size:13px;">السبب</label><textarea name="reason" class="form-control" rows="2"><?= e(old('reason') ?? '') ?></textarea></div>
<div style="grid-column:1/-1;"><label style="display:block;margin-bottom:4px;font-size:13px;">ملاحظات</label><textarea name="notes" class="form-control" rows="2"><?= e(old('notes') ?? '') ?></textarea></div> <div style="grid-column:1/-1;"><label style="display:block;margin-bottom:4px;font-size:13px;">ملاحظات</label><textarea name="notes" class="form-control" rows="2"><?= e(old('notes') ?? '') ?></textarea></div>
</div> </div>
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
<div class="form-group"><label class="form-label">المبلغ الإجمالي <span style="color:#DC2626;">*</span></label><input type="number" name="total_amount" value="<?= e($member['membership_value'] ?? '') ?>" class="form-input" step="0.01" required style="direction:ltr;text-align:left;"></div> <div class="form-group"><label class="form-label">المبلغ الإجمالي <span style="color:#DC2626;">*</span></label><input type="number" name="total_amount" value="<?= e($member['membership_value'] ?? '') ?>" class="form-input" step="0.01" required style="direction:ltr;text-align:left;"></div>
<div class="form-group"><label class="form-label">المقدم (≥25%) <span style="color:#DC2626;">*</span></label><input type="number" name="down_payment" class="form-input" step="0.01" required style="direction:ltr;text-align:left;" placeholder="الحد الأدنى 25%"></div> <div class="form-group"><label class="form-label">المقدم (≥25%) <span style="color:#DC2626;">*</span></label><input type="number" name="down_payment" class="form-input" step="0.01" required style="direction:ltr;text-align:left;" placeholder="الحد الأدنى 25%"></div>
<div class="form-group"><label class="form-label">عدد الأشهر (≤30) <span style="color:#DC2626;">*</span></label><input type="number" name="number_of_months" class="form-input" min="1" max="30" required></div> <div class="form-group"><label class="form-label">عدد الأشهر (≤30) <span style="color:#DC2626;">*</span></label><input type="number" name="number_of_months" class="form-input" min="1" max="30" required></div>
<div class="form-group"><label class="form-label">تاريخ البداية</label><input type="date" name="start_date" value="<?= e(date('Y-m-d')) ?>" class="form-input"></div> <div class="form-group"><label class="form-label">تاريخ البداية <span style="color:#DC2626;">*</span></label><input type="date" name="start_date" value="<?= e(date('Y-m-d')) ?>" class="form-input" required></div>
<div class="form-group"><label class="form-label">رقم إيصال المقدم</label><input type="text" name="down_payment_receipt" class="form-input"></div> <div class="form-group"><label class="form-label">رقم إيصال المقدم</label><input type="text" name="down_payment_receipt" class="form-input"></div>
<div class="form-group" style="grid-column:1/-1;"><label class="form-label">ملاحظات</label><textarea name="notes" class="form-textarea" rows="2"></textarea></div> <div class="form-group" style="grid-column:1/-1;"><label class="form-label">ملاحظات</label><textarea name="notes" class="form-textarea" rows="2"></textarea></div>
</div> </div>
......
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
<div> <div>
<label style="font-size:11px;color:#6B7280;display:block;">تاريخ الميلاد</label> <label style="font-size:11px;color:#6B7280;display:block;">تاريخ الميلاد</label>
<strong id="dob_display" style="font-size:14px;color:#065F46;"></strong> <strong id="dob_display" style="font-size:14px;color:#065F46;"></strong>
<input type="hidden" name="date_of_birth" id="date_of_birth" value="<?= e(old('date_of_birth')) ?>"> <input type="hidden" name="date_of_birth" id="date_of_birth" value="<?= e(old('date_of_birth')) ?>" required>
</div> </div>
<div> <div>
<label style="font-size:11px;color:#6B7280;display:block;">السن</label> <label style="font-size:11px;color:#6B7280;display:block;">السن</label>
...@@ -67,7 +67,7 @@ ...@@ -67,7 +67,7 @@
<div> <div>
<label style="font-size:11px;color:#6B7280;display:block;">النوع</label> <label style="font-size:11px;color:#6B7280;display:block;">النوع</label>
<strong id="gender_display" style="font-size:14px;color:#065F46;"></strong> <strong id="gender_display" style="font-size:14px;color:#065F46;"></strong>
<input type="hidden" name="gender" id="gender_hidden" value="<?= e(old('gender')) ?>"> <input type="hidden" name="gender" id="gender_hidden" value="<?= e(old('gender')) ?>" required>
</div> </div>
<div> <div>
<label style="font-size:11px;color:#6B7280;display:block;">محافظة الميلاد</label> <label style="font-size:11px;color:#6B7280;display:block;">محافظة الميلاد</label>
......
...@@ -88,6 +88,8 @@ final class PaymentService ...@@ -88,6 +88,8 @@ final class PaymentService
'notes' => $data['notes'] ?? ($guestName ? $description . ' — ' . $guestName : $description), 'notes' => $data['notes'] ?? ($guestName ? $description . ' — ' . $guestName : $description),
'payment_date' => $data['payment_date'] ?? date('Y-m-d'), 'payment_date' => $data['payment_date'] ?? date('Y-m-d'),
'received_by_employee_id' => $employee ? (int) $employee->id : null, 'received_by_employee_id' => $employee ? (int) $employee->id : null,
'treasury_id' => isset($data['treasury_id']) ? (int) $data['treasury_id'] : null,
'session_id' => isset($data['session_id']) ? (int) $data['session_id'] : null,
'is_voided' => 0, 'is_voided' => 0,
'created_at' => date('Y-m-d H:i:s'), 'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'),
...@@ -132,6 +134,7 @@ final class PaymentService ...@@ -132,6 +134,7 @@ final class PaymentService
'payment_type' => $paymentType, 'payment_type' => $paymentType,
'amount' => $amount, 'amount' => $amount,
'method' => $paymentMethod, 'method' => $paymentMethod,
'treasury_id' => isset($data['treasury_id']) ? (int) $data['treasury_id'] : null,
]); ]);
Logger::info("Payment processed", [ Logger::info("Payment processed", [
......
...@@ -10,6 +10,7 @@ use App\Core\App; ...@@ -10,6 +10,7 @@ use App\Core\App;
use App\Core\Pagination; use App\Core\Pagination;
use App\Modules\SportsActivity\Models\Coach; use App\Modules\SportsActivity\Models\Coach;
use App\Modules\SportsActivity\Models\Discipline; use App\Modules\SportsActivity\Models\Discipline;
use App\Modules\Members\Services\NationalIdParser;
class CoachController extends Controller class CoachController extends Controller
{ {
...@@ -115,6 +116,14 @@ class CoachController extends Controller ...@@ -115,6 +116,14 @@ class CoachController extends Controller
return $this->redirect('/sa/coaches/create'); return $this->redirect('/sa/coaches/create');
} }
if ($nationalId !== '' && strlen($nationalId) === 14) {
$parsed = NationalIdParser::parse($nationalId);
if ($parsed['is_valid']) {
$dateOfBirth = $parsed['dob'];
$gender = $parsed['gender'];
}
}
$coach = Coach::create([ $coach = Coach::create([
'code' => $code, 'code' => $code,
'full_name_ar' => $fullNameAr, 'full_name_ar' => $fullNameAr,
...@@ -259,6 +268,14 @@ class CoachController extends Controller ...@@ -259,6 +268,14 @@ class CoachController extends Controller
return $this->redirect("/sa/coaches/{$id}/edit"); return $this->redirect("/sa/coaches/{$id}/edit");
} }
if ($nationalId !== '' && strlen($nationalId) === 14) {
$parsed = NationalIdParser::parse($nationalId);
if ($parsed['is_valid']) {
$dateOfBirth = $parsed['dob'];
$gender = $parsed['gender'];
}
}
$db->update('sa_coaches', [ $db->update('sa_coaches', [
'code' => $code, 'code' => $code,
'full_name_ar' => $fullNameAr, 'full_name_ar' => $fullNameAr,
......
...@@ -8,6 +8,7 @@ use App\Core\Request; ...@@ -8,6 +8,7 @@ use App\Core\Request;
use App\Core\Response; use App\Core\Response;
use App\Core\App; use App\Core\App;
use App\Core\Pagination; use App\Core\Pagination;
use App\Modules\Members\Services\NationalIdParser;
class PlayerController extends Controller class PlayerController extends Controller
{ {
...@@ -99,6 +100,15 @@ class PlayerController extends Controller ...@@ -99,6 +100,15 @@ class PlayerController extends Controller
$guardianRelationship = trim((string) $request->post('guardian_relationship', '')); $guardianRelationship = trim((string) $request->post('guardian_relationship', ''));
$notes = trim((string) $request->post('notes', '')); $notes = trim((string) $request->post('notes', ''));
// Auto-extract DOB and gender from national ID
if ($nationalId !== '' && strlen($nationalId) === 14) {
$parsed = NationalIdParser::parse($nationalId);
if ($parsed['is_valid']) {
$dateOfBirth = $parsed['dob'];
$gender = $parsed['gender'];
}
}
// Validation // Validation
$errors = []; $errors = [];
if ($fullNameAr === '') { if ($fullNameAr === '') {
...@@ -245,6 +255,15 @@ class PlayerController extends Controller ...@@ -245,6 +255,15 @@ class PlayerController extends Controller
$guardianRelationship = trim((string) $request->post('guardian_relationship', '')); $guardianRelationship = trim((string) $request->post('guardian_relationship', ''));
$notes = trim((string) $request->post('notes', '')); $notes = trim((string) $request->post('notes', ''));
// Auto-extract DOB and gender from national ID
if ($nationalId !== '' && strlen($nationalId) === 14) {
$parsed = NationalIdParser::parse($nationalId);
if ($parsed['is_valid']) {
$dateOfBirth = $parsed['dob'];
$gender = $parsed['gender'];
}
}
// Validation // Validation
$errors = []; $errors = [];
if ($fullNameAr === '') { if ($fullNameAr === '') {
......
...@@ -56,6 +56,8 @@ final class SaPaymentService ...@@ -56,6 +56,8 @@ final class SaPaymentService
'visa_reference' => $extra['visa_reference'] ?? null, 'visa_reference' => $extra['visa_reference'] ?? null,
'transfer_reference' => $extra['transfer_reference'] ?? null, 'transfer_reference' => $extra['transfer_reference'] ?? null,
'transfer_bank' => $extra['transfer_bank'] ?? null, 'transfer_bank' => $extra['transfer_bank'] ?? null,
'treasury_id' => $extra['treasury_id'] ?? null,
'session_id' => $extra['session_id'] ?? null,
]; ];
if ($memberId === null && !empty($subscription['player_name'])) { if ($memberId === null && !empty($subscription['player_name'])) {
...@@ -129,6 +131,8 @@ final class SaPaymentService ...@@ -129,6 +131,8 @@ final class SaPaymentService
'visa_reference' => $extra['visa_reference'] ?? null, 'visa_reference' => $extra['visa_reference'] ?? null,
'transfer_reference' => $extra['transfer_reference'] ?? null, 'transfer_reference' => $extra['transfer_reference'] ?? null,
'transfer_bank' => $extra['transfer_bank'] ?? null, 'transfer_bank' => $extra['transfer_bank'] ?? null,
'treasury_id' => $extra['treasury_id'] ?? null,
'session_id' => $extra['session_id'] ?? null,
]; ];
if ($memberId === null && !empty($booking['booker_name'])) { if ($memberId === null && !empty($booking['booker_name'])) {
......
...@@ -34,7 +34,8 @@ ...@@ -34,7 +34,8 @@
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:20px;margin-top:15px;"> <div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group"> <div class="form-group">
<label class="form-label">الرقم القومي</label> <label class="form-label">الرقم القومي</label>
<input type="text" name="national_id" value="<?= e(old('national_id')) ?>" class="form-input" placeholder="14 رقم" maxlength="14" style="direction:ltr;text-align:left;"> <input type="text" name="national_id" id="nidInput" value="<?= e(old('national_id')) ?>" class="form-input" placeholder="14 رقم" maxlength="14" style="direction:ltr;text-align:left;">
<small id="nidStatus" style="display:none;margin-top:4px;font-size:11px;"></small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">الهاتف</label> <label class="form-label">الهاتف</label>
...@@ -46,17 +47,18 @@ ...@@ -46,17 +47,18 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">تاريخ الميلاد</label> <label class="form-label">تاريخ الميلاد</label>
<input type="date" name="date_of_birth" value="<?= e(old('date_of_birth')) ?>" class="form-input" style="direction:ltr;text-align:left;"> <input type="date" name="date_of_birth" id="dobInput" value="<?= e(old('date_of_birth')) ?>" class="form-input" readonly style="direction:ltr;text-align:left;background:#F9FAFB;">
</div> </div>
</div> </div>
<div style="display:grid;grid-template-columns:1fr 3fr;gap:20px;margin-top:15px;"> <div style="display:grid;grid-template-columns:1fr 3fr;gap:20px;margin-top:15px;">
<div class="form-group"> <div class="form-group">
<label class="form-label">النوع</label> <label class="form-label">النوع</label>
<select name="gender" class="form-select"> <select id="genderSelect" class="form-select" disabled>
<option value="">-- اختر --</option> <option value="">-- اختر --</option>
<option value="male" <?= old('gender') === 'male' ? 'selected' : '' ?>>ذكر</option> <option value="male" <?= old('gender') === 'male' ? 'selected' : '' ?>>ذكر</option>
<option value="female" <?= old('gender') === 'female' ? 'selected' : '' ?>>أنثى</option> <option value="female" <?= old('gender') === 'female' ? 'selected' : '' ?>>أنثى</option>
</select> </select>
<input type="hidden" name="gender" id="genderHidden" value="<?= e(old('gender')) ?>">
</div> </div>
</div> </div>
</div> </div>
...@@ -207,6 +209,45 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -207,6 +209,45 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
updateVisibility(); updateVisibility();
// NID auto-parse
var nid = document.getElementById('nidInput');
var dob = document.getElementById('dobInput');
var genderSelect = document.getElementById('genderSelect');
var genderHidden = document.getElementById('genderHidden');
var nidStatus = document.getElementById('nidStatus');
function parseNid(val) {
if (val.length !== 14) { nidStatus.style.display = 'none'; dob.readOnly = false; dob.style.background = ''; genderSelect.disabled = false; return; }
fetch('/api/members/parse-nid', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'},
body: JSON.stringify({national_id: val})
})
.then(function(r){ return r.json(); })
.then(function(d){
if (d.success && d.data) {
dob.value = d.data.dob || '';
dob.readOnly = true;
dob.style.background = '#F9FAFB';
var g = d.data.gender || '';
genderSelect.value = g;
genderSelect.disabled = true;
genderHidden.value = g;
nidStatus.style.display = 'block';
nidStatus.style.color = '#059669';
nidStatus.textContent = (d.data.governorate || '') + ' — ' + (d.data.age || '') + ' سنة';
} else {
nidStatus.style.display = 'block';
nidStatus.style.color = '#DC2626';
nidStatus.textContent = d.message || 'رقم قومي غير صحيح';
}
}).catch(function(){});
}
nid.addEventListener('input', function(){ parseNid(this.value.trim()); });
genderSelect.addEventListener('change', function(){ genderHidden.value = this.value; });
if (nid.value.trim().length === 14) parseNid(nid.value.trim());
}); });
</script> </script>
<?php $__template->endSection(); ?> <?php $__template->endSection(); ?>
...@@ -43,7 +43,8 @@ $val = function(string $field) use ($coach) { ...@@ -43,7 +43,8 @@ $val = function(string $field) use ($coach) {
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:20px;margin-top:15px;"> <div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group"> <div class="form-group">
<label class="form-label">الرقم القومي</label> <label class="form-label">الرقم القومي</label>
<input type="text" name="national_id" value="<?= e($val('national_id')) ?>" class="form-input" placeholder="14 رقم" maxlength="14" style="direction:ltr;text-align:left;"> <input type="text" name="national_id" id="nidInput" value="<?= e($val('national_id')) ?>" class="form-input" placeholder="14 رقم" maxlength="14" style="direction:ltr;text-align:left;">
<small id="nidStatus" style="display:none;margin-top:4px;font-size:11px;"></small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">الهاتف</label> <label class="form-label">الهاتف</label>
...@@ -55,17 +56,18 @@ $val = function(string $field) use ($coach) { ...@@ -55,17 +56,18 @@ $val = function(string $field) use ($coach) {
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">تاريخ الميلاد</label> <label class="form-label">تاريخ الميلاد</label>
<input type="date" name="date_of_birth" value="<?= e($val('date_of_birth')) ?>" class="form-input" style="direction:ltr;text-align:left;"> <input type="date" name="date_of_birth" id="dobInput" value="<?= e($val('date_of_birth')) ?>" class="form-input" readonly style="direction:ltr;text-align:left;background:#F9FAFB;">
</div> </div>
</div> </div>
<div style="display:grid;grid-template-columns:1fr 3fr;gap:20px;margin-top:15px;"> <div style="display:grid;grid-template-columns:1fr 3fr;gap:20px;margin-top:15px;">
<div class="form-group"> <div class="form-group">
<label class="form-label">النوع</label> <label class="form-label">النوع</label>
<select name="gender" class="form-select"> <select id="genderSelect" class="form-select" disabled>
<option value="">-- اختر --</option> <option value="">-- اختر --</option>
<option value="male" <?= $val('gender') === 'male' ? 'selected' : '' ?>>ذكر</option> <option value="male" <?= $val('gender') === 'male' ? 'selected' : '' ?>>ذكر</option>
<option value="female" <?= $val('gender') === 'female' ? 'selected' : '' ?>>أنثى</option> <option value="female" <?= $val('gender') === 'female' ? 'selected' : '' ?>>أنثى</option>
</select> </select>
<input type="hidden" name="gender" id="genderHidden" value="<?= e($val('gender')) ?>">
</div> </div>
</div> </div>
</div> </div>
...@@ -239,6 +241,45 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -239,6 +241,45 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
updateVisibility(); updateVisibility();
// NID auto-parse
var nid = document.getElementById('nidInput');
var dob = document.getElementById('dobInput');
var genderSelect = document.getElementById('genderSelect');
var genderHidden = document.getElementById('genderHidden');
var nidStatus = document.getElementById('nidStatus');
function parseNid(val) {
if (val.length !== 14) { nidStatus.style.display = 'none'; dob.readOnly = false; dob.style.background = ''; genderSelect.disabled = false; return; }
fetch('/api/members/parse-nid', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'},
body: JSON.stringify({national_id: val})
})
.then(function(r){ return r.json(); })
.then(function(d){
if (d.success && d.data) {
dob.value = d.data.dob || '';
dob.readOnly = true;
dob.style.background = '#F9FAFB';
var g = d.data.gender || '';
genderSelect.value = g;
genderSelect.disabled = true;
genderHidden.value = g;
nidStatus.style.display = 'block';
nidStatus.style.color = '#059669';
nidStatus.textContent = (d.data.governorate || '') + ' — ' + (d.data.age || '') + ' سنة';
} else {
nidStatus.style.display = 'block';
nidStatus.style.color = '#DC2626';
nidStatus.textContent = d.message || 'رقم قومي غير صحيح';
}
}).catch(function(){});
}
nid.addEventListener('input', function(){ parseNid(this.value.trim()); });
genderSelect.addEventListener('change', function(){ genderHidden.value = this.value; });
if (nid.value.trim().length === 14) parseNid(nid.value.trim());
}); });
</script> </script>
<?php $__template->endSection(); ?> <?php $__template->endSection(); ?>
...@@ -40,18 +40,21 @@ $__template->layout('Layout.main'); ...@@ -40,18 +40,21 @@ $__template->layout('Layout.main');
</div> </div>
<div> <div>
<label class="form-label">الرقم القومي</label> <label class="form-label">الرقم القومي</label>
<input type="text" name="national_id" value="<?= e(old('national_id') ?? '') ?>" class="form-input" maxlength="14" dir="ltr"> <input type="text" name="national_id" value="<?= e(old('national_id') ?? '') ?>" class="form-input" maxlength="14" dir="ltr" id="nidInput">
<small id="nidStatus" style="display:none;margin-top:4px;font-size:11px;"></small>
</div> </div>
<div> <div>
<label class="form-label">تاريخ الميلاد</label> <label class="form-label">تاريخ الميلاد</label>
<input type="date" name="date_of_birth" value="<?= e(old('date_of_birth') ?? '') ?>" class="form-input"> <input type="date" name="date_of_birth" value="<?= e(old('date_of_birth') ?? '') ?>" class="form-input" id="dobInput" readonly style="background:#F9FAFB;">
<input type="hidden" name="age_years" id="ageYears">
</div> </div>
<div> <div>
<label class="form-label">النوع</label> <label class="form-label">النوع</label>
<select name="gender" class="form-select"> <select name="gender" class="form-select" id="genderInput" disabled style="background:#F9FAFB;">
<option value="male" <?= old('gender') === 'male' || old('gender') === '' ? 'selected' : '' ?>>ذكر</option> <option value="male" <?= old('gender') === 'male' || old('gender') === '' ? 'selected' : '' ?>>ذكر</option>
<option value="female" <?= old('gender') === 'female' ? 'selected' : '' ?>>أنثى</option> <option value="female" <?= old('gender') === 'female' ? 'selected' : '' ?>>أنثى</option>
</select> </select>
<input type="hidden" name="gender" id="genderHidden" value="<?= e(old('gender') ?? 'male') ?>">
</div> </div>
<div> <div>
<label class="form-label">الهاتف</label> <label class="form-label">الهاتف</label>
...@@ -125,6 +128,54 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -125,6 +128,54 @@ document.addEventListener('DOMContentLoaded', function() {
} }
playerType.addEventListener('change', toggleMemberId); playerType.addEventListener('change', toggleMemberId);
toggleMemberId(); toggleMemberId();
var nidInput = document.getElementById('nidInput');
var nidStatus = document.getElementById('nidStatus');
var dobInput = document.getElementById('dobInput');
var ageYears = document.getElementById('ageYears');
var genderInput = document.getElementById('genderInput');
var genderHidden = document.getElementById('genderHidden');
if (nidInput) {
nidInput.addEventListener('input', function() {
var v = this.value.replace(/\D/g, '');
this.value = v;
if (v.length === 14) {
fetch('/api/members/parse-nid', {
method: 'POST',
headers: {'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},
body: JSON.stringify({national_id: v})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.parsed && data.parsed.is_valid) {
dobInput.value = data.parsed.dob;
dobInput.style.background = '#ECFDF5';
ageYears.value = data.parsed.age_years;
genderInput.value = data.parsed.gender;
genderHidden.value = data.parsed.gender;
genderInput.style.background = '#ECFDF5';
nidStatus.style.display = 'block';
nidStatus.style.color = '#059669';
nidStatus.textContent = '✓ ' + data.parsed.governorate_name_ar + ' — ' + data.parsed.age_years + ' سنة';
} else {
nidStatus.style.display = 'block';
nidStatus.style.color = '#DC2626';
nidStatus.textContent = (data.parsed && data.parsed.errors) ? data.parsed.errors[0] : 'رقم قومي غير صالح';
}
});
} else {
nidStatus.style.display = 'none';
if (v.length === 0) {
dobInput.value = '';
dobInput.style.background = '#F9FAFB';
dobInput.removeAttribute('readonly');
genderInput.disabled = false;
genderInput.style.background = '';
}
}
});
}
}); });
</script> </script>
<?php $__template->endSection(); ?> <?php $__template->endSection(); ?>
...@@ -40,18 +40,20 @@ $__template->layout('Layout.main'); ...@@ -40,18 +40,20 @@ $__template->layout('Layout.main');
</div> </div>
<div> <div>
<label class="form-label">الرقم القومي</label> <label class="form-label">الرقم القومي</label>
<input type="text" name="national_id" value="<?= e(old('national_id') ?? $player['national_id'] ?? '') ?>" class="form-input" maxlength="14" dir="ltr"> <input type="text" name="national_id" id="nidInput" value="<?= e(old('national_id') ?? $player['national_id'] ?? '') ?>" class="form-input" maxlength="14" dir="ltr">
<small id="nidStatus" style="display:none;margin-top:4px;font-size:12px;"></small>
</div> </div>
<div> <div>
<label class="form-label">تاريخ الميلاد</label> <label class="form-label">تاريخ الميلاد</label>
<input type="date" name="date_of_birth" value="<?= e(old('date_of_birth') ?? $player['date_of_birth'] ?? '') ?>" class="form-input"> <input type="date" name="date_of_birth" id="dobInput" value="<?= e(old('date_of_birth') ?? $player['date_of_birth'] ?? '') ?>" class="form-input" readonly style="background:#F9FAFB;">
</div> </div>
<div> <div>
<label class="form-label">النوع</label> <label class="form-label">النوع</label>
<select name="gender" class="form-select"> <select id="genderSelect" class="form-select" disabled>
<option value="male" <?= (old('gender') ?? $player['gender'] ?? '') === 'male' ? 'selected' : '' ?>>ذكر</option> <option value="male" <?= (old('gender') ?? $player['gender'] ?? '') === 'male' ? 'selected' : '' ?>>ذكر</option>
<option value="female" <?= (old('gender') ?? $player['gender'] ?? '') === 'female' ? 'selected' : '' ?>>أنثى</option> <option value="female" <?= (old('gender') ?? $player['gender'] ?? '') === 'female' ? 'selected' : '' ?>>أنثى</option>
</select> </select>
<input type="hidden" name="gender" id="genderHidden" value="<?= e(old('gender') ?? $player['gender'] ?? 'male') ?>">
</div> </div>
<div> <div>
<label class="form-label">الهاتف</label> <label class="form-label">الهاتف</label>
...@@ -126,4 +128,47 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -126,4 +128,47 @@ document.addEventListener('DOMContentLoaded', function() {
playerType.addEventListener('change', toggleMemberId); playerType.addEventListener('change', toggleMemberId);
}); });
</script> </script>
<script>
(function(){
var nid = document.getElementById('nidInput');
var dob = document.getElementById('dobInput');
var genderSelect = document.getElementById('genderSelect');
var genderHidden = document.getElementById('genderHidden');
var status = document.getElementById('nidStatus');
if (!nid) return;
function parseNid(val) {
if (val.length !== 14) { status.style.display = 'none'; dob.readOnly = false; dob.style.background = ''; genderSelect.disabled = false; return; }
fetch('/api/members/parse-nid', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'},
body: JSON.stringify({national_id: val})
})
.then(function(r){ return r.json(); })
.then(function(d){
if (d.success && d.data) {
dob.value = d.data.dob || '';
dob.readOnly = true;
dob.style.background = '#F9FAFB';
var g = d.data.gender || '';
genderSelect.value = g;
genderSelect.disabled = true;
genderHidden.value = g;
status.style.display = 'block';
status.style.color = '#059669';
status.textContent = (d.data.governorate || '') + ' — ' + (d.data.age || '') + ' سنة';
} else {
status.style.display = 'block';
status.style.color = '#DC2626';
status.textContent = d.message || 'رقم قومي غير صحيح';
}
}).catch(function(){});
}
nid.addEventListener('input', function(){ parseNid(this.value.trim()); });
genderSelect.addEventListener('change', function(){ genderHidden.value = this.value; });
if (nid.value.trim().length === 14) parseNid(nid.value.trim());
})();
</script>
<?php $__template->endSection(); ?> <?php $__template->endSection(); ?>
...@@ -31,19 +31,21 @@ ...@@ -31,19 +31,21 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">الرقم القومي</label> <label class="form-label">الرقم القومي</label>
<input type="text" name="national_id" value="<?= e(old('national_id')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;"> <input type="text" name="national_id" id="nidInput" value="<?= e(old('national_id')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;">
<small id="nidStatus" style="display:none;margin-top:4px;font-size:11px;"></small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">تاريخ الميلاد <span style="color:#DC2626;">*</span></label> <label class="form-label">تاريخ الميلاد <span style="color:#DC2626;">*</span></label>
<input type="date" name="date_of_birth" value="<?= e(old('date_of_birth')) ?>" class="form-input" required> <input type="date" name="date_of_birth" id="dobInput" value="<?= e(old('date_of_birth')) ?>" class="form-input" required readonly style="background:#F9FAFB;">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">النوع <span style="color:#DC2626;">*</span></label> <label class="form-label">النوع <span style="color:#DC2626;">*</span></label>
<select name="gender" class="form-select" required> <select name="gender" id="genderSelect" class="form-select" required disabled>
<option value="">-- اختر --</option> <option value="">-- اختر --</option>
<option value="male" <?= old('gender') === 'male' ? 'selected' : '' ?>>ذكر</option> <option value="male" <?= old('gender') === 'male' ? 'selected' : '' ?>>ذكر</option>
<option value="female" <?= old('gender') === 'female' ? 'selected' : '' ?>>أنثى</option> <option value="female" <?= old('gender') === 'female' ? 'selected' : '' ?>>أنثى</option>
</select> </select>
<input type="hidden" name="gender" id="genderHidden" value="<?= e(old('gender')) ?>">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">صلة القرابة</label> <label class="form-label">صلة القرابة</label>
...@@ -74,4 +76,47 @@ ...@@ -74,4 +76,47 @@
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">إلغاء</a> <a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">إلغاء</a>
</div> </div>
</form> </form>
<script>
(function(){
var nid = document.getElementById('nidInput');
var dob = document.getElementById('dobInput');
var genderSelect = document.getElementById('genderSelect');
var genderHidden = document.getElementById('genderHidden');
var status = document.getElementById('nidStatus');
if (!nid) return;
function parseNid(val) {
if (val.length !== 14) { status.style.display = 'none'; dob.readOnly = false; dob.style.background = ''; genderSelect.disabled = false; return; }
fetch('/api/members/parse-nid', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'},
body: JSON.stringify({national_id: val})
})
.then(function(r){ return r.json(); })
.then(function(d){
if (d.success && d.data) {
dob.value = d.data.dob || '';
dob.readOnly = true;
dob.style.background = '#F9FAFB';
var g = d.data.gender || '';
genderSelect.value = g;
genderSelect.disabled = true;
genderHidden.value = g;
status.style.display = 'block';
status.style.color = '#059669';
status.textContent = (d.data.governorate || '') + ' — ' + (d.data.age || '') + ' سنة';
} else {
status.style.display = 'block';
status.style.color = '#DC2626';
status.textContent = d.message || 'رقم قومي غير صحيح';
}
}).catch(function(){});
}
nid.addEventListener('input', function(){ parseNid(this.value.trim()); });
genderSelect.addEventListener('change', function(){ genderHidden.value = this.value; });
if (nid.value.trim().length === 14) parseNid(nid.value.trim());
})();
</script>
<?php $__template->endSection(); ?> <?php $__template->endSection(); ?>
\ No newline at end of file
...@@ -15,6 +15,7 @@ use App\Modules\Payments\Services\PaymentService; ...@@ -15,6 +15,7 @@ use App\Modules\Payments\Services\PaymentService;
use App\Modules\Cashier\Services\PaymentRequestService; use App\Modules\Cashier\Services\PaymentRequestService;
use App\Modules\Forms\Services\FormBridge; use App\Modules\Forms\Services\FormBridge;
use App\Modules\Rules\Services\RuleEngine; use App\Modules\Rules\Services\RuleEngine;
use App\Modules\Members\Services\NationalIdParser;
class TransferController extends Controller class TransferController extends Controller
{ {
...@@ -140,6 +141,14 @@ class TransferController extends Controller ...@@ -140,6 +141,14 @@ class TransferController extends Controller
if (empty($recipientData['full_name_ar']) || empty($recipientData['national_id'])) { if (empty($recipientData['full_name_ar']) || empty($recipientData['national_id'])) {
return $this->redirect("/transfers/create/{$memberId}")->withError('بيانات المستلم مطلوبة (الاسم والرقم القومي)'); return $this->redirect("/transfers/create/{$memberId}")->withError('بيانات المستلم مطلوبة (الاسم والرقم القومي)');
} }
$rNid = $recipientData['national_id'] ?? '';
if ($rNid !== '' && strlen($rNid) === 14) {
$parsed = NationalIdParser::parse($rNid);
if ($parsed['is_valid']) {
$recipientData['date_of_birth'] = $parsed['dob'];
$recipientData['gender'] = $parsed['gender'];
}
}
} }
$employee = App::getInstance()->currentEmployee(); $employee = App::getInstance()->currentEmployee();
......
...@@ -59,18 +59,20 @@ ...@@ -59,18 +59,20 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">الرقم القومي <span style="color:#DC2626;">*</span></label> <label class="form-label">الرقم القومي <span style="color:#DC2626;">*</span></label>
<input type="text" name="recipient_national_id" class="form-input" maxlength="14"> <input type="text" name="recipient_national_id" id="recipientNidInput" class="form-input" maxlength="14" style="direction:ltr;text-align:left;">
<small id="recipientNidStatus" style="display:none;margin-top:4px;font-size:11px;"></small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">تاريخ الميلاد</label> <label class="form-label">تاريخ الميلاد</label>
<input type="date" name="recipient_date_of_birth" class="form-input"> <input type="date" name="recipient_date_of_birth" id="recipientDobInput" class="form-input" readonly style="background:#F9FAFB;">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">النوع</label> <label class="form-label">النوع</label>
<select name="recipient_gender" class="form-select"> <select name="recipient_gender" id="recipientGenderSelect" class="form-select" disabled>
<option value="male">ذكر</option> <option value="male">ذكر</option>
<option value="female">أنثى</option> <option value="female">أنثى</option>
</select> </select>
<input type="hidden" name="recipient_gender" id="recipientGenderHidden" value="male">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">الهاتف المحمول</label> <label class="form-label">الهاتف المحمول</label>
...@@ -252,6 +254,44 @@ ...@@ -252,6 +254,44 @@
childSelect.addEventListener('change', fetchFeePreview); childSelect.addEventListener('change', fetchFeePreview);
targetSpouses.addEventListener('change', updateChildrenAgeInputs); targetSpouses.addEventListener('change', updateChildrenAgeInputs);
targetChildren.addEventListener('change', updateChildrenAgeInputs); targetChildren.addEventListener('change', updateChildrenAgeInputs);
// Recipient NID auto-parse
var rNid = document.getElementById('recipientNidInput');
var rDob = document.getElementById('recipientDobInput');
var rGenderSelect = document.getElementById('recipientGenderSelect');
var rGenderHidden = document.getElementById('recipientGenderHidden');
var rStatus = document.getElementById('recipientNidStatus');
function parseRecipientNid(val) {
if (val.length !== 14) { rStatus.style.display = 'none'; rDob.readOnly = false; rDob.style.background = ''; rGenderSelect.disabled = false; return; }
fetch('/api/members/parse-nid', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'},
body: JSON.stringify({national_id: val})
})
.then(function(r){ return r.json(); })
.then(function(d){
if (d.success && d.data) {
rDob.value = d.data.dob || '';
rDob.readOnly = true;
rDob.style.background = '#F9FAFB';
var g = d.data.gender || '';
rGenderSelect.value = g;
rGenderSelect.disabled = true;
rGenderHidden.value = g;
rStatus.style.display = 'block';
rStatus.style.color = '#059669';
rStatus.textContent = (d.data.governorate || '') + ' — ' + (d.data.age || '') + ' سنة';
} else {
rStatus.style.display = 'block';
rStatus.style.color = '#DC2626';
rStatus.textContent = d.message || 'رقم قومي غير صحيح';
}
}).catch(function(){});
}
rNid.addEventListener('input', function(){ parseRecipientNid(this.value.trim()); });
rGenderSelect.addEventListener('change', function(){ rGenderHidden.value = this.value; });
})(); })();
</script> </script>
<?php $__template->endSection(); ?> <?php $__template->endSection(); ?>
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Treasury\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Treasury\Services\TreasuryService;
use App\Modules\Treasury\Services\SessionService;
class SessionController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('treasury.view_sessions');
$treasury = TreasuryService::getSubTreasury();
if (!$treasury) {
return $this->redirect('/treasury')->withError('لم يتم تعريف الخزنة الفرعية');
}
$sessions = SessionService::getSessionHistory((int) $treasury['id']);
return $this->view('Treasury.Views.sessions.index', [
'sessions' => $sessions,
'treasury' => $treasury,
]);
}
public function current(Request $request): Response
{
$this->authorize('treasury.open_session');
$employee = App::getInstance()->currentEmployee();
$treasury = TreasuryService::getSubTreasury();
if (!$treasury) {
return $this->redirect('/treasury')->withError('لم يتم تعريف الخزنة الفرعية');
}
$session = SessionService::getCurrentSession((int) $treasury['id'], (int) $employee->id);
if (!$session) {
return $this->redirect('/treasury')->withWarning('لا توجد وردية مفتوحة حالياً');
}
$db = App::getInstance()->db();
$payments = $db->select(
"SELECT p.*, m.full_name_ar as member_name, r.receipt_number
FROM payments p
LEFT JOIN members m ON m.id = p.member_id
LEFT JOIN receipts r ON r.payment_id = p.id
WHERE p.session_id = ? AND p.is_voided = 0
ORDER BY p.id DESC",
[(int) $session['id']]
);
return $this->view('Treasury.Views.sessions.current', [
'session' => $session,
'payments' => $payments,
'treasury' => $treasury,
]);
}
public function open(Request $request): Response
{
$this->authorize('treasury.open_session');
$employee = App::getInstance()->currentEmployee();
$treasury = TreasuryService::getSubTreasury();
if (!$treasury) {
return $this->redirect('/treasury')->withError('لم يتم تعريف الخزنة الفرعية');
}
$result = SessionService::openSession((int) $treasury['id'], (int) $employee->id);
if (!$result['success']) {
return $this->redirect('/treasury')->withError($result['error']);
}
return $this->redirect('/treasury')->withSuccess('تم فتح الوردية — رقم: ' . $result['session_number']);
}
public function close(Request $request, string $id): Response
{
$this->authorize('treasury.close_session');
$result = SessionService::closeSession((int) $id);
if (!$result['success']) {
return $this->redirect('/treasury/sessions/current')->withError($result['error']);
}
return $this->redirect('/treasury')->withSuccess(
'تم إغلاق الوردية — الإجمالي: ' . money($result['closing_balance']) . ' (' . $result['total_receipts'] . ' إيصال)'
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Treasury\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Treasury\Services\TreasuryService;
use App\Modules\Treasury\Services\SessionService;
use App\Modules\Treasury\Services\SettlementService;
class SettlementController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('treasury.view_settlements');
$treasury = TreasuryService::getSubTreasury();
if (!$treasury) {
return $this->redirect('/treasury')->withError('لم يتم تعريف الخزنة الفرعية');
}
$settlements = SettlementService::getSettlementHistory((int) $treasury['id']);
return $this->view('Treasury.Views.settlements.index', [
'settlements' => $settlements,
'treasury' => $treasury,
]);
}
public function create(Request $request): Response
{
$this->authorize('treasury.initiate_settlement');
$treasury = TreasuryService::getSubTreasury();
if (!$treasury) {
return $this->redirect('/treasury')->withError('لم يتم تعريف الخزنة الفرعية');
}
$db = App::getInstance()->db();
$closedSessions = $db->select(
"SELECT ts.*, e.full_name_ar as cashier_name
FROM treasury_sessions ts
LEFT JOIN employees e ON e.id = ts.cashier_id
WHERE ts.treasury_id = ? AND ts.status = 'closed'
ORDER BY ts.closed_at DESC
LIMIT 20",
[(int) $treasury['id']]
);
return $this->view('Treasury.Views.settlements.create', [
'sessions' => $closedSessions,
'treasury' => $treasury,
]);
}
public function store(Request $request): Response
{
$this->authorize('treasury.initiate_settlement');
$sessionId = (int) $request->post('session_id', 0);
if ($sessionId <= 0) {
return $this->redirect('/treasury/settlements/create')->withError('يجب اختيار وردية');
}
$result = SettlementService::initiateSettlement($sessionId);
if (!$result['success']) {
return $this->redirect('/treasury/settlements/create')->withError($result['error']);
}
return $this->redirect('/treasury/settlements')->withSuccess(
'تم إجراء التسوية — رقم: ' . $result['settlement_number'] . ' — المبلغ: ' . money($result['amount'])
);
}
public function show(Request $request, string $id): Response
{
$this->authorize('treasury.view_settlements');
$settlement = SettlementService::find((int) $id);
if (!$settlement) {
return $this->redirect('/treasury/settlements')->withError('التسوية غير موجودة');
}
return $this->view('Treasury.Views.settlements.show', [
'settlement' => $settlement,
]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Treasury\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Treasury\Services\TreasuryService;
use App\Modules\Treasury\Services\SessionService;
use App\Modules\Treasury\Services\CustodyService;
use App\Modules\Cashier\Services\PaymentRequestService;
class TreasuryController extends Controller
{
public function dashboard(Request $request): Response
{
$this->authorize('treasury.view_dashboard');
$employee = App::getInstance()->currentEmployee();
$treasury = TreasuryService::getSubTreasury();
if (!$treasury) {
return $this->view('Treasury.Views.dashboard', [
'error' => 'لم يتم تعريف الخزنة الفرعية',
'stats' => null,
]);
}
$stats = TreasuryService::getDashboardStats((int) $treasury['id'], (int) $employee->id);
return $this->view('Treasury.Views.dashboard', [
'treasury' => $treasury,
'stats' => $stats,
'error' => null,
]);
}
public function queue(Request $request): Response
{
$this->authorize('treasury.collect_payment');
$employee = App::getInstance()->currentEmployee();
$treasury = TreasuryService::getSubTreasury();
if (!$treasury) {
return $this->redirect('/treasury')->withError('لم يتم تعريف الخزنة الفرعية');
}
$currentSession = SessionService::getCurrentSession((int) $treasury['id'], (int) $employee->id);
if (!$currentSession) {
return $this->redirect('/treasury')->withError('يجب فتح وردية أولاً قبل التحصيل');
}
$filters = [
'status' => trim((string) $request->get('status', '')),
'search' => trim((string) $request->get('search', '')),
];
$requests = TreasuryService::getQueueForTreasury((int) $treasury['id'], $filters);
return $this->view('Treasury.Views.queue', [
'requests' => $requests,
'filters' => $filters,
'currentSession' => $currentSession,
'treasury' => $treasury,
]);
}
public function processForm(Request $request, string $id): Response
{
$this->authorize('treasury.collect_payment');
$db = App::getInstance()->db();
$pr = $db->selectOne(
"SELECT pr.*, m.full_name_ar as member_name, m.form_number, m.membership_number, m.phone_mobile,
e.full_name_ar as requested_by_name
FROM payment_requests pr
LEFT JOIN members m ON m.id = pr.member_id
LEFT JOIN employees e ON e.id = pr.requested_by
WHERE pr.id = ? AND pr.is_voided = 0",
[(int) $id]
);
if (!$pr) {
return $this->redirect('/treasury/queue')->withError('طلب الدفع غير موجود');
}
if ($pr['status'] === 'completed') {
return $this->redirect('/treasury/queue')->withError('طلب الدفع مكتمل بالفعل');
}
return $this->view('Treasury.Views.process', [
'pr' => $pr,
]);
}
public function collect(Request $request, string $id): Response
{
$this->authorize('treasury.collect_payment');
$employee = App::getInstance()->currentEmployee();
$treasury = TreasuryService::getSubTreasury();
if (!$treasury) {
return $this->redirect('/treasury/queue')->withError('لم يتم تعريف الخزنة الفرعية');
}
$currentSession = SessionService::getCurrentSession((int) $treasury['id'], (int) $employee->id);
if (!$currentSession) {
return $this->redirect('/treasury')->withError('يجب فتح وردية أولاً');
}
$paymentMethod = trim((string) $request->post('payment_method', 'cash'));
if ($paymentMethod === '') {
return $this->redirect('/treasury/queue/' . $id)->withError('طريقة الدفع مطلوبة');
}
$result = TreasuryService::collectPayment((int) $currentSession['id'], (int) $id, $paymentMethod);
if (!$result['success']) {
return $this->redirect('/treasury/queue/' . $id)->withError($result['error']);
}
$receiptId = $result['receipt_id'] ?? null;
$printParam = $receiptId ? '?print_receipt=' . $receiptId : '';
return $this->redirect('/treasury/queue' . $printParam)->withSuccess(
'تم تحصيل الدفعة — إيصال: ' . ($result['receipt_number'] ?? '')
);
}
public function createAndCollect(Request $request): Response
{
$this->authorize('treasury.collect_payment');
$employee = App::getInstance()->currentEmployee();
$treasury = TreasuryService::getSubTreasury();
if (!$treasury) {
return $this->redirect('/treasury/queue')->withError('لم يتم تعريف الخزنة الفرعية');
}
$currentSession = SessionService::getCurrentSession((int) $treasury['id'], (int) $employee->id);
if (!$currentSession) {
return $this->redirect('/treasury')->withError('يجب فتح وردية أولاً');
}
$memberId = (int) $request->post('member_id', 0);
$paymentType = trim((string) $request->post('payment_type', ''));
$amount = trim((string) $request->post('amount', '0'));
$paymentMethod = trim((string) $request->post('payment_method', 'cash'));
$description = trim((string) $request->post('description_ar', ''));
$entityType = trim((string) $request->post('related_entity_type', ''));
$entityId = (int) $request->post('related_entity_id', 0);
$errors = [];
if ($memberId <= 0) {
$errors[] = 'العضو مطلوب';
}
if ($paymentType === '') {
$errors[] = 'نوع الدفعة مطلوب';
}
if (bccomp($amount, '0.01', 2) < 0) {
$errors[] = 'المبلغ يجب أن يكون أكبر من صفر';
}
if ($paymentMethod === '') {
$errors[] = 'طريقة الدفع مطلوبة';
}
if (!empty($errors)) {
return $this->redirect('/treasury/queue')->withError(implode('، ', $errors));
}
$data = [
'member_id' => $memberId,
'payment_type' => $paymentType,
'amount' => $amount,
'description_ar' => $description,
'related_entity_type' => $entityType !== '' ? $entityType : null,
'related_entity_id' => $entityId > 0 ? $entityId : null,
'currency' => 'EGP',
];
$result = TreasuryService::createAndCollectPayment((int) $currentSession['id'], $data, $paymentMethod);
if (!$result['success']) {
return $this->redirect('/treasury/queue')->withError($result['error']);
}
$receiptId = $result['receipt_id'] ?? null;
$printParam = $receiptId ? '?print_receipt=' . $receiptId : '';
return $this->redirect('/treasury/queue' . $printParam)->withSuccess(
'تم إنشاء وتحصيل الدفعة — إيصال: ' . ($result['receipt_number'] ?? '')
);
}
public function custody(Request $request): Response
{
$this->authorize('treasury.view_custody');
$employee = App::getInstance()->currentEmployee();
$treasury = TreasuryService::getSubTreasury();
if (!$treasury) {
return $this->redirect('/treasury')->withError('لم يتم تعريف الخزنة الفرعية');
}
$log = CustodyService::getLog((int) $treasury['id'], (int) $employee->id);
$balance = CustodyService::getCurrentBalance((int) $treasury['id'], (int) $employee->id);
return $this->view('Treasury.Views.custody.index', [
'log' => $log,
'balance' => $balance,
'treasury' => $treasury,
]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Treasury\Models;
use App\Core\Model;
class Treasury extends Model
{
protected static string $table = 'treasuries';
protected static array $fillable = [
'code', 'name_ar', 'name_en', 'type', 'account_code', 'branch_id', 'is_active',
];
protected static bool $timestamps = true;
protected static bool $softDelete = false;
}
<?php
declare(strict_types=1);
namespace App\Modules\Treasury\Models;
use App\Core\Model;
class TreasuryCustodyLog extends Model
{
protected static string $table = 'treasury_custody_log';
protected static array $fillable = [
'treasury_id', 'employee_id', 'action', 'amount', 'balance_after',
'reference_type', 'reference_id', 'description_ar',
];
protected static bool $timestamps = false;
protected static bool $softDelete = false;
}
<?php
declare(strict_types=1);
namespace App\Modules\Treasury\Models;
use App\Core\Model;
class TreasuryDeposit extends Model
{
protected static string $table = 'treasury_deposits';
protected static array $fillable = [
'deposit_number', 'treasury_id', 'bank_account_id', 'amount', 'deposit_date',
'bank_receipt_serial', 'bank_receipt_path', 'deposited_by', 'deposited_at',
'confirmed_by', 'confirmed_at', 'status', 'rejection_reason', 'journal_entry_id', 'notes',
];
protected static bool $timestamps = true;
protected static bool $softDelete = false;
}
<?php
declare(strict_types=1);
namespace App\Modules\Treasury\Models;
use App\Core\Model;
class TreasurySession extends Model
{
protected static string $table = 'treasury_sessions';
protected static array $fillable = [
'treasury_id', 'cashier_id', 'session_number', 'opened_at', 'closed_at',
'opening_balance', 'closing_balance', 'total_collected', 'total_receipts',
'status', 'notes',
];
protected static bool $timestamps = true;
protected static bool $softDelete = false;
}
<?php
declare(strict_types=1);
namespace App\Modules\Treasury\Models;
use App\Core\Model;
class TreasurySettlement extends Model
{
protected static string $table = 'treasury_settlements';
protected static array $fillable = [
'settlement_number', 'from_treasury_id', 'to_treasury_id', 'session_id',
'amount', 'settled_by', 'received_by', 'settled_at', 'received_at',
'status', 'rejection_reason', 'receipt_count', 'journal_entry_id', 'notes',
];
protected static bool $timestamps = true;
protected static bool $softDelete = false;
}
<?php
declare(strict_types=1);
return [
// Dashboard
['GET', '/treasury', 'Treasury\Controllers\TreasuryController@dashboard', ['auth'], 'treasury.view_dashboard'],
// Payment queue
['GET', '/treasury/queue', 'Treasury\Controllers\TreasuryController@queue', ['auth'], 'treasury.collect_payment'],
['GET', '/treasury/queue/{id:\d+}', 'Treasury\Controllers\TreasuryController@processForm', ['auth'], 'treasury.collect_payment'],
['POST', '/treasury/queue/{id:\d+}/collect', 'Treasury\Controllers\TreasuryController@collect', ['auth', 'csrf'], 'treasury.collect_payment'],
['POST', '/treasury/collect', 'Treasury\Controllers\TreasuryController@createAndCollect', ['auth', 'csrf'], 'treasury.collect_payment'],
// Sessions
['GET', '/treasury/sessions', 'Treasury\Controllers\SessionController@index', ['auth'], 'treasury.view_sessions'],
['GET', '/treasury/sessions/current', 'Treasury\Controllers\SessionController@current', ['auth'], 'treasury.open_session'],
['POST', '/treasury/sessions/open', 'Treasury\Controllers\SessionController@open', ['auth', 'csrf'], 'treasury.open_session'],
['POST', '/treasury/sessions/{id:\d+}/close', 'Treasury\Controllers\SessionController@close', ['auth', 'csrf'], 'treasury.close_session'],
// Settlements
['GET', '/treasury/settlements', 'Treasury\Controllers\SettlementController@index', ['auth'], 'treasury.view_settlements'],
['GET', '/treasury/settlements/create', 'Treasury\Controllers\SettlementController@create', ['auth'], 'treasury.initiate_settlement'],
['POST', '/treasury/settlements', 'Treasury\Controllers\SettlementController@store', ['auth', 'csrf'], 'treasury.initiate_settlement'],
['GET', '/treasury/settlements/{id:\d+}', 'Treasury\Controllers\SettlementController@show', ['auth'], 'treasury.view_settlements'],
// Custody
['GET', '/treasury/custody', 'Treasury\Controllers\TreasuryController@custody', ['auth'], 'treasury.view_custody'],
];
<?php
declare(strict_types=1);
namespace App\Modules\Treasury\Services;
use App\Core\App;
use App\Core\Logger;
final class CustodyService
{
public static function recordCollection(int $treasuryId, int $employeeId, string $amount, int $paymentId, string $description = ''): void
{
$db = App::getInstance()->db();
$currentBalance = self::getCurrentBalance($treasuryId, $employeeId);
$newBalance = bcadd($currentBalance, $amount, 2);
$db->insert('treasury_custody_log', [
'treasury_id' => $treasuryId,
'employee_id' => $employeeId,
'action' => 'collection',
'amount' => $amount,
'balance_after' => $newBalance,
'reference_type' => 'payment',
'reference_id' => $paymentId,
'description_ar' => $description ?: 'تحصيل دفعة',
'created_at' => date('Y-m-d H:i:s'),
]);
}
public static function recordSettlementOut(int $treasuryId, int $employeeId, string $amount, int $settlementId): void
{
$db = App::getInstance()->db();
$currentBalance = self::getCurrentBalance($treasuryId, $employeeId);
$newBalance = bcsub($currentBalance, $amount, 2);
$db->insert('treasury_custody_log', [
'treasury_id' => $treasuryId,
'employee_id' => $employeeId,
'action' => 'settlement_out',
'amount' => $amount,
'balance_after' => $newBalance,
'reference_type' => 'settlement',
'reference_id' => $settlementId,
'description_ar' => 'تسوية للخزنة الرئيسية',
'created_at' => date('Y-m-d H:i:s'),
]);
}
public static function recordSettlementIn(int $treasuryId, int $employeeId, string $amount, int $settlementId): void
{
$db = App::getInstance()->db();
$currentBalance = self::getCurrentBalance($treasuryId, $employeeId);
$newBalance = bcadd($currentBalance, $amount, 2);
$db->insert('treasury_custody_log', [
'treasury_id' => $treasuryId,
'employee_id' => $employeeId,
'action' => 'settlement_in',
'amount' => $amount,
'balance_after' => $newBalance,
'reference_type' => 'settlement',
'reference_id' => $settlementId,
'description_ar' => 'استلام تسوية من خزنة فرعية',
'created_at' => date('Y-m-d H:i:s'),
]);
}
public static function recordDeposit(int $treasuryId, int $employeeId, string $amount, int $depositId): void
{
$db = App::getInstance()->db();
$currentBalance = self::getCurrentBalance($treasuryId, $employeeId);
$newBalance = bcsub($currentBalance, $amount, 2);
$db->insert('treasury_custody_log', [
'treasury_id' => $treasuryId,
'employee_id' => $employeeId,
'action' => 'deposit',
'amount' => $amount,
'balance_after' => $newBalance,
'reference_type' => 'deposit',
'reference_id' => $depositId,
'description_ar' => 'إيداع في البنك',
'created_at' => date('Y-m-d H:i:s'),
]);
}
public static function recordDepositConfirmed(int $treasuryId, int $employeeId, string $amount, int $depositId): void
{
$db = App::getInstance()->db();
$db->insert('treasury_custody_log', [
'treasury_id' => $treasuryId,
'employee_id' => $employeeId,
'action' => 'deposit_confirmed',
'amount' => $amount,
'balance_after' => self::getCurrentBalance($treasuryId, $employeeId),
'reference_type' => 'deposit',
'reference_id' => $depositId,
'description_ar' => 'تأكيد إيداع بنكي من مدير الحسابات',
'created_at' => date('Y-m-d H:i:s'),
]);
}
public static function recordVoidReversal(int $treasuryId, int $employeeId, string $amount, int $paymentId): void
{
$db = App::getInstance()->db();
$currentBalance = self::getCurrentBalance($treasuryId, $employeeId);
$newBalance = bcsub($currentBalance, $amount, 2);
$db->insert('treasury_custody_log', [
'treasury_id' => $treasuryId,
'employee_id' => $employeeId,
'action' => 'void_reversal',
'amount' => $amount,
'balance_after' => $newBalance,
'reference_type' => 'payment',
'reference_id' => $paymentId,
'description_ar' => 'إلغاء دفعة - عكس العهدة',
'created_at' => date('Y-m-d H:i:s'),
]);
}
public static function getCurrentBalance(int $treasuryId, int $employeeId): string
{
$db = App::getInstance()->db();
$last = $db->selectOne(
"SELECT balance_after FROM treasury_custody_log WHERE treasury_id = ? AND employee_id = ? ORDER BY id DESC LIMIT 1",
[$treasuryId, $employeeId]
);
return $last ? (string) $last['balance_after'] : '0.00';
}
public static function getTreasuryCustodyBalance(int $treasuryId): string
{
$db = App::getInstance()->db();
$result = $db->selectOne(
"SELECT COALESCE(
(SELECT balance_after FROM treasury_custody_log WHERE treasury_id = ? ORDER BY id DESC LIMIT 1),
0.00
) as balance",
[$treasuryId]
);
return (string) ($result['balance'] ?? '0.00');
}
public static function getLog(int $treasuryId, ?int $employeeId = null, int $limit = 100): array
{
$db = App::getInstance()->db();
$where = 'cl.treasury_id = ?';
$params = [$treasuryId];
if ($employeeId !== null) {
$where .= ' AND cl.employee_id = ?';
$params[] = $employeeId;
}
return $db->select(
"SELECT cl.*, e.full_name_ar as employee_name
FROM treasury_custody_log cl
LEFT JOIN employees e ON e.id = cl.employee_id
WHERE {$where}
ORDER BY cl.id DESC
LIMIT ?",
array_merge($params, [$limit])
);
}
public static function getActionLabel(string $action): string
{
return match ($action) {
'collection' => 'تحصيل',
'settlement_out' => 'تسوية صادرة',
'settlement_in' => 'تسوية واردة',
'deposit' => 'إيداع بنكي',
'deposit_confirmed' => 'تأكيد إيداع',
'void_reversal' => 'عكس إلغاء',
default => $action,
};
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Treasury\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
final class SessionService
{
public static function openSession(int $treasuryId, int $cashierId): array
{
$db = App::getInstance()->db();
$openSession = $db->selectOne(
"SELECT id FROM treasury_sessions WHERE treasury_id = ? AND status = 'open'",
[$treasuryId]
);
if ($openSession) {
return ['success' => false, 'error' => 'يوجد وردية مفتوحة بالفعل لهذه الخزنة'];
}
$sessionNumber = self::generateSessionNumber($db);
$now = date('Y-m-d H:i:s');
$id = $db->insert('treasury_sessions', [
'treasury_id' => $treasuryId,
'cashier_id' => $cashierId,
'session_number' => $sessionNumber,
'opened_at' => $now,
'opening_balance' => '0.00',
'total_collected' => '0.00',
'total_receipts' => 0,
'status' => 'open',
'created_at' => $now,
'updated_at' => $now,
]);
EventBus::dispatch('treasury.session.opened', [
'session_id' => $id,
'treasury_id' => $treasuryId,
'cashier_id' => $cashierId,
]);
Logger::info("Treasury session opened", ['id' => $id, 'number' => $sessionNumber, 'treasury' => $treasuryId]);
return [
'success' => true,
'session_id' => $id,
'session_number' => $sessionNumber,
];
}
public static function closeSession(int $sessionId): array
{
$db = App::getInstance()->db();
$session = $db->selectOne(
"SELECT * FROM treasury_sessions WHERE id = ? AND status = 'open'",
[$sessionId]
);
if (!$session) {
return ['success' => false, 'error' => 'الوردية غير موجودة أو مغلقة بالفعل'];
}
$totalCollected = $db->selectOne(
"SELECT COALESCE(SUM(amount), 0) as total, COUNT(*) as cnt
FROM payments WHERE session_id = ? AND is_voided = 0 AND payment_method = 'cash'",
[$sessionId]
);
$closingBalance = (string) ($totalCollected['total'] ?? '0.00');
$receiptCount = (int) ($totalCollected['cnt'] ?? 0);
$now = date('Y-m-d H:i:s');
$db->update('treasury_sessions', [
'closed_at' => $now,
'closing_balance' => $closingBalance,
'total_collected' => $closingBalance,
'total_receipts' => $receiptCount,
'status' => 'closed',
'updated_at' => $now,
], '`id` = ?', [$sessionId]);
EventBus::dispatch('treasury.session.closed', [
'session_id' => $sessionId,
'treasury_id' => (int) $session['treasury_id'],
'cashier_id' => (int) $session['cashier_id'],
'total_collected' => $closingBalance,
'total_receipts' => $receiptCount,
]);
Logger::info("Treasury session closed", ['id' => $sessionId, 'total' => $closingBalance]);
return [
'success' => true,
'closing_balance' => $closingBalance,
'total_receipts' => $receiptCount,
];
}
public static function getCurrentSession(int $treasuryId, ?int $cashierId = null): ?array
{
$db = App::getInstance()->db();
$sql = "SELECT ts.*, e.full_name_ar as cashier_name
FROM treasury_sessions ts
LEFT JOIN employees e ON e.id = ts.cashier_id
WHERE ts.treasury_id = ? AND ts.status = 'open'";
$params = [$treasuryId];
if ($cashierId !== null) {
$sql .= ' AND ts.cashier_id = ?';
$params[] = $cashierId;
}
$sql .= ' LIMIT 1';
return $db->selectOne($sql, $params);
}
public static function getSessionHistory(int $treasuryId, int $limit = 50): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT ts.*, e.full_name_ar as cashier_name
FROM treasury_sessions ts
LEFT JOIN employees e ON e.id = ts.cashier_id
WHERE ts.treasury_id = ?
ORDER BY ts.id DESC
LIMIT ?",
[$treasuryId, $limit]
);
}
public static function find(int $sessionId): ?array
{
$db = App::getInstance()->db();
return $db->selectOne(
"SELECT ts.*, e.full_name_ar as cashier_name, t.name_ar as treasury_name
FROM treasury_sessions ts
LEFT JOIN employees e ON e.id = ts.cashier_id
LEFT JOIN treasuries t ON t.id = ts.treasury_id
WHERE ts.id = ?",
[$sessionId]
);
}
private static function generateSessionNumber($db): string
{
$year = date('Y');
$row = $db->selectOne(
"SELECT MAX(CAST(SUBSTRING(session_number, 10) AS UNSIGNED)) as max_num
FROM treasury_sessions WHERE session_number LIKE ?",
["TSS-{$year}-%"]
);
$next = ((int) ($row['max_num'] ?? 0)) + 1;
return sprintf('TSS-%s-%06d', $year, $next);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Treasury\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
final class SettlementService
{
public static function initiateSettlement(int $sessionId): array
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$session = $db->selectOne(
"SELECT * FROM treasury_sessions WHERE id = ? AND status = 'closed'",
[$sessionId]
);
if (!$session) {
return ['success' => false, 'error' => 'الوردية غير موجودة أو لم تُغلق بعد'];
}
$existingSettlement = $db->selectOne(
"SELECT id FROM treasury_settlements WHERE session_id = ? AND status != 'rejected'",
[$sessionId]
);
if ($existingSettlement) {
return ['success' => false, 'error' => 'تم إجراء تسوية لهذه الوردية بالفعل'];
}
$amount = (string) $session['total_collected'];
if (bccomp($amount, '0.01', 2) < 0) {
return ['success' => false, 'error' => 'لا يوجد مبلغ للتسوية'];
}
$mainTreasury = $db->selectOne("SELECT id FROM treasuries WHERE type = 'main' AND is_active = 1 LIMIT 1");
if (!$mainTreasury) {
return ['success' => false, 'error' => 'لم يتم تعريف الخزنة الرئيسية'];
}
$receiptCount = (int) $session['total_receipts'];
$settlementNumber = self::generateSettlementNumber($db);
$now = date('Y-m-d H:i:s');
$id = $db->insert('treasury_settlements', [
'settlement_number' => $settlementNumber,
'from_treasury_id' => (int) $session['treasury_id'],
'to_treasury_id' => (int) $mainTreasury['id'],
'session_id' => $sessionId,
'amount' => $amount,
'settled_by' => $employee ? (int) $employee->id : 0,
'settled_at' => $now,
'status' => 'pending',
'receipt_count' => $receiptCount,
'created_at' => $now,
'updated_at' => $now,
]);
$db->update('treasury_sessions', [
'status' => 'settled',
'updated_at' => $now,
], '`id` = ?', [$sessionId]);
CustodyService::recordSettlementOut(
(int) $session['treasury_id'],
(int) $session['cashier_id'],
$amount,
$id
);
EventBus::dispatch('treasury.settlement.initiated', [
'settlement_id' => $id,
'from_treasury_id' => (int) $session['treasury_id'],
'to_treasury_id' => (int) $mainTreasury['id'],
'amount' => $amount,
'session_id' => $sessionId,
]);
Logger::info("Treasury settlement initiated", ['id' => $id, 'amount' => $amount, 'session' => $sessionId]);
return [
'success' => true,
'settlement_id' => $id,
'settlement_number' => $settlementNumber,
'amount' => $amount,
];
}
public static function receiveSettlement(int $settlementId, int $receivedBy): array
{
$db = App::getInstance()->db();
$settlement = $db->selectOne(
"SELECT * FROM treasury_settlements WHERE id = ? AND status = 'pending'",
[$settlementId]
);
if (!$settlement) {
return ['success' => false, 'error' => 'التسوية غير موجودة أو تمت معالجتها'];
}
$now = date('Y-m-d H:i:s');
$db->update('treasury_settlements', [
'received_by' => $receivedBy,
'received_at' => $now,
'status' => 'received',
'updated_at' => $now,
], '`id` = ?', [$settlementId]);
CustodyService::recordSettlementIn(
(int) $settlement['to_treasury_id'],
$receivedBy,
(string) $settlement['amount'],
$settlementId
);
EventBus::dispatch('treasury.settlement.received', [
'settlement_id' => $settlementId,
'from_treasury_id' => (int) $settlement['from_treasury_id'],
'to_treasury_id' => (int) $settlement['to_treasury_id'],
'amount' => (string) $settlement['amount'],
'received_by' => $receivedBy,
]);
Logger::info("Treasury settlement received", ['id' => $settlementId, 'amount' => $settlement['amount']]);
return ['success' => true, 'amount' => (string) $settlement['amount']];
}
public static function rejectSettlement(int $settlementId, int $rejectedBy, string $reason): array
{
$db = App::getInstance()->db();
$settlement = $db->selectOne(
"SELECT * FROM treasury_settlements WHERE id = ? AND status = 'pending'",
[$settlementId]
);
if (!$settlement) {
return ['success' => false, 'error' => 'التسوية غير موجودة أو تمت معالجتها'];
}
$now = date('Y-m-d H:i:s');
$db->update('treasury_settlements', [
'received_by' => $rejectedBy,
'received_at' => $now,
'status' => 'rejected',
'rejection_reason' => $reason,
'updated_at' => $now,
], '`id` = ?', [$settlementId]);
// Revert session to closed so a new settlement can be initiated
$db->update('treasury_sessions', [
'status' => 'closed',
'updated_at' => $now,
], '`id` = ?', [(int) $settlement['session_id']]);
// Reverse the custody out
$session = $db->selectOne("SELECT cashier_id FROM treasury_sessions WHERE id = ?", [(int) $settlement['session_id']]);
if ($session) {
CustodyService::recordSettlementIn(
(int) $settlement['from_treasury_id'],
(int) $session['cashier_id'],
(string) $settlement['amount'],
$settlementId
);
}
EventBus::dispatch('treasury.settlement.rejected', [
'settlement_id' => $settlementId,
'reason' => $reason,
'rejected_by' => $rejectedBy,
]);
Logger::info("Treasury settlement rejected", ['id' => $settlementId, 'reason' => $reason]);
return ['success' => true];
}
public static function getPendingSettlements(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT s.*, t_from.name_ar as from_treasury_name, t_to.name_ar as to_treasury_name,
e.full_name_ar as settled_by_name, ts.session_number
FROM treasury_settlements s
LEFT JOIN treasuries t_from ON t_from.id = s.from_treasury_id
LEFT JOIN treasuries t_to ON t_to.id = s.to_treasury_id
LEFT JOIN employees e ON e.id = s.settled_by
LEFT JOIN treasury_sessions ts ON ts.id = s.session_id
WHERE s.status = 'pending'
ORDER BY s.settled_at ASC"
);
}
public static function getSettlementHistory(int $treasuryId, int $limit = 50): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT s.*, t_from.name_ar as from_treasury_name, t_to.name_ar as to_treasury_name,
e_settled.full_name_ar as settled_by_name, e_received.full_name_ar as received_by_name,
ts.session_number
FROM treasury_settlements s
LEFT JOIN treasuries t_from ON t_from.id = s.from_treasury_id
LEFT JOIN treasuries t_to ON t_to.id = s.to_treasury_id
LEFT JOIN employees e_settled ON e_settled.id = s.settled_by
LEFT JOIN employees e_received ON e_received.id = s.received_by
LEFT JOIN treasury_sessions ts ON ts.id = s.session_id
WHERE s.from_treasury_id = ? OR s.to_treasury_id = ?
ORDER BY s.id DESC
LIMIT ?",
[$treasuryId, $treasuryId, $limit]
);
}
public static function find(int $settlementId): ?array
{
$db = App::getInstance()->db();
return $db->selectOne(
"SELECT s.*, t_from.name_ar as from_treasury_name, t_to.name_ar as to_treasury_name,
e_settled.full_name_ar as settled_by_name, e_received.full_name_ar as received_by_name,
ts.session_number
FROM treasury_settlements s
LEFT JOIN treasuries t_from ON t_from.id = s.from_treasury_id
LEFT JOIN treasuries t_to ON t_to.id = s.to_treasury_id
LEFT JOIN employees e_settled ON e_settled.id = s.settled_by
LEFT JOIN employees e_received ON e_received.id = s.received_by
LEFT JOIN treasury_sessions ts ON ts.id = s.session_id
WHERE s.id = ?",
[$settlementId]
);
}
private static function generateSettlementNumber($db): string
{
$year = date('Y');
$row = $db->selectOne(
"SELECT MAX(CAST(SUBSTRING(settlement_number, 10) AS UNSIGNED)) as max_num
FROM treasury_settlements WHERE settlement_number LIKE ?",
["STL-{$year}-%"]
);
$next = ((int) ($row['max_num'] ?? 0)) + 1;
return sprintf('STL-%s-%06d', $year, $next);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Treasury\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
use App\Modules\Payments\Services\PaymentService;
use App\Modules\Cashier\Services\PaymentRequestService;
final class TreasuryService
{
public static function getSubTreasury(): ?array
{
$db = App::getInstance()->db();
return $db->selectOne("SELECT * FROM treasuries WHERE code = 'SUB_SA' AND is_active = 1");
}
public static function getMainTreasury(): ?array
{
$db = App::getInstance()->db();
return $db->selectOne("SELECT * FROM treasuries WHERE type = 'main' AND is_active = 1 LIMIT 1");
}
public static function find(int $id): ?array
{
$db = App::getInstance()->db();
return $db->selectOne("SELECT * FROM treasuries WHERE id = ?", [$id]);
}
public static function collectPayment(int $sessionId, int $requestId, string $paymentMethod): array
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$session = $db->selectOne(
"SELECT * FROM treasury_sessions WHERE id = ? AND status = 'open'",
[$sessionId]
);
if (!$session) {
return ['success' => false, 'error' => 'لا توجد وردية مفتوحة'];
}
$request = $db->selectOne(
"SELECT * FROM payment_requests WHERE id = ? AND is_voided = 0 AND status IN ('pending','processing')",
[$requestId]
);
if (!$request) {
return ['success' => false, 'error' => 'طلب الدفع غير موجود أو تم معالجته'];
}
$db->query(
"UPDATE payment_requests SET status = 'processing', updated_at = NOW() WHERE id = ? AND status = 'pending'",
[$requestId]
);
$treasuryId = (int) $session['treasury_id'];
$paymentResult = PaymentService::processPayment([
'member_id' => (int) $request['member_id'],
'amount' => (string) $request['amount'],
'payment_type' => $request['payment_type'],
'payment_method' => $paymentMethod,
'description' => $request['description_ar'] ?? '',
'related_entity_type' => $request['related_entity_type'],
'related_entity_id' => $request['related_entity_id'] ? (int) $request['related_entity_id'] : null,
'currency' => $request['currency'] ?? 'EGP',
'treasury_id' => $treasuryId,
'session_id' => $sessionId,
]);
if (!$paymentResult['success']) {
$db->query("UPDATE payment_requests SET status = 'pending', updated_at = NOW() WHERE id = ?", [$requestId]);
return $paymentResult;
}
$now = date('Y-m-d H:i:s');
$db->update('payment_requests', [
'status' => 'completed',
'payment_id' => (int) $paymentResult['payment_id'],
'receipt_number' => $paymentResult['receipt_number'] ?? null,
'processed_by' => $employee ? (int) $employee->id : null,
'processed_at' => $now,
'updated_at' => $now,
], '`id` = ?', [$requestId]);
if ($paymentMethod === 'cash') {
CustodyService::recordCollection(
$treasuryId,
(int) $session['cashier_id'],
(string) $request['amount'],
(int) $paymentResult['payment_id'],
PaymentRequestService::getPaymentTypeLabel($request['payment_type'])
);
}
$db->query(
"UPDATE treasury_sessions SET total_collected = total_collected + ?, total_receipts = total_receipts + 1, updated_at = NOW() WHERE id = ?",
[(string) $request['amount'], $sessionId]
);
EventBus::dispatch('payment_request.completed', [
'request_id' => $requestId,
'request_number' => $request['request_number'],
'member_id' => (int) $request['member_id'],
'payment_type' => $request['payment_type'],
'amount' => $request['amount'],
'payment_id' => $paymentResult['payment_id'],
'receipt_number' => $paymentResult['receipt_number'] ?? '',
'related_entity_type' => $request['related_entity_type'] ?? null,
'related_entity_id' => $request['related_entity_id'] ?? null,
]);
EventBus::dispatch('treasury.payment.collected', [
'payment_id' => $paymentResult['payment_id'],
'treasury_id' => $treasuryId,
'session_id' => $sessionId,
'cashier_id' => (int) $session['cashier_id'],
'amount' => (string) $request['amount'],
'receipt_number' => $paymentResult['receipt_number'] ?? '',
]);
Logger::info("Treasury payment collected", [
'payment_id' => $paymentResult['payment_id'],
'session' => $sessionId,
'amount' => $request['amount'],
]);
return [
'success' => true,
'payment_id' => $paymentResult['payment_id'],
'receipt_id' => $paymentResult['receipt_id'] ?? null,
'receipt_number' => $paymentResult['receipt_number'] ?? '',
];
}
public static function createAndCollectPayment(int $sessionId, array $data, string $paymentMethod): array
{
$requestResult = PaymentRequestService::createRequest($data);
if (!$requestResult['success']) {
return $requestResult;
}
return self::collectPayment($sessionId, (int) $requestResult['request_id'], $paymentMethod);
}
public static function getQueueForTreasury(int $treasuryId, array $filters = []): array
{
$db = App::getInstance()->db();
$treasury = self::find($treasuryId);
if (!$treasury) {
return [];
}
$where = "pr.is_voided = 0";
$params = [];
$status = $filters['status'] ?? '';
if ($status !== '') {
$where .= ' AND pr.status = ?';
$params[] = $status;
} else {
$where .= " AND pr.status IN ('pending','processing')";
}
// Filter by sports-related payment types for sub-treasury
if ($treasury['type'] === 'sub') {
$where .= " AND pr.payment_type IN ('activity_subscription','hourly_booking','sports_registration')";
}
$search = trim($filters['search'] ?? '');
if ($search !== '') {
$where .= ' AND (m.full_name_ar LIKE ? OR m.form_number LIKE ? OR pr.request_number LIKE ?)';
$term = '%' . $search . '%';
$params = array_merge($params, [$term, $term, $term]);
}
return $db->select(
"SELECT pr.*, m.full_name_ar as member_name, m.form_number,
e.full_name_ar as requested_by_name
FROM payment_requests pr
LEFT JOIN members m ON m.id = pr.member_id
LEFT JOIN employees e ON e.id = pr.requested_by
WHERE {$where}
ORDER BY pr.created_at ASC
LIMIT 200",
$params
);
}
public static function getDashboardStats(int $treasuryId, int $cashierId): array
{
$db = App::getInstance()->db();
$currentSession = SessionService::getCurrentSession($treasuryId, $cashierId);
$custodyBalance = CustodyService::getCurrentBalance($treasuryId, $cashierId);
$todayCollections = $db->selectOne(
"SELECT COALESCE(SUM(p.amount), 0) as total, COUNT(*) as count
FROM payments p
WHERE p.treasury_id = ? AND p.is_voided = 0 AND DATE(p.payment_date) = CURDATE()",
[$treasuryId]
);
$pendingSettlements = $db->selectOne(
"SELECT COUNT(*) as count FROM treasury_settlements WHERE from_treasury_id = ? AND status = 'pending'",
[$treasuryId]
);
$pendingQueue = $db->selectOne(
"SELECT COUNT(*) as count FROM payment_requests WHERE status IN ('pending','processing') AND is_voided = 0 AND payment_type IN ('activity_subscription','hourly_booking','sports_registration')"
);
return [
'current_session' => $currentSession,
'custody_balance' => $custodyBalance,
'today_total' => (string) ($todayCollections['total'] ?? '0.00'),
'today_count' => (int) ($todayCollections['count'] ?? 0),
'pending_settlements' => (int) ($pendingSettlements['count'] ?? 0),
'pending_queue' => (int) ($pendingQueue['count'] ?? 0),
];
}
}
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تحصيل مباشر — الخزنة الفرعية<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="max-width:600px;margin:0 auto;">
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;font-weight:600;">إنشاء وتحصيل دفعة مباشرة</div>
<div style="padding:20px;">
<form method="POST" action="/treasury/collect">
<?= csrf_field() ?>
<div style="margin-bottom:15px;">
<label class="form-label">العضو <span style="color:red;">*</span></label>
<select name="member_id" class="form-select member-search" required>
<option value="">اختر العضو...</option>
</select>
</div>
<div style="margin-bottom:15px;">
<label class="form-label">نوع الدفعة <span style="color:red;">*</span></label>
<select name="payment_type" class="form-select" required>
<option value="">اختر...</option>
<option value="activity_subscription">اشتراك نشاط</option>
<option value="hourly_booking">حجز بالساعة</option>
<option value="sports_registration">تسجيل رياضي</option>
</select>
</div>
<div style="margin-bottom:15px;">
<label class="form-label">المبلغ <span style="color:red;">*</span></label>
<input type="number" name="amount" class="form-input" step="0.01" min="0.01" required placeholder="0.00">
</div>
<div style="margin-bottom:15px;">
<label class="form-label">طريقة الدفع <span style="color:red;">*</span></label>
<select name="payment_method" class="form-select" required>
<option value="cash">نقدي</option>
<option value="visa">فيزا</option>
<option value="bank_transfer">تحويل بنكي</option>
</select>
</div>
<div style="margin-bottom:15px;">
<label class="form-label">الوصف</label>
<input type="text" name="description_ar" class="form-input" placeholder="وصف اختياري...">
</div>
<input type="hidden" name="related_entity_type" value="">
<input type="hidden" name="related_entity_id" value="0">
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary" style="flex:1;">إنشاء وتحصيل</button>
<a href="/treasury/queue" class="btn btn-outline">رجوع</a>
</div>
</form>
</div>
</div>
</div>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>سجل العهدة<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Balance Card -->
<div class="card" style="margin-bottom:15px;background:#F0FDF4;border:1px solid #BBF7D0;">
<div style="padding:20px;display:flex;justify-content:space-between;align-items:center;">
<div>
<div style="font-size:13px;color:#065F46;">رصيد العهدة الحالي</div>
<div style="font-size:28px;font-weight:700;color:#059669;"><?= money($balance) ?></div>
</div>
<div style="font-size:13px;color:#6B7280;"><?= e($treasury['name_ar']) ?></div>
</div>
</div>
<!-- Log Table -->
<div class="card">
<div style="padding:12px 20px;border-bottom:1px solid #E5E7EB;font-weight:600;">سجل الحركات</div>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>التاريخ</th>
<th>الإجراء</th>
<th>المبلغ</th>
<th>الرصيد بعد</th>
<th>الوصف</th>
<th>المرجع</th>
</tr>
</thead>
<tbody>
<?php if (empty($log)): ?>
<tr><td colspan="6" style="text-align:center;padding:40px;color:#9CA3AF;">لا توجد حركات</td></tr>
<?php else: ?>
<?php foreach ($log as $entry):
$isDebit = in_array($entry['action'], ['collection', 'settlement_in']);
$actionLabel = \App\Modules\Treasury\Services\CustodyService::getActionLabel($entry['action']);
?>
<tr>
<td style="font-size:12px;color:#6B7280;"><?= e($entry['created_at']) ?></td>
<td style="font-size:12px;font-weight:600;"><?= e($actionLabel) ?></td>
<td style="font-weight:600;color:<?= $isDebit ? '#059669' : '#DC2626' ?>;">
<?= $isDebit ? '+' : '-' ?><?= money($entry['amount']) ?>
</td>
<td style="font-weight:600;"><?= money($entry['balance_after']) ?></td>
<td style="font-size:12px;"><?= e($entry['description_ar'] ?? '') ?></td>
<td style="font-size:11px;color:#9CA3AF;font-family:monospace;">
<?= e($entry['reference_type'] ?? '') ?>#<?= (int) ($entry['reference_id'] ?? 0) ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>الخزنة الفرعية<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php if (!empty($error)): ?>
<div class="card" style="padding:40px;text-align:center;">
<p style="color:#DC2626;font-size:16px;"><?= e($error) ?></p>
</div>
<?php else: ?>
<!-- Custody Balance Card -->
<div class="card" style="margin-bottom:20px;background:linear-gradient(135deg,#1E40AF,#3B82F6);color:#fff;">
<div style="padding:25px 30px;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<div>
<div style="font-size:13px;opacity:0.8;margin-bottom:5px;">رصيد العهدة الحالي</div>
<div style="font-size:32px;font-weight:700;"><?= money($stats['custody_balance'] ?? '0.00') ?></div>
<div style="font-size:12px;opacity:0.7;margin-top:5px;"><?= e($treasury['name_ar']) ?></div>
</div>
<div style="text-align:left;">
<?php if ($stats['current_session']): ?>
<span style="background:rgba(255,255,255,0.2);padding:4px 12px;border-radius:20px;font-size:12px;">
وردية مفتوحة: <?= e($stats['current_session']['session_number']) ?>
</span>
<?php else: ?>
<span style="background:rgba(255,255,255,0.2);padding:4px 12px;border-radius:20px;font-size:12px;">
لا توجد وردية مفتوحة
</span>
<?php endif; ?>
</div>
</div>
</div>
</div>
<!-- Stats Grid -->
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:15px;margin-bottom:20px;">
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">تحصيلات اليوم</div>
<div style="font-size:24px;font-weight:700;color:#059669;margin-top:5px;"><?= money($stats['today_total']) ?></div>
<div style="font-size:12px;color:#9CA3AF;margin-top:3px;"><?= $stats['today_count'] ?> إيصال</div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">في طابور التحصيل</div>
<div style="font-size:24px;font-weight:700;color:#F59E0B;margin-top:5px;"><?= $stats['pending_queue'] ?></div>
<div style="font-size:12px;color:#9CA3AF;margin-top:3px;">طلب معلق</div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">تسويات معلقة</div>
<div style="font-size:24px;font-weight:700;color:#DC2626;margin-top:5px;"><?= $stats['pending_settlements'] ?></div>
<div style="font-size:12px;color:#9CA3AF;margin-top:3px;">في انتظار الاستلام</div>
</div>
</div>
<!-- Quick Actions -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;font-weight:600;">إجراءات سريعة</div>
<div style="padding:20px;display:flex;gap:10px;flex-wrap:wrap;">
<?php if (!$stats['current_session']): ?>
<form method="POST" action="/treasury/sessions/open" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-primary">فتح وردية جديدة</button>
</form>
<?php else: ?>
<a href="/treasury/queue" class="btn btn-primary">طابور التحصيل</a>
<a href="/treasury/sessions/current" class="btn btn-outline">تفاصيل الوردية</a>
<form method="POST" action="/treasury/sessions/<?= (int) $stats['current_session']['id'] ?>/close" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-outline" style="color:#DC2626;border-color:#DC2626;"
onclick="return confirm('هل تريد إغلاق الوردية؟ لن يمكنك التحصيل بعد الإغلاق.')">
إغلاق الوردية
</button>
</form>
<?php endif; ?>
<a href="/treasury/settlements/create" class="btn btn-outline">إجراء تسوية</a>
<a href="/treasury/custody" class="btn btn-outline">سجل العهدة</a>
</div>
</div>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تحصيل دفعة<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="max-width:600px;margin:0 auto;">
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;font-weight:600;">تحصيل دفعة</div>
<div style="padding:20px;">
<!-- Payment Details -->
<div style="background:#F9FAFB;border-radius:8px;padding:15px;margin-bottom:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;font-size:14px;">
<div><span style="color:#6B7280;">رقم الطلب:</span></div>
<div style="font-weight:600;font-family:monospace;"><?= e($pr['request_number']) ?></div>
<div><span style="color:#6B7280;">العضو:</span></div>
<div style="font-weight:600;"><?= e($pr['member_name'] ?? '—') ?></div>
<div><span style="color:#6B7280;">رقم الاستمارة:</span></div>
<div><?= e($pr['form_number'] ?? '—') ?></div>
<div><span style="color:#6B7280;">نوع الدفعة:</span></div>
<div><?= e(\App\Modules\Cashier\Services\PaymentRequestService::getPaymentTypeLabel($pr['payment_type'])) ?></div>
<div><span style="color:#6B7280;">المبلغ:</span></div>
<div style="font-size:20px;font-weight:700;color:#059669;"><?= money($pr['amount']) ?></div>
</div>
<?php if (!empty($pr['description_ar'])): ?>
<div style="margin-top:10px;padding-top:10px;border-top:1px solid #E5E7EB;font-size:13px;color:#4B5563;">
<?= e($pr['description_ar']) ?>
</div>
<?php endif; ?>
</div>
<!-- Payment Form -->
<form method="POST" action="/treasury/queue/<?= (int) $pr['id'] ?>/collect">
<?= csrf_field() ?>
<div style="margin-bottom:15px;">
<label class="form-label">طريقة الدفع</label>
<select name="payment_method" class="form-select" required>
<option value="cash">نقدي</option>
<option value="visa">فيزا</option>
<option value="bank_transfer">تحويل بنكي</option>
</select>
</div>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary" style="flex:1;" onclick="return confirm('تأكيد تحصيل <?= money($pr['amount']) ?>؟')">
تحصيل الدفعة
</button>
<a href="/treasury/queue" class="btn btn-outline">رجوع</a>
</div>
</form>
</div>
</div>
</div>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>طابور التحصيل — الخزنة الفرعية<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Session Info Bar -->
<div style="background:#ECFDF5;border:1px solid #A7F3D0;border-radius:8px;padding:10px 20px;margin-bottom:15px;display:flex;justify-content:space-between;align-items:center;">
<div style="font-size:13px;color:#065F46;">
<strong>الوردية:</strong> <?= e($currentSession['session_number']) ?>
&nbsp;|&nbsp;
<strong>الإجمالي:</strong> <?= money($currentSession['total_collected'] ?? '0.00') ?>
&nbsp;|&nbsp;
<strong>الإيصالات:</strong> <?= (int) ($currentSession['total_receipts'] ?? 0) ?>
</div>
</div>
<!-- Filters -->
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;">
<form method="GET" action="/treasury/queue" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div>
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="search" class="form-input" value="<?= e($filters['search'] ?? '') ?>" placeholder="اسم العضو، رقم الاستمارة...">
</div>
<div>
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-select">
<option value="">معلق + قيد التنفيذ</option>
<option value="pending" <?= ($filters['status'] ?? '') === 'pending' ? 'selected' : '' ?>>معلق</option>
<option value="completed" <?= ($filters['status'] ?? '') === 'completed' ? 'selected' : '' ?>>مكتمل</option>
</select>
</div>
<button type="submit" class="btn btn-outline">بحث</button>
<?php if (!empty($filters['search']) || !empty($filters['status'])): ?>
<a href="/treasury/queue" class="btn btn-outline" style="color:#DC2626;border-color:#DC2626;">مسح</a>
<?php endif; ?>
</form>
</div>
</div>
<!-- Queue Table -->
<div class="card">
<?php
$pendingCount = 0;
foreach ($requests as $r) {
if ($r['status'] === 'pending') $pendingCount++;
}
?>
<?php if ($pendingCount > 0): ?>
<div style="padding:12px 20px;background:#FEF2F2;border-bottom:1px solid #FECACA;font-size:14px;color:#DC2626;font-weight:600;">
<?= $pendingCount ?> طلب في الانتظار
</div>
<?php endif; ?>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>رقم الطلب</th>
<th>العضو</th>
<th>نوع الدفعة</th>
<th>المبلغ</th>
<th>الحالة</th>
<th>وقت الانتظار</th>
<th></th>
</tr>
</thead>
<tbody>
<?php if (empty($requests)): ?>
<tr><td colspan="7" style="text-align:center;padding:40px;color:#9CA3AF;">لا توجد طلبات</td></tr>
<?php else: ?>
<?php foreach ($requests as $r):
$statusColor = match($r['status']) {
'pending' => '#F59E0B',
'processing' => '#3B82F6',
'completed' => '#059669',
default => '#6B7280',
};
$statusLabel = match($r['status']) {
'pending' => 'معلق',
'processing' => 'قيد التنفيذ',
'completed' => 'مكتمل',
default => $r['status'],
};
$typeLabel = \App\Modules\Cashier\Services\PaymentRequestService::getPaymentTypeLabel($r['payment_type']);
$waitMinutes = (int) ((time() - strtotime($r['created_at'])) / 60);
?>
<tr>
<td style="font-family:monospace;font-size:12px;"><?= e($r['request_number']) ?></td>
<td>
<div style="font-weight:600;"><?= e($r['member_name'] ?? '—') ?></div>
<div style="font-size:11px;color:#9CA3AF;"><?= e($r['form_number'] ?? '') ?></div>
</td>
<td><?= e($typeLabel) ?></td>
<td style="font-weight:600;"><?= money($r['amount']) ?></td>
<td><span style="color:<?= $statusColor ?>;font-weight:600;font-size:12px;"><?= e($statusLabel) ?></span></td>
<td style="font-size:12px;<?= $waitMinutes > 30 ? 'color:#DC2626;font-weight:700;' : 'color:#6B7280;' ?>">
<?= $waitMinutes ?> د
</td>
<td>
<?php if ($r['status'] === 'pending'): ?>
<a href="/treasury/queue/<?= (int) $r['id'] ?>" class="btn btn-primary" style="font-size:12px;padding:4px 12px;">تحصيل</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<script>
setTimeout(function(){ location.reload(); }, 30000);
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>الوردية الحالية<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Session Header -->
<div class="card" style="margin-bottom:15px;">
<div style="padding:20px;display:flex;justify-content:space-between;align-items:center;">
<div>
<div style="font-size:18px;font-weight:700;">الوردية: <?= e($session['session_number']) ?></div>
<div style="font-size:13px;color:#6B7280;margin-top:3px;">
منذ <?= arabic_date($session['opened_at']) ?>
&nbsp;|&nbsp;
الصراف: <?= e($session['cashier_name'] ?? '—') ?>
</div>
</div>
<div style="text-align:left;">
<div style="font-size:13px;color:#6B7280;">إجمالي التحصيل</div>
<div style="font-size:24px;font-weight:700;color:#059669;"><?= money($session['total_collected'] ?? '0.00') ?></div>
<div style="font-size:12px;color:#9CA3AF;"><?= (int) $session['total_receipts'] ?> إيصال</div>
</div>
</div>
</div>
<!-- Payments List -->
<div class="card">
<div style="padding:12px 20px;border-bottom:1px solid #E5E7EB;font-weight:600;">مدفوعات هذه الوردية</div>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>الإيصال</th>
<th>العضو</th>
<th>المبلغ</th>
<th>الطريقة</th>
<th>الوقت</th>
</tr>
</thead>
<tbody>
<?php if (empty($payments)): ?>
<tr><td colspan="5" style="text-align:center;padding:30px;color:#9CA3AF;">لا توجد مدفوعات بعد</td></tr>
<?php else: ?>
<?php foreach ($payments as $p): ?>
<tr>
<td style="font-family:monospace;font-size:12px;"><?= e($p['receipt_number'] ?? '—') ?></td>
<td><?= e($p['member_name'] ?? '—') ?></td>
<td style="font-weight:600;"><?= money($p['amount']) ?></td>
<td style="font-size:12px;">
<?= match($p['payment_method'] ?? '') {
'cash' => 'نقدي',
'visa' => 'فيزا',
'bank_transfer' => 'تحويل',
default => $p['payment_method'] ?? '—',
} ?>
</td>
<td style="font-size:12px;color:#6B7280;"><?= e($p['created_at'] ?? '') ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- Actions -->
<div style="margin-top:15px;display:flex;gap:10px;">
<a href="/treasury/queue" class="btn btn-primary">العودة للطابور</a>
<form method="POST" action="/treasury/sessions/<?= (int) $session['id'] ?>/close" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-outline" style="color:#DC2626;border-color:#DC2626;"
onclick="return confirm('هل تريد إغلاق الوردية؟')">
إغلاق الوردية
</button>
</form>
</div>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>الورديات — الخزنة الفرعية<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<span style="font-weight:600;">سجل الورديات</span>
</div>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>رقم الوردية</th>
<th>الصراف</th>
<th>فتح</th>
<th>إغلاق</th>
<th>الإجمالي</th>
<th>الإيصالات</th>
<th>الحالة</th>
</tr>
</thead>
<tbody>
<?php if (empty($sessions)): ?>
<tr><td colspan="7" style="text-align:center;padding:40px;color:#9CA3AF;">لا توجد ورديات</td></tr>
<?php else: ?>
<?php foreach ($sessions as $s):
$statusColor = match($s['status']) {
'open' => '#059669',
'closed' => '#F59E0B',
'settled' => '#3B82F6',
default => '#6B7280',
};
$statusLabel = match($s['status']) {
'open' => 'مفتوحة',
'closed' => 'مغلقة',
'settled' => 'تمت التسوية',
default => $s['status'],
};
?>
<tr>
<td style="font-family:monospace;font-size:12px;"><?= e($s['session_number']) ?></td>
<td><?= e($s['cashier_name'] ?? '—') ?></td>
<td style="font-size:12px;"><?= e($s['opened_at']) ?></td>
<td style="font-size:12px;"><?= e($s['closed_at'] ?? '—') ?></td>
<td style="font-weight:600;"><?= money($s['total_collected'] ?? '0.00') ?></td>
<td><?= (int) $s['total_receipts'] ?></td>
<td><span style="color:<?= $statusColor ?>;font-weight:600;font-size:12px;"><?= e($statusLabel) ?></span></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إجراء تسوية<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="max-width:600px;margin:0 auto;">
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;font-weight:600;">تسوية مع الخزنة الرئيسية</div>
<div style="padding:20px;">
<?php if (empty($sessions)): ?>
<div style="text-align:center;padding:30px;color:#9CA3AF;">
<p>لا توجد ورديات مغلقة جاهزة للتسوية</p>
<p style="font-size:12px;margin-top:10px;">يجب إغلاق الوردية أولاً قبل إجراء التسوية</p>
</div>
<?php else: ?>
<form method="POST" action="/treasury/settlements">
<?= csrf_field() ?>
<div style="margin-bottom:20px;">
<label class="form-label">اختر الوردية المغلقة</label>
<select name="session_id" class="form-select" required>
<option value="">اختر...</option>
<?php foreach ($sessions as $s): ?>
<option value="<?= (int) $s['id'] ?>">
<?= e($s['session_number']) ?><?= money($s['total_collected']) ?> (<?= (int) $s['total_receipts'] ?> إيصال) — <?= e($s['cashier_name'] ?? '') ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div style="background:#FEF3C7;border:1px solid #FCD34D;border-radius:8px;padding:12px;margin-bottom:20px;font-size:13px;color:#92400E;">
<strong>ملاحظة:</strong> سيتم تحويل كامل مبلغ الوردية للخزنة الرئيسية. لا يُسمح بالاحتفاظ بأي مبلغ.
</div>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary" style="flex:1;" onclick="return confirm('تأكيد إجراء التسوية؟ سيتم تحويل المبلغ بالكامل.')">
إجراء التسوية
</button>
<a href="/treasury/settlements" class="btn btn-outline">رجوع</a>
</div>
</form>
<?php endif; ?>
</div>
</div>
</div>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>التسويات — الخزنة الفرعية<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<span style="font-weight:600;">سجل التسويات</span>
<a href="/treasury/settlements/create" class="btn btn-primary" style="font-size:13px;">تسوية جديدة</a>
</div>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>رقم التسوية</th>
<th>الوردية</th>
<th>المبلغ</th>
<th>عدد الإيصالات</th>
<th>بواسطة</th>
<th>استلم بواسطة</th>
<th>الحالة</th>
<th>التاريخ</th>
</tr>
</thead>
<tbody>
<?php if (empty($settlements)): ?>
<tr><td colspan="8" style="text-align:center;padding:40px;color:#9CA3AF;">لا توجد تسويات</td></tr>
<?php else: ?>
<?php foreach ($settlements as $s):
$statusColor = match($s['status']) {
'pending' => '#F59E0B',
'received' => '#059669',
'rejected' => '#DC2626',
default => '#6B7280',
};
$statusLabel = match($s['status']) {
'pending' => 'في الانتظار',
'received' => 'تم الاستلام',
'rejected' => 'مرفوضة',
default => $s['status'],
};
?>
<tr>
<td style="font-family:monospace;font-size:12px;">
<a href="/treasury/settlements/<?= (int) $s['id'] ?>" style="color:#3B82F6;"><?= e($s['settlement_number']) ?></a>
</td>
<td style="font-size:12px;"><?= e($s['session_number'] ?? '—') ?></td>
<td style="font-weight:600;"><?= money($s['amount']) ?></td>
<td><?= (int) $s['receipt_count'] ?></td>
<td style="font-size:12px;"><?= e($s['settled_by_name'] ?? '—') ?></td>
<td style="font-size:12px;"><?= e($s['received_by_name'] ?? '—') ?></td>
<td><span style="color:<?= $statusColor ?>;font-weight:600;font-size:12px;"><?= e($statusLabel) ?></span></td>
<td style="font-size:12px;"><?= e($s['settled_at'] ?? '') ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تفاصيل التسوية<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="max-width:700px;margin:0 auto;">
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;font-weight:600;">
تسوية: <?= e($settlement['settlement_number']) ?>
</div>
<div style="padding:20px;">
<?php
$statusColor = match($settlement['status']) {
'pending' => '#F59E0B',
'received' => '#059669',
'rejected' => '#DC2626',
default => '#6B7280',
};
$statusLabel = match($settlement['status']) {
'pending' => 'في انتظار الاستلام',
'received' => 'تم الاستلام',
'rejected' => 'مرفوضة',
default => $settlement['status'],
};
?>
<div style="background:#F9FAFB;border-radius:8px;padding:15px;margin-bottom:15px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;font-size:14px;">
<div><span style="color:#6B7280;">الحالة:</span></div>
<div><span style="color:<?= $statusColor ?>;font-weight:700;"><?= e($statusLabel) ?></span></div>
<div><span style="color:#6B7280;">المبلغ:</span></div>
<div style="font-size:20px;font-weight:700;color:#059669;"><?= money($settlement['amount']) ?></div>
<div><span style="color:#6B7280;">من:</span></div>
<div><?= e($settlement['from_treasury_name'] ?? '—') ?></div>
<div><span style="color:#6B7280;">إلى:</span></div>
<div><?= e($settlement['to_treasury_name'] ?? '—') ?></div>
<div><span style="color:#6B7280;">رقم الوردية:</span></div>
<div style="font-family:monospace;"><?= e($settlement['session_number'] ?? '—') ?></div>
<div><span style="color:#6B7280;">عدد الإيصالات:</span></div>
<div><?= (int) $settlement['receipt_count'] ?></div>
<div><span style="color:#6B7280;">أجراها:</span></div>
<div><?= e($settlement['settled_by_name'] ?? '—') ?><?= e($settlement['settled_at'] ?? '') ?></div>
<?php if ($settlement['received_by_name']): ?>
<div><span style="color:#6B7280;">استلم بواسطة:</span></div>
<div><?= e($settlement['received_by_name']) ?><?= e($settlement['received_at'] ?? '') ?></div>
<?php endif; ?>
<?php if ($settlement['rejection_reason']): ?>
<div><span style="color:#6B7280;">سبب الرفض:</span></div>
<div style="color:#DC2626;"><?= e($settlement['rejection_reason']) ?></div>
<?php endif; ?>
</div>
</div>
<a href="/treasury/settlements" class="btn btn-outline">العودة للقائمة</a>
</div>
</div>
</div>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
use App\Core\Registries\MenuRegistry;
use App\Core\Registries\PermissionRegistry;
use App\Core\EventBus;
MenuRegistry::register('treasury', [
'label_ar' => 'الخزنة الفرعية',
'label_en' => 'Sub-Treasury',
'icon' => 'safe',
'route' => '/treasury',
'permission' => 'treasury.view_dashboard',
'parent' => null,
'order' => 255,
'children' => [
['label_ar' => 'لوحة التحكم', 'label_en' => 'Dashboard', 'route' => '/treasury', 'permission' => 'treasury.view_dashboard', 'order' => 1],
['label_ar' => 'طابور التحصيل', 'label_en' => 'Queue', 'route' => '/treasury/queue', 'permission' => 'treasury.collect_payment', 'order' => 2],
['label_ar' => 'الورديات', 'label_en' => 'Sessions', 'route' => '/treasury/sessions', 'permission' => 'treasury.view_sessions', 'order' => 3],
['label_ar' => 'التسويات', 'label_en' => 'Settlements', 'route' => '/treasury/settlements', 'permission' => 'treasury.view_settlements', 'order' => 4],
['label_ar' => 'سجل العهدة', 'label_en' => 'Custody Log', 'route' => '/treasury/custody', 'permission' => 'treasury.view_custody', 'order' => 5],
],
]);
PermissionRegistry::register('treasury', [
'treasury.view_dashboard' => ['ar' => 'لوحة تحكم الخزنة الفرعية', 'en' => 'Sub-Treasury Dashboard'],
'treasury.collect_payment' => ['ar' => 'تحصيل مدفوعات', 'en' => 'Collect Payments'],
'treasury.open_session' => ['ar' => 'فتح وردية', 'en' => 'Open Session'],
'treasury.close_session' => ['ar' => 'إغلاق وردية', 'en' => 'Close Session'],
'treasury.initiate_settlement' => ['ar' => 'إجراء تسوية للخزنة الرئيسية', 'en' => 'Initiate Settlement'],
'treasury.view_custody' => ['ar' => 'عرض رصيد العهدة', 'en' => 'View Custody Balance'],
'treasury.view_sessions' => ['ar' => 'عرض الورديات', 'en' => 'View Sessions'],
'treasury.view_settlements' => ['ar' => 'عرض التسويات', 'en' => 'View Settlements'],
]);
// Listen for payment voids on sub-treasury payments to reverse custody
EventBus::listen('payment.voided', function (array $data) {
try {
$paymentId = (int) ($data['payment_id'] ?? 0);
if ($paymentId <= 0) return;
$db = \App\Core\App::getInstance()->db();
$payment = $db->selectOne(
"SELECT treasury_id, session_id, amount, payment_method FROM payments WHERE id = ?",
[$paymentId]
);
if (!$payment || !$payment['treasury_id'] || $payment['payment_method'] !== 'cash') return;
$session = $db->selectOne("SELECT cashier_id FROM treasury_sessions WHERE id = ?", [(int) $payment['session_id']]);
if (!$session) return;
\App\Modules\Treasury\Services\CustodyService::recordVoidReversal(
(int) $payment['treasury_id'],
(int) $session['cashier_id'],
(string) $payment['amount'],
$paymentId
);
} catch (\Throwable $e) {
\App\Core\Logger::error("Treasury payment.voided listener failed: " . $e->getMessage());
}
}, 40);
...@@ -6,6 +6,7 @@ namespace App\Modules\Tutorials\Controllers; ...@@ -6,6 +6,7 @@ namespace App\Modules\Tutorials\Controllers;
use App\Core\Controller; use App\Core\Controller;
use App\Core\Request; use App\Core\Request;
use App\Core\Response; use App\Core\Response;
use App\Modules\Tutorials\TutorialRegistry;
class TutorialController extends Controller class TutorialController extends Controller
{ {
...@@ -222,6 +223,26 @@ class TutorialController extends Controller ...@@ -222,6 +223,26 @@ class TutorialController extends Controller
'scenarios' => ['label' => 'سيناريوهات كاملة', 'icon' => 'play-circle', 'color' => '#6366F1'], 'scenarios' => ['label' => 'سيناريوهات كاملة', 'icon' => 'play-circle', 'color' => '#6366F1'],
]; ];
private const TREASURY_TUTORIALS = [
'open-session' => ['title' => 'فتح وردية', 'subtitle' => 'بدء وردية جديدة في الخزنة الفرعية', 'icon' => 'play', 'color' => '#059669', 'category' => 'operations', 'order' => 1],
'collect-payment' => ['title' => 'تحصيل دفعة', 'subtitle' => 'تحصيل اشتراك نشاط رياضي من الطابور', 'icon' => 'credit-card', 'color' => '#3B82F6', 'category' => 'operations', 'order' => 2],
'close-session' => ['title' => 'إغلاق وردية', 'subtitle' => 'إغلاق الوردية وعرض الإجمالي', 'icon' => 'square', 'color' => '#F59E0B', 'category' => 'operations', 'order' => 3],
'initiate-settlement' => ['title' => 'تسوية مع الرئيسية', 'subtitle' => 'تحويل كامل المحصل للخزنة الرئيسية', 'icon' => 'arrow-up-right', 'color' => '#8B5CF6', 'category' => 'settlement', 'order' => 4],
'receive-settlement' => ['title' => 'استلام تسوية', 'subtitle' => 'تأكيد استلام التسوية في الخزنة الرئيسية', 'icon' => 'arrow-down-left', 'color' => '#EC4899', 'category' => 'settlement', 'order' => 5],
'bank-deposit' => ['title' => 'إيداع في البنك', 'subtitle' => 'إيداع النقدية ورفع إيصال البنك', 'icon' => 'building-2', 'color' => '#06B6D4', 'category' => 'deposit', 'order' => 6],
'confirm-deposit' => ['title' => 'تأكيد الإيداع', 'subtitle' => 'تأكيد مدير الحسابات وإخلاء العهدة', 'icon' => 'check-circle', 'color' => '#059669', 'category' => 'deposit', 'order' => 7],
'custody-tracking' => ['title' => 'متابعة العهدة', 'subtitle' => 'فهم رصيد العهدة وسجل الحركات', 'icon' => 'eye', 'color' => '#F97316', 'category' => 'custody', 'order' => 8],
'full-cycle' => ['title' => 'دورة التحصيلات الكاملة', 'subtitle' => 'من التحصيل حتى إخلاء العهدة', 'icon' => 'play-circle', 'color' => '#6366F1', 'category' => 'scenarios', 'order' => 9],
];
private const TREASURY_CATEGORIES = [
'operations' => ['label' => 'العمليات اليومية', 'icon' => 'monitor', 'color' => '#059669'],
'settlement' => ['label' => 'التسويات', 'icon' => 'arrow-right-left', 'color' => '#8B5CF6'],
'deposit' => ['label' => 'الإيداعات البنكية', 'icon' => 'building-2', 'color' => '#06B6D4'],
'custody' => ['label' => 'العهدة', 'icon' => 'eye', 'color' => '#F97316'],
'scenarios' => ['label' => 'سيناريوهات كاملة', 'icon' => 'play-circle', 'color' => '#6366F1'],
];
private const MEMBERSHIP_TUTORIALS = [ private const MEMBERSHIP_TUTORIALS = [
'new-member-registration' => ['title' => 'تسجيل عضو جديد', 'subtitle' => 'إنشاء استمارة عضوية من الصفر', 'icon' => 'user-plus', 'color' => '#8B5CF6', 'category' => 'registration', 'order' => 1], 'new-member-registration' => ['title' => 'تسجيل عضو جديد', 'subtitle' => 'إنشاء استمارة عضوية من الصفر', 'icon' => 'user-plus', 'color' => '#8B5CF6', 'category' => 'registration', 'order' => 1],
'pay-form-fee' => ['title' => 'دفع رسوم الاستمارة', 'subtitle' => 'إرسال طلب دفع 505 ج.م للخزينة', 'icon' => 'receipt', 'color' => '#059669', 'category' => 'registration', 'order' => 2], 'pay-form-fee' => ['title' => 'دفع رسوم الاستمارة', 'subtitle' => 'إرسال طلب دفع 505 ج.م للخزينة', 'icon' => 'receipt', 'color' => '#059669', 'category' => 'registration', 'order' => 2],
...@@ -264,23 +285,83 @@ class TutorialController extends Controller ...@@ -264,23 +285,83 @@ class TutorialController extends Controller
public function index(Request $request): Response public function index(Request $request): Response
{ {
return $this->view('Tutorials.Views.index', [ $modules = [
'modules' => [ 'sports-activity' => [
'sports-activity' => [ 'title' => 'النشاط الرياضي',
'title' => 'النشاط الرياضي', 'subtitle' => 'إدارة المرافق والأنشطة والأكاديميات',
'subtitle' => 'إدارة المرافق والأنشطة والأكاديميات', 'icon' => 'activity',
'icon' => 'activity', 'color' => '#8B5CF6',
'color' => '#8B5CF6', 'count' => count(self::SA_TUTORIALS),
'count' => count(self::SA_TUTORIALS), ],
], 'membership' => [
'membership' => [ 'title' => 'شئون العضوية',
'title' => 'شئون العضوية', 'subtitle' => 'التسجيل والرسوم والتحويلات',
'subtitle' => 'التسجيل والرسوم والتحويلات', 'icon' => 'users',
'icon' => 'users', 'color' => '#3B82F6',
'color' => '#3B82F6', 'count' => count(self::MEMBERSHIP_TUTORIALS),
'count' => count(self::MEMBERSHIP_TUTORIALS),
],
], ],
'treasury' => [
'title' => 'دورة التحصيلات',
'subtitle' => 'الخزنة الفرعية والرئيسية والإيداعات',
'icon' => 'safe',
'color' => '#059669',
'count' => count(self::TREASURY_TUTORIALS),
],
];
foreach (TutorialRegistry::getSections() as $key => $section) {
$modules[$key] = [
'title' => $section['title'],
'subtitle' => $section['subtitle'],
'icon' => $section['icon'],
'color' => $section['color'],
'count' => count(TutorialRegistry::getTutorials($key)),
];
}
return $this->view('Tutorials.Views.index', ['modules' => $modules]);
}
public function treasury(Request $request): Response
{
$grouped = [];
foreach (self::TREASURY_TUTORIALS as $slug => $tutorial) {
$cat = $tutorial['category'];
if (!isset($grouped[$cat])) {
$grouped[$cat] = self::TREASURY_CATEGORIES[$cat];
$grouped[$cat]['tutorials'] = [];
}
$grouped[$cat]['tutorials'][$slug] = $tutorial;
}
return $this->view('Tutorials.Views.treasury.index', [
'categories' => $grouped,
'totalCount' => count(self::TREASURY_TUTORIALS),
]);
}
public function showTreasury(Request $request, string $slug): Response
{
if (!isset(self::TREASURY_TUTORIALS[$slug])) {
return $this->redirect('/tutorials/treasury')->withError('الشرح غير موجود');
}
$tutorial = self::TREASURY_TUTORIALS[$slug];
$viewFile = 'Tutorials.Views.treasury.' . str_replace('-', '_', $slug);
$keys = array_keys(self::TREASURY_TUTORIALS);
$currentIdx = array_search($slug, $keys);
$prev = $currentIdx > 0 ? $keys[$currentIdx - 1] : null;
$next = $currentIdx < count($keys) - 1 ? $keys[$currentIdx + 1] : null;
return $this->view($viewFile, [
'tutorial' => $tutorial,
'slug' => $slug,
'prevSlug' => $prev,
'nextSlug' => $next,
'prevTitle' => $prev ? self::TREASURY_TUTORIALS[$prev]['title'] : null,
'nextTitle' => $next ? self::TREASURY_TUTORIALS[$next]['title'] : null,
'categories' => self::TREASURY_CATEGORIES,
]); ]);
} }
...@@ -368,4 +449,66 @@ class TutorialController extends Controller ...@@ -368,4 +449,66 @@ class TutorialController extends Controller
'categories' => self::CATEGORIES, 'categories' => self::CATEGORIES,
]); ]);
} }
public function section(Request $request, string $section): Response
{
$sections = TutorialRegistry::getSections();
if (!isset($sections[$section])) {
return $this->redirect('/tutorials')->withError('القسم غير موجود');
}
$tutorials = TutorialRegistry::getTutorials($section);
$categories = TutorialRegistry::getCategories($section);
$grouped = [];
foreach ($tutorials as $slug => $tutorial) {
$cat = $tutorial['category'];
if (!isset($grouped[$cat])) {
$grouped[$cat] = $categories[$cat] ?? ['label' => $cat, 'icon' => 'folder', 'color' => '#6B7280'];
$grouped[$cat]['tutorials'] = [];
}
$grouped[$cat]['tutorials'][$slug] = $tutorial;
}
return $this->view('Tutorials.Views.section_index', [
'section' => $sections[$section],
'sectionKey' => $section,
'categories' => $grouped,
'totalCount' => count($tutorials),
]);
}
public function showSection(Request $request, string $section, string $slug): Response
{
$sections = TutorialRegistry::getSections();
if (!isset($sections[$section])) {
return $this->redirect('/tutorials')->withError('القسم غير موجود');
}
$tutorials = TutorialRegistry::getTutorials($section);
if (!isset($tutorials[$slug])) {
return $this->redirect("/tutorials/{$section}")->withError('الشرح غير موجود');
}
$tutorial = $tutorials[$slug];
$keys = array_keys($tutorials);
$currentIdx = array_search($slug, $keys);
$prev = $currentIdx > 0 ? $keys[$currentIdx - 1] : null;
$next = $currentIdx < count($keys) - 1 ? $keys[$currentIdx + 1] : null;
$steps = TutorialRegistry::getSteps($section, $slug);
return $this->view('Tutorials.Views.section_show', [
'tutorial' => $tutorial,
'section' => $sections[$section],
'sectionKey' => $section,
'slug' => $slug,
'steps' => $steps,
'prevSlug' => $prev,
'nextSlug' => $next,
'prevTitle' => $prev ? $tutorials[$prev]['title'] : null,
'nextTitle' => $next ? $tutorials[$next]['title'] : null,
]);
}
} }
...@@ -7,4 +7,8 @@ return [ ...@@ -7,4 +7,8 @@ return [
['GET', '/tutorials/sports-activity/{slug}', 'Tutorials\Controllers\TutorialController@show', ['auth'], 'tutorials.view'], ['GET', '/tutorials/sports-activity/{slug}', 'Tutorials\Controllers\TutorialController@show', ['auth'], 'tutorials.view'],
['GET', '/tutorials/membership', 'Tutorials\Controllers\TutorialController@membership', ['auth'], 'tutorials.view'], ['GET', '/tutorials/membership', 'Tutorials\Controllers\TutorialController@membership', ['auth'], 'tutorials.view'],
['GET', '/tutorials/membership/{slug}', 'Tutorials\Controllers\TutorialController@showMembership', ['auth'], 'tutorials.view'], ['GET', '/tutorials/membership/{slug}', 'Tutorials\Controllers\TutorialController@showMembership', ['auth'], 'tutorials.view'],
['GET', '/tutorials/treasury', 'Tutorials\Controllers\TutorialController@treasury', ['auth'], 'tutorials.view'],
['GET', '/tutorials/treasury/{slug}', 'Tutorials\Controllers\TutorialController@showTreasury', ['auth'], 'tutorials.view'],
['GET', '/tutorials/{section}', 'Tutorials\Controllers\TutorialController@section', ['auth'], 'tutorials.view'],
['GET', '/tutorials/{section}/{slug}', 'Tutorials\Controllers\TutorialController@showSection', ['auth'], 'tutorials.view'],
]; ];
This source diff could not be displayed because it is too large. You can view the blob instead.
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>شروحات <?= e($section['title']) ?><?php $__template->endSection(); ?>
<?php $__template->section('styles'); ?>
<style>
.tut-hero { text-align:center; margin-bottom:36px; padding:30px 20px; background:linear-gradient(135deg,<?= e($section['color']) ?>10,<?= e($section['color']) ?>05); border-radius:20px; border:1px solid <?= e($section['color']) ?>20; }
.tut-hero-icon { width:64px;height:64px;background:<?= e($section['color']) ?>;border-radius:16px;display:inline-flex;align-items:center;justify-content:center;margin-bottom:14px; }
.tut-category { margin-bottom:32px; }
.tut-category-header { display:flex;align-items:center;gap:12px;margin-bottom:14px;padding-bottom:10px;border-bottom:2px solid #F3F4F6; }
.tut-category-icon { width:36px;height:36px;border-radius:10px;display:flex;align-items:center;justify-content:center; }
.tut-category-label { font-size:17px;font-weight:700;color:#1A1A2E; }
.tut-grid { display:grid;grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));gap:14px; }
.tut-card { display:flex;align-items:center;gap:14px;padding:16px 18px;border-radius:12px;border:1px solid #E5E7EB;background:#fff;text-decoration:none;color:inherit;transition:all .15s ease; }
.tut-card:hover { border-color:var(--card-color);box-shadow:0 4px 12px rgba(0,0,0,.06);transform:translateY(-1px); }
.tut-card-icon { width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;flex-shrink:0; }
.tut-card-title { font-size:14px;font-weight:600;color:#1A1A2E;margin:0 0 2px; }
.tut-card-sub { font-size:12px;color:#6B7280;margin:0; }
.tut-card-num { font-size:11px;font-weight:700;color:#fff;background:var(--card-color);width:22px;height:22px;border-radius:6px;display:flex;align-items:center;justify-content:center;flex-shrink:0; }
.tut-breadcrumb { display:flex;align-items:center;gap:8px;margin-bottom:20px;font-size:13px; }
.tut-breadcrumb a { color:#6B7280;text-decoration:none; }
.tut-breadcrumb a:hover { color:#8B5CF6; }
.tut-breadcrumb span { color:#9CA3AF; }
</style>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="max-width:1000px;margin:0 auto;">
<div class="tut-breadcrumb">
<a href="/tutorials"><i data-lucide="graduation-cap" style="width:14px;height:14px;"></i></a>
<span>/</span>
<span style="color:#1A1A2E;font-weight:600;"><?= e($section['title']) ?></span>
</div>
<div class="tut-hero">
<div class="tut-hero-icon">
<i data-lucide="<?= e($section['icon']) ?>" style="width:32px;height:32px;color:#fff;"></i>
</div>
<h2 style="font-size:24px;font-weight:800;color:#1A1A2E;margin:0 0 6px;">شروحات <?= e($section['title']) ?></h2>
<p style="font-size:14px;color:#6B7280;margin:0;"><?= (int) $totalCount ?> شرح تفصيلي</p>
</div>
<?php $num = 1; foreach ($categories as $catKey => $cat): ?>
<div class="tut-category">
<div class="tut-category-header">
<div class="tut-category-icon" style="background:<?= e($cat['color']) ?>15;">
<i data-lucide="<?= e($cat['icon']) ?>" style="width:18px;height:18px;color:<?= e($cat['color']) ?>;"></i>
</div>
<span class="tut-category-label"><?= e($cat['label']) ?></span>
<span style="font-size:12px;color:#9CA3AF;margin-right:auto;"><?= count($cat['tutorials']) ?> شرح</span>
</div>
<div class="tut-grid">
<?php foreach ($cat['tutorials'] as $slug => $tut): ?>
<a href="/tutorials/<?= e($sectionKey) ?>/<?= e($slug) ?>" class="tut-card" style="--card-color:<?= e($tut['color']) ?>;">
<div class="tut-card-num" style="background:<?= e($tut['color']) ?>;"><?= $num ?></div>
<div class="tut-card-icon" style="background:<?= e($tut['color']) ?>12;">
<i data-lucide="<?= e($tut['icon']) ?>" style="width:20px;height:20px;color:<?= e($tut['color']) ?>;"></i>
</div>
<div style="flex:1;min-width:0;">
<p class="tut-card-title"><?= e($tut['title']) ?></p>
<p class="tut-card-sub"><?= e($tut['subtitle']) ?></p>
</div>
<i data-lucide="chevron-left" style="width:16px;height:16px;color:#D1D5DB;flex-shrink:0;"></i>
</a>
<?php $num++; endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<script>document.addEventListener('DOMContentLoaded', function() { if (typeof lucide !== 'undefined') lucide.createIcons(); });</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>شرح: <?= e($tutorial['title']) ?><?php $__template->endSection(); ?>
<?php $__template->section('styles'); ?>
<style>
.tut-page{max-width:860px;margin:0 auto}.tut-breadcrumb{display:flex;align-items:center;gap:8px;margin-bottom:20px;font-size:13px}.tut-breadcrumb a{color:#6B7280;text-decoration:none}.tut-breadcrumb a:hover{color:<?= e($section['color']) ?>}.tut-breadcrumb span{color:#9CA3AF}.tut-header{display:flex;align-items:center;gap:18px;margin-bottom:30px;padding:24px;background:linear-gradient(135deg,<?= e($section['color']) ?>08,<?= e($section['color']) ?>04);border-radius:16px;border:1px solid <?= e($section['color']) ?>20}.tut-header-icon{width:56px;height:56px;background:<?= e($tutorial['color']) ?>;border-radius:14px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.tut-header h1{font-size:22px;font-weight:800;color:#1A1A2E;margin:0 0 4px}.tut-header p{font-size:14px;color:#6B7280;margin:0}.tut-step{position:relative;padding:20px 20px 20px 60px;margin-bottom:16px;background:#fff;border:1px solid #E5E7EB;border-radius:12px;transition:border-color .2s}.tut-step:hover{border-color:<?= e($section['color']) ?>60}.tut-step-num{position:absolute;right:18px;top:20px;width:32px;height:32px;background:<?= e($section['color']) ?>15;color:<?= e($section['color']) ?>;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:800}.tut-step-title{font-size:15px;font-weight:700;color:#1A1A2E;margin:0 0 6px}.tut-step-body{font-size:13.5px;color:#374151;line-height:1.8}.tut-step-body ul{margin:8px 0;padding-right:18px}.tut-step-body li{margin-bottom:4px}.tut-step-body .field{display:inline-block;background:#F3F4F6;color:#1A1A2E;padding:1px 8px;border-radius:4px;font-size:12px;font-weight:600;font-family:monospace;direction:ltr}.tut-step-body .warn{display:block;background:#FEF3C7;border:1px solid #F59E0B30;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#92400E}.tut-step-body .info{display:block;background:#DBEAFE;border:1px solid #3B82F630;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#1E40AF}.tut-step-body .success{display:block;background:#ECFDF5;border:1px solid #05966930;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#065F46}.tut-nav{display:flex;justify-content:space-between;align-items:center;margin-top:32px;padding-top:20px;border-top:1px solid #E5E7EB}.tut-nav a{display:inline-flex;align-items:center;gap:6px;padding:10px 16px;background:#F9FAFB;border:1px solid #E5E7EB;border-radius:10px;text-decoration:none;color:#374151;font-size:13px;font-weight:600;transition:all .15s}.tut-nav a:hover{background:<?= e($section['color']) ?>08;border-color:<?= e($section['color']) ?>;color:<?= e($section['color']) ?>}
</style>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="tut-page">
<div class="tut-breadcrumb">
<a href="/tutorials"><i data-lucide="graduation-cap" style="width:14px;height:14px;"></i></a>
<span>/</span>
<a href="/tutorials/<?= e($sectionKey) ?>"><?= e($section['title']) ?></a>
<span>/</span>
<span style="color:#1A1A2E;font-weight:600;"><?= e($tutorial['title']) ?></span>
</div>
<div class="tut-header">
<div class="tut-header-icon"><i data-lucide="<?= e($tutorial['icon']) ?>" style="width:28px;height:28px;color:#fff;"></i></div>
<div><h1><?= e($tutorial['title']) ?></h1><p><?= e($tutorial['subtitle']) ?></p></div>
</div>
<?php $num = 1; foreach ($steps as $step): ?>
<div class="tut-step">
<div class="tut-step-num"><?= $num ?></div>
<h3 class="tut-step-title"><?= e($step['title']) ?></h3>
<div class="tut-step-body"><?= $step['body'] ?></div>
</div>
<?php $num++; endforeach; ?>
<div class="tut-nav">
<?php if ($prevSlug): ?>
<a href="/tutorials/<?= e($sectionKey) ?>/<?= e($prevSlug) ?>"><i data-lucide="arrow-right" style="width:14px;height:14px;"></i> <?= e($prevTitle) ?></a>
<?php else: ?><span></span><?php endif; ?>
<?php if ($nextSlug): ?>
<a href="/tutorials/<?= e($sectionKey) ?>/<?= e($nextSlug) ?>"><?= e($nextTitle) ?> <i data-lucide="arrow-left" style="width:14px;height:14px;"></i></a>
<?php else: ?><span></span><?php endif; ?>
</div>
</div>
<script>document.addEventListener('DOMContentLoaded', function() { if (typeof lucide !== 'undefined') lucide.createIcons(); });</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>شرح: إيداع بنكي<?php $__template->endSection(); ?>
<?php $__template->section('styles'); ?>
<style>
.tut-page{max-width:860px;margin:0 auto}.tut-breadcrumb{display:flex;align-items:center;gap:8px;margin-bottom:20px;font-size:13px}.tut-breadcrumb a{color:#6B7280;text-decoration:none}.tut-breadcrumb a:hover{color:#059669}.tut-breadcrumb span{color:#9CA3AF}.tut-header{display:flex;align-items:center;gap:18px;margin-bottom:30px;padding:24px;background:linear-gradient(135deg,#05966908,#05966904);border-radius:16px;border:1px solid #05966920}.tut-header-icon{width:56px;height:56px;background:#059669;border-radius:14px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.tut-header h1{font-size:22px;font-weight:800;color:#1A1A2E;margin:0 0 4px}.tut-header p{font-size:14px;color:#6B7280;margin:0}.tut-step{position:relative;padding:20px 20px 20px 60px;margin-bottom:16px;background:#fff;border:1px solid #E5E7EB;border-radius:12px;transition:border-color .2s}.tut-step:hover{border-color:#05966960}.tut-step-num{position:absolute;right:18px;top:20px;width:32px;height:32px;background:#05966915;color:#059669;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:800}.tut-step-title{font-size:15px;font-weight:700;color:#1A1A2E;margin:0 0 6px}.tut-step-body{font-size:13.5px;color:#374151;line-height:1.8}.tut-step-body ul{margin:8px 0;padding-right:18px}.tut-step-body li{margin-bottom:4px}.tut-step-body .field{display:inline-block;background:#F3F4F6;color:#1A1A2E;padding:1px 8px;border-radius:4px;font-size:12px;font-weight:600;font-family:monospace;direction:ltr}.tut-step-body .warn{display:block;background:#FEF3C7;border:1px solid #F59E0B30;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#92400E}.tut-step-body .info{display:block;background:#DBEAFE;border:1px solid #3B82F630;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#1E40AF}.tut-step-body .success{display:block;background:#ECFDF5;border:1px solid #05966930;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#065F46}.tut-nav{display:flex;justify-content:space-between;align-items:center;margin-top:32px;padding-top:20px;border-top:1px solid #E5E7EB}.tut-nav a{display:inline-flex;align-items:center;gap:6px;padding:10px 16px;background:#F9FAFB;border:1px solid #E5E7EB;border-radius:10px;text-decoration:none;color:#374151;font-size:13px;font-weight:600;transition:all .15s}.tut-nav a:hover{background:#ECFDF5;border-color:#059669;color:#059669}.tut-diagram{background:#F8FAFC;border:1px solid #E2E8F0;border-radius:12px;padding:16px 20px;margin:12px 0;font-family:monospace;font-size:12px;direction:ltr;text-align:left;line-height:1.6;overflow-x:auto}
</style>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="tut-page">
<div class="tut-breadcrumb">
<a href="/tutorials"><i data-lucide="graduation-cap" style="width:14px;height:14px;"></i></a>
<span>/</span>
<a href="/tutorials/treasury">دورة التحصيلات</a>
<span>/</span>
<span style="color:#1A1A2E;font-weight:600;">إيداع بنكي</span>
</div>
<div class="tut-header">
<div class="tut-header-icon"><i data-lucide="upload" style="width:28px;height:28px;color:#fff;"></i></div>
<div><h1>إيداع بنكي</h1><p>تسجيل إيداع المبالغ في الحساب البنكي</p></div>
</div>
<div class="tut-step"><div class="tut-step-num">1</div><h3 class="tut-step-title">الذهاب إلى إيداعات البنك</h3><div class="tut-step-body">من القائمة الجانبية اذهب إلى <span class="field">الخزنة</span> > <span class="field">إيداعات البنك</span>. ستظهر قائمة الإيداعات السابقة.</div></div>
<div class="tut-step"><div class="tut-step-num">2</div><h3 class="tut-step-title">إنشاء إيداع جديد</h3><div class="tut-step-body">اضغط <span class="field">إيداع جديد</span>. ستظهر نافذة إدخال بيانات الإيداع.</div></div>
<div class="tut-step"><div class="tut-step-num">3</div><h3 class="tut-step-title">إدخال المبلغ</h3><div class="tut-step-body">أدخل المبلغ المراد إيداعه. يجب أن يتطابق مع رصيد العهدة الحالي أو جزء منه.<span class="info">النظام يعرض رصيد العهدة الحالي كمرجع. يُفضل إيداع المبلغ كاملاً لتصفير العهدة.</span></div></div>
<div class="tut-step"><div class="tut-step-num">4</div><h3 class="tut-step-title">اختيار البنك</h3><div class="tut-step-body">اختر البنك المودَع إليه من القائمة المنسدلة. البنوك تُعرَّف مسبقاً في إعدادات النظام.</div></div>
<div class="tut-step"><div class="tut-step-num">5</div><h3 class="tut-step-title">إدخال رقم إيصال البنك</h3><div class="tut-step-body">أدخل الرقم التسلسلي لإيصال الإيداع البنكي في حقل <span class="field">رقم الإيصال</span>. هذا الرقم مطبوع على الإيصال الذي تحصل عليه من البنك.</div></div>
<div class="tut-step"><div class="tut-step-num">6</div><h3 class="tut-step-title">رفع صورة الإيصال</h3><div class="tut-step-body">اضغط <span class="field">رفع صورة</span> وأرفق صورة واضحة لإيصال البنك. هذه الصورة ستُستخدم للمراجعة والتأكيد من قسم المحاسبة.<span class="warn">تأكد أن الصورة واضحة ويظهر فيها المبلغ ورقم الإيصال والتاريخ.</span></div></div>
<div class="tut-step"><div class="tut-step-num">7</div><h3 class="tut-step-title">إرسال الطلب</h3><div class="tut-step-body">اضغط <span class="field">إرسال</span>. الإيداع يُسجَّل بحالة <span class="field">في انتظار التأكيد</span> ويظهر لمدير المحاسبة للمراجعة.<span class="success">تم تسجيل الإيداع بنجاح. ينتظر تأكيد مدير المحاسبة لإخلاء العهدة.</span></div></div>
<div class="tut-nav">
<a href="/tutorials/treasury/receive-settlement">استلام تسوية <i data-lucide="arrow-right" style="width:14px;height:14px;"></i></a>
<a href="/tutorials/treasury/confirm-deposit"><i data-lucide="arrow-left" style="width:14px;height:14px;"></i> تأكيد إيداع</a>
</div>
</div>
<script>document.addEventListener('DOMContentLoaded', function() { if (typeof lucide !== 'undefined') lucide.createIcons(); });</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>شرح: إغلاق وردية<?php $__template->endSection(); ?>
<?php $__template->section('styles'); ?>
<style>
.tut-page{max-width:860px;margin:0 auto}.tut-breadcrumb{display:flex;align-items:center;gap:8px;margin-bottom:20px;font-size:13px}.tut-breadcrumb a{color:#6B7280;text-decoration:none}.tut-breadcrumb a:hover{color:#059669}.tut-breadcrumb span{color:#9CA3AF}.tut-header{display:flex;align-items:center;gap:18px;margin-bottom:30px;padding:24px;background:linear-gradient(135deg,#05966908,#05966904);border-radius:16px;border:1px solid #05966920}.tut-header-icon{width:56px;height:56px;background:#059669;border-radius:14px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.tut-header h1{font-size:22px;font-weight:800;color:#1A1A2E;margin:0 0 4px}.tut-header p{font-size:14px;color:#6B7280;margin:0}.tut-step{position:relative;padding:20px 20px 20px 60px;margin-bottom:16px;background:#fff;border:1px solid #E5E7EB;border-radius:12px;transition:border-color .2s}.tut-step:hover{border-color:#05966960}.tut-step-num{position:absolute;right:18px;top:20px;width:32px;height:32px;background:#05966915;color:#059669;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:800}.tut-step-title{font-size:15px;font-weight:700;color:#1A1A2E;margin:0 0 6px}.tut-step-body{font-size:13.5px;color:#374151;line-height:1.8}.tut-step-body ul{margin:8px 0;padding-right:18px}.tut-step-body li{margin-bottom:4px}.tut-step-body .field{display:inline-block;background:#F3F4F6;color:#1A1A2E;padding:1px 8px;border-radius:4px;font-size:12px;font-weight:600;font-family:monospace;direction:ltr}.tut-step-body .warn{display:block;background:#FEF3C7;border:1px solid #F59E0B30;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#92400E}.tut-step-body .info{display:block;background:#DBEAFE;border:1px solid #3B82F630;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#1E40AF}.tut-step-body .success{display:block;background:#ECFDF5;border:1px solid #05966930;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#065F46}.tut-nav{display:flex;justify-content:space-between;align-items:center;margin-top:32px;padding-top:20px;border-top:1px solid #E5E7EB}.tut-nav a{display:inline-flex;align-items:center;gap:6px;padding:10px 16px;background:#F9FAFB;border:1px solid #E5E7EB;border-radius:10px;text-decoration:none;color:#374151;font-size:13px;font-weight:600;transition:all .15s}.tut-nav a:hover{background:#ECFDF5;border-color:#059669;color:#059669}.tut-diagram{background:#F8FAFC;border:1px solid #E2E8F0;border-radius:12px;padding:16px 20px;margin:12px 0;font-family:monospace;font-size:12px;direction:ltr;text-align:left;line-height:1.6;overflow-x:auto}
</style>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="tut-page">
<div class="tut-breadcrumb">
<a href="/tutorials"><i data-lucide="graduation-cap" style="width:14px;height:14px;"></i></a>
<span>/</span>
<a href="/tutorials/treasury">دورة التحصيلات</a>
<span>/</span>
<span style="color:#1A1A2E;font-weight:600;">إغلاق وردية</span>
</div>
<div class="tut-header">
<div class="tut-header-icon"><i data-lucide="log-out" style="width:28px;height:28px;color:#fff;"></i></div>
<div><h1>إغلاق وردية</h1><p>إنهاء الوردية الحالية ومراجعة إجمالي التحصيلات</p></div>
</div>
<div class="tut-step"><div class="tut-step-num">1</div><h3 class="tut-step-title">الذهاب إلى لوحة التحكم</h3><div class="tut-step-body">من لوحة الخزنة الفرعية، ستجد ملخص الوردية الحالية يعرض عدد الإيصالات وإجمالي المبالغ المحصّلة.</div></div>
<div class="tut-step"><div class="tut-step-num">2</div><h3 class="tut-step-title">الضغط على إغلاق الوردية</h3><div class="tut-step-body">اضغط زر <span class="field">إغلاق الوردية</span>. سيطلب النظام تأكيداً قبل الإغلاق.<span class="warn">بعد الإغلاق لا يمكن إضافة تحصيلات جديدة على هذه الوردية. تأكد من إتمام كل العمليات المعلّقة.</span></div></div>
<div class="tut-step"><div class="tut-step-num">3</div><h3 class="tut-step-title">مراجعة الإجمالي</h3><div class="tut-step-body">النظام يعرض ملخص الوردية:
<ul>
<li>إجمالي المبلغ المحصّل</li>
<li>عدد الإيصالات الصادرة</li>
<li>تفصيل حسب طريقة الدفع (نقدي / بطاقة / تحويل)</li>
<li>وقت الفتح ووقت الإغلاق</li>
</ul>
</div></div>
<div class="tut-step"><div class="tut-step-num">4</div><h3 class="tut-step-title">حالة الوردية بعد الإغلاق</h3><div class="tut-step-body">الوردية تتحول إلى حالة <span class="field">مغلقة</span> ويمكنك الآن إنشاء تسوية لتحويل المبالغ إلى الخزنة الرئيسية.<span class="success">تم إغلاق الوردية بنجاح. الخطوة التالية: إنشاء تسوية لتسليم المبالغ.</span></div></div>
<div class="tut-nav">
<a href="/tutorials/treasury/collect-payment">تحصيل مبلغ <i data-lucide="arrow-right" style="width:14px;height:14px;"></i></a>
<a href="/tutorials/treasury/initiate-settlement"><i data-lucide="arrow-left" style="width:14px;height:14px;"></i> إنشاء تسوية</a>
</div>
</div>
<script>document.addEventListener('DOMContentLoaded', function() { if (typeof lucide !== 'undefined') lucide.createIcons(); });</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>شرح: تحصيل مبلغ<?php $__template->endSection(); ?>
<?php $__template->section('styles'); ?>
<style>
.tut-page{max-width:860px;margin:0 auto}.tut-breadcrumb{display:flex;align-items:center;gap:8px;margin-bottom:20px;font-size:13px}.tut-breadcrumb a{color:#6B7280;text-decoration:none}.tut-breadcrumb a:hover{color:#059669}.tut-breadcrumb span{color:#9CA3AF}.tut-header{display:flex;align-items:center;gap:18px;margin-bottom:30px;padding:24px;background:linear-gradient(135deg,#05966908,#05966904);border-radius:16px;border:1px solid #05966920}.tut-header-icon{width:56px;height:56px;background:#059669;border-radius:14px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.tut-header h1{font-size:22px;font-weight:800;color:#1A1A2E;margin:0 0 4px}.tut-header p{font-size:14px;color:#6B7280;margin:0}.tut-step{position:relative;padding:20px 20px 20px 60px;margin-bottom:16px;background:#fff;border:1px solid #E5E7EB;border-radius:12px;transition:border-color .2s}.tut-step:hover{border-color:#05966960}.tut-step-num{position:absolute;right:18px;top:20px;width:32px;height:32px;background:#05966915;color:#059669;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:800}.tut-step-title{font-size:15px;font-weight:700;color:#1A1A2E;margin:0 0 6px}.tut-step-body{font-size:13.5px;color:#374151;line-height:1.8}.tut-step-body ul{margin:8px 0;padding-right:18px}.tut-step-body li{margin-bottom:4px}.tut-step-body .field{display:inline-block;background:#F3F4F6;color:#1A1A2E;padding:1px 8px;border-radius:4px;font-size:12px;font-weight:600;font-family:monospace;direction:ltr}.tut-step-body .warn{display:block;background:#FEF3C7;border:1px solid #F59E0B30;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#92400E}.tut-step-body .info{display:block;background:#DBEAFE;border:1px solid #3B82F630;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#1E40AF}.tut-step-body .success{display:block;background:#ECFDF5;border:1px solid #05966930;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#065F46}.tut-nav{display:flex;justify-content:space-between;align-items:center;margin-top:32px;padding-top:20px;border-top:1px solid #E5E7EB}.tut-nav a{display:inline-flex;align-items:center;gap:6px;padding:10px 16px;background:#F9FAFB;border:1px solid #E5E7EB;border-radius:10px;text-decoration:none;color:#374151;font-size:13px;font-weight:600;transition:all .15s}.tut-nav a:hover{background:#ECFDF5;border-color:#059669;color:#059669}.tut-diagram{background:#F8FAFC;border:1px solid #E2E8F0;border-radius:12px;padding:16px 20px;margin:12px 0;font-family:monospace;font-size:12px;direction:ltr;text-align:left;line-height:1.6;overflow-x:auto}
</style>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="tut-page">
<div class="tut-breadcrumb">
<a href="/tutorials"><i data-lucide="graduation-cap" style="width:14px;height:14px;"></i></a>
<span>/</span>
<a href="/tutorials/treasury">دورة التحصيلات</a>
<span>/</span>
<span style="color:#1A1A2E;font-weight:600;">تحصيل مبلغ</span>
</div>
<div class="tut-header">
<div class="tut-header-icon"><i data-lucide="banknote" style="width:28px;height:28px;color:#fff;"></i></div>
<div><h1>تحصيل مبلغ</h1><p>استلام دفعة من عضو أو لاعب وإصدار إيصال</p></div>
</div>
<div class="tut-step"><div class="tut-step-num">1</div><h3 class="tut-step-title">فتح قائمة الانتظار</h3><div class="tut-step-body">من لوحة الخزنة الفرعية، اضغط على <span class="field">طلبات التحصيل</span> أو <span class="field">قائمة الانتظار</span>. ستظهر جميع الطلبات المعلّقة التي تنتظر الدفع.</div></div>
<div class="tut-step"><div class="tut-step-num">2</div><h3 class="tut-step-title">اختيار الطلب المعلّق</h3><div class="tut-step-body">اختر الطلب المطلوب تحصيله. ستظهر تفاصيل المبلغ المستحق واسم العضو ونوع الخدمة (اشتراك، تجديد، نشاط رياضي، إلخ).<span class="info">يمكنك البحث بالاسم أو رقم العضوية للوصول السريع للطلب.</span></div></div>
<div class="tut-step"><div class="tut-step-num">3</div><h3 class="tut-step-title">اختيار طريقة الدفع</h3><div class="tut-step-body">حدد طريقة الدفع:
<ul>
<li><span class="field">نقدي</span> — دفع كاش مباشر</li>
<li><span class="field">بطاقة</span> — دفع ببطاقة ائتمان/خصم</li>
<li><span class="field">تحويل</span> — تحويل بنكي</li>
</ul>
<span class="warn">تأكد من اختيار الطريقة الصحيحة لأنها تؤثر على تقارير التسوية.</span>
</div></div>
<div class="tut-step"><div class="tut-step-num">4</div><h3 class="tut-step-title">تأكيد التحصيل</h3><div class="tut-step-body">اضغط <span class="field">تأكيد التحصيل</span>. النظام يسجّل العملية ويربطها بالوردية الحالية.</div></div>
<div class="tut-step"><div class="tut-step-num">5</div><h3 class="tut-step-title">طباعة الإيصال</h3><div class="tut-step-body">يُطبع إيصال استلام تلقائياً يحتوي على رقم الإيصال والمبلغ واسم العضو وتاريخ ووقت التحصيل. سلّم نسخة للعضو.<span class="success">تم التحصيل بنجاح — رصيد العهدة زاد بقيمة المبلغ المحصّل.</span></div></div>
<div class="tut-nav">
<a href="/tutorials/treasury/open-session">فتح وردية <i data-lucide="arrow-right" style="width:14px;height:14px;"></i></a>
<a href="/tutorials/treasury/close-session"><i data-lucide="arrow-left" style="width:14px;height:14px;"></i> إغلاق وردية</a>
</div>
</div>
<script>document.addEventListener('DOMContentLoaded', function() { if (typeof lucide !== 'undefined') lucide.createIcons(); });</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>شرح: تأكيد إيداع<?php $__template->endSection(); ?>
<?php $__template->section('styles'); ?>
<style>
.tut-page{max-width:860px;margin:0 auto}.tut-breadcrumb{display:flex;align-items:center;gap:8px;margin-bottom:20px;font-size:13px}.tut-breadcrumb a{color:#6B7280;text-decoration:none}.tut-breadcrumb a:hover{color:#059669}.tut-breadcrumb span{color:#9CA3AF}.tut-header{display:flex;align-items:center;gap:18px;margin-bottom:30px;padding:24px;background:linear-gradient(135deg,#05966908,#05966904);border-radius:16px;border:1px solid #05966920}.tut-header-icon{width:56px;height:56px;background:#059669;border-radius:14px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.tut-header h1{font-size:22px;font-weight:800;color:#1A1A2E;margin:0 0 4px}.tut-header p{font-size:14px;color:#6B7280;margin:0}.tut-step{position:relative;padding:20px 20px 20px 60px;margin-bottom:16px;background:#fff;border:1px solid #E5E7EB;border-radius:12px;transition:border-color .2s}.tut-step:hover{border-color:#05966960}.tut-step-num{position:absolute;right:18px;top:20px;width:32px;height:32px;background:#05966915;color:#059669;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:800}.tut-step-title{font-size:15px;font-weight:700;color:#1A1A2E;margin:0 0 6px}.tut-step-body{font-size:13.5px;color:#374151;line-height:1.8}.tut-step-body ul{margin:8px 0;padding-right:18px}.tut-step-body li{margin-bottom:4px}.tut-step-body .field{display:inline-block;background:#F3F4F6;color:#1A1A2E;padding:1px 8px;border-radius:4px;font-size:12px;font-weight:600;font-family:monospace;direction:ltr}.tut-step-body .warn{display:block;background:#FEF3C7;border:1px solid #F59E0B30;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#92400E}.tut-step-body .info{display:block;background:#DBEAFE;border:1px solid #3B82F630;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#1E40AF}.tut-step-body .success{display:block;background:#ECFDF5;border:1px solid #05966930;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#065F46}.tut-nav{display:flex;justify-content:space-between;align-items:center;margin-top:32px;padding-top:20px;border-top:1px solid #E5E7EB}.tut-nav a{display:inline-flex;align-items:center;gap:6px;padding:10px 16px;background:#F9FAFB;border:1px solid #E5E7EB;border-radius:10px;text-decoration:none;color:#374151;font-size:13px;font-weight:600;transition:all .15s}.tut-nav a:hover{background:#ECFDF5;border-color:#059669;color:#059669}.tut-diagram{background:#F8FAFC;border:1px solid #E2E8F0;border-radius:12px;padding:16px 20px;margin:12px 0;font-family:monospace;font-size:12px;direction:ltr;text-align:left;line-height:1.6;overflow-x:auto}
</style>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="tut-page">
<div class="tut-breadcrumb">
<a href="/tutorials"><i data-lucide="graduation-cap" style="width:14px;height:14px;"></i></a>
<span>/</span>
<a href="/tutorials/treasury">دورة التحصيلات</a>
<span>/</span>
<span style="color:#1A1A2E;font-weight:600;">تأكيد إيداع</span>
</div>
<div class="tut-header">
<div class="tut-header-icon"><i data-lucide="check-circle" style="width:28px;height:28px;color:#fff;"></i></div>
<div><h1>تأكيد إيداع</h1><p>مراجعة واعتماد الإيداع البنكي من مدير المحاسبة</p></div>
</div>
<div class="tut-step"><div class="tut-step-num">1</div><h3 class="tut-step-title">فتح شاشة الإيداعات</h3><div class="tut-step-body">مدير المحاسبة يذهب إلى <span class="field">المحاسبة</span> > <span class="field">إيداعات البنك</span> أو من الإشعارات الواردة بوجود إيداع معلّق.</div></div>
<div class="tut-step"><div class="tut-step-num">2</div><h3 class="tut-step-title">عرض الإيداعات المعلّقة</h3><div class="tut-step-body">ستظهر قائمة الإيداعات بحالة <span class="field">في انتظار التأكيد</span>. اختر الإيداع المراد مراجعته.</div></div>
<div class="tut-step"><div class="tut-step-num">3</div><h3 class="tut-step-title">التحقق من صورة الإيصال والرقم التسلسلي</h3><div class="tut-step-body">راجع التفاصيل:
<ul>
<li>صورة إيصال البنك — تأكد أنها واضحة وصحيحة</li>
<li>الرقم التسلسلي — طابقه مع الظاهر في الصورة</li>
<li>المبلغ — تأكد أنه يتطابق مع المسجّل في النظام</li>
<li>اسم البنك وتاريخ الإيداع</li>
</ul>
<span class="warn">في حالة وجود أي اختلاف أو شك، ارفض الإيداع واطلب توضيحاً من أمين الخزنة.</span>
</div></div>
<div class="tut-step"><div class="tut-step-num">4</div><h3 class="tut-step-title">الضغط على تأكيد</h3><div class="tut-step-body">بعد التحقق، اضغط <span class="field">تأكيد</span>. النظام يقوم بـ:
<ul>
<li>تحويل حالة الإيداع إلى <span class="field">مؤكَّد</span></li>
<li>تصفير عهدة أمين الخزنة بقيمة المبلغ المودَع</li>
<li>ترحيل قيد محاسبي تلقائي (من حـ/ الخزنة إلى حـ/ البنك)</li>
</ul>
</div></div>
<div class="tut-step"><div class="tut-step-num">5</div><h3 class="tut-step-title">القيد المحاسبي</h3><div class="tut-step-body"><div class="tut-diagram">Dr: Bank Account (XXX) ← المبلغ المودَع
Cr: Cash in Hand (XXX) ← إخلاء العهدة</div><span class="success">تم التأكيد — العهدة أُخليت والقيد المحاسبي تُرحّل تلقائياً.</span></div></div>
<div class="tut-nav">
<a href="/tutorials/treasury/bank-deposit">إيداع بنكي <i data-lucide="arrow-right" style="width:14px;height:14px;"></i></a>
<a href="/tutorials/treasury/custody-tracking"><i data-lucide="arrow-left" style="width:14px;height:14px;"></i> متابعة العهدة</a>
</div>
</div>
<script>document.addEventListener('DOMContentLoaded', function() { if (typeof lucide !== 'undefined') lucide.createIcons(); });</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>شرح: متابعة العهدة<?php $__template->endSection(); ?>
<?php $__template->section('styles'); ?>
<style>
.tut-page{max-width:860px;margin:0 auto}.tut-breadcrumb{display:flex;align-items:center;gap:8px;margin-bottom:20px;font-size:13px}.tut-breadcrumb a{color:#6B7280;text-decoration:none}.tut-breadcrumb a:hover{color:#059669}.tut-breadcrumb span{color:#9CA3AF}.tut-header{display:flex;align-items:center;gap:18px;margin-bottom:30px;padding:24px;background:linear-gradient(135deg,#05966908,#05966904);border-radius:16px;border:1px solid #05966920}.tut-header-icon{width:56px;height:56px;background:#059669;border-radius:14px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.tut-header h1{font-size:22px;font-weight:800;color:#1A1A2E;margin:0 0 4px}.tut-header p{font-size:14px;color:#6B7280;margin:0}.tut-step{position:relative;padding:20px 20px 20px 60px;margin-bottom:16px;background:#fff;border:1px solid #E5E7EB;border-radius:12px;transition:border-color .2s}.tut-step:hover{border-color:#05966960}.tut-step-num{position:absolute;right:18px;top:20px;width:32px;height:32px;background:#05966915;color:#059669;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:800}.tut-step-title{font-size:15px;font-weight:700;color:#1A1A2E;margin:0 0 6px}.tut-step-body{font-size:13.5px;color:#374151;line-height:1.8}.tut-step-body ul{margin:8px 0;padding-right:18px}.tut-step-body li{margin-bottom:4px}.tut-step-body .field{display:inline-block;background:#F3F4F6;color:#1A1A2E;padding:1px 8px;border-radius:4px;font-size:12px;font-weight:600;font-family:monospace;direction:ltr}.tut-step-body .warn{display:block;background:#FEF3C7;border:1px solid #F59E0B30;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#92400E}.tut-step-body .info{display:block;background:#DBEAFE;border:1px solid #3B82F630;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#1E40AF}.tut-step-body .success{display:block;background:#ECFDF5;border:1px solid #05966930;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#065F46}.tut-nav{display:flex;justify-content:space-between;align-items:center;margin-top:32px;padding-top:20px;border-top:1px solid #E5E7EB}.tut-nav a{display:inline-flex;align-items:center;gap:6px;padding:10px 16px;background:#F9FAFB;border:1px solid #E5E7EB;border-radius:10px;text-decoration:none;color:#374151;font-size:13px;font-weight:600;transition:all .15s}.tut-nav a:hover{background:#ECFDF5;border-color:#059669;color:#059669}.tut-diagram{background:#F8FAFC;border:1px solid #E2E8F0;border-radius:12px;padding:16px 20px;margin:12px 0;font-family:monospace;font-size:12px;direction:ltr;text-align:left;line-height:1.6;overflow-x:auto}
</style>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="tut-page">
<div class="tut-breadcrumb">
<a href="/tutorials"><i data-lucide="graduation-cap" style="width:14px;height:14px;"></i></a>
<span>/</span>
<a href="/tutorials/treasury">دورة التحصيلات</a>
<span>/</span>
<span style="color:#1A1A2E;font-weight:600;">متابعة العهدة</span>
</div>
<div class="tut-header">
<div class="tut-header-icon"><i data-lucide="wallet" style="width:28px;height:28px;color:#fff;"></i></div>
<div><h1>متابعة العهدة</h1><p>فهم رصيد العهدة وكيفية مراقبته وتصفيره</p></div>
</div>
<div class="tut-step"><div class="tut-step-num">1</div><h3 class="tut-step-title">ما هي العهدة؟</h3><div class="tut-step-body">العهدة هي المبلغ المالي الموجود في حوزة الموظف والذي لم يُسلَّم بعد. كل موظف خزنة له رصيد عهدة يتغير مع العمليات اليومية.<span class="info">العهدة ليست ملكاً للموظف — هي أموال النادي المؤتمن عليها مؤقتاً حتى تسليمها.</span></div></div>
<div class="tut-step"><div class="tut-step-num">2</div><h3 class="tut-step-title">أين ترى رصيد العهدة؟</h3><div class="tut-step-body">رصيد العهدة يظهر في:
<ul>
<li><span class="field">لوحة الخزنة الفرعية</span> — الرصيد الحالي بشكل بارز</li>
<li><span class="field">لوحة الخزنة الرئيسية</span> — عهدة أمين الخزنة</li>
<li><span class="field">تقارير العهد</span> — تفصيل لكل موظف</li>
</ul>
</div></div>
<div class="tut-step"><div class="tut-step-num">3</div><h3 class="tut-step-title">كيف تزداد العهدة؟</h3><div class="tut-step-body">العهدة تزداد عند:
<ul>
<li>تحصيل مبلغ من عضو أو لاعب (كل إيصال يزيد العهدة)</li>
<li>استلام تسوية من خزنة فرعية (لأمين الخزنة الرئيسية)</li>
</ul>
<div class="tut-diagram">تحصيل 500 ج → العهدة: 0 + 500 = 500 ج
تحصيل 300 ج → العهدة: 500 + 300 = 800 ج</div>
</div></div>
<div class="tut-step"><div class="tut-step-num">4</div><h3 class="tut-step-title">كيف تنقص العهدة؟</h3><div class="tut-step-body">العهدة تنقص عند:
<ul>
<li><span class="field">التسوية</span> — تحويل المبلغ للخزنة الرئيسية (للخزنة الفرعية)</li>
<li><span class="field">الإيداع البنكي المؤكَّد</span> — بعد تأكيد المحاسبة (للخزنة الرئيسية)</li>
</ul>
<div class="tut-diagram">العهدة: 800 ج
تسوية 800 ج → العهدة: 800 - 800 = 0 ج ✓</div>
</div></div>
<div class="tut-step"><div class="tut-step-num">5</div><h3 class="tut-step-title">ماذا يعني رصيد غير صفري نهاية اليوم؟</h3><div class="tut-step-body"><span class="warn">إذا كان رصيد العهدة غير صفري في نهاية اليوم، فهذا يعني أن هناك أموالاً لم تُسلَّم بعد. يجب على الموظف إتمام التسوية أو الإيداع قبل مغادرة العمل.</span>
<ul>
<li>عهدة الخزنة الفرعية غير صفرية = لم تُنشأ تسوية</li>
<li>عهدة الخزنة الرئيسية غير صفرية = لم يُودَع في البنك أو الإيداع لم يُؤكَّد</li>
</ul>
<span class="success">الوضع المثالي: عهدة الجميع = صفر في نهاية كل يوم عمل.</span>
</div></div>
<div class="tut-nav">
<a href="/tutorials/treasury/confirm-deposit">تأكيد إيداع <i data-lucide="arrow-right" style="width:14px;height:14px;"></i></a>
<a href="/tutorials/treasury/full-cycle"><i data-lucide="arrow-left" style="width:14px;height:14px;"></i> الدورة الكاملة</a>
</div>
</div>
<script>document.addEventListener('DOMContentLoaded', function() { if (typeof lucide !== 'undefined') lucide.createIcons(); });</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>شرح: الدورة الكاملة<?php $__template->endSection(); ?>
<?php $__template->section('styles'); ?>
<style>
.tut-page{max-width:860px;margin:0 auto}.tut-breadcrumb{display:flex;align-items:center;gap:8px;margin-bottom:20px;font-size:13px}.tut-breadcrumb a{color:#6B7280;text-decoration:none}.tut-breadcrumb a:hover{color:#059669}.tut-breadcrumb span{color:#9CA3AF}.tut-header{display:flex;align-items:center;gap:18px;margin-bottom:30px;padding:24px;background:linear-gradient(135deg,#05966908,#05966904);border-radius:16px;border:1px solid #05966920}.tut-header-icon{width:56px;height:56px;background:#059669;border-radius:14px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.tut-header h1{font-size:22px;font-weight:800;color:#1A1A2E;margin:0 0 4px}.tut-header p{font-size:14px;color:#6B7280;margin:0}.tut-step{position:relative;padding:20px 20px 20px 60px;margin-bottom:16px;background:#fff;border:1px solid #E5E7EB;border-radius:12px;transition:border-color .2s}.tut-step:hover{border-color:#05966960}.tut-step-num{position:absolute;right:18px;top:20px;width:32px;height:32px;background:#05966915;color:#059669;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:800}.tut-step-title{font-size:15px;font-weight:700;color:#1A1A2E;margin:0 0 6px}.tut-step-body{font-size:13.5px;color:#374151;line-height:1.8}.tut-step-body ul{margin:8px 0;padding-right:18px}.tut-step-body li{margin-bottom:4px}.tut-step-body .field{display:inline-block;background:#F3F4F6;color:#1A1A2E;padding:1px 8px;border-radius:4px;font-size:12px;font-weight:600;font-family:monospace;direction:ltr}.tut-step-body .warn{display:block;background:#FEF3C7;border:1px solid #F59E0B30;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#92400E}.tut-step-body .info{display:block;background:#DBEAFE;border:1px solid #3B82F630;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#1E40AF}.tut-step-body .success{display:block;background:#ECFDF5;border:1px solid #05966930;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#065F46}.tut-nav{display:flex;justify-content:space-between;align-items:center;margin-top:32px;padding-top:20px;border-top:1px solid #E5E7EB}.tut-nav a{display:inline-flex;align-items:center;gap:6px;padding:10px 16px;background:#F9FAFB;border:1px solid #E5E7EB;border-radius:10px;text-decoration:none;color:#374151;font-size:13px;font-weight:600;transition:all .15s}.tut-nav a:hover{background:#ECFDF5;border-color:#059669;color:#059669}.tut-diagram{background:#F8FAFC;border:1px solid #E2E8F0;border-radius:12px;padding:16px 20px;margin:12px 0;font-family:monospace;font-size:12px;direction:ltr;text-align:left;line-height:1.6;overflow-x:auto}
</style>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="tut-page">
<div class="tut-breadcrumb">
<a href="/tutorials"><i data-lucide="graduation-cap" style="width:14px;height:14px;"></i></a>
<span>/</span>
<a href="/tutorials/treasury">دورة التحصيلات</a>
<span>/</span>
<span style="color:#1A1A2E;font-weight:600;">الدورة الكاملة</span>
</div>
<div class="tut-header">
<div class="tut-header-icon"><i data-lucide="refresh-cw" style="width:28px;height:28px;color:#fff;"></i></div>
<div><h1>الدورة الكاملة</h1><p>من لحظة دفع اللاعب حتى القيد المحاسبي النهائي</p></div>
</div>
<div class="tut-step"><div class="tut-step-num">1</div><h3 class="tut-step-title">الصورة الكاملة</h3><div class="tut-step-body"><div class="tut-diagram">Player pays 500 EGP
┌─────────────────────────────┐
│ Sub-Treasury (Cashier) │ ← custody +500
│ Session: SES-2026-0547 │
│ Receipt: RCP-2026-1234 │
└─────────────────────────────┘
│ Close session + Settlement
┌─────────────────────────────┐
│ Main Treasury (Treasurer) │ ← custody +500
│ Settlement: STL-2026-0198 │
└─────────────────────────────┘
│ Bank deposit
┌─────────────────────────────┐
│ Bank Deposit (Pending) │
│ Deposit: DEP-2026-0087 │
│ Image + Serial attached │
└─────────────────────────────┘
│ Accounting confirms
┌─────────────────────────────┐
│ Journal Entry (Auto) │
│ Dr: Bank 500 │
│ Cr: Cash 500 │
│ Custody = 0 ✓ │
└─────────────────────────────┘</div></div></div>
<div class="tut-step"><div class="tut-step-num">2</div><h3 class="tut-step-title">سيناريو يوم عمل كامل</h3><div class="tut-step-body">
<ul>
<li><strong>8:00 ص</strong> — موظف الخزنة الفرعية يفتح وردية</li>
<li><strong>8:15 ص</strong> — لاعب أحمد يدفع اشتراك شهري 500 ج (إيصال + عهدة = 500)</li>
<li><strong>9:30 ص</strong> — لاعب محمد يدفع تجديد عضوية 1000 ج (عهدة = 1500)</li>
<li><strong>11:00 ص</strong> — عضو سارة تدفع نشاط سباحة 300 ج (عهدة = 1800)</li>
<li><strong>2:00 م</strong> — الموظف يغلق الوردية (إجمالي: 1800 ج، 3 إيصالات)</li>
<li><strong>2:05 م</strong> — إنشاء تسوية 1800 ج (عهدة الموظف = 0)</li>
<li><strong>2:10 م</strong> — أمين الخزنة الرئيسية يستلم (عهدته = 1800)</li>
<li><strong>3:00 م</strong> — أمين الخزنة يذهب للبنك ويودع 1800 ج</li>
<li><strong>3:30 م</strong> — يرفع صورة الإيصال في النظام</li>
<li><strong>4:00 م</strong> — مدير المحاسبة يؤكد الإيداع (عهدة الأمين = 0)</li>
</ul>
<span class="success">نهاية اليوم: عهدة الجميع = صفر. كل المبالغ في البنك. القيد المحاسبي مُرحَّل.</span>
</div></div>
<div class="tut-step"><div class="tut-step-num">3</div><h3 class="tut-step-title">الأدوار والمسؤوليات</h3><div class="tut-step-body">
<ul>
<li><strong>موظف الخزنة الفرعية:</strong> فتح وردية، تحصيل، إغلاق وردية، إنشاء تسوية</li>
<li><strong>أمين الخزنة الرئيسية:</strong> استلام تسويات، إيداع بنكي</li>
<li><strong>مدير المحاسبة:</strong> تأكيد الإيداعات، مراجعة القيود</li>
</ul>
<span class="info">كل دور له صلاحيات محددة في النظام. لا يمكن لموظف أن يؤكد إيداعه بنفسه — الفصل بين المهام يضمن الرقابة.</span>
</div></div>
<div class="tut-step"><div class="tut-step-num">4</div><h3 class="tut-step-title">ماذا لو حدث خطأ؟</h3><div class="tut-step-body">
<ul>
<li><strong>فرق في المبلغ عند التسوية:</strong> لا يمكن إتمام التسوية — راجع الإيصالات</li>
<li><strong>إيصال بنكي غير واضح:</strong> المحاسبة ترفض الإيداع ويُطلب إعادة الرفع</li>
<li><strong>عهدة غير صفرية نهاية اليوم:</strong> تنبيه تلقائي للإدارة</li>
</ul>
<span class="warn">النظام مصمم بحيث لا يضيع أي مبلغ. كل جنيه يُتتبع من لحظة استلامه حتى وصوله للبنك.</span>
</div></div>
<div class="tut-nav">
<a href="/tutorials/treasury/custody-tracking">متابعة العهدة <i data-lucide="arrow-right" style="width:14px;height:14px;"></i></a>
<span></span>
</div>
</div>
<script>document.addEventListener('DOMContentLoaded', function() { if (typeof lucide !== 'undefined') lucide.createIcons(); });</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>شروحات الخزنة والتحصيلات<?php $__template->endSection(); ?>
<?php $__template->section('styles'); ?>
<style>
.tut-hero { text-align:center; margin-bottom:36px; padding:30px 20px; background:linear-gradient(135deg,#05966910,#05966908); border-radius:20px; border:1px solid #05966920; }
.tut-hero-icon { width:64px;height:64px;background:linear-gradient(135deg,#059669,#047857);border-radius:16px;display:inline-flex;align-items:center;justify-content:center;margin-bottom:14px; }
.tut-category { margin-bottom:32px; }
.tut-category-header { display:flex;align-items:center;gap:12px;margin-bottom:14px;padding-bottom:10px;border-bottom:2px solid #F3F4F6; }
.tut-category-icon { width:36px;height:36px;border-radius:10px;display:flex;align-items:center;justify-content:center; }
.tut-category-label { font-size:17px;font-weight:700;color:#1A1A2E; }
.tut-grid { display:grid;grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));gap:14px; }
.tut-card { display:flex;align-items:center;gap:14px;padding:16px 18px;border-radius:12px;border:1px solid #E5E7EB;background:#fff;text-decoration:none;color:inherit;transition:all .15s ease; }
.tut-card:hover { border-color:var(--card-color);box-shadow:0 4px 12px rgba(0,0,0,.06);transform:translateY(-1px); }
.tut-card-icon { width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;flex-shrink:0; }
.tut-card-title { font-size:14px;font-weight:600;color:#1A1A2E;margin:0 0 2px; }
.tut-card-sub { font-size:12px;color:#6B7280;margin:0; }
.tut-card-num { font-size:11px;font-weight:700;color:#fff;background:var(--card-color);width:22px;height:22px;border-radius:6px;display:flex;align-items:center;justify-content:center;flex-shrink:0; }
.tut-breadcrumb { display:flex;align-items:center;gap:8px;margin-bottom:20px;font-size:13px; }
.tut-breadcrumb a { color:#6B7280;text-decoration:none; }
.tut-breadcrumb a:hover { color:#059669; }
.tut-breadcrumb span { color:#9CA3AF; }
</style>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="max-width:1000px;margin:0 auto;">
<div class="tut-breadcrumb">
<a href="/tutorials"><i data-lucide="graduation-cap" style="width:14px;height:14px;"></i></a>
<span>/</span>
<span style="color:#1A1A2E;font-weight:600;">دورة التحصيلات</span>
</div>
<div class="tut-hero">
<div class="tut-hero-icon">
<i data-lucide="landmark" style="width:32px;height:32px;color:#fff;"></i>
</div>
<h2 style="font-size:24px;font-weight:800;color:#1A1A2E;margin:0 0 6px;">شروحات الخزنة والتحصيلات</h2>
<p style="font-size:14px;color:#6B7280;margin:0;"><?= (int) $totalCount ?> شرح تفصيلي يغطي دورة التحصيل الكاملة من فتح الوردية حتى تأكيد الإيداع</p>
</div>
<?php $num = 1; foreach ($categories as $catKey => $cat): ?>
<div class="tut-category">
<div class="tut-category-header">
<div class="tut-category-icon" style="background:<?= e($cat['color']) ?>15;">
<i data-lucide="<?= e($cat['icon']) ?>" style="width:18px;height:18px;color:<?= e($cat['color']) ?>;"></i>
</div>
<span class="tut-category-label"><?= e($cat['label']) ?></span>
<span style="font-size:12px;color:#9CA3AF;margin-right:auto;"><?= count($cat['tutorials']) ?> شرح</span>
</div>
<div class="tut-grid">
<?php foreach ($cat['tutorials'] as $slug => $tut): ?>
<a href="/tutorials/treasury/<?= e($slug) ?>" class="tut-card" style="--card-color:<?= e($tut['color']) ?>;">
<div class="tut-card-num" style="background:<?= e($tut['color']) ?>;"><?= $num ?></div>
<div class="tut-card-icon" style="background:<?= e($tut['color']) ?>12;">
<i data-lucide="<?= e($tut['icon']) ?>" style="width:20px;height:20px;color:<?= e($tut['color']) ?>;"></i>
</div>
<div style="flex:1;min-width:0;">
<p class="tut-card-title"><?= e($tut['title']) ?></p>
<p class="tut-card-sub"><?= e($tut['subtitle']) ?></p>
</div>
<i data-lucide="chevron-left" style="width:16px;height:16px;color:#D1D5DB;flex-shrink:0;"></i>
</a>
<?php $num++; endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<script>document.addEventListener('DOMContentLoaded', function() { if (typeof lucide !== 'undefined') lucide.createIcons(); });</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>شرح: إنشاء تسوية<?php $__template->endSection(); ?>
<?php $__template->section('styles'); ?>
<style>
.tut-page{max-width:860px;margin:0 auto}.tut-breadcrumb{display:flex;align-items:center;gap:8px;margin-bottom:20px;font-size:13px}.tut-breadcrumb a{color:#6B7280;text-decoration:none}.tut-breadcrumb a:hover{color:#059669}.tut-breadcrumb span{color:#9CA3AF}.tut-header{display:flex;align-items:center;gap:18px;margin-bottom:30px;padding:24px;background:linear-gradient(135deg,#05966908,#05966904);border-radius:16px;border:1px solid #05966920}.tut-header-icon{width:56px;height:56px;background:#059669;border-radius:14px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.tut-header h1{font-size:22px;font-weight:800;color:#1A1A2E;margin:0 0 4px}.tut-header p{font-size:14px;color:#6B7280;margin:0}.tut-step{position:relative;padding:20px 20px 20px 60px;margin-bottom:16px;background:#fff;border:1px solid #E5E7EB;border-radius:12px;transition:border-color .2s}.tut-step:hover{border-color:#05966960}.tut-step-num{position:absolute;right:18px;top:20px;width:32px;height:32px;background:#05966915;color:#059669;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:800}.tut-step-title{font-size:15px;font-weight:700;color:#1A1A2E;margin:0 0 6px}.tut-step-body{font-size:13.5px;color:#374151;line-height:1.8}.tut-step-body ul{margin:8px 0;padding-right:18px}.tut-step-body li{margin-bottom:4px}.tut-step-body .field{display:inline-block;background:#F3F4F6;color:#1A1A2E;padding:1px 8px;border-radius:4px;font-size:12px;font-weight:600;font-family:monospace;direction:ltr}.tut-step-body .warn{display:block;background:#FEF3C7;border:1px solid #F59E0B30;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#92400E}.tut-step-body .info{display:block;background:#DBEAFE;border:1px solid #3B82F630;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#1E40AF}.tut-step-body .success{display:block;background:#ECFDF5;border:1px solid #05966930;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#065F46}.tut-nav{display:flex;justify-content:space-between;align-items:center;margin-top:32px;padding-top:20px;border-top:1px solid #E5E7EB}.tut-nav a{display:inline-flex;align-items:center;gap:6px;padding:10px 16px;background:#F9FAFB;border:1px solid #E5E7EB;border-radius:10px;text-decoration:none;color:#374151;font-size:13px;font-weight:600;transition:all .15s}.tut-nav a:hover{background:#ECFDF5;border-color:#059669;color:#059669}.tut-diagram{background:#F8FAFC;border:1px solid #E2E8F0;border-radius:12px;padding:16px 20px;margin:12px 0;font-family:monospace;font-size:12px;direction:ltr;text-align:left;line-height:1.6;overflow-x:auto}
</style>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="tut-page">
<div class="tut-breadcrumb">
<a href="/tutorials"><i data-lucide="graduation-cap" style="width:14px;height:14px;"></i></a>
<span>/</span>
<a href="/tutorials/treasury">دورة التحصيلات</a>
<span>/</span>
<span style="color:#1A1A2E;font-weight:600;">إنشاء تسوية</span>
</div>
<div class="tut-header">
<div class="tut-header-icon"><i data-lucide="send" style="width:28px;height:28px;color:#fff;"></i></div>
<div><h1>إنشاء تسوية</h1><p>تحويل المبالغ المحصّلة من الخزنة الفرعية إلى الخزنة الرئيسية</p></div>
</div>
<div class="tut-step"><div class="tut-step-num">1</div><h3 class="tut-step-title">الذهاب إلى التسويات</h3><div class="tut-step-body">من القائمة الجانبية اذهب إلى <span class="field">الخزنة</span> > <span class="field">التسويات</span>. ستظهر قائمة التسويات السابقة وزر إنشاء تسوية جديدة.</div></div>
<div class="tut-step"><div class="tut-step-num">2</div><h3 class="tut-step-title">إنشاء تسوية جديدة</h3><div class="tut-step-body">اضغط <span class="field">تسوية جديدة</span>. النظام يعرض الورديات المغلقة التي لم تُسوَّ بعد.</div></div>
<div class="tut-step"><div class="tut-step-num">3</div><h3 class="tut-step-title">اختيار الوردية المغلقة</h3><div class="tut-step-body">اختر الوردية المراد تسويتها. ستظهر تفاصيل المبلغ الإجمالي وعدد الإيصالات.<span class="info">يمكنك تسوية أكثر من وردية في نفس الوقت إذا كان لديك ورديات مغلقة متعددة.</span></div></div>
<div class="tut-step"><div class="tut-step-num">4</div><h3 class="tut-step-title">تأكيد تحويل 100%</h3><div class="tut-step-body">تأكد أن المبلغ المحوّل يساوي إجمالي التحصيلات ثم اضغط <span class="field">تأكيد التسوية</span>.<span class="warn">يجب أن يتطابق المبلغ المحوّل مع إجمالي تحصيلات الوردية. لا يُسمح بتسوية جزئية.</span></div></div>
<div class="tut-step"><div class="tut-step-num">5</div><h3 class="tut-step-title">رقم التسوية وخروج المبلغ من العهدة</h3><div class="tut-step-body">النظام يولّد رقم تسوية (مثال: <span class="field">STL-2026-0198</span>) والمبلغ يخرج من عهدتك ويصبح في انتظار الاستلام من الخزنة الرئيسية.<span class="success">تمت التسوية — المبلغ خرج من عهدتك وينتظر استلام أمين الخزنة الرئيسية.</span></div></div>
<div class="tut-nav">
<a href="/tutorials/treasury/close-session">إغلاق وردية <i data-lucide="arrow-right" style="width:14px;height:14px;"></i></a>
<a href="/tutorials/treasury/receive-settlement"><i data-lucide="arrow-left" style="width:14px;height:14px;"></i> استلام تسوية</a>
</div>
</div>
<script>document.addEventListener('DOMContentLoaded', function() { if (typeof lucide !== 'undefined') lucide.createIcons(); });</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>شرح: فتح وردية<?php $__template->endSection(); ?>
<?php $__template->section('styles'); ?>
<style>
.tut-page{max-width:860px;margin:0 auto}.tut-breadcrumb{display:flex;align-items:center;gap:8px;margin-bottom:20px;font-size:13px}.tut-breadcrumb a{color:#6B7280;text-decoration:none}.tut-breadcrumb a:hover{color:#059669}.tut-breadcrumb span{color:#9CA3AF}.tut-header{display:flex;align-items:center;gap:18px;margin-bottom:30px;padding:24px;background:linear-gradient(135deg,#05966908,#05966904);border-radius:16px;border:1px solid #05966920}.tut-header-icon{width:56px;height:56px;background:#059669;border-radius:14px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.tut-header h1{font-size:22px;font-weight:800;color:#1A1A2E;margin:0 0 4px}.tut-header p{font-size:14px;color:#6B7280;margin:0}.tut-step{position:relative;padding:20px 20px 20px 60px;margin-bottom:16px;background:#fff;border:1px solid #E5E7EB;border-radius:12px;transition:border-color .2s}.tut-step:hover{border-color:#05966960}.tut-step-num{position:absolute;right:18px;top:20px;width:32px;height:32px;background:#05966915;color:#059669;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:800}.tut-step-title{font-size:15px;font-weight:700;color:#1A1A2E;margin:0 0 6px}.tut-step-body{font-size:13.5px;color:#374151;line-height:1.8}.tut-step-body ul{margin:8px 0;padding-right:18px}.tut-step-body li{margin-bottom:4px}.tut-step-body .field{display:inline-block;background:#F3F4F6;color:#1A1A2E;padding:1px 8px;border-radius:4px;font-size:12px;font-weight:600;font-family:monospace;direction:ltr}.tut-step-body .warn{display:block;background:#FEF3C7;border:1px solid #F59E0B30;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#92400E}.tut-step-body .info{display:block;background:#DBEAFE;border:1px solid #3B82F630;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#1E40AF}.tut-step-body .success{display:block;background:#ECFDF5;border:1px solid #05966930;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#065F46}.tut-nav{display:flex;justify-content:space-between;align-items:center;margin-top:32px;padding-top:20px;border-top:1px solid #E5E7EB}.tut-nav a{display:inline-flex;align-items:center;gap:6px;padding:10px 16px;background:#F9FAFB;border:1px solid #E5E7EB;border-radius:10px;text-decoration:none;color:#374151;font-size:13px;font-weight:600;transition:all .15s}.tut-nav a:hover{background:#ECFDF5;border-color:#059669;color:#059669}.tut-diagram{background:#F8FAFC;border:1px solid #E2E8F0;border-radius:12px;padding:16px 20px;margin:12px 0;font-family:monospace;font-size:12px;direction:ltr;text-align:left;line-height:1.6;overflow-x:auto}
</style>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="tut-page">
<div class="tut-breadcrumb">
<a href="/tutorials"><i data-lucide="graduation-cap" style="width:14px;height:14px;"></i></a>
<span>/</span>
<a href="/tutorials/treasury">دورة التحصيلات</a>
<span>/</span>
<span style="color:#1A1A2E;font-weight:600;">فتح وردية</span>
</div>
<div class="tut-header">
<div class="tut-header-icon"><i data-lucide="log-in" style="width:28px;height:28px;color:#fff;"></i></div>
<div><h1>فتح وردية</h1><p>بدء يوم العمل في الخزنة الفرعية وتفعيل استقبال المدفوعات</p></div>
</div>
<div class="tut-step"><div class="tut-step-num">1</div><h3 class="tut-step-title">الدخول إلى الخزنة الفرعية</h3><div class="tut-step-body">من القائمة الجانبية اذهب إلى <span class="field">الخزنة</span> > <span class="field">الخزنة الفرعية</span>. ستظهر لك لوحة التحكم الخاصة بالخزنة.</div></div>
<div class="tut-step"><div class="tut-step-num">2</div><h3 class="tut-step-title">الضغط على فتح وردية</h3><div class="tut-step-body">اضغط على زر <span class="field">فتح وردية</span> الموجود أعلى الصفحة. لن يظهر الزر إذا كانت هناك وردية مفتوحة بالفعل.<span class="warn">لا يمكن فتح أكثر من وردية واحدة في نفس الوقت لنفس الموظف.</span></div></div>
<div class="tut-step"><div class="tut-step-num">3</div><h3 class="tut-step-title">توليد رقم الوردية</h3><div class="tut-step-body">النظام يولّد رقم وردية تلقائي (مثال: <span class="field">SES-2026-0547</span>) ويسجّل وقت الفتح واسم الموظف المسؤول.<span class="info">رقم الوردية يُستخدم لاحقاً في التسوية وربط كل الإيصالات بهذه الفترة.</span></div></div>
<div class="tut-step"><div class="tut-step-num">4</div><h3 class="tut-step-title">بدء استقبال المدفوعات</h3><div class="tut-step-body">بعد فتح الوردية بنجاح، يمكنك الآن:
<ul>
<li>تحصيل مبالغ من الأعضاء واللاعبين</li>
<li>إصدار إيصالات استلام</li>
<li>رؤية رصيد العهدة يزداد مع كل تحصيل</li>
</ul>
<span class="success">الوردية مفتوحة الآن وأنت جاهز لاستقبال المدفوعات.</span>
</div></div>
<div class="tut-nav">
<span></span>
<a href="/tutorials/treasury/collect-payment"><i data-lucide="arrow-left" style="width:14px;height:14px;"></i> تحصيل مبلغ</a>
</div>
</div>
<script>document.addEventListener('DOMContentLoaded', function() { if (typeof lucide !== 'undefined') lucide.createIcons(); });</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>شرح: استلام تسوية<?php $__template->endSection(); ?>
<?php $__template->section('styles'); ?>
<style>
.tut-page{max-width:860px;margin:0 auto}.tut-breadcrumb{display:flex;align-items:center;gap:8px;margin-bottom:20px;font-size:13px}.tut-breadcrumb a{color:#6B7280;text-decoration:none}.tut-breadcrumb a:hover{color:#059669}.tut-breadcrumb span{color:#9CA3AF}.tut-header{display:flex;align-items:center;gap:18px;margin-bottom:30px;padding:24px;background:linear-gradient(135deg,#05966908,#05966904);border-radius:16px;border:1px solid #05966920}.tut-header-icon{width:56px;height:56px;background:#059669;border-radius:14px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.tut-header h1{font-size:22px;font-weight:800;color:#1A1A2E;margin:0 0 4px}.tut-header p{font-size:14px;color:#6B7280;margin:0}.tut-step{position:relative;padding:20px 20px 20px 60px;margin-bottom:16px;background:#fff;border:1px solid #E5E7EB;border-radius:12px;transition:border-color .2s}.tut-step:hover{border-color:#05966960}.tut-step-num{position:absolute;right:18px;top:20px;width:32px;height:32px;background:#05966915;color:#059669;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:800}.tut-step-title{font-size:15px;font-weight:700;color:#1A1A2E;margin:0 0 6px}.tut-step-body{font-size:13.5px;color:#374151;line-height:1.8}.tut-step-body ul{margin:8px 0;padding-right:18px}.tut-step-body li{margin-bottom:4px}.tut-step-body .field{display:inline-block;background:#F3F4F6;color:#1A1A2E;padding:1px 8px;border-radius:4px;font-size:12px;font-weight:600;font-family:monospace;direction:ltr}.tut-step-body .warn{display:block;background:#FEF3C7;border:1px solid #F59E0B30;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#92400E}.tut-step-body .info{display:block;background:#DBEAFE;border:1px solid #3B82F630;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#1E40AF}.tut-step-body .success{display:block;background:#ECFDF5;border:1px solid #05966930;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#065F46}.tut-nav{display:flex;justify-content:space-between;align-items:center;margin-top:32px;padding-top:20px;border-top:1px solid #E5E7EB}.tut-nav a{display:inline-flex;align-items:center;gap:6px;padding:10px 16px;background:#F9FAFB;border:1px solid #E5E7EB;border-radius:10px;text-decoration:none;color:#374151;font-size:13px;font-weight:600;transition:all .15s}.tut-nav a:hover{background:#ECFDF5;border-color:#059669;color:#059669}.tut-diagram{background:#F8FAFC;border:1px solid #E2E8F0;border-radius:12px;padding:16px 20px;margin:12px 0;font-family:monospace;font-size:12px;direction:ltr;text-align:left;line-height:1.6;overflow-x:auto}
</style>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="tut-page">
<div class="tut-breadcrumb">
<a href="/tutorials"><i data-lucide="graduation-cap" style="width:14px;height:14px;"></i></a>
<span>/</span>
<a href="/tutorials/treasury">دورة التحصيلات</a>
<span>/</span>
<span style="color:#1A1A2E;font-weight:600;">استلام تسوية</span>
</div>
<div class="tut-header">
<div class="tut-header-icon"><i data-lucide="download" style="width:28px;height:28px;color:#fff;"></i></div>
<div><h1>استلام تسوية</h1><p>تأكيد استلام المبالغ في الخزنة الرئيسية من الخزنات الفرعية</p></div>
</div>
<div class="tut-step"><div class="tut-step-num">1</div><h3 class="tut-step-title">فتح شاشة الخزنة الرئيسية</h3><div class="tut-step-body">من القائمة الجانبية اذهب إلى <span class="field">الخزنة</span> > <span class="field">الخزنة الرئيسية</span>. ستظهر التسويات الواردة التي تنتظر الاستلام.</div></div>
<div class="tut-step"><div class="tut-step-num">2</div><h3 class="tut-step-title">مراجعة التسويات الواردة</h3><div class="tut-step-body">اختر التسوية المطلوب استلامها. تحقق من:
<ul>
<li>المبلغ الإجمالي</li>
<li>عدد الإيصالات المرتبطة</li>
<li>اسم موظف الخزنة الفرعية</li>
<li>رقم الوردية المصدر</li>
</ul>
</div></div>
<div class="tut-step"><div class="tut-step-num">3</div><h3 class="tut-step-title">التحقق من المبلغ والإيصالات</h3><div class="tut-step-body">عُد الأموال المستلمة فعلياً وطابقها مع المبلغ الظاهر في النظام. تحقق أن عدد الإيصالات الورقية يطابق العدد المسجّل.<span class="warn">في حالة وجود فرق بين المبلغ الفعلي والمسجّل، لا تستلم التسوية وأبلغ المسؤول فوراً.</span></div></div>
<div class="tut-step"><div class="tut-step-num">4</div><h3 class="tut-step-title">الضغط على استلام</h3><div class="tut-step-body">بعد التأكد من المطابقة، اضغط <span class="field">استلام</span>. النظام ينقل العهدة من موظف الخزنة الفرعية إلى أمين الخزنة الرئيسية.<span class="success">تم الاستلام — المبلغ أصبح في عهدة أمين الخزنة الرئيسية وجاهز للإيداع البنكي.</span></div></div>
<div class="tut-nav">
<a href="/tutorials/treasury/initiate-settlement">إنشاء تسوية <i data-lucide="arrow-right" style="width:14px;height:14px;"></i></a>
<a href="/tutorials/treasury/bank-deposit"><i data-lucide="arrow-left" style="width:14px;height:14px;"></i> إيداع بنكي</a>
</div>
</div>
<script>document.addEventListener('DOMContentLoaded', function() { if (typeof lucide !== 'undefined') lucide.createIcons(); });</script>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE `treasuries` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`code` VARCHAR(20) NOT NULL UNIQUE,
`name_ar` VARCHAR(100) NOT NULL,
`name_en` VARCHAR(100) NULL,
`type` ENUM('main','sub') NOT NULL,
`account_code` VARCHAR(20) NOT NULL,
`branch_id` BIGINT UNSIGNED NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_treasuries_type` (`type`),
INDEX `idx_treasuries_branch` (`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `treasuries`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE `treasury_sessions` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`treasury_id` BIGINT UNSIGNED NOT NULL,
`cashier_id` BIGINT UNSIGNED NOT NULL,
`session_number` VARCHAR(30) NOT NULL UNIQUE,
`opened_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`closed_at` TIMESTAMP NULL,
`opening_balance` DECIMAL(14,2) NOT NULL DEFAULT 0.00,
`closing_balance` DECIMAL(14,2) NULL,
`total_collected` DECIMAL(14,2) NOT NULL DEFAULT 0.00,
`total_receipts` INT UNSIGNED NOT NULL DEFAULT 0,
`status` ENUM('open','closed','settled') NOT NULL DEFAULT 'open',
`notes` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_tsessions_treasury` (`treasury_id`),
INDEX `idx_tsessions_cashier` (`cashier_id`),
INDEX `idx_tsessions_status` (`status`),
CONSTRAINT `fk_tsessions_treasury` FOREIGN KEY (`treasury_id`) REFERENCES `treasuries`(`id`),
CONSTRAINT `fk_tsessions_cashier` FOREIGN KEY (`cashier_id`) REFERENCES `employees`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `treasury_sessions`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE `treasury_settlements` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`settlement_number` VARCHAR(30) NOT NULL UNIQUE,
`from_treasury_id` BIGINT UNSIGNED NOT NULL,
`to_treasury_id` BIGINT UNSIGNED NOT NULL,
`session_id` BIGINT UNSIGNED NOT NULL,
`amount` DECIMAL(14,2) NOT NULL,
`settled_by` BIGINT UNSIGNED NOT NULL,
`received_by` BIGINT UNSIGNED NULL,
`settled_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`received_at` TIMESTAMP NULL,
`status` ENUM('pending','received','rejected') NOT NULL DEFAULT 'pending',
`rejection_reason` TEXT NULL,
`receipt_count` INT UNSIGNED NOT NULL DEFAULT 0,
`journal_entry_id` BIGINT UNSIGNED NULL,
`notes` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_settlements_from` (`from_treasury_id`),
INDEX `idx_settlements_to` (`to_treasury_id`),
INDEX `idx_settlements_session` (`session_id`),
INDEX `idx_settlements_status` (`status`),
CONSTRAINT `fk_settlements_from` FOREIGN KEY (`from_treasury_id`) REFERENCES `treasuries`(`id`),
CONSTRAINT `fk_settlements_to` FOREIGN KEY (`to_treasury_id`) REFERENCES `treasuries`(`id`),
CONSTRAINT `fk_settlements_session` FOREIGN KEY (`session_id`) REFERENCES `treasury_sessions`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `treasury_settlements`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE `treasury_deposits` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`deposit_number` VARCHAR(30) NOT NULL UNIQUE,
`treasury_id` BIGINT UNSIGNED NOT NULL,
`bank_account_id` BIGINT UNSIGNED NULL,
`amount` DECIMAL(14,2) NOT NULL,
`deposit_date` DATE NOT NULL,
`bank_receipt_serial` VARCHAR(100) NULL,
`bank_receipt_path` VARCHAR(255) NULL,
`deposited_by` BIGINT UNSIGNED NOT NULL,
`deposited_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`confirmed_by` BIGINT UNSIGNED NULL,
`confirmed_at` TIMESTAMP NULL,
`status` ENUM('pending_deposit','deposited','confirmed','rejected') NOT NULL DEFAULT 'pending_deposit',
`rejection_reason` TEXT NULL,
`journal_entry_id` BIGINT UNSIGNED NULL,
`notes` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_deposits_treasury` (`treasury_id`),
INDEX `idx_deposits_status` (`status`),
INDEX `idx_deposits_date` (`deposit_date`),
CONSTRAINT `fk_deposits_treasury` FOREIGN KEY (`treasury_id`) REFERENCES `treasuries`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `treasury_deposits`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE `treasury_custody_log` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`treasury_id` BIGINT UNSIGNED NOT NULL,
`employee_id` BIGINT UNSIGNED NOT NULL,
`action` ENUM('collection','settlement_out','settlement_in','deposit','deposit_confirmed','void_reversal') NOT NULL,
`amount` DECIMAL(14,2) NOT NULL,
`balance_after` DECIMAL(14,2) NOT NULL,
`reference_type` VARCHAR(30) NULL,
`reference_id` BIGINT UNSIGNED NULL,
`description_ar` VARCHAR(255) NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_custody_treasury` (`treasury_id`),
INDEX `idx_custody_employee` (`employee_id`),
INDEX `idx_custody_created` (`created_at`),
CONSTRAINT `fk_custody_treasury` FOREIGN KEY (`treasury_id`) REFERENCES `treasuries`(`id`),
CONSTRAINT `fk_custody_employee` FOREIGN KEY (`employee_id`) REFERENCES `employees`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `treasury_custody_log`",
];
<?php
declare(strict_types=1);
return [
'up' => "
ALTER TABLE `payments` ADD COLUMN `treasury_id` BIGINT UNSIGNED NULL AFTER `received_by_employee_id`;
ALTER TABLE `payments` ADD COLUMN `session_id` BIGINT UNSIGNED NULL AFTER `treasury_id`;
ALTER TABLE `payments` ADD INDEX `idx_payments_treasury` (`treasury_id`);
ALTER TABLE `payments` ADD INDEX `idx_payments_session` (`session_id`)
",
'down' => "
ALTER TABLE `payments` DROP INDEX `idx_payments_session`;
ALTER TABLE `payments` DROP INDEX `idx_payments_treasury`;
ALTER TABLE `payments` DROP COLUMN `session_id`;
ALTER TABLE `payments` DROP COLUMN `treasury_id`
",
];
<?php
declare(strict_types=1);
return function (\App\Core\Database $db): void {
$exists = $db->selectOne(
"SELECT 1 FROM chart_of_accounts WHERE account_code = ?",
['12060102']
);
if (!$exists) {
$db->insert('chart_of_accounts', [
'account_code' => '12060102',
'name_ar' => 'صندوق الخزنة الفرعية - الأنشطة الرياضية',
'name_en' => 'Sub-Treasury Cash - Sports Activities',
'parent_code' => '120601',
'level' => 5,
'account_nature' => 'debit',
'is_header' => 0,
'is_active' => 1,
'current_balance' => '0.00',
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
}
};
<?php
declare(strict_types=1);
return function (\App\Core\Database $db): void {
$now = date('Y-m-d H:i:s');
$treasuries = [
[
'code' => 'MAIN',
'name_ar' => 'الخزنة الرئيسية',
'name_en' => 'Main Treasury',
'type' => 'main',
'account_code' => '12060101',
'branch_id' => null,
'is_active' => 1,
'created_at' => $now,
'updated_at' => $now,
],
[
'code' => 'SUB_SA',
'name_ar' => 'خزنة الأنشطة الرياضية',
'name_en' => 'Sports Activities Sub-Treasury',
'type' => 'sub',
'account_code' => '12060102',
'branch_id' => null,
'is_active' => 1,
'created_at' => $now,
'updated_at' => $now,
],
];
foreach ($treasuries as $treasury) {
$exists = $db->selectOne("SELECT 1 FROM treasuries WHERE code = ?", [$treasury['code']]);
if (!$exists) {
$db->insert('treasuries', $treasury);
}
}
};
<?php
declare(strict_types=1);
return function (\App\Core\Database $db): void {
$now = date('Y-m-d H:i:s');
$permissions = [
// Treasury (sub-treasury) permissions
['key' => 'treasury.view_dashboard', 'name_ar' => 'لوحة تحكم الخزنة الفرعية', 'name_en' => 'Sub-Treasury Dashboard', 'module' => 'treasury'],
['key' => 'treasury.collect_payment', 'name_ar' => 'تحصيل مدفوعات', 'name_en' => 'Collect Payments', 'module' => 'treasury'],
['key' => 'treasury.open_session', 'name_ar' => 'فتح وردية', 'name_en' => 'Open Session', 'module' => 'treasury'],
['key' => 'treasury.close_session', 'name_ar' => 'إغلاق وردية', 'name_en' => 'Close Session', 'module' => 'treasury'],
['key' => 'treasury.initiate_settlement', 'name_ar' => 'إجراء تسوية للخزنة الرئيسية', 'name_en' => 'Initiate Settlement', 'module' => 'treasury'],
['key' => 'treasury.view_custody', 'name_ar' => 'عرض رصيد العهدة', 'name_en' => 'View Custody Balance', 'module' => 'treasury'],
['key' => 'treasury.view_sessions', 'name_ar' => 'عرض الورديات', 'name_en' => 'View Sessions', 'module' => 'treasury'],
['key' => 'treasury.view_settlements', 'name_ar' => 'عرض التسويات', 'name_en' => 'View Settlements', 'module' => 'treasury'],
// Cashier enhancements
['key' => 'cashier.receive_settlement', 'name_ar' => 'استلام تسوية من خزنة فرعية', 'name_en' => 'Receive Sub-Treasury Settlement', 'module' => 'cashier'],
['key' => 'cashier.manage_deposits', 'name_ar' => 'إدارة إيداعات البنك', 'name_en' => 'Manage Bank Deposits', 'module' => 'cashier'],
['key' => 'cashier.confirm_deposit', 'name_ar' => 'تأكيد إيداع بنكي', 'name_en' => 'Confirm Bank Deposit', 'module' => 'cashier'],
['key' => 'cashier.view_custody', 'name_ar' => 'عرض رصيد عهدة الخزنة الرئيسية', 'name_en' => 'View Main Treasury Custody', 'module' => 'cashier'],
];
foreach ($permissions as $perm) {
$exists = $db->selectOne("SELECT 1 FROM permissions WHERE `key` = ?", [$perm['key']]);
if (!$exists) {
$db->insert('permissions', [
'key' => $perm['key'],
'name_ar' => $perm['name_ar'],
'name_en' => $perm['name_en'],
'module' => $perm['module'],
'created_at' => $now,
'updated_at' => $now,
]);
}
}
// Assign all treasury permissions to super_admin role
$superAdmin = $db->selectOne("SELECT id FROM roles WHERE role_code = 'super_admin'");
if ($superAdmin) {
foreach ($permissions as $perm) {
$permRecord = $db->selectOne("SELECT id FROM permissions WHERE `key` = ?", [$perm['key']]);
if ($permRecord) {
$exists = $db->selectOne(
"SELECT 1 FROM role_permissions WHERE role_id = ? AND permission_id = ?",
[$superAdmin['id'], $permRecord['id']]
);
if (!$exists) {
$db->insert('role_permissions', [
'role_id' => $superAdmin['id'],
'permission_id' => $permRecord['id'],
'created_at' => $now,
]);
}
}
}
}
};
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