Commit ac32be3c authored by Mahmoud Aglan's avatar Mahmoud Aglan

add these

parent 0c5becba
This diff is collapsed.
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Accounting\Services\AccountStatementService;
class AccountStatementController extends Controller
{
public function customerStatement(Request $request): Response
{
$this->authorize('accounting.statements.view');
$db = App::getInstance()->db();
$memberId = (int) $request->get('member_id', 0);
$dateFrom = $request->get('date_from', '');
$dateTo = $request->get('date_to', '');
$statement = null;
$member = null;
if ($memberId > 0) {
$member = $db->selectOne("SELECT id, full_name_ar, membership_number, credit_limit, credit_balance FROM members WHERE id = ?", [$memberId]);
$statement = AccountStatementService::getCustomerStatement(
$memberId,
$dateFrom ?: null,
$dateTo ?: null
);
}
$members = $db->select("SELECT id, full_name_ar, membership_number FROM members WHERE is_archived = 0 ORDER BY full_name_ar LIMIT 1000");
return $this->view('Accounting.Views.statements.customer', [
'members' => $members,
'member' => $member,
'statement' => $statement,
'filters' => ['member_id' => $memberId, 'date_from' => $dateFrom, 'date_to' => $dateTo],
]);
}
public function supplierStatement(Request $request): Response
{
$this->authorize('accounting.statements.view');
$db = App::getInstance()->db();
$supplierId = (int) $request->get('supplier_id', 0);
$dateFrom = $request->get('date_from', '');
$dateTo = $request->get('date_to', '');
$statement = null;
$supplier = null;
if ($supplierId > 0) {
$supplier = $db->selectOne("SELECT id, name_ar, code, credit_limit, credit_balance FROM suppliers WHERE id = ?", [$supplierId]);
$statement = AccountStatementService::getSupplierStatement(
$supplierId,
$dateFrom ?: null,
$dateTo ?: null
);
}
$suppliers = $db->select("SELECT id, name_ar, code FROM suppliers WHERE is_archived = 0 ORDER BY name_ar");
return $this->view('Accounting.Views.statements.supplier', [
'suppliers' => $suppliers,
'supplier' => $supplier,
'statement' => $statement,
'filters' => ['supplier_id' => $supplierId, 'date_from' => $dateFrom, 'date_to' => $dateTo],
]);
}
public function dailyCash(Request $request): Response
{
$this->authorize('accounting.cash.view');
$db = App::getInstance()->db();
$date = $request->get('date', date('Y-m-d'));
$movements = $db->select(
"SELECT * FROM daily_cash_movements WHERE movement_date = ? ORDER BY bank_account_id, safe_id",
[$date]
);
$bankAccounts = $db->select("SELECT id, account_name_ar FROM bank_accounts WHERE is_active = 1");
return $this->view('Accounting.Views.statements.daily_cash', [
'movements' => $movements,
'bankAccounts' => $bankAccounts,
'date' => $date,
]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Accounting\Services\BankLoanService;
class BankLoanController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('accounting.loans.view');
$db = App::getInstance()->db();
$status = $request->get('status', '');
$where = '1=1';
$params = [];
if ($status) {
$where .= ' AND l.status = ?';
$params[] = $status;
}
$loans = $db->select(
"SELECT l.*, ba.account_name_ar as bank_name
FROM bank_loans l
LEFT JOIN bank_accounts ba ON ba.id = l.bank_account_id
WHERE {$where}
ORDER BY l.created_at DESC",
$params
);
return $this->view('Accounting.Views.loans.index', [
'loans' => $loans,
'status' => $status,
]);
}
public function create(Request $request): Response
{
$this->authorize('accounting.loans.manage');
$db = App::getInstance()->db();
$bankAccounts = $db->select("SELECT id, account_name_ar FROM bank_accounts WHERE is_active = 1 ORDER BY account_name_ar");
return $this->view('Accounting.Views.loans.form', [
'loan' => null,
'bankAccounts' => $bankAccounts,
]);
}
public function store(Request $request): Response
{
$this->authorize('accounting.loans.manage');
$data = [
'bank_account_id' => (int) $request->post('bank_account_id'),
'loan_type' => $request->post('loan_type', 'term'),
'principal_amount' => $request->post('principal_amount'),
'interest_rate' => $request->post('interest_rate'),
'term_months' => (int) $request->post('term_months'),
'start_date' => $request->post('start_date'),
'collateral_description' => $request->post('collateral_description'),
'notes' => $request->post('notes'),
];
$result = BankLoanService::createLoan($data);
if (!$result['success']) {
return $this->redirect('/accounting/loans/create')->withError($result['error']);
}
return $this->redirect('/accounting/loans/' . $result['loan_id'])->withSuccess('تم إنشاء القرض بنجاح');
}
public function show(Request $request, string $id): Response
{
$this->authorize('accounting.loans.view');
$db = App::getInstance()->db();
$loan = $db->selectOne(
"SELECT l.*, ba.account_name_ar as bank_name
FROM bank_loans l
LEFT JOIN bank_accounts ba ON ba.id = l.bank_account_id
WHERE l.id = ?",
[(int) $id]
);
if (!$loan) {
return $this->redirect('/accounting/loans')->withError('القرض غير موجود');
}
$schedule = BankLoanService::getSchedule((int) $id);
return $this->view('Accounting.Views.loans.show', [
'loan' => $loan,
'schedule' => $schedule,
]);
}
public function recordPayment(Request $request, string $id): Response
{
$this->authorize('accounting.loans.manage');
$installmentNumber = (int) $request->post('installment_number');
$paidAmount = $request->post('paid_amount');
$paidDate = $request->post('paid_date');
$result = BankLoanService::recordPayment((int) $id, $installmentNumber, $paidAmount, $paidDate);
if (!$result['success']) {
return $this->redirect('/accounting/loans/' . $id)->withError($result['error']);
}
return $this->redirect('/accounting/loans/' . $id)->withSuccess('تم تسجيل السداد بنجاح');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
class DocumentaryCreditController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('accounting.lc.view');
$db = App::getInstance()->db();
$status = $request->get('status', '');
$where = '1=1';
$params = [];
if ($status) {
$where .= ' AND lc.status = ?';
$params[] = $status;
}
$credits = $db->select(
"SELECT lc.*, ba.account_name_ar as bank_name, s.name_ar as supplier_name
FROM documentary_credits lc
LEFT JOIN bank_accounts ba ON ba.id = lc.issuing_bank_id
LEFT JOIN suppliers s ON s.id = lc.beneficiary_supplier_id
WHERE {$where}
ORDER BY lc.created_at DESC",
$params
);
return $this->view('Accounting.Views.documentary_credits.index', [
'credits' => $credits,
'status' => $status,
]);
}
public function create(Request $request): Response
{
$this->authorize('accounting.lc.manage');
$db = App::getInstance()->db();
$bankAccounts = $db->select("SELECT id, account_name_ar FROM bank_accounts WHERE is_active = 1 ORDER BY account_name_ar");
$suppliers = $db->select("SELECT id, name_ar FROM suppliers WHERE is_archived = 0 ORDER BY name_ar");
return $this->view('Accounting.Views.documentary_credits.form', [
'credit' => null,
'bankAccounts' => $bankAccounts,
'suppliers' => $suppliers,
]);
}
public function store(Request $request): Response
{
$this->authorize('accounting.lc.manage');
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$lcNumber = 'LC-' . date('Ymd') . '-' . str_pad((string) random_int(1, 9999), 4, '0', STR_PAD_LEFT);
$amount = (string) $request->post('amount');
$marginPct = (string) $request->post('margin_percentage', '0');
$marginAmount = bcmul($amount, bcdiv($marginPct, '100', 4), 2);
$lcId = $db->insert('documentary_credits', [
'lc_number' => $lcNumber,
'issuing_bank_id' => (int) $request->post('issuing_bank_id'),
'beneficiary_supplier_id' => $request->post('beneficiary_supplier_id') ?: null,
'beneficiary_name' => $request->post('beneficiary_name'),
'amount' => $amount,
'currency_code' => $request->post('currency_code', 'EGP'),
'margin_percentage' => $marginPct,
'margin_amount' => $marginAmount,
'opening_date' => $request->post('opening_date'),
'expiry_date' => $request->post('expiry_date'),
'shipment_date' => $request->post('shipment_date') ?: null,
'status' => 'opened',
'terms' => $request->post('terms'),
'purchase_order_id' => $request->post('purchase_order_id') ?: null,
'notes' => $request->post('notes'),
'created_by' => $employee ? (int) $employee->id : null,
]);
return $this->redirect('/accounting/documentary-credits/' . $lcId)->withSuccess('تم فتح الاعتماد المستندي بنجاح — رقم: ' . $lcNumber);
}
public function show(Request $request, string $id): Response
{
$this->authorize('accounting.lc.view');
$db = App::getInstance()->db();
$credit = $db->selectOne(
"SELECT lc.*, ba.account_name_ar as bank_name, s.name_ar as supplier_name
FROM documentary_credits lc
LEFT JOIN bank_accounts ba ON ba.id = lc.issuing_bank_id
LEFT JOIN suppliers s ON s.id = lc.beneficiary_supplier_id
WHERE lc.id = ?",
[(int) $id]
);
if (!$credit) {
return $this->redirect('/accounting/documentary-credits')->withError('الاعتماد غير موجود');
}
$documents = $db->select("SELECT * FROM lc_documents WHERE lc_id = ? ORDER BY created_at DESC", [(int) $id]);
return $this->view('Accounting.Views.documentary_credits.show', [
'credit' => $credit,
'documents' => $documents,
]);
}
public function updateStatus(Request $request, string $id): Response
{
$this->authorize('accounting.lc.manage');
$db = App::getInstance()->db();
$newStatus = $request->post('status');
$db->update('documentary_credits', ['status' => $newStatus], 'id = ?', [(int) $id]);
return $this->redirect('/accounting/documentary-credits/' . $id)->withSuccess('تم تحديث حالة الاعتماد');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
class LetterOfGuaranteeController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('accounting.guarantee.view');
$db = App::getInstance()->db();
$status = $request->get('status', '');
$type = $request->get('type', '');
$where = '1=1';
$params = [];
if ($status) {
$where .= ' AND lg.status = ?';
$params[] = $status;
}
if ($type) {
$where .= ' AND lg.guarantee_type = ?';
$params[] = $type;
}
$guarantees = $db->select(
"SELECT lg.*, ba.account_name_ar as bank_name
FROM letters_of_guarantee lg
LEFT JOIN bank_accounts ba ON ba.id = lg.bank_account_id
WHERE {$where}
ORDER BY lg.expiry_date ASC",
$params
);
return $this->view('Accounting.Views.guarantees.index', [
'guarantees' => $guarantees,
'status' => $status,
'type' => $type,
]);
}
public function create(Request $request): Response
{
$this->authorize('accounting.guarantee.manage');
$db = App::getInstance()->db();
$bankAccounts = $db->select("SELECT id, account_name_ar FROM bank_accounts WHERE is_active = 1 ORDER BY account_name_ar");
return $this->view('Accounting.Views.guarantees.form', [
'guarantee' => null,
'bankAccounts' => $bankAccounts,
]);
}
public function store(Request $request): Response
{
$this->authorize('accounting.guarantee.manage');
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$guaranteeNumber = 'LG-' . date('Ymd') . '-' . str_pad((string) random_int(1, 9999), 4, '0', STR_PAD_LEFT);
$amount = (string) $request->post('amount');
$marginPct = (string) $request->post('margin_percentage', '0');
$marginAmount = bcmul($amount, bcdiv($marginPct, '100', 4), 2);
$commissionRate = (string) $request->post('commission_rate', '0');
$commissionAmount = bcmul($amount, bcdiv($commissionRate, '100', 6), 2);
$lgId = $db->insert('letters_of_guarantee', [
'guarantee_number' => $guaranteeNumber,
'bank_account_id' => (int) $request->post('bank_account_id'),
'beneficiary_name' => $request->post('beneficiary_name'),
'guarantee_type' => $request->post('guarantee_type'),
'amount' => $amount,
'currency_code' => $request->post('currency_code', 'EGP'),
'margin_percentage' => $marginPct,
'margin_amount' => $marginAmount,
'commission_rate' => $commissionRate,
'commission_amount' => $commissionAmount,
'issue_date' => $request->post('issue_date'),
'expiry_date' => $request->post('expiry_date'),
'status' => 'issued',
'related_contract' => $request->post('related_contract'),
'notes' => $request->post('notes'),
'created_by' => $employee ? (int) $employee->id : null,
]);
return $this->redirect('/accounting/guarantees/' . $lgId)->withSuccess('تم إصدار خطاب الضمان بنجاح — رقم: ' . $guaranteeNumber);
}
public function show(Request $request, string $id): Response
{
$this->authorize('accounting.guarantee.view');
$db = App::getInstance()->db();
$guarantee = $db->selectOne(
"SELECT lg.*, ba.account_name_ar as bank_name
FROM letters_of_guarantee lg
LEFT JOIN bank_accounts ba ON ba.id = lg.bank_account_id
WHERE lg.id = ?",
[(int) $id]
);
if (!$guarantee) {
return $this->redirect('/accounting/guarantees')->withError('خطاب الضمان غير موجود');
}
return $this->view('Accounting.Views.guarantees.show', ['guarantee' => $guarantee]);
}
public function updateStatus(Request $request, string $id): Response
{
$this->authorize('accounting.guarantee.manage');
$db = App::getInstance()->db();
$newStatus = $request->post('status');
$updateData = ['status' => $newStatus];
if ($newStatus === 'renewed') {
$updateData['renewal_date'] = date('Y-m-d');
$updateData['expiry_date'] = $request->post('new_expiry_date');
}
$db->update('letters_of_guarantee', $updateData, 'id = ?', [(int) $id]);
return $this->redirect('/accounting/guarantees/' . $id)->withSuccess('تم تحديث حالة خطاب الضمان');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Accounting\Services\CrossEntitySettlementService;
class SettlementController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('accounting.settlements.view');
$db = App::getInstance()->db();
$settlements = $db->select(
"SELECT * FROM cross_entity_settlements ORDER BY created_at DESC LIMIT 100"
);
return $this->view('Accounting.Views.settlements.index', [
'settlements' => $settlements,
]);
}
public function create(Request $request): Response
{
$this->authorize('accounting.settlements.manage');
$db = App::getInstance()->db();
$suppliers = $db->select("SELECT id, name_ar FROM suppliers WHERE is_archived = 0 ORDER BY name_ar");
$members = $db->select("SELECT id, full_name_ar, membership_number FROM members WHERE is_archived = 0 ORDER BY full_name_ar LIMIT 500");
$bankAccounts = $db->select("SELECT id, account_name_ar FROM bank_accounts WHERE is_active = 1 ORDER BY account_name_ar");
return $this->view('Accounting.Views.settlements.form', [
'suppliers' => $suppliers,
'members' => $members,
'bankAccounts' => $bankAccounts,
]);
}
public function store(Request $request): Response
{
$this->authorize('accounting.settlements.manage');
$data = [
'settlement_date' => $request->post('settlement_date'),
'from_entity_type' => $request->post('from_entity_type'),
'from_entity_id' => $request->post('from_entity_id'),
'from_entity_name' => $request->post('from_entity_name'),
'to_entity_type' => $request->post('to_entity_type'),
'to_entity_id' => $request->post('to_entity_id'),
'to_entity_name' => $request->post('to_entity_name'),
'amount' => $request->post('amount'),
'purpose' => $request->post('purpose', 'payment'),
'description' => $request->post('description'),
];
$result = CrossEntitySettlementService::create($data);
if (!$result['success']) {
return $this->redirect('/accounting/settlements/create')->withError($result['error']);
}
return $this->redirect('/accounting/settlements')->withSuccess('تم إنشاء التسوية بنجاح — رقم: ' . $result['settlement_number']);
}
public function approve(Request $request, string $id): Response
{
$this->authorize('accounting.settlements.approve');
$result = CrossEntitySettlementService::approve((int) $id);
if (!$result['success']) {
return $this->redirect('/accounting/settlements')->withError($result['error']);
}
return $this->redirect('/accounting/settlements')->withSuccess('تم اعتماد وترحيل التسوية بنجاح');
}
}
......@@ -118,4 +118,36 @@ return [
['GET', '/accounting/reports/member-statement', 'Accounting\Controllers\ReportController@memberStatement', ['auth'], 'accounting.reports.member_statement'],
['GET', '/accounting/reports/treasury', 'Accounting\Controllers\ReportController@treasury', ['auth'], 'accounting.reports.treasury'],
['GET', '/accounting/reports/revenue-analysis', 'Accounting\Controllers\ReportController@revenueAnalysis', ['auth'], 'accounting.reports.revenue_analysis'],
// ── Account Statements ──────────────────────────────────
['GET', '/accounting/statements/customer', 'Accounting\Controllers\AccountStatementController@customerStatement', ['auth'], 'accounting.statements.view'],
['GET', '/accounting/statements/supplier', 'Accounting\Controllers\AccountStatementController@supplierStatement', ['auth'], 'accounting.statements.view'],
['GET', '/accounting/statements/daily-cash', 'Accounting\Controllers\AccountStatementController@dailyCash', ['auth'], 'accounting.cash.view'],
// ── Bank Loans ──────────────────────────────────────────
['GET', '/accounting/loans', 'Accounting\Controllers\BankLoanController@index', ['auth'], 'accounting.loans.view'],
['GET', '/accounting/loans/create', 'Accounting\Controllers\BankLoanController@create', ['auth'], 'accounting.loans.manage'],
['POST', '/accounting/loans', 'Accounting\Controllers\BankLoanController@store', ['auth', 'csrf'], 'accounting.loans.manage'],
['GET', '/accounting/loans/{id:\d+}', 'Accounting\Controllers\BankLoanController@show', ['auth'], 'accounting.loans.view'],
['POST', '/accounting/loans/{id:\d+}/payment', 'Accounting\Controllers\BankLoanController@recordPayment', ['auth', 'csrf'], 'accounting.loans.manage'],
// ── Cross-Entity Settlements ────────────────────────────
['GET', '/accounting/settlements', 'Accounting\Controllers\SettlementController@index', ['auth'], 'accounting.settlements.view'],
['GET', '/accounting/settlements/create', 'Accounting\Controllers\SettlementController@create', ['auth'], 'accounting.settlements.manage'],
['POST', '/accounting/settlements', 'Accounting\Controllers\SettlementController@store', ['auth', 'csrf'], 'accounting.settlements.manage'],
['POST', '/accounting/settlements/{id:\d+}/approve', 'Accounting\Controllers\SettlementController@approve', ['auth', 'csrf'], 'accounting.settlements.approve'],
// ── Documentary Credits ─────────────────────────────────
['GET', '/accounting/documentary-credits', 'Accounting\Controllers\DocumentaryCreditController@index', ['auth'], 'accounting.lc.view'],
['GET', '/accounting/documentary-credits/create', 'Accounting\Controllers\DocumentaryCreditController@create', ['auth'], 'accounting.lc.manage'],
['POST', '/accounting/documentary-credits', 'Accounting\Controllers\DocumentaryCreditController@store', ['auth', 'csrf'], 'accounting.lc.manage'],
['GET', '/accounting/documentary-credits/{id:\d+}', 'Accounting\Controllers\DocumentaryCreditController@show', ['auth'], 'accounting.lc.view'],
['POST', '/accounting/documentary-credits/{id:\d+}/status', 'Accounting\Controllers\DocumentaryCreditController@updateStatus', ['auth', 'csrf'], 'accounting.lc.manage'],
// ── Letters of Guarantee ────────────────────────────────
['GET', '/accounting/guarantees', 'Accounting\Controllers\LetterOfGuaranteeController@index', ['auth'], 'accounting.guarantee.view'],
['GET', '/accounting/guarantees/create', 'Accounting\Controllers\LetterOfGuaranteeController@create', ['auth'], 'accounting.guarantee.manage'],
['POST', '/accounting/guarantees', 'Accounting\Controllers\LetterOfGuaranteeController@store', ['auth', 'csrf'], 'accounting.guarantee.manage'],
['GET', '/accounting/guarantees/{id:\d+}', 'Accounting\Controllers\LetterOfGuaranteeController@show', ['auth'], 'accounting.guarantee.view'],
['POST', '/accounting/guarantees/{id:\d+}/status', 'Accounting\Controllers\LetterOfGuaranteeController@updateStatus', ['auth', 'csrf'], 'accounting.guarantee.manage'],
];
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Services;
use App\Core\App;
final class AccountStatementService
{
public static function getCustomerStatement(int $memberId, ?string $dateFrom = null, ?string $dateTo = null): array
{
$db = App::getInstance()->db();
$where = 'member_id = ?';
$params = [$memberId];
if ($dateFrom) {
$where .= ' AND transaction_date >= ?';
$params[] = $dateFrom;
}
if ($dateTo) {
$where .= ' AND transaction_date <= ?';
$params[] = $dateTo;
}
$transactions = $db->select(
"SELECT * FROM customer_transactions WHERE {$where} ORDER BY transaction_date ASC, id ASC",
$params
);
$runningBalance = '0.00';
if ($dateFrom) {
$priorRow = $db->selectOne(
"SELECT COALESCE(SUM(debit) - SUM(credit), 0) as balance
FROM customer_transactions WHERE member_id = ? AND transaction_date < ?",
[$memberId, $dateFrom]
);
$runningBalance = (string) ($priorRow['balance'] ?? '0.00');
}
$result = [];
foreach ($transactions as $tx) {
$runningBalance = bcadd(bcsub($runningBalance, (string) $tx['credit'], 2), (string) $tx['debit'], 2);
$tx['running_balance'] = $runningBalance;
$result[] = $tx;
}
$totalDebit = '0.00';
$totalCredit = '0.00';
foreach ($transactions as $tx) {
$totalDebit = bcadd($totalDebit, (string) $tx['debit'], 2);
$totalCredit = bcadd($totalCredit, (string) $tx['credit'], 2);
}
return [
'transactions' => $result,
'opening_balance' => $dateFrom ? (string) ($priorRow['balance'] ?? '0.00') : '0.00',
'total_debit' => $totalDebit,
'total_credit' => $totalCredit,
'closing_balance' => $runningBalance,
];
}
public static function getSupplierStatement(int $supplierId, ?string $dateFrom = null, ?string $dateTo = null): array
{
$db = App::getInstance()->db();
$where = 'supplier_id = ?';
$params = [$supplierId];
if ($dateFrom) {
$where .= ' AND transaction_date >= ?';
$params[] = $dateFrom;
}
if ($dateTo) {
$where .= ' AND transaction_date <= ?';
$params[] = $dateTo;
}
$transactions = $db->select(
"SELECT * FROM supplier_transactions WHERE {$where} ORDER BY transaction_date ASC, id ASC",
$params
);
$runningBalance = '0.00';
if ($dateFrom) {
$priorRow = $db->selectOne(
"SELECT COALESCE(SUM(credit) - SUM(debit), 0) as balance
FROM supplier_transactions WHERE supplier_id = ? AND transaction_date < ?",
[$supplierId, $dateFrom]
);
$runningBalance = (string) ($priorRow['balance'] ?? '0.00');
}
$result = [];
foreach ($transactions as $tx) {
$runningBalance = bcadd(bcsub($runningBalance, (string) $tx['debit'], 2), (string) $tx['credit'], 2);
$tx['running_balance'] = $runningBalance;
$result[] = $tx;
}
$totalDebit = '0.00';
$totalCredit = '0.00';
foreach ($transactions as $tx) {
$totalDebit = bcadd($totalDebit, (string) $tx['debit'], 2);
$totalCredit = bcadd($totalCredit, (string) $tx['credit'], 2);
}
return [
'transactions' => $result,
'opening_balance' => $dateFrom ? (string) ($priorRow['balance'] ?? '0.00') : '0.00',
'total_debit' => $totalDebit,
'total_credit' => $totalCredit,
'closing_balance' => $runningBalance,
];
}
public static function recordCustomerTransaction(array $data): int
{
$db = App::getInstance()->db();
return $db->insert('customer_transactions', [
'member_id' => (int) $data['member_id'],
'transaction_date' => $data['transaction_date'] ?? date('Y-m-d'),
'document_type' => $data['document_type'],
'document_id' => $data['document_id'] ?? null,
'document_number' => $data['document_number'] ?? null,
'description' => $data['description'],
'debit' => $data['debit'] ?? '0.00',
'credit' => $data['credit'] ?? '0.00',
'branch_id' => $data['branch_id'] ?? null,
]);
}
public static function recordSupplierTransaction(array $data): int
{
$db = App::getInstance()->db();
return $db->insert('supplier_transactions', [
'supplier_id' => (int) $data['supplier_id'],
'transaction_date' => $data['transaction_date'] ?? date('Y-m-d'),
'document_type' => $data['document_type'],
'document_id' => $data['document_id'] ?? null,
'document_number' => $data['document_number'] ?? null,
'description' => $data['description'],
'debit' => $data['debit'] ?? '0.00',
'credit' => $data['credit'] ?? '0.00',
]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Services;
use App\Core\App;
use App\Core\EventBus;
final class BankLoanService
{
public static function createLoan(array $data): array
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$principal = (string) $data['principal_amount'];
$rate = (string) $data['interest_rate'];
$termMonths = (int) $data['term_months'];
$startDate = $data['start_date'];
$monthlyRate = bcdiv($rate, '1200', 8);
$monthlyPayment = self::calculateMonthlyPayment($principal, $monthlyRate, $termMonths);
$totalInterest = bcsub(bcmul($monthlyPayment, (string) $termMonths, 2), $principal, 2);
$endDate = date('Y-m-d', strtotime($startDate . " +{$termMonths} months"));
$loanNumber = 'LOAN-' . date('Ymd') . '-' . str_pad((string) random_int(1, 9999), 4, '0', STR_PAD_LEFT);
$db->beginTransaction();
try {
$loanId = $db->insert('bank_loans', [
'loan_number' => $loanNumber,
'bank_account_id' => (int) $data['bank_account_id'],
'loan_type' => $data['loan_type'] ?? 'term',
'principal_amount' => $principal,
'interest_rate' => $rate,
'term_months' => $termMonths,
'start_date' => $startDate,
'end_date' => $endDate,
'monthly_payment' => $monthlyPayment,
'outstanding_balance' => $principal,
'total_interest' => $totalInterest,
'collateral_description' => $data['collateral_description'] ?? null,
'notes' => $data['notes'] ?? null,
'created_by' => $employee ? (int) $employee->id : null,
]);
self::generateAmortizationSchedule($loanId, $principal, $monthlyRate, $monthlyPayment, $termMonths, $startDate);
$db->commit();
EventBus::dispatch('bank_loan.created', ['loan_id' => $loanId]);
return ['success' => true, 'loan_id' => $loanId, 'loan_number' => $loanNumber];
} catch (\Throwable $e) {
$db->rollBack();
return ['success' => false, 'error' => $e->getMessage()];
}
}
private static function calculateMonthlyPayment(string $principal, string $monthlyRate, int $termMonths): string
{
if (bccomp($monthlyRate, '0', 8) === 0) {
return bcdiv($principal, (string) $termMonths, 2);
}
$onePlusR = bcadd('1', $monthlyRate, 8);
$power = bcpow($onePlusR, (string) $termMonths, 8);
$numerator = bcmul($principal, bcmul($monthlyRate, $power, 8), 8);
$denominator = bcsub($power, '1', 8);
return bcdiv($numerator, $denominator, 2);
}
private static function generateAmortizationSchedule(int $loanId, string $principal, string $monthlyRate, string $monthlyPayment, int $termMonths, string $startDate): void
{
$db = App::getInstance()->db();
$balance = $principal;
for ($i = 1; $i <= $termMonths; $i++) {
$interest = bcmul($balance, $monthlyRate, 2);
$principalPart = bcsub($monthlyPayment, $interest, 2);
if ($i === $termMonths) {
$principalPart = $balance;
$monthlyPayment = bcadd($principalPart, $interest, 2);
}
$balance = bcsub($balance, $principalPart, 2);
$dueDate = date('Y-m-d', strtotime($startDate . " +{$i} months"));
$db->insert('bank_loan_schedule', [
'loan_id' => $loanId,
'installment_number' => $i,
'due_date' => $dueDate,
'principal_amount' => $principalPart,
'interest_amount' => $interest,
'total_amount' => bcadd($principalPart, $interest, 2),
'status' => 'upcoming',
]);
}
}
public static function recordPayment(int $loanId, int $installmentNumber, string $paidAmount, ?string $paidDate = null): array
{
$db = App::getInstance()->db();
$schedule = $db->selectOne(
"SELECT * FROM bank_loan_schedule WHERE loan_id = ? AND installment_number = ?",
[$loanId, $installmentNumber]
);
if (!$schedule) {
return ['success' => false, 'error' => 'القسط غير موجود'];
}
$db->beginTransaction();
try {
$db->update('bank_loan_schedule', [
'paid_amount' => $paidAmount,
'paid_date' => $paidDate ?? date('Y-m-d'),
'status' => 'paid',
], 'id = ?', [$schedule['id']]);
$db->query(
"UPDATE bank_loans SET outstanding_balance = outstanding_balance - ?, total_paid = total_paid + ? WHERE id = ?",
[$schedule['principal_amount'], $paidAmount, $loanId]
);
$allPaid = $db->selectOne(
"SELECT COUNT(*) as cnt FROM bank_loan_schedule WHERE loan_id = ? AND status != 'paid'",
[$loanId]
);
if ((int) ($allPaid['cnt'] ?? 1) === 0) {
$db->update('bank_loans', ['status' => 'paid_off'], 'id = ?', [$loanId]);
}
$db->commit();
EventBus::dispatch('bank_loan.payment', [
'loan_id' => $loanId,
'installment_number' => $installmentNumber,
'amount' => $paidAmount,
]);
return ['success' => true];
} catch (\Throwable $e) {
$db->rollBack();
return ['success' => false, 'error' => $e->getMessage()];
}
}
public static function getOverdueInstallments(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT s.*, l.loan_number, l.bank_account_id, ba.account_name_ar as bank_name
FROM bank_loan_schedule s
JOIN bank_loans l ON l.id = s.loan_id
LEFT JOIN bank_accounts ba ON ba.id = l.bank_account_id
WHERE s.status IN ('upcoming','due') AND s.due_date < CURDATE()
ORDER BY s.due_date ASC"
);
}
public static function getSchedule(int $loanId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM bank_loan_schedule WHERE loan_id = ? ORDER BY installment_number ASC",
[$loanId]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Services;
use App\Core\App;
use App\Core\EventBus;
final class CheckLifecycleService
{
private static array $validTransitions = [
'in_hand' => ['under_collection', 'endorsed', 'cancelled', 'returned'],
'under_collection' => ['collected', 'bounced', 'returned'],
'collected' => [],
'bounced' => ['in_hand', 'under_collection', 'cancelled'],
'endorsed' => ['returned'],
'cancelled' => [],
'paid' => [],
'returned' => ['in_hand'],
];
public static function canTransition(string $fromStatus, string $toStatus): bool
{
$allowed = self::$validTransitions[$fromStatus] ?? [];
return in_array($toStatus, $allowed, true);
}
public static function transition(int $instrumentId, string $toStatus, array $options = []): array
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$instrument = $db->selectOne(
"SELECT * FROM negotiable_instruments WHERE id = ?",
[$instrumentId]
);
if (!$instrument) {
return ['success' => false, 'error' => 'الورقة غير موجودة'];
}
$fromStatus = (string) $instrument['status'];
if (!self::canTransition($fromStatus, $toStatus)) {
return ['success' => false, 'error' => 'لا يمكن تغيير الحالة من "' . $fromStatus . '" إلى "' . $toStatus . '"'];
}
$db->beginTransaction();
try {
$updateData = ['status' => $toStatus];
switch ($toStatus) {
case 'under_collection':
$updateData['deposited_date'] = $options['date'] ?? date('Y-m-d');
if (!empty($options['bank_account_id'])) {
$updateData['bank_account_id'] = (int) $options['bank_account_id'];
}
break;
case 'collected':
$updateData['collected_date'] = $options['date'] ?? date('Y-m-d');
break;
case 'bounced':
$updateData['bounced_date'] = $options['date'] ?? date('Y-m-d');
$updateData['bounce_reason'] = $options['bounce_reason'] ?? null;
break;
case 'endorsed':
$updateData['endorsed_to'] = $options['endorsed_to'] ?? null;
$updateData['endorsed_date'] = $options['date'] ?? date('Y-m-d');
break;
}
$db->update('negotiable_instruments', $updateData, 'id = ?', [$instrumentId]);
$db->insert('instrument_status_history', [
'instrument_id' => $instrumentId,
'from_status' => $fromStatus,
'to_status' => $toStatus,
'transition_date' => $options['date'] ?? date('Y-m-d'),
'bank_account_id' => $options['bank_account_id'] ?? null,
'endorsed_to' => $options['endorsed_to'] ?? null,
'bounce_reason' => $options['bounce_reason'] ?? null,
'notes' => $options['notes'] ?? null,
'created_by' => $employee ? (int) $employee->id : null,
]);
EventBus::dispatch('instrument.status_changed', [
'instrument_id' => $instrumentId,
'from_status' => $fromStatus,
'to_status' => $toStatus,
'instrument' => $instrument,
]);
$db->commit();
return ['success' => true, 'instrument_id' => $instrumentId];
} catch (\Throwable $e) {
$db->rollBack();
return ['success' => false, 'error' => $e->getMessage()];
}
}
public static function batchTransition(array $instrumentIds, string $toStatus, array $options = []): array
{
$results = ['success' => 0, 'failed' => 0, 'errors' => []];
foreach ($instrumentIds as $id) {
$result = self::transition((int) $id, $toStatus, $options);
if ($result['success']) {
$results['success']++;
} else {
$results['failed']++;
$results['errors'][] = "#{$id}: " . $result['error'];
}
}
return $results;
}
public static function getStatusHistory(int $instrumentId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT h.*, e.full_name_ar as created_by_name
FROM instrument_status_history h
LEFT JOIN employees e ON e.id = h.created_by
WHERE h.instrument_id = ?
ORDER BY h.created_at DESC",
[$instrumentId]
);
}
public static function endorse(int $instrumentId, string $endorsedTo, ?string $date = null, ?string $notes = null): array
{
return self::transition($instrumentId, 'endorsed', [
'endorsed_to' => $endorsedTo,
'date' => $date ?? date('Y-m-d'),
'notes' => $notes,
]);
}
public static function deposit(int $instrumentId, int $bankAccountId, ?string $date = null): array
{
return self::transition($instrumentId, 'under_collection', [
'bank_account_id' => $bankAccountId,
'date' => $date ?? date('Y-m-d'),
]);
}
public static function collect(int $instrumentId, ?string $date = null): array
{
return self::transition($instrumentId, 'collected', [
'date' => $date ?? date('Y-m-d'),
]);
}
public static function bounce(int $instrumentId, string $reason, ?string $date = null): array
{
return self::transition($instrumentId, 'bounced', [
'bounce_reason' => $reason,
'date' => $date ?? date('Y-m-d'),
]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Services;
use App\Core\App;
use App\Core\EventBus;
final class CrossEntitySettlementService
{
public static function create(array $data): array
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$settlementNumber = 'SET-' . date('Ymd') . '-' . str_pad((string) random_int(1, 9999), 4, '0', STR_PAD_LEFT);
$db->beginTransaction();
try {
$settlementId = $db->insert('cross_entity_settlements', [
'settlement_number' => $settlementNumber,
'settlement_date' => $data['settlement_date'] ?? date('Y-m-d'),
'from_entity_type' => $data['from_entity_type'],
'from_entity_id' => (int) $data['from_entity_id'],
'from_entity_name' => $data['from_entity_name'],
'to_entity_type' => $data['to_entity_type'],
'to_entity_id' => (int) $data['to_entity_id'],
'to_entity_name' => $data['to_entity_name'],
'amount' => $data['amount'],
'purpose' => $data['purpose'] ?? 'payment',
'description' => $data['description'] ?? null,
'status' => 'draft',
'created_by' => $employee ? (int) $employee->id : null,
]);
$db->commit();
return ['success' => true, 'settlement_id' => $settlementId, 'settlement_number' => $settlementNumber];
} catch (\Throwable $e) {
$db->rollBack();
return ['success' => false, 'error' => $e->getMessage()];
}
}
public static function approve(int $settlementId): array
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$settlement = $db->selectOne("SELECT * FROM cross_entity_settlements WHERE id = ?", [$settlementId]);
if (!$settlement) {
return ['success' => false, 'error' => 'التسوية غير موجودة'];
}
if ($settlement['status'] !== 'draft') {
return ['success' => false, 'error' => 'التسوية ليست في حالة مسودة'];
}
$db->beginTransaction();
try {
$db->update('cross_entity_settlements', [
'status' => 'approved',
'approved_by' => $employee ? (int) $employee->id : null,
'approved_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$settlementId]);
self::postSettlementToAccounts($settlement);
$db->update('cross_entity_settlements', ['status' => 'posted'], 'id = ?', [$settlementId]);
$db->commit();
EventBus::dispatch('settlement.posted', [
'settlement_id' => $settlementId,
'amount' => $settlement['amount'],
]);
return ['success' => true];
} catch (\Throwable $e) {
$db->rollBack();
return ['success' => false, 'error' => $e->getMessage()];
}
}
private static function postSettlementToAccounts(array $settlement): void
{
$amount = (string) $settlement['amount'];
$date = $settlement['settlement_date'];
if ($settlement['from_entity_type'] === 'customer') {
AccountStatementService::recordCustomerTransaction([
'member_id' => (int) $settlement['from_entity_id'],
'transaction_date' => $date,
'document_type' => 'adjustment',
'document_number' => $settlement['settlement_number'],
'description' => 'تسوية لصالح: ' . $settlement['to_entity_name'],
'credit' => $amount,
]);
} elseif ($settlement['from_entity_type'] === 'supplier') {
AccountStatementService::recordSupplierTransaction([
'supplier_id' => (int) $settlement['from_entity_id'],
'transaction_date' => $date,
'document_type' => 'adjustment',
'document_number' => $settlement['settlement_number'],
'description' => 'تسوية لصالح: ' . $settlement['to_entity_name'],
'debit' => $amount,
]);
}
if ($settlement['to_entity_type'] === 'customer') {
AccountStatementService::recordCustomerTransaction([
'member_id' => (int) $settlement['to_entity_id'],
'transaction_date' => $date,
'document_type' => 'adjustment',
'document_number' => $settlement['settlement_number'],
'description' => 'تسوية من: ' . $settlement['from_entity_name'],
'debit' => $amount,
]);
} elseif ($settlement['to_entity_type'] === 'supplier') {
AccountStatementService::recordSupplierTransaction([
'supplier_id' => (int) $settlement['to_entity_id'],
'transaction_date' => $date,
'document_type' => 'adjustment',
'document_number' => $settlement['settlement_number'],
'description' => 'تسوية من: ' . $settlement['from_entity_name'],
'credit' => $amount,
]);
}
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Services;
use App\Core\App;
use App\Core\EventBus;
final class DailyCashMovementService
{
public static function registerListeners(): void
{
EventBus::listen('sale.completed', [self::class, 'onSaleCompleted'], 30);
EventBus::listen('payment.completed', [self::class, 'onPaymentReceived'], 30);
EventBus::listen('procurement.payment_completed', [self::class, 'onPaymentMade'], 30);
}
public static function record(array $data): int
{
$db = App::getInstance()->db();
return $db->insert('daily_cash_movements', [
'movement_date' => $data['movement_date'] ?? date('Y-m-d'),
'movement_type' => $data['movement_type'],
'direction' => $data['direction'],
'amount' => $data['amount'],
'description' => $data['description'] ?? null,
'reference_type' => $data['reference_type'] ?? null,
'reference_id' => $data['reference_id'] ?? null,
'bank_account_id' => $data['bank_account_id'] ?? null,
'created_at' => date('Y-m-d H:i:s'),
]);
}
public static function onSaleCompleted(array $data): void
{
$amount = (string) ($data['total_amount'] ?? '0');
if (bccomp($amount, '0', 2) <= 0) return;
self::record([
'movement_type' => 'sale',
'direction' => 'in',
'amount' => $amount,
'description' => 'مبيعات — ' . ($data['invoice_number'] ?? ''),
'reference_type' => 'sales',
'reference_id' => $data['sale_id'] ?? null,
]);
}
public static function onPaymentReceived(array $data): void
{
$amount = (string) ($data['amount'] ?? '0');
if (bccomp($amount, '0', 2) <= 0) return;
self::record([
'movement_type' => 'collection',
'direction' => 'in',
'amount' => $amount,
'description' => 'تحصيل — ' . ($data['receipt_number'] ?? ''),
'reference_type' => 'payments',
'reference_id' => $data['payment_id'] ?? null,
]);
}
public static function onPaymentMade(array $data): void
{
$amount = (string) ($data['amount'] ?? '0');
if (bccomp($amount, '0', 2) <= 0) return;
self::record([
'movement_type' => 'disbursement',
'direction' => 'out',
'amount' => $amount,
'description' => 'سداد مورد — ' . ($data['payment_number'] ?? ''),
'reference_type' => 'vendor_payments',
'reference_id' => $data['payment_id'] ?? null,
]);
}
public static function getDailySummary(string $date): array
{
$db = App::getInstance()->db();
$inflows = $db->selectOne(
"SELECT COALESCE(SUM(amount), 0) as total FROM daily_cash_movements WHERE movement_date = ? AND direction = 'in'",
[$date]
);
$outflows = $db->selectOne(
"SELECT COALESCE(SUM(amount), 0) as total FROM daily_cash_movements WHERE movement_date = ? AND direction = 'out'",
[$date]
);
$totalIn = (string) ($inflows['total'] ?? '0.00');
$totalOut = (string) ($outflows['total'] ?? '0.00');
return [
'date' => $date,
'total_in' => $totalIn,
'total_out' => $totalOut,
'net' => bcsub($totalIn, $totalOut, 2),
];
}
public static function getMovements(string $date): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM daily_cash_movements WHERE movement_date = ? ORDER BY created_at",
[$date]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Services;
use App\Core\App;
use App\Core\EventBus;
final class DocumentaryCreditService
{
private const VALID_TRANSITIONS = [
'draft' => ['submitted'],
'submitted' => ['opened', 'cancelled'],
'opened' => ['partially_utilized', 'fully_utilized', 'expired', 'cancelled'],
'partially_utilized' => ['fully_utilized', 'expired'],
'fully_utilized' => ['closed'],
'expired' => ['closed'],
];
public static function create(array $data): int
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
return $db->insert('documentary_credits', [
'credit_number' => $data['credit_number'],
'type' => $data['type'] ?? 'import',
'beneficiary_name' => $data['beneficiary_name'],
'issuing_bank' => $data['issuing_bank'],
'advising_bank' => $data['advising_bank'] ?? null,
'amount' => $data['amount'],
'currency_code' => $data['currency_code'] ?? 'EGP',
'opening_date' => $data['opening_date'] ?? date('Y-m-d'),
'expiry_date' => $data['expiry_date'],
'shipment_date' => $data['shipment_date'] ?? null,
'purchase_order_id' => $data['purchase_order_id'] ?? null,
'bank_account_id' => $data['bank_account_id'] ?? null,
'margin_amount' => $data['margin_amount'] ?? '0.00',
'commission_rate' => $data['commission_rate'] ?? '0.00',
'status' => 'draft',
'notes' => $data['notes'] ?? null,
'created_by' => $employee ? (int) $employee->id : null,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
}
public static function transition(int $creditId, string $newStatus): array
{
$db = App::getInstance()->db();
$credit = $db->selectOne("SELECT * FROM documentary_credits WHERE id = ?", [$creditId]);
if (!$credit) {
return ['success' => false, 'error' => 'الاعتماد المستندي غير موجود'];
}
$current = $credit['status'];
$allowed = self::VALID_TRANSITIONS[$current] ?? [];
if (!in_array($newStatus, $allowed, true)) {
return ['success' => false, 'error' => "لا يمكن الانتقال من {$current} إلى {$newStatus}"];
}
$db->update('documentary_credits', [
'status' => $newStatus,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$creditId]);
EventBus::dispatch('accounting.lc.status_changed', [
'credit_id' => $creditId,
'from_status' => $current,
'to_status' => $newStatus,
'amount' => (string) $credit['amount'],
]);
return ['success' => true];
}
public static function addDocument(int $creditId, array $data): int
{
$db = App::getInstance()->db();
return $db->insert('lc_documents', [
'credit_id' => $creditId,
'document_type' => $data['document_type'],
'document_number' => $data['document_number'] ?? null,
'description' => $data['description'] ?? null,
'received_date' => $data['received_date'] ?? date('Y-m-d'),
'status' => $data['status'] ?? 'received',
'created_at' => date('Y-m-d H:i:s'),
]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Services;
use App\Core\App;
use App\Core\EventBus;
final class LetterOfGuaranteeService
{
private const VALID_TRANSITIONS = [
'draft' => ['active'],
'active' => ['extended', 'released', 'called'],
'extended' => ['released', 'called'],
'called' => ['settled'],
];
public static function create(array $data): int
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
return $db->insert('letters_of_guarantee', [
'guarantee_number' => $data['guarantee_number'],
'type' => $data['type'] ?? 'bid',
'beneficiary_name' => $data['beneficiary_name'],
'issuing_bank' => $data['issuing_bank'],
'amount' => $data['amount'],
'currency_code' => $data['currency_code'] ?? 'EGP',
'issue_date' => $data['issue_date'] ?? date('Y-m-d'),
'expiry_date' => $data['expiry_date'],
'purpose' => $data['purpose'] ?? null,
'bank_account_id' => $data['bank_account_id'] ?? null,
'margin_amount' => $data['margin_amount'] ?? '0.00',
'commission_rate' => $data['commission_rate'] ?? '0.00',
'status' => 'draft',
'notes' => $data['notes'] ?? null,
'created_by' => $employee ? (int) $employee->id : null,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
}
public static function transition(int $guaranteeId, string $newStatus): array
{
$db = App::getInstance()->db();
$guarantee = $db->selectOne("SELECT * FROM letters_of_guarantee WHERE id = ?", [$guaranteeId]);
if (!$guarantee) {
return ['success' => false, 'error' => 'خطاب الضمان غير موجود'];
}
$current = $guarantee['status'];
$allowed = self::VALID_TRANSITIONS[$current] ?? [];
if (!in_array($newStatus, $allowed, true)) {
return ['success' => false, 'error' => "لا يمكن الانتقال من {$current} إلى {$newStatus}"];
}
$updateData = [
'status' => $newStatus,
'updated_at' => date('Y-m-d H:i:s'),
];
if ($newStatus === 'released') {
$updateData['released_date'] = date('Y-m-d');
}
$db->update('letters_of_guarantee', $updateData, 'id = ?', [$guaranteeId]);
EventBus::dispatch('accounting.guarantee.status_changed', [
'guarantee_id' => $guaranteeId,
'from_status' => $current,
'to_status' => $newStatus,
'amount' => (string) $guarantee['amount'],
]);
return ['success' => true];
}
public static function getExpiringWithinDays(int $days = 30): array
{
$db = App::getInstance()->db();
$futureDate = date('Y-m-d', strtotime("+{$days} days"));
return $db->select(
"SELECT * FROM letters_of_guarantee
WHERE status IN ('active', 'extended')
AND expiry_date <= ?
ORDER BY expiry_date ASC",
[$futureDate]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Services;
use App\Core\App;
final class MultiCurrencyService
{
public static function getBaseCurrency(): array
{
$db = App::getInstance()->db();
$currency = $db->selectOne("SELECT * FROM currencies WHERE is_base_currency = 1 LIMIT 1");
return $currency ?: ['code' => 'EGP', 'name_ar' => 'جنيه مصري', 'symbol' => 'ج.م', 'decimal_places' => 2];
}
public static function getExchangeRate(string $currencyCode, ?string $date = null): array
{
if ($currencyCode === 'EGP') {
return ['buy_rate' => '1.000000', 'sell_rate' => '1.000000', 'mid_rate' => '1.000000'];
}
$db = App::getInstance()->db();
$date = $date ?? date('Y-m-d');
$currency = $db->selectOne("SELECT id FROM currencies WHERE code = ?", [$currencyCode]);
if (!$currency) {
return ['buy_rate' => '1.000000', 'sell_rate' => '1.000000', 'mid_rate' => '1.000000'];
}
$rate = $db->selectOne(
"SELECT * FROM exchange_rates WHERE currency_id = ? AND rate_date <= ? ORDER BY rate_date DESC LIMIT 1",
[(int) $currency['id'], $date]
);
if (!$rate) {
return ['buy_rate' => '1.000000', 'sell_rate' => '1.000000', 'mid_rate' => '1.000000'];
}
return [
'buy_rate' => (string) $rate['buy_rate'],
'sell_rate' => (string) $rate['sell_rate'],
'mid_rate' => (string) $rate['mid_rate'],
];
}
public static function convert(string $amount, string $fromCurrency, string $toCurrency, ?string $date = null): array
{
if ($fromCurrency === $toCurrency) {
return ['converted_amount' => $amount, 'rate' => '1.000000'];
}
$baseCurrency = self::getBaseCurrency()['code'];
if ($fromCurrency === $baseCurrency) {
$rate = self::getExchangeRate($toCurrency, $date);
$converted = bcdiv($amount, $rate['mid_rate'], 2);
return ['converted_amount' => $converted, 'rate' => $rate['mid_rate']];
}
if ($toCurrency === $baseCurrency) {
$rate = self::getExchangeRate($fromCurrency, $date);
$converted = bcmul($amount, $rate['mid_rate'], 2);
return ['converted_amount' => $converted, 'rate' => $rate['mid_rate']];
}
$fromRate = self::getExchangeRate($fromCurrency, $date);
$toRate = self::getExchangeRate($toCurrency, $date);
$baseAmount = bcmul($amount, $fromRate['mid_rate'], 6);
$converted = bcdiv($baseAmount, $toRate['mid_rate'], 2);
$crossRate = bcdiv($fromRate['mid_rate'], $toRate['mid_rate'], 6);
return ['converted_amount' => $converted, 'rate' => $crossRate];
}
public static function calculateGainLoss(string $originalAmount, string $originalRate, string $currentRate, string $currency): array
{
$originalBase = bcmul($originalAmount, $originalRate, 2);
$currentBase = bcmul($originalAmount, $currentRate, 2);
$difference = bcsub($currentBase, $originalBase, 2);
return [
'original_base_amount' => $originalBase,
'current_base_amount' => $currentBase,
'gain_loss' => $difference,
'is_gain' => bccomp($difference, '0', 2) > 0,
];
}
public static function getActiveCurrencies(): array
{
$db = App::getInstance()->db();
return $db->select("SELECT * FROM currencies WHERE is_active = 1 ORDER BY is_base_currency DESC, name_ar ASC");
}
public static function setExchangeRate(string $currencyCode, string $buyRate, string $sellRate, ?string $date = null): bool
{
$db = App::getInstance()->db();
$date = $date ?? date('Y-m-d');
$currency = $db->selectOne("SELECT id FROM currencies WHERE code = ?", [$currencyCode]);
if (!$currency) return false;
$midRate = bcdiv(bcadd($buyRate, $sellRate, 6), '2', 6);
$existing = $db->selectOne(
"SELECT id FROM exchange_rates WHERE currency_id = ? AND rate_date = ?",
[(int) $currency['id'], $date]
);
if ($existing) {
$db->update('exchange_rates', [
'buy_rate' => $buyRate,
'sell_rate' => $sellRate,
'mid_rate' => $midRate,
], 'id = ?', [(int) $existing['id']]);
} else {
$db->insert('exchange_rates', [
'currency_id' => (int) $currency['id'],
'rate_date' => $date,
'buy_rate' => $buyRate,
'sell_rate' => $sellRate,
'mid_rate' => $midRate,
]);
}
return true;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
use App\Modules\Inventory\Services\CreditLimitService;
final class StatementIntegrationService
{
public static function registerListeners(): void
{
EventBus::listen('sale.completed', [self::class, 'onSaleCompleted'], 40);
EventBus::listen('sale.refunded', [self::class, 'onSaleRefunded'], 40);
EventBus::listen('payment.completed', [self::class, 'onPaymentCompleted'], 40);
EventBus::listen('procurement.invoice_approved', [self::class, 'onVendorInvoiceApproved'], 40);
EventBus::listen('procurement.payment_completed', [self::class, 'onVendorPaymentCompleted'], 40);
EventBus::listen('procurement.rtv_completed', [self::class, 'onReturnToVendor'], 40);
EventBus::listen('fine.imposed', [self::class, 'onFineImposed'], 40);
EventBus::listen('subscription.paid', [self::class, 'onSubscriptionPaid'], 40);
}
public static function onSaleCompleted(array $data): void
{
try {
$memberId = (int) ($data['member_id'] ?? 0);
if ($memberId <= 0) return;
$amount = (string) ($data['total_amount'] ?? '0');
$saleNumber = $data['sale_number'] ?? '';
AccountStatementService::recordCustomerTransaction([
'member_id' => $memberId,
'transaction_date' => date('Y-m-d'),
'document_type' => 'sale',
'document_id' => $data['sale_id'] ?? null,
'document_number' => $saleNumber,
'description' => 'فاتورة مبيعات: ' . $saleNumber,
'debit' => $amount,
]);
CreditLimitService::updateCustomerBalance($memberId, $amount, 'increase');
} catch (\Throwable $e) {
Logger::error('StatementIntegration sale.completed failed: ' . $e->getMessage());
}
}
public static function onSaleRefunded(array $data): void
{
try {
$memberId = (int) ($data['member_id'] ?? 0);
if ($memberId <= 0) return;
$amount = (string) ($data['refund_amount'] ?? '0');
$refundNumber = $data['refund_number'] ?? '';
AccountStatementService::recordCustomerTransaction([
'member_id' => $memberId,
'transaction_date' => date('Y-m-d'),
'document_type' => 'refund',
'document_id' => $data['refund_id'] ?? null,
'document_number' => $refundNumber,
'description' => 'مرتجع مبيعات: ' . $refundNumber,
'credit' => $amount,
]);
CreditLimitService::updateCustomerBalance($memberId, $amount, 'decrease');
} catch (\Throwable $e) {
Logger::error('StatementIntegration sale.refunded failed: ' . $e->getMessage());
}
}
public static function onPaymentCompleted(array $data): void
{
try {
$memberId = (int) ($data['member_id'] ?? 0);
if ($memberId <= 0) return;
$amount = (string) ($data['amount'] ?? '0');
$receiptNumber = $data['receipt_number'] ?? '';
AccountStatementService::recordCustomerTransaction([
'member_id' => $memberId,
'transaction_date' => date('Y-m-d'),
'document_type' => 'payment',
'document_id' => $data['payment_id'] ?? null,
'document_number' => $receiptNumber,
'description' => 'سداد: ' . $receiptNumber,
'credit' => $amount,
]);
CreditLimitService::updateCustomerBalance($memberId, $amount, 'decrease');
} catch (\Throwable $e) {
Logger::error('StatementIntegration payment.completed failed: ' . $e->getMessage());
}
}
public static function onVendorInvoiceApproved(array $data): void
{
try {
$supplierId = (int) ($data['supplier_id'] ?? 0);
if ($supplierId <= 0) return;
$amount = (string) ($data['total_amount'] ?? '0');
$invoiceNumber = $data['invoice_number'] ?? '';
AccountStatementService::recordSupplierTransaction([
'supplier_id' => $supplierId,
'transaction_date' => date('Y-m-d'),
'document_type' => 'invoice',
'document_id' => $data['invoice_id'] ?? null,
'document_number' => $invoiceNumber,
'description' => 'فاتورة مورد: ' . $invoiceNumber,
'credit' => $amount,
]);
CreditLimitService::updateSupplierBalance($supplierId, $amount, 'increase');
} catch (\Throwable $e) {
Logger::error('StatementIntegration vendor_invoice_approved failed: ' . $e->getMessage());
}
}
public static function onVendorPaymentCompleted(array $data): void
{
try {
$supplierId = (int) ($data['supplier_id'] ?? 0);
if ($supplierId <= 0) return;
$amount = (string) ($data['amount'] ?? '0');
$paymentNumber = $data['payment_number'] ?? '';
AccountStatementService::recordSupplierTransaction([
'supplier_id' => $supplierId,
'transaction_date' => date('Y-m-d'),
'document_type' => 'payment',
'document_id' => $data['payment_id'] ?? null,
'document_number' => $paymentNumber,
'description' => 'سداد مورد: ' . $paymentNumber,
'debit' => $amount,
]);
CreditLimitService::updateSupplierBalance($supplierId, $amount, 'decrease');
} catch (\Throwable $e) {
Logger::error('StatementIntegration vendor_payment_completed failed: ' . $e->getMessage());
}
}
public static function onReturnToVendor(array $data): void
{
try {
$supplierId = (int) ($data['supplier_id'] ?? 0);
if ($supplierId <= 0) return;
$amount = (string) ($data['total_amount'] ?? '0');
$rtvNumber = $data['rtv_number'] ?? '';
AccountStatementService::recordSupplierTransaction([
'supplier_id' => $supplierId,
'transaction_date' => date('Y-m-d'),
'document_type' => 'return',
'document_id' => $data['rtv_id'] ?? null,
'document_number' => $rtvNumber,
'description' => 'مرتجع مورد: ' . $rtvNumber,
'debit' => $amount,
]);
CreditLimitService::updateSupplierBalance($supplierId, $amount, 'decrease');
} catch (\Throwable $e) {
Logger::error('StatementIntegration rtv_completed failed: ' . $e->getMessage());
}
}
public static function onFineImposed(array $data): void
{
try {
$memberId = (int) ($data['member_id'] ?? 0);
if ($memberId <= 0) return;
$amount = (string) ($data['amount'] ?? '0');
AccountStatementService::recordCustomerTransaction([
'member_id' => $memberId,
'transaction_date' => date('Y-m-d'),
'document_type' => 'fine',
'document_id' => $data['fine_id'] ?? null,
'description' => 'غرامة: ' . ($data['reason'] ?? ''),
'debit' => $amount,
]);
CreditLimitService::updateCustomerBalance($memberId, $amount, 'increase');
} catch (\Throwable $e) {
Logger::error('StatementIntegration fine.imposed failed: ' . $e->getMessage());
}
}
public static function onSubscriptionPaid(array $data): void
{
try {
$memberId = (int) ($data['member_id'] ?? 0);
if ($memberId <= 0) return;
$amount = (string) ($data['amount'] ?? '0');
AccountStatementService::recordCustomerTransaction([
'member_id' => $memberId,
'transaction_date' => date('Y-m-d'),
'document_type' => 'subscription',
'document_id' => $data['subscription_id'] ?? null,
'description' => 'اشتراك: ' . ($data['description'] ?? ''),
'debit' => $amount,
]);
} catch (\Throwable $e) {
Logger::error('StatementIntegration subscription.paid failed: ' . $e->getMessage());
}
}
}
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>الاعتمادات المستندية<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/accounting/documentary-credits/create" class="btn btn-primary">+ اعتماد جديد</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filters -->
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;">
<form method="GET" action="/accounting/documentary-credits" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div>
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-select" onchange="this.form.submit()">
<option value="">الكل</option>
<option value="draft" <?= ($status ?? '') === 'draft' ? 'selected' : '' ?>>مسودة</option>
<option value="opened" <?= ($status ?? '') === 'opened' ? 'selected' : '' ?>>مفتوح</option>
<option value="partially_utilized" <?= ($status ?? '') === 'partially_utilized' ? 'selected' : '' ?>>مستخدم جزئياً</option>
<option value="fully_utilized" <?= ($status ?? '') === 'fully_utilized' ? 'selected' : '' ?>>مستخدم بالكامل</option>
<option value="expired" <?= ($status ?? '') === 'expired' ? 'selected' : '' ?>>منتهي</option>
<option value="cancelled" <?= ($status ?? '') === 'cancelled' ? 'selected' : '' ?>>ملغي</option>
</select>
</div>
</form>
</div>
</div>
<!-- Credits Table -->
<div class="card">
<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 foreach ($credits as $lc): ?>
<?php
$statusColor = match($lc['status'] ?? '') {
'draft' => '#F59E0B',
'opened' => '#059669',
'partially_utilized' => '#0284C7',
'fully_utilized' => '#6B7280',
'expired' => '#DC2626',
'cancelled' => '#9CA3AF',
default => '#374151',
};
$statusLabel = match($lc['status'] ?? '') {
'draft' => 'مسودة',
'opened' => 'مفتوح',
'partially_utilized' => 'مستخدم جزئياً',
'fully_utilized' => 'مستخدم بالكامل',
'expired' => 'منتهي',
'cancelled' => 'ملغي',
default => $lc['status'] ?? '—',
};
?>
<tr>
<td style="font-weight:600;direction:ltr;text-align:right;"><?= e($lc['credit_number'] ?? '') ?></td>
<td><?= e($lc['beneficiary_name'] ?? '—') ?></td>
<td><?= e($lc['issuing_bank'] ?? '—') ?></td>
<td style="direction:ltr;text-align:right;font-weight:600;"><?= money($lc['amount'] ?? 0) ?></td>
<td style="direction:ltr;text-align:right;"><?= e($lc['opening_date'] ?? '—') ?></td>
<td style="direction:ltr;text-align:right;"><?= e($lc['expiry_date'] ?? '—') ?></td>
<td><span style="color:<?= $statusColor ?>;font-weight:600;"><?= $statusLabel ?></span></td>
<td><a href="/accounting/documentary-credits/<?= (int)$lc['id'] ?>" class="btn btn-sm btn-outline">عرض</a></td>
</tr>
<?php endforeach; ?>
<?php if (empty($credits)): ?>
<tr><td colspan="8" style="text-align:center;color:#6B7280;padding:30px;">لا توجد اعتمادات مستندية</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>خطابات الضمان<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/accounting/guarantees/create" class="btn btn-primary">+ خطاب ضمان جديد</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filters -->
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;">
<form method="GET" action="/accounting/guarantees" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div>
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-select" onchange="this.form.submit()">
<option value="">الكل</option>
<option value="active" <?= ($status ?? '') === 'active' ? 'selected' : '' ?>>نشط</option>
<option value="expired" <?= ($status ?? '') === 'expired' ? 'selected' : '' ?>>منتهي</option>
<option value="claimed" <?= ($status ?? '') === 'claimed' ? 'selected' : '' ?>>مُطالب به</option>
<option value="released" <?= ($status ?? '') === 'released' ? 'selected' : '' ?>>محرر</option>
<option value="cancelled" <?= ($status ?? '') === 'cancelled' ? 'selected' : '' ?>>ملغي</option>
</select>
</div>
<div>
<label class="form-label" style="font-size:12px;">النوع</label>
<select name="type" class="form-select" onchange="this.form.submit()">
<option value="">الكل</option>
<option value="bid_bond" <?= ($type ?? '') === 'bid_bond' ? 'selected' : '' ?>>ضمان ابتدائي</option>
<option value="performance_bond" <?= ($type ?? '') === 'performance_bond' ? 'selected' : '' ?>>ضمان حسن تنفيذ</option>
<option value="advance_payment" <?= ($type ?? '') === 'advance_payment' ? 'selected' : '' ?>>ضمان دفعة مقدمة</option>
<option value="retention" <?= ($type ?? '') === 'retention' ? 'selected' : '' ?>>ضمان محتجزات</option>
<option value="customs" <?= ($type ?? '') === 'customs' ? 'selected' : '' ?>>ضمان جمركي</option>
</select>
</div>
</form>
</div>
</div>
<!-- Guarantees Table -->
<div class="card">
<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 foreach ($guarantees as $g): ?>
<?php
$statusColor = match($g['status'] ?? '') {
'active' => '#059669',
'expired' => '#DC2626',
'claimed' => '#D97706',
'released' => '#6B7280',
'cancelled' => '#9CA3AF',
default => '#374151',
};
$statusLabel = match($g['status'] ?? '') {
'active' => 'نشط',
'expired' => 'منتهي',
'claimed' => 'مُطالب به',
'released' => 'محرر',
'cancelled' => 'ملغي',
default => $g['status'] ?? '—',
};
$typeLabel = match($g['guarantee_type'] ?? $g['type'] ?? '') {
'bid_bond' => 'ضمان ابتدائي',
'performance_bond' => 'ضمان حسن تنفيذ',
'advance_payment' => 'ضمان دفعة مقدمة',
'retention' => 'ضمان محتجزات',
'customs' => 'ضمان جمركي',
default => $g['guarantee_type'] ?? $g['type'] ?? '—',
};
?>
<tr>
<td style="font-weight:600;direction:ltr;text-align:right;"><?= e($g['guarantee_number'] ?? '') ?></td>
<td><?= $typeLabel ?></td>
<td><?= e($g['beneficiary_name'] ?? '—') ?></td>
<td><?= e($g['issuing_bank'] ?? '—') ?></td>
<td style="direction:ltr;text-align:right;font-weight:600;"><?= money($g['amount'] ?? 0) ?></td>
<td style="direction:ltr;text-align:right;"><?= e($g['expiry_date'] ?? '—') ?></td>
<td><span style="color:<?= $statusColor ?>;font-weight:600;"><?= $statusLabel ?></span></td>
<td><a href="/accounting/guarantees/<?= (int)$g['id'] ?>" class="btn btn-sm btn-outline">عرض</a></td>
</tr>
<?php endforeach; ?>
<?php if (empty($guarantees)): ?>
<tr><td colspan="8" style="text-align:center;color:#6B7280;padding:30px;">لا توجد خطابات ضمان</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?><?= !empty($loan['id']) ? 'تعديل قرض' : 'قرض جديد' ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;"><?= !empty($loan['id']) ? 'تعديل قرض' : 'إضافة قرض بنكي جديد' ?></h3>
</div>
<div style="padding:20px;">
<form method="POST" action="<?= !empty($loan['id']) ? '/accounting/loans/' . (int)$loan['id'] : '/accounting/loans' ?>">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div>
<label class="form-label">الحساب البنكي <span style="color:#DC2626;">*</span></label>
<select name="bank_account_id" class="form-select" required>
<option value="">— اختر حساب بنكي —</option>
<?php foreach ($bankAccounts as $ba): ?>
<option value="<?= (int)$ba['id'] ?>" <?= (int)($loan['bank_account_id'] ?? 0) === (int)$ba['id'] ? 'selected' : '' ?>><?= e($ba['account_name_ar'] ?? $ba['bank_name'] ?? '') ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">نوع القرض <span style="color:#DC2626;">*</span></label>
<select name="loan_type" class="form-select" required>
<option value="">— اختر —</option>
<option value="term_loan" <?= ($loan['loan_type'] ?? '') === 'term_loan' ? 'selected' : '' ?>>قرض لأجل</option>
<option value="revolving" <?= ($loan['loan_type'] ?? '') === 'revolving' ? 'selected' : '' ?>>تسهيل ائتماني متجدد</option>
<option value="overdraft" <?= ($loan['loan_type'] ?? '') === 'overdraft' ? 'selected' : '' ?>>سحب على المكشوف</option>
<option value="mortgage" <?= ($loan['loan_type'] ?? '') === 'mortgage' ? 'selected' : '' ?>>قرض عقاري</option>
<option value="equipment" <?= ($loan['loan_type'] ?? '') === 'equipment' ? 'selected' : '' ?>>تمويل معدات</option>
</select>
</div>
<div>
<label class="form-label">المبلغ الأصلي <span style="color:#DC2626;">*</span></label>
<input type="number" name="principal_amount" class="form-input" step="0.01" min="0.01" required dir="ltr" value="<?= e($loan['principal_amount'] ?? '') ?>">
</div>
<div>
<label class="form-label">سعر الفائدة (%) <span style="color:#DC2626;">*</span></label>
<input type="number" name="interest_rate" class="form-input" step="0.01" min="0" required dir="ltr" value="<?= e($loan['interest_rate'] ?? '') ?>">
</div>
<div>
<label class="form-label">مدة القرض (أشهر) <span style="color:#DC2626;">*</span></label>
<input type="number" name="term_months" class="form-input" min="1" required dir="ltr" value="<?= e($loan['term_months'] ?? '') ?>">
</div>
<div>
<label class="form-label">تاريخ البداية <span style="color:#DC2626;">*</span></label>
<input type="date" name="start_date" class="form-input" required dir="ltr" value="<?= e($loan['start_date'] ?? date('Y-m-d')) ?>">
</div>
<div>
<label class="form-label">رقم القرض</label>
<input type="text" name="loan_number" class="form-input" dir="ltr" value="<?= e($loan['loan_number'] ?? '') ?>">
</div>
<div>
<label class="form-label">اسم البنك</label>
<input type="text" name="bank_name" class="form-input" value="<?= e($loan['bank_name'] ?? '') ?>">
</div>
<div style="grid-column:1/-1;">
<label class="form-label">الضمان</label>
<textarea name="collateral" class="form-textarea" rows="2"><?= e($loan['collateral'] ?? '') ?></textarea>
</div>
<div style="grid-column:1/-1;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-textarea" rows="2"><?= e($loan['notes'] ?? '') ?></textarea>
</div>
</div>
<div style="margin-top:20px;display:flex;gap:8px;">
<button type="submit" class="btn btn-primary">حفظ</button>
<a href="/accounting/loans" class="btn btn-outline">إلغاء</a>
</div>
</form>
</div>
</div>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>القروض البنكية<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/accounting/loans/create" class="btn btn-primary">+ قرض جديد</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filters -->
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;">
<form method="GET" action="/accounting/loans" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div>
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-select" onchange="this.form.submit()">
<option value="">الكل</option>
<option value="active" <?= ($status ?? '') === 'active' ? 'selected' : '' ?>>نشط</option>
<option value="paid_off" <?= ($status ?? '') === 'paid_off' ? 'selected' : '' ?>>مسدد</option>
<option value="defaulted" <?= ($status ?? '') === 'defaulted' ? 'selected' : '' ?>>متعثر</option>
<option value="restructured" <?= ($status ?? '') === 'restructured' ? 'selected' : '' ?>>مُعاد هيكلته</option>
</select>
</div>
</form>
</div>
</div>
<!-- Loans Table -->
<div class="card">
<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 ($loans as $loan): ?>
<?php
$statusColor = match($loan['status'] ?? '') {
'active' => '#059669',
'paid_off' => '#6B7280',
'defaulted' => '#DC2626',
'restructured' => '#D97706',
default => '#374151',
};
$statusLabel = match($loan['status'] ?? '') {
'active' => 'نشط',
'paid_off' => 'مسدد',
'defaulted' => 'متعثر',
'restructured' => 'مُعاد هيكلته',
default => $loan['status'] ?? '—',
};
?>
<tr>
<td style="font-weight:600;direction:ltr;text-align:right;"><?= e($loan['loan_number'] ?? '') ?></td>
<td><?= e($loan['bank_name'] ?? '—') ?></td>
<td style="direction:ltr;text-align:right;"><?= money($loan['principal_amount'] ?? 0) ?></td>
<td style="direction:ltr;text-align:right;"><?= e($loan['interest_rate'] ?? 0) ?>%</td>
<td style="direction:ltr;text-align:right;font-weight:600;"><?= money($loan['outstanding_balance'] ?? 0) ?></td>
<td><span style="color:<?= $statusColor ?>;font-weight:600;"><?= $statusLabel ?></span></td>
<td><a href="/accounting/loans/<?= (int)$loan['id'] ?>" class="btn btn-sm btn-outline">عرض</a></td>
</tr>
<?php endforeach; ?>
<?php if (empty($loans)): ?>
<tr><td colspan="7" style="text-align:center;color:#6B7280;padding:30px;">لا توجد قروض</td></tr>
<?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
$statusColor = match($loan['status'] ?? '') {
'active' => '#059669',
'paid_off' => '#6B7280',
'defaulted' => '#DC2626',
'restructured' => '#D97706',
default => '#374151',
};
$statusLabel = match($loan['status'] ?? '') {
'active' => 'نشط',
'paid_off' => 'مسدد',
'defaulted' => 'متعثر',
'restructured' => 'مُعاد هيكلته',
default => $loan['status'] ?? '—',
};
?>
<!-- Loan Details -->
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0;">قرض رقم: <?= e($loan['loan_number'] ?? '') ?></h3>
<span style="color:<?= $statusColor ?>;font-weight:700;font-size:14px;"><?= $statusLabel ?></span>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:15px;">
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">البنك</div>
<div style="font-size:16px;font-weight:700;"><?= e($loan['bank_name'] ?? '—') ?></div>
</div>
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">المبلغ الأصلي</div>
<div style="font-size:18px;font-weight:700;direction:ltr;"><?= money($loan['principal_amount'] ?? 0) ?></div>
</div>
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">سعر الفائدة</div>
<div style="font-size:18px;font-weight:700;direction:ltr;"><?= e($loan['interest_rate'] ?? 0) ?>%</div>
</div>
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">الرصيد القائم</div>
<div style="font-size:18px;font-weight:700;direction:ltr;color:#DC2626;"><?= money($loan['outstanding_balance'] ?? 0) ?></div>
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:15px;margin-top:15px;">
<div>
<span style="font-size:12px;color:#6B7280;">نوع القرض:</span>
<span style="font-weight:600;"><?= e($loan['loan_type'] ?? '—') ?></span>
</div>
<div>
<span style="font-size:12px;color:#6B7280;">مدة القرض:</span>
<span style="font-weight:600;"><?= (int)($loan['term_months'] ?? 0) ?> شهر</span>
</div>
<div>
<span style="font-size:12px;color:#6B7280;">تاريخ البداية:</span>
<span style="font-weight:600;direction:ltr;display:inline-block;"><?= e($loan['start_date'] ?? '—') ?></span>
</div>
<div>
<span style="font-size:12px;color:#6B7280;">الضمان:</span>
<span style="font-weight:600;"><?= e($loan['collateral'] ?? '—') ?></span>
</div>
</div>
</div>
</div>
<!-- Amortization Schedule -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;">جدول السداد</h3>
</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 foreach ($schedule as $inst): ?>
<?php
$instStatusColor = match($inst['status'] ?? '') {
'paid' => '#059669',
'due' => '#D97706',
'overdue' => '#DC2626',
'upcoming' => '#6B7280',
default => '#374151',
};
$instStatusLabel = match($inst['status'] ?? '') {
'paid' => 'مسدد',
'due' => 'مستحق',
'overdue' => 'متأخر',
'upcoming' => 'قادم',
default => $inst['status'] ?? '—',
};
?>
<tr>
<td style="text-align:center;"><?= (int)($inst['installment_number'] ?? 0) ?></td>
<td style="direction:ltr;text-align:right;"><?= e($inst['due_date'] ?? '—') ?></td>
<td style="direction:ltr;text-align:right;"><?= money($inst['principal'] ?? 0) ?></td>
<td style="direction:ltr;text-align:right;"><?= money($inst['interest'] ?? 0) ?></td>
<td style="direction:ltr;text-align:right;font-weight:600;"><?= money($inst['total'] ?? 0) ?></td>
<td><span style="color:<?= $instStatusColor ?>;font-weight:600;"><?= $instStatusLabel ?></span></td>
<td style="direction:ltr;text-align:right;"><?= e($inst['paid_date'] ?? '—') ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($schedule)): ?>
<tr><td colspan="7" style="text-align:center;color:#6B7280;padding:30px;">لا يوجد جدول سداد</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<div style="margin-top:15px;">
<a href="/accounting/loans" class="btn btn-outline">رجوع للقائمة</a>
</div>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>التسويات<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/accounting/settlements/create" class="btn btn-primary">+ تسوية جديدة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card">
<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): ?>
<?php
$statusColor = match($s['status'] ?? '') {
'draft' => '#F59E0B',
'approved' => '#059669',
'posted' => '#0284C7',
'rejected' => '#DC2626',
default => '#6B7280',
};
$statusLabel = match($s['status'] ?? '') {
'draft' => 'مسودة',
'approved' => 'معتمدة',
'posted' => 'مرحّلة',
'rejected' => 'مرفوضة',
default => $s['status'] ?? '—',
};
$typeLabel = match($s['type'] ?? '') {
'debt_settlement' => 'تسوية ديون',
'advance_settlement' => 'تسوية سلف',
'account_settlement' => 'تسوية حسابات',
default => $s['type'] ?? '—',
};
?>
<tr>
<td style="font-weight:600;direction:ltr;text-align:right;"><?= e($s['settlement_number'] ?? '') ?></td>
<td style="direction:ltr;text-align:right;"><?= e($s['settlement_date'] ?? $s['created_at'] ?? '—') ?></td>
<td><?= $typeLabel ?></td>
<td><?= e($s['description'] ?? '—') ?></td>
<td style="direction:ltr;text-align:right;font-weight:600;"><?= money($s['amount'] ?? 0) ?></td>
<td><span style="color:<?= $statusColor ?>;font-weight:600;"><?= $statusLabel ?></span></td>
<td><a href="/accounting/settlements/<?= (int)$s['id'] ?>" class="btn btn-sm btn-outline">عرض</a></td>
</tr>
<?php endforeach; ?>
<?php if (empty($settlements)): ?>
<tr><td colspan="7" style="text-align:center;color:#6B7280;padding:30px;">لا توجد تسويات</td></tr>
<?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'); ?>
<!-- Filters -->
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;">
<form method="GET" action="/accounting/statements/customer" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div>
<label class="form-label" style="font-size:12px;">العضو</label>
<select name="member_id" class="form-select">
<option value="">— اختر عضو —</option>
<?php foreach ($members as $m): ?>
<option value="<?= (int)$m['id'] ?>" <?= (int)($filters['member_id'] ?? 0) === (int)$m['id'] ? 'selected' : '' ?>><?= e($m['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label" style="font-size:12px;">من تاريخ</label>
<input type="date" name="date_from" class="form-input" value="<?= e($filters['date_from'] ?? '') ?>" dir="ltr">
</div>
<div>
<label class="form-label" style="font-size:12px;">إلى تاريخ</label>
<input type="date" name="date_to" class="form-input" value="<?= e($filters['date_to'] ?? '') ?>" dir="ltr">
</div>
<button type="submit" class="btn btn-primary">عرض</button>
</form>
</div>
</div>
<?php if (!empty($member) && !empty($statement)): ?>
<!-- Statement -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0;">كشف حساب: <?= e($member['name']) ?></h3>
<button type="button" class="btn btn-outline" onclick="window.print()">طباعة</button>
</div>
<!-- Opening Balance -->
<div style="padding:10px 20px;background:#F9FAFB;border-bottom:1px solid #E5E7EB;">
<strong>الرصيد الافتتاحي:</strong>
<span style="font-weight:700;direction:ltr;display:inline-block;"><?= money($statement['opening_balance'] ?? 0) ?></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>
</tr>
</thead>
<tbody>
<?php foreach ($statement['entries'] ?? [] as $entry): ?>
<tr>
<td style="direction:ltr;text-align:right;"><?= e($entry['date']) ?></td>
<td><?= e($entry['doc_type'] ?? '—') ?></td>
<td><?= e($entry['description'] ?? '') ?></td>
<td style="direction:ltr;text-align:right;color:#DC2626;"><?= (float)($entry['debit'] ?? 0) > 0 ? money($entry['debit']) : '' ?></td>
<td style="direction:ltr;text-align:right;color:#059669;"><?= (float)($entry['credit'] ?? 0) > 0 ? money($entry['credit']) : '' ?></td>
<td style="direction:ltr;text-align:right;font-weight:600;"><?= money($entry['balance'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($statement['entries'])): ?>
<tr><td colspan="6" style="text-align:center;color:#6B7280;padding:30px;">لا توجد حركات</td></tr>
<?php endif; ?>
</tbody>
<tfoot>
<tr style="font-weight:700;background:#F9FAFB;">
<td colspan="3">الإجمالي</td>
<td style="direction:ltr;text-align:right;color:#DC2626;"><?= money($statement['total_debit'] ?? 0) ?></td>
<td style="direction:ltr;text-align:right;color:#059669;"><?= money($statement['total_credit'] ?? 0) ?></td>
<td style="direction:ltr;text-align:right;"><?= money($statement['closing_balance'] ?? 0) ?></td>
</tr>
</tfoot>
</table>
</div>
<!-- Closing Balance -->
<div style="padding:10px 20px;background:#F0FDF4;border-top:1px solid #E5E7EB;">
<strong>الرصيد الختامي:</strong>
<span style="font-weight:700;direction:ltr;display:inline-block;"><?= money($statement['closing_balance'] ?? 0) ?></span>
</div>
</div>
<?php elseif (!empty($filters['member_id'])): ?>
<div class="card">
<div style="padding:30px;text-align:center;color:#6B7280;">لا توجد بيانات للعرض</div>
</div>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>كشف حساب مورد<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filters -->
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;">
<form method="GET" action="/accounting/statements/supplier" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div>
<label class="form-label" style="font-size:12px;">المورد</label>
<select name="supplier_id" class="form-select">
<option value="">— اختر مورد —</option>
<?php foreach ($suppliers as $s): ?>
<option value="<?= (int)$s['id'] ?>" <?= (int)($filters['supplier_id'] ?? 0) === (int)$s['id'] ? 'selected' : '' ?>><?= e($s['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label" style="font-size:12px;">من تاريخ</label>
<input type="date" name="date_from" class="form-input" value="<?= e($filters['date_from'] ?? '') ?>" dir="ltr">
</div>
<div>
<label class="form-label" style="font-size:12px;">إلى تاريخ</label>
<input type="date" name="date_to" class="form-input" value="<?= e($filters['date_to'] ?? '') ?>" dir="ltr">
</div>
<button type="submit" class="btn btn-primary">عرض</button>
</form>
</div>
</div>
<?php if (!empty($supplier) && !empty($statement)): ?>
<!-- Statement -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0;">كشف حساب: <?= e($supplier['name']) ?></h3>
<button type="button" class="btn btn-outline" onclick="window.print()">طباعة</button>
</div>
<!-- Opening Balance -->
<div style="padding:10px 20px;background:#F9FAFB;border-bottom:1px solid #E5E7EB;">
<strong>الرصيد الافتتاحي:</strong>
<span style="font-weight:700;direction:ltr;display:inline-block;"><?= money($statement['opening_balance'] ?? 0) ?></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>
</tr>
</thead>
<tbody>
<?php foreach ($statement['entries'] ?? [] as $entry): ?>
<tr>
<td style="direction:ltr;text-align:right;"><?= e($entry['date']) ?></td>
<td><?= e($entry['doc_type'] ?? '—') ?></td>
<td><?= e($entry['description'] ?? '') ?></td>
<td style="direction:ltr;text-align:right;color:#DC2626;"><?= (float)($entry['debit'] ?? 0) > 0 ? money($entry['debit']) : '' ?></td>
<td style="direction:ltr;text-align:right;color:#059669;"><?= (float)($entry['credit'] ?? 0) > 0 ? money($entry['credit']) : '' ?></td>
<td style="direction:ltr;text-align:right;font-weight:600;"><?= money($entry['balance'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($statement['entries'])): ?>
<tr><td colspan="6" style="text-align:center;color:#6B7280;padding:30px;">لا توجد حركات</td></tr>
<?php endif; ?>
</tbody>
<tfoot>
<tr style="font-weight:700;background:#F9FAFB;">
<td colspan="3">الإجمالي</td>
<td style="direction:ltr;text-align:right;color:#DC2626;"><?= money($statement['total_debit'] ?? 0) ?></td>
<td style="direction:ltr;text-align:right;color:#059669;"><?= money($statement['total_credit'] ?? 0) ?></td>
<td style="direction:ltr;text-align:right;"><?= money($statement['closing_balance'] ?? 0) ?></td>
</tr>
</tfoot>
</table>
</div>
<!-- Closing Balance -->
<div style="padding:10px 20px;background:#F0FDF4;border-top:1px solid #E5E7EB;">
<strong>الرصيد الختامي:</strong>
<span style="font-weight:700;direction:ltr;display:inline-block;"><?= money($statement['closing_balance'] ?? 0) ?></span>
</div>
</div>
<?php elseif (!empty($filters['supplier_id'])): ?>
<div class="card">
<div style="padding:30px;text-align:center;color:#6B7280;">لا توجد بيانات للعرض</div>
</div>
<?php endif; ?>
<?php $__template->endSection(); ?>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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