Commit 27dbd900 authored by Mahmoud Aglan's avatar Mahmoud Aglan

test

parent 3fcec655
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\App;
use App\Core\Response;
use App\Modules\Accounting\Models\BankAccount;
use App\Modules\Accounting\Models\Account;
class BankAccountController extends Controller
{
public function index(): Response
{
$this->authorize('accounting.bank_account.view');
$accounts = BankAccount::query()
->where('is_archived', '=', 0)
->orderBy('account_name_ar', 'ASC')
->get();
return $this->view('Accounting/Views/bank_accounts/index', [
'accounts' => $accounts,
]);
}
public function create(): Response
{
$this->authorize('accounting.bank_account.manage');
$glAccounts = Account::query()
->where('is_archived', '=', 0)
->where('is_bank_account', '=', 1)
->where('is_header', '=', 0)
->orderBy('account_code', 'ASC')
->get();
if (empty($glAccounts)) {
$glAccounts = Account::query()
->where('is_archived', '=', 0)
->where('account_code', 'LIKE', '1102%')
->where('is_header', '=', 0)
->orderBy('account_code', 'ASC')
->get();
}
return $this->view('Accounting/Views/bank_accounts/create', [
'gl_accounts' => $glAccounts,
]);
}
public function store(): Response
{
$this->authorize('accounting.bank_account.manage');
$data = $this->validate($_POST, [
'account_name_ar' => 'required',
'bank_name_ar' => 'required',
'account_number' => 'required',
]);
$isDefault = (int) ($_POST['is_default'] ?? 0);
if ($isDefault) {
$db = App::getInstance()->db();
$db->execute("UPDATE bank_accounts SET is_default = 0 WHERE is_default = 1");
}
$ba = BankAccount::create([
'account_name_ar' => $data['account_name_ar'],
'account_name_en' => $_POST['account_name_en'] ?? '',
'bank_name_ar' => $data['bank_name_ar'],
'bank_name_en' => $_POST['bank_name_en'] ?? null,
'account_number' => $data['account_number'],
'iban' => $_POST['iban'] ?? null,
'swift_code' => $_POST['swift_code'] ?? null,
'branch_name' => $_POST['branch_name'] ?? null,
'currency' => $_POST['currency'] ?? 'EGP',
'gl_account_id' => !empty($_POST['gl_account_id']) ? (int) $_POST['gl_account_id'] : null,
'opening_balance' => $_POST['opening_balance'] ?? '0.00',
'current_balance' => $_POST['opening_balance'] ?? '0.00',
'is_default' => $isDefault,
'is_active' => 1,
'notes' => $_POST['notes'] ?? null,
]);
// Link GL account
if (!empty($_POST['gl_account_id'])) {
$db = App::getInstance()->db();
$db->update('chart_of_accounts', [
'is_bank_account' => 1,
'bank_account_id' => $ba->id,
], '`id` = ?', [(int) $_POST['gl_account_id']]);
}
$session = App::getInstance()->session();
$session->flash('_alerts', [['type' => 'success', 'message' => 'تم إنشاء الحساب البنكي بنجاح']]);
return $this->redirect('/accounting/bank-accounts');
}
public function edit(int $id): Response
{
$this->authorize('accounting.bank_account.manage');
$account = BankAccount::findOrFail($id);
$glAccounts = Account::query()
->where('is_archived', '=', 0)
->where('account_type', '=', 'asset')
->where('is_header', '=', 0)
->orderBy('account_code', 'ASC')
->get();
return $this->view('Accounting/Views/bank_accounts/edit', [
'account' => $account->toArray(),
'gl_accounts' => $glAccounts,
]);
}
public function update(int $id): Response
{
$this->authorize('accounting.bank_account.manage');
$account = BankAccount::findOrFail($id);
$data = $this->validate($_POST, [
'account_name_ar' => 'required',
'bank_name_ar' => 'required',
]);
$account->update([
'account_name_ar' => $data['account_name_ar'],
'account_name_en' => $_POST['account_name_en'] ?? '',
'bank_name_ar' => $data['bank_name_ar'],
'bank_name_en' => $_POST['bank_name_en'] ?? null,
'iban' => $_POST['iban'] ?? null,
'swift_code' => $_POST['swift_code'] ?? null,
'branch_name' => $_POST['branch_name'] ?? null,
'is_active' => (int) ($_POST['is_active'] ?? 1),
'notes' => $_POST['notes'] ?? null,
]);
$session = App::getInstance()->session();
$session->flash('_alerts', [['type' => 'success', 'message' => 'تم تحديث الحساب البنكي بنجاح']]);
return $this->redirect('/accounting/bank-accounts');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\App;
use App\Core\Response;
use App\Modules\Accounting\Models\BankReconciliation;
use App\Modules\Accounting\Models\BankAccount;
use App\Modules\Accounting\Services\BankReconciliationService;
class BankReconciliationController extends Controller
{
public function index(): Response
{
$this->authorize('accounting.bank_recon.view');
$reconciliations = BankReconciliation::query()
->where('is_archived', '=', 0)
->orderBy('reconciliation_date', 'DESC')
->limit(50)
->get();
$db = App::getInstance()->db();
foreach ($reconciliations as &$r) {
$ba = $db->selectOne("SELECT account_name_ar FROM bank_accounts WHERE id = ?", [(int) $r['bank_account_id']]);
$r['bank_account_name'] = $ba['account_name_ar'] ?? '—';
}
unset($r);
return $this->view('Accounting/Views/bank_reconciliation/index', [
'reconciliations' => $reconciliations,
]);
}
public function create(): Response
{
$this->authorize('accounting.bank_recon.manage');
$bankAccounts = BankAccount::getActive();
return $this->view('Accounting/Views/bank_reconciliation/create', [
'bank_accounts' => $bankAccounts,
]);
}
public function store(): Response
{
$this->authorize('accounting.bank_recon.manage');
$this->validate($_POST, [
'bank_account_id' => 'required|numeric',
'statement_date' => 'required',
'statement_balance' => 'required|numeric',
]);
$result = BankReconciliationService::create($_POST);
$session = App::getInstance()->session();
if ($result['success']) {
$session->flash('_alerts', [['type' => 'success', 'message' => 'تم إنشاء المطابقة البنكية']]);
return $this->redirect('/accounting/bank-reconciliation/' . $result['reconciliation_id']);
}
$session->flash('_alerts', [['type' => 'error', 'message' => $result['error']]]);
return $this->redirect('/accounting/bank-reconciliation/create');
}
public function show(int $id): Response
{
$this->authorize('accounting.bank_recon.view');
$recon = BankReconciliation::findOrFail($id);
$items = $recon->items();
$bankAccount = BankAccount::find((int) $recon->bank_account_id);
$transactions = BankReconciliationService::getUnreconciledTransactions(
(int) $recon->bank_account_id,
$recon->statement_date
);
return $this->view('Accounting/Views/bank_reconciliation/show', [
'recon' => $recon->toArray(),
'items' => $items,
'bank_account' => $bankAccount ? $bankAccount->toArray() : [],
'transactions' => $transactions,
]);
}
public function addItem(int $id): Response
{
$this->authorize('accounting.bank_recon.manage');
$this->validate($_POST, [
'item_type' => 'required',
'description_ar' => 'required',
'amount' => 'required|numeric',
]);
$result = BankReconciliationService::addItem($id, $_POST);
$session = App::getInstance()->session();
if ($result['success']) {
$session->flash('_alerts', [['type' => 'success', 'message' => 'تم إضافة بند المطابقة']]);
} else {
$session->flash('_alerts', [['type' => 'error', 'message' => $result['error']]]);
}
return $this->redirect('/accounting/bank-reconciliation/' . $id);
}
public function complete(int $id): Response
{
$this->authorize('accounting.bank_recon.manage');
$result = BankReconciliationService::complete($id);
$session = App::getInstance()->session();
if ($result['success']) {
$session->flash('_alerts', [['type' => 'success', 'message' => 'تم إكمال المطابقة البنكية بنجاح']]);
} else {
$session->flash('_alerts', [['type' => 'error', 'message' => $result['error']]]);
}
return $this->redirect('/accounting/bank-reconciliation/' . $id);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\App;
use App\Core\Response;
use App\Modules\Accounting\Models\Account;
use App\Modules\Accounting\Models\CostCenter;
class ChartOfAccountsController extends Controller
{
public function index(): Response
{
$this->authorize('accounting.coa.view');
$tree = Account::getTree();
return $this->view('Accounting/Views/chart_of_accounts/index', [
'tree' => $tree,
]);
}
public function create(): Response
{
$this->authorize('accounting.coa.manage');
$parentAccounts = Account::query()
->where('is_archived', '=', 0)
->where('is_header', '=', 1)
->orderBy('account_code', 'ASC')
->get();
$costCenters = CostCenter::getActive();
return $this->view('Accounting/Views/chart_of_accounts/create', [
'parents' => $parentAccounts,
'cost_centers' => $costCenters,
]);
}
public function store(): Response
{
$this->authorize('accounting.coa.manage');
$data = $this->validate($_POST, [
'account_code' => 'required',
'name_ar' => 'required',
'name_en' => 'required',
'account_type' => 'required|in:asset,liability,equity,revenue,expense',
'account_nature' => 'required|in:debit,credit',
]);
// Check unique code
if (Account::findByCode($data['account_code'])) {
$session = App::getInstance()->session();
$session->flash('_alerts', [['type' => 'error', 'message' => 'كود الحساب مستخدم بالفعل']]);
return $this->redirect('/accounting/chart-of-accounts/create');
}
$parentId = !empty($_POST['parent_id']) ? (int) $_POST['parent_id'] : null;
$level = 1;
if ($parentId) {
$parent = Account::find($parentId);
if ($parent) {
$level = (int) $parent->level + 1;
}
}
Account::create([
'account_code' => $data['account_code'],
'name_ar' => $data['name_ar'],
'name_en' => $data['name_en'],
'account_type' => $data['account_type'],
'account_nature' => $data['account_nature'],
'parent_id' => $parentId,
'level' => $level,
'is_header' => (int) ($_POST['is_header'] ?? 0),
'is_active' => 1,
'description_ar' => $_POST['description_ar'] ?? null,
'description_en' => $_POST['description_en'] ?? null,
'opening_balance' => $_POST['opening_balance'] ?? '0.00',
'cost_center_id' => !empty($_POST['cost_center_id']) ? (int) $_POST['cost_center_id'] : null,
]);
$session = App::getInstance()->session();
$session->flash('_alerts', [['type' => 'success', 'message' => 'تم إنشاء الحساب بنجاح']]);
return $this->redirect('/accounting/chart-of-accounts');
}
public function edit(int $id): Response
{
$this->authorize('accounting.coa.manage');
$account = Account::findOrFail($id);
$parentAccounts = Account::query()
->where('is_archived', '=', 0)
->where('is_header', '=', 1)
->where('id', '!=', $id)
->orderBy('account_code', 'ASC')
->get();
$costCenters = CostCenter::getActive();
return $this->view('Accounting/Views/chart_of_accounts/edit', [
'account' => $account->toArray(),
'parents' => $parentAccounts,
'cost_centers' => $costCenters,
]);
}
public function update(int $id): Response
{
$this->authorize('accounting.coa.manage');
$account = Account::findOrFail($id);
$data = $this->validate($_POST, [
'name_ar' => 'required',
'name_en' => 'required',
'account_type' => 'required|in:asset,liability,equity,revenue,expense',
'account_nature' => 'required|in:debit,credit',
]);
// Cannot change code of system accounts
if ((int) $account->is_system === 1 && ($_POST['account_code'] ?? '') !== $account->account_code) {
$session = App::getInstance()->session();
$session->flash('_alerts', [['type' => 'error', 'message' => 'لا يمكن تعديل كود حساب نظامي']]);
return $this->redirect('/accounting/chart-of-accounts/' . $id . '/edit');
}
$account->update([
'name_ar' => $data['name_ar'],
'name_en' => $data['name_en'],
'account_type' => $data['account_type'],
'account_nature' => $data['account_nature'],
'is_header' => (int) ($_POST['is_header'] ?? 0),
'is_active' => (int) ($_POST['is_active'] ?? 1),
'description_ar' => $_POST['description_ar'] ?? null,
'description_en' => $_POST['description_en'] ?? null,
'cost_center_id' => !empty($_POST['cost_center_id']) ? (int) $_POST['cost_center_id'] : null,
]);
$session = App::getInstance()->session();
$session->flash('_alerts', [['type' => 'success', 'message' => 'تم تحديث الحساب بنجاح']]);
return $this->redirect('/accounting/chart-of-accounts');
}
public function search(): Response
{
$this->authorize('accounting.coa.view');
$term = $_GET['q'] ?? '';
$results = Account::search($term);
return $this->json($results);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\App;
use App\Core\Response;
use App\Modules\Accounting\Models\CostCenter;
class CostCenterController extends Controller
{
public function index(): Response
{
$this->authorize('accounting.cost_center.view');
$centers = CostCenter::query()
->where('is_archived', '=', 0)
->orderBy('code', 'ASC')
->get();
// Fetch branch names
$db = App::getInstance()->db();
$branches = $db->select("SELECT id, name_ar FROM branches WHERE is_archived = 0 ORDER BY name_ar");
return $this->view('Accounting/Views/cost_centers/index', [
'centers' => $centers,
'branches' => $branches,
]);
}
public function create(): Response
{
$this->authorize('accounting.cost_center.manage');
$db = App::getInstance()->db();
$branches = $db->select("SELECT id, name_ar, name_en FROM branches WHERE is_archived = 0 ORDER BY name_ar");
$parents = CostCenter::query()
->where('is_archived', '=', 0)
->orderBy('code', 'ASC')
->get();
return $this->view('Accounting/Views/cost_centers/create', [
'branches' => $branches,
'parents' => $parents,
]);
}
public function store(): Response
{
$this->authorize('accounting.cost_center.manage');
$data = $this->validate($_POST, [
'code' => 'required',
'name_ar' => 'required',
'name_en' => 'required',
'type' => 'required|in:cost_center,profit_center',
]);
if (CostCenter::findByCode($data['code'])) {
$session = App::getInstance()->session();
$session->flash('_alerts', [['type' => 'error', 'message' => 'كود مركز التكلفة مستخدم بالفعل']]);
return $this->redirect('/accounting/cost-centers/create');
}
CostCenter::create([
'code' => $data['code'],
'name_ar' => $data['name_ar'],
'name_en' => $data['name_en'],
'type' => $data['type'],
'branch_id' => !empty($_POST['branch_id']) ? (int) $_POST['branch_id'] : null,
'parent_id' => !empty($_POST['parent_id']) ? (int) $_POST['parent_id'] : null,
'is_active' => 1,
'description' => $_POST['description'] ?? null,
]);
$session = App::getInstance()->session();
$session->flash('_alerts', [['type' => 'success', 'message' => 'تم إنشاء مركز التكلفة بنجاح']]);
return $this->redirect('/accounting/cost-centers');
}
public function edit(int $id): Response
{
$this->authorize('accounting.cost_center.manage');
$center = CostCenter::findOrFail($id);
$db = App::getInstance()->db();
$branches = $db->select("SELECT id, name_ar, name_en FROM branches WHERE is_archived = 0 ORDER BY name_ar");
$parents = CostCenter::query()
->where('is_archived', '=', 0)
->where('id', '!=', $id)
->orderBy('code', 'ASC')
->get();
return $this->view('Accounting/Views/cost_centers/edit', [
'center' => $center->toArray(),
'branches' => $branches,
'parents' => $parents,
]);
}
public function update(int $id): Response
{
$this->authorize('accounting.cost_center.manage');
$center = CostCenter::findOrFail($id);
$data = $this->validate($_POST, [
'name_ar' => 'required',
'name_en' => 'required',
'type' => 'required|in:cost_center,profit_center',
]);
$center->update([
'name_ar' => $data['name_ar'],
'name_en' => $data['name_en'],
'type' => $data['type'],
'branch_id' => !empty($_POST['branch_id']) ? (int) $_POST['branch_id'] : null,
'parent_id' => !empty($_POST['parent_id']) ? (int) $_POST['parent_id'] : null,
'is_active' => (int) ($_POST['is_active'] ?? 1),
'description' => $_POST['description'] ?? null,
]);
$session = App::getInstance()->session();
$session->flash('_alerts', [['type' => 'success', 'message' => 'تم تحديث مركز التكلفة بنجاح']]);
return $this->redirect('/accounting/cost-centers');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\App;
use App\Core\Response;
use App\Modules\Accounting\Models\FiscalYear;
use App\Modules\Accounting\Services\PeriodClosingService;
class FiscalYearController extends Controller
{
public function index(): Response
{
$this->authorize('accounting.fiscal_year.view');
$years = FiscalYear::query()
->where('is_archived', '=', 0)
->orderBy('start_date', 'DESC')
->get();
return $this->view('Accounting/Views/fiscal_years/index', [
'years' => $years,
]);
}
public function create(): Response
{
$this->authorize('accounting.fiscal_year.manage');
$config = App::getInstance()->config('app');
$startMonth = (int) ($config['financial_year_start_month'] ?? 7);
$currentYear = (int) date('Y');
return $this->view('Accounting/Views/fiscal_years/create', [
'start_month' => $startMonth,
'current_year' => $currentYear,
]);
}
public function store(): Response
{
$this->authorize('accounting.fiscal_year.manage');
$data = $this->validate($_POST, [
'name_ar' => 'required',
'name_en' => 'required',
'start_date' => 'required',
'end_date' => 'required',
]);
// Validate dates
if (strtotime($data['end_date']) <= strtotime($data['start_date'])) {
$session = App::getInstance()->session();
$session->flash('_alerts', [['type' => 'error', 'message' => 'تاريخ النهاية يجب أن يكون بعد تاريخ البداية']]);
return $this->redirect('/accounting/fiscal-years/create');
}
// Check overlap
$db = App::getInstance()->db();
$overlap = $db->selectOne(
"SELECT id FROM fiscal_years
WHERE is_archived = 0
AND ((start_date <= ? AND end_date >= ?) OR (start_date <= ? AND end_date >= ?))",
[$data['end_date'], $data['start_date'], $data['start_date'], $data['end_date']]
);
if ($overlap) {
$session = App::getInstance()->session();
$session->flash('_alerts', [['type' => 'error', 'message' => 'يوجد تداخل مع سنة مالية أخرى']]);
return $this->redirect('/accounting/fiscal-years/create');
}
$isCurrent = (int) ($_POST['is_current'] ?? 0);
if ($isCurrent) {
$db->execute("UPDATE fiscal_years SET is_current = 0 WHERE is_current = 1");
}
$fy = FiscalYear::create([
'name_ar' => $data['name_ar'],
'name_en' => $data['name_en'],
'start_date' => $data['start_date'],
'end_date' => $data['end_date'],
'is_current' => $isCurrent,
'status' => 'open',
'notes' => $_POST['notes'] ?? null,
]);
$session = App::getInstance()->session();
$session->flash('_alerts', [['type' => 'success', 'message' => 'تم إنشاء السنة المالية بنجاح']]);
return $this->redirect('/accounting/fiscal-years');
}
public function show(int $id): Response
{
$this->authorize('accounting.fiscal_year.view');
$fy = FiscalYear::findOrFail($id);
$periods = PeriodClosingService::getPeriodSummary($id);
return $this->view('Accounting/Views/fiscal_years/show', [
'fiscal_year' => $fy->toArray(),
'periods' => $periods,
]);
}
public function close(int $id): Response
{
$this->authorize('accounting.fiscal_year.close');
$result = PeriodClosingService::closeFiscalYear($id);
$session = App::getInstance()->session();
if ($result['success']) {
$session->flash('_alerts', [['type' => 'success', 'message' => 'تم إقفال السنة المالية — صافي الربح: ' . $result['net_income'] . ' ج.م']]);
} else {
$session->flash('_alerts', [['type' => 'error', 'message' => $result['error']]]);
}
return $this->redirect('/accounting/fiscal-years/' . $id);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\App;
use App\Core\Response;
use App\Modules\Accounting\Models\JournalEntry;
use App\Modules\Accounting\Models\JournalEntryLine;
use App\Modules\Accounting\Models\Account;
use App\Modules\Accounting\Models\FiscalYear;
use App\Modules\Accounting\Models\CostCenter;
use App\Modules\Accounting\Services\JournalService;
class JournalEntryController extends Controller
{
public function index(): Response
{
$this->authorize('accounting.journal.view');
$page = max(1, (int) ($_GET['page'] ?? 1));
$filters = [
'status' => $_GET['status'] ?? null,
'date_from' => $_GET['date_from'] ?? null,
'date_to' => $_GET['date_to'] ?? null,
'source_module' => $_GET['source_module'] ?? null,
'fiscal_year_id' => $_GET['fiscal_year_id'] ?? null,
'search' => $_GET['search'] ?? null,
];
$result = JournalEntry::search($filters, $page);
$fiscalYears = FiscalYear::query()
->where('is_archived', '=', 0)
->orderBy('start_date', 'DESC')
->get();
return $this->view('Accounting/Views/journal_entries/index', [
'entries' => $result['data'],
'total' => $result['total'],
'page' => $result['page'],
'pages' => $result['pages'],
'filters' => $filters,
'fiscal_years' => $fiscalYears,
]);
}
public function create(): Response
{
$this->authorize('accounting.journal.create');
$accounts = Account::getPostableAccounts();
$costCenters = CostCenter::getActive();
$currentFY = FiscalYear::current();
$db = App::getInstance()->db();
$branches = $db->select("SELECT id, name_ar FROM branches WHERE is_archived = 0 ORDER BY name_ar");
return $this->view('Accounting/Views/journal_entries/create', [
'accounts' => $accounts,
'cost_centers' => $costCenters,
'fiscal_year' => $currentFY ? $currentFY->toArray() : null,
'branches' => $branches,
]);
}
public function store(): Response
{
$this->authorize('accounting.journal.create');
$data = $this->validate($_POST, [
'entry_date' => 'required',
'description_ar' => 'required',
]);
// Parse lines from POST
$lines = [];
$lineAccounts = $_POST['line_account_id'] ?? [];
$lineDebits = $_POST['line_debit'] ?? [];
$lineCredits = $_POST['line_credit'] ?? [];
$lineDescriptions = $_POST['line_description'] ?? [];
for ($i = 0; $i < count($lineAccounts); $i++) {
if (empty($lineAccounts[$i])) {
continue;
}
$lines[] = [
'account_id' => (int) $lineAccounts[$i],
'debit' => $lineDebits[$i] ?? '0.00',
'credit' => $lineCredits[$i] ?? '0.00',
'description_ar' => $lineDescriptions[$i] ?? null,
'cost_center_id' => !empty($_POST['line_cost_center_id'][$i]) ? (int) $_POST['line_cost_center_id'][$i] : null,
'branch_id' => !empty($_POST['line_branch_id'][$i]) ? (int) $_POST['line_branch_id'][$i] : null,
];
}
$result = JournalService::createEntry([
'entry_date' => $data['entry_date'],
'description_ar' => $data['description_ar'],
'description_en' => $_POST['description_en'] ?? null,
'reference_type' => 'manual',
'reference_number' => $_POST['reference_number'] ?? null,
'branch_id' => !empty($_POST['branch_id']) ? (int) $_POST['branch_id'] : null,
'cost_center_id' => !empty($_POST['cost_center_id']) ? (int) $_POST['cost_center_id'] : null,
'notes' => $_POST['notes'] ?? null,
], $lines, false);
$session = App::getInstance()->session();
if ($result['success']) {
$session->flash('_alerts', [['type' => 'success', 'message' => 'تم إنشاء القيد ' . $result['entry_number'] . ' بنجاح']]);
return $this->redirect('/accounting/journal-entries/' . $result['journal_entry_id']);
}
$session->flash('_alerts', [['type' => 'error', 'message' => $result['error']]]);
return $this->redirect('/accounting/journal-entries/create');
}
public function show(int $id): Response
{
$this->authorize('accounting.journal.view');
$entry = JournalEntry::findOrFail($id);
$lines = JournalEntryLine::getByJournalEntry($id);
// Get employee names
$db = App::getInstance()->db();
$createdBy = null;
if ($entry->created_by) {
$createdBy = $db->selectOne("SELECT full_name_ar FROM employees WHERE id = ?", [(int) $entry->created_by]);
}
$postedBy = null;
if ($entry->posted_by) {
$postedBy = $db->selectOne("SELECT full_name_ar FROM employees WHERE id = ?", [(int) $entry->posted_by]);
}
return $this->view('Accounting/Views/journal_entries/show', [
'entry' => $entry->toArray(),
'lines' => $lines,
'created_by' => $createdBy['full_name_ar'] ?? '—',
'posted_by' => $postedBy['full_name_ar'] ?? '—',
]);
}
public function post(int $id): Response
{
$this->authorize('accounting.journal.post');
$result = JournalService::postEntry($id);
$session = App::getInstance()->session();
if ($result['success']) {
$session->flash('_alerts', [['type' => 'success', 'message' => 'تم ترحيل القيد بنجاح']]);
} else {
$session->flash('_alerts', [['type' => 'error', 'message' => $result['error']]]);
}
return $this->redirect('/accounting/journal-entries/' . $id);
}
public function reverse(int $id): Response
{
$this->authorize('accounting.journal.reverse');
$reason = $_POST['reason'] ?? 'عكس قيد';
$result = JournalService::reverseEntry($id, $reason);
$session = App::getInstance()->session();
if ($result['success']) {
$session->flash('_alerts', [['type' => 'success', 'message' => 'تم عكس القيد — القيد العكسي: ' . $result['reversal_number']]]);
} else {
$session->flash('_alerts', [['type' => 'error', 'message' => $result['error']]]);
}
return $this->redirect('/accounting/journal-entries/' . $id);
}
public function searchAccounts(): Response
{
$this->authorize('accounting.journal.view');
$term = $_GET['q'] ?? '';
$results = Account::search($term);
return $this->json($results);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\App;
use App\Core\Response;
use App\Modules\Accounting\Models\FiscalYear;
use App\Modules\Accounting\Services\PeriodClosingService;
class PeriodClosingController extends Controller
{
public function index(): Response
{
$this->authorize('accounting.period.view');
$currentFY = FiscalYear::current();
$fiscalYearId = (int) ($_GET['fiscal_year_id'] ?? ($currentFY ? $currentFY->id : 0));
$periods = [];
$fiscalYear = null;
if ($fiscalYearId > 0) {
$fiscalYear = FiscalYear::find($fiscalYearId);
$periods = PeriodClosingService::getPeriodSummary($fiscalYearId);
}
$fiscalYears = FiscalYear::query()
->where('is_archived', '=', 0)
->orderBy('start_date', 'DESC')
->get();
return $this->view('Accounting/Views/period_closing/index', [
'periods' => $periods,
'fiscal_year' => $fiscalYear ? $fiscalYear->toArray() : null,
'fiscal_years' => $fiscalYears,
'fiscal_year_id' => $fiscalYearId,
]);
}
public function closeMonth(): Response
{
$this->authorize('accounting.period.close');
$fiscalYearId = (int) ($_POST['fiscal_year_id'] ?? 0);
$period = $_POST['period'] ?? '';
if ($fiscalYearId <= 0 || empty($period)) {
$session = App::getInstance()->session();
$session->flash('_alerts', [['type' => 'error', 'message' => 'بيانات غير مكتملة']]);
return $this->redirect('/accounting/period-closing');
}
$result = PeriodClosingService::closeMonth($fiscalYearId, $period);
$session = App::getInstance()->session();
if ($result['success']) {
$session->flash('_alerts', [['type' => 'success', 'message' => 'تم إغلاق الفترة ' . $period . ' بنجاح']]);
} else {
$session->flash('_alerts', [['type' => 'error', 'message' => $result['error']]]);
}
return $this->redirect('/accounting/period-closing?fiscal_year_id=' . $fiscalYearId);
}
public function reopenMonth(): Response
{
$this->authorize('accounting.period.reopen');
$fiscalYearId = (int) ($_POST['fiscal_year_id'] ?? 0);
$period = $_POST['period'] ?? '';
$reason = $_POST['reason'] ?? '';
if ($fiscalYearId <= 0 || empty($period) || empty($reason)) {
$session = App::getInstance()->session();
$session->flash('_alerts', [['type' => 'error', 'message' => 'يجب تقديم سبب إعادة الفتح']]);
return $this->redirect('/accounting/period-closing');
}
$result = PeriodClosingService::reopenMonth($fiscalYearId, $period, $reason);
$session = App::getInstance()->session();
if ($result['success']) {
$session->flash('_alerts', [['type' => 'success', 'message' => 'تم إعادة فتح الفترة ' . $period]]);
} else {
$session->flash('_alerts', [['type' => 'error', 'message' => $result['error']]]);
}
return $this->redirect('/accounting/period-closing?fiscal_year_id=' . $fiscalYearId);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\App;
use App\Core\Response;
use App\Modules\Accounting\Models\FiscalYear;
use App\Modules\Accounting\Models\CostCenter;
use App\Modules\Accounting\Models\AccountPayable;
use App\Modules\Accounting\Models\AccountReceivable;
use App\Modules\Accounting\Services\LedgerService;
use App\Modules\Accounting\Services\FinancialReportService;
class ReportController extends Controller
{
public function dashboard(): Response
{
$this->authorize('accounting.reports.view');
$db = App::getInstance()->db();
$currentFY = FiscalYear::current();
// Quick stats
$today = date('Y-m-d');
$monthStart = date('Y-m-01');
$monthlyTotals = $db->selectOne(
"SELECT COALESCE(SUM(total_debit), 0) as total_debit, COUNT(*) as count
FROM journal_entries
WHERE status = 'posted' AND entry_date >= ? AND entry_date <= ? AND is_archived = 0",
[$monthStart, $today]
);
$arSummary = AccountReceivable::getAgingSummary();
$apSummary = AccountPayable::getAgingSummary();
$draftCount = $db->selectOne(
"SELECT COUNT(*) as cnt FROM journal_entries WHERE status = 'draft' AND is_archived = 0"
);
return $this->view('Accounting/Views/dashboard/index', [
'fiscal_year' => $currentFY ? $currentFY->toArray() : null,
'monthly_totals' => $monthlyTotals,
'ar_summary' => $arSummary,
'ap_summary' => $apSummary,
'draft_count' => (int) ($draftCount['cnt'] ?? 0),
]);
}
public function trialBalance(): Response
{
$this->authorize('accounting.reports.trial_balance');
$currentFY = FiscalYear::current();
$dateFrom = $_GET['date_from'] ?? ($currentFY ? $currentFY->start_date : date('Y-01-01'));
$dateTo = $_GET['date_to'] ?? date('Y-m-d');
$costCenterId = !empty($_GET['cost_center_id']) ? (int) $_GET['cost_center_id'] : null;
$branchId = !empty($_GET['branch_id']) ? (int) $_GET['branch_id'] : null;
$result = LedgerService::getTrialBalance($dateFrom, $dateTo, null, $costCenterId, $branchId);
$costCenters = CostCenter::getActive();
$db = App::getInstance()->db();
$branches = $db->select("SELECT id, name_ar FROM branches WHERE is_archived = 0 ORDER BY name_ar");
return $this->view('Accounting/Views/reports/trial_balance', [
'result' => $result,
'date_from' => $dateFrom,
'date_to' => $dateTo,
'cost_centers' => $costCenters,
'branches' => $branches,
'filters' => [
'cost_center_id' => $costCenterId,
'branch_id' => $branchId,
],
]);
}
public function generalLedger(): Response
{
$this->authorize('accounting.reports.general_ledger');
$accountId = (int) ($_GET['account_id'] ?? 0);
$currentFY = FiscalYear::current();
$dateFrom = $_GET['date_from'] ?? ($currentFY ? $currentFY->start_date : date('Y-01-01'));
$dateTo = $_GET['date_to'] ?? date('Y-m-d');
$costCenterId = !empty($_GET['cost_center_id']) ? (int) $_GET['cost_center_id'] : null;
$branchId = !empty($_GET['branch_id']) ? (int) $_GET['branch_id'] : null;
$ledger = null;
if ($accountId > 0) {
$ledger = LedgerService::getAccountLedger($accountId, $dateFrom, $dateTo, $costCenterId, $branchId);
}
$costCenters = CostCenter::getActive();
$db = App::getInstance()->db();
$branches = $db->select("SELECT id, name_ar FROM branches WHERE is_archived = 0 ORDER BY name_ar");
return $this->view('Accounting/Views/reports/general_ledger', [
'ledger' => $ledger,
'account_id' => $accountId,
'date_from' => $dateFrom,
'date_to' => $dateTo,
'cost_centers' => $costCenters,
'branches' => $branches,
'filters' => [
'cost_center_id' => $costCenterId,
'branch_id' => $branchId,
],
]);
}
public function incomeStatement(): Response
{
$this->authorize('accounting.reports.income_statement');
$currentFY = FiscalYear::current();
$dateFrom = $_GET['date_from'] ?? ($currentFY ? $currentFY->start_date : date('Y-01-01'));
$dateTo = $_GET['date_to'] ?? date('Y-m-d');
$costCenterId = !empty($_GET['cost_center_id']) ? (int) $_GET['cost_center_id'] : null;
$branchId = !empty($_GET['branch_id']) ? (int) $_GET['branch_id'] : null;
$result = FinancialReportService::getIncomeStatement($dateFrom, $dateTo, $costCenterId, $branchId);
$costCenters = CostCenter::getActive();
$db = App::getInstance()->db();
$branches = $db->select("SELECT id, name_ar FROM branches WHERE is_archived = 0 ORDER BY name_ar");
return $this->view('Accounting/Views/reports/income_statement', [
'result' => $result,
'date_from' => $dateFrom,
'date_to' => $dateTo,
'cost_centers' => $costCenters,
'branches' => $branches,
'filters' => [
'cost_center_id' => $costCenterId,
'branch_id' => $branchId,
],
]);
}
public function balanceSheet(): Response
{
$this->authorize('accounting.reports.balance_sheet');
$asOfDate = $_GET['as_of_date'] ?? date('Y-m-d');
$costCenterId = !empty($_GET['cost_center_id']) ? (int) $_GET['cost_center_id'] : null;
$branchId = !empty($_GET['branch_id']) ? (int) $_GET['branch_id'] : null;
$result = FinancialReportService::getBalanceSheet($asOfDate, $costCenterId, $branchId);
$costCenters = CostCenter::getActive();
$db = App::getInstance()->db();
$branches = $db->select("SELECT id, name_ar FROM branches WHERE is_archived = 0 ORDER BY name_ar");
return $this->view('Accounting/Views/reports/balance_sheet', [
'result' => $result,
'as_of_date' => $asOfDate,
'cost_centers' => $costCenters,
'branches' => $branches,
'filters' => [
'cost_center_id' => $costCenterId,
'branch_id' => $branchId,
],
]);
}
public function consolidatedBalanceSheet(): Response
{
$this->authorize('accounting.reports.consolidated');
$asOfDate = $_GET['as_of_date'] ?? date('Y-m-d');
$result = FinancialReportService::getConsolidatedBalanceSheet($asOfDate);
return $this->view('Accounting/Views/reports/consolidated_balance_sheet', [
'result' => $result,
'as_of_date' => $asOfDate,
]);
}
public function accountsReceivable(): Response
{
$this->authorize('accounting.reports.ar');
$memberId = !empty($_GET['member_id']) ? (int) $_GET['member_id'] : null;
$outstanding = AccountReceivable::getOutstanding($memberId);
$aging = AccountReceivable::getAgingSummary();
return $this->view('Accounting/Views/reports/accounts_receivable', [
'outstanding' => $outstanding,
'aging' => $aging,
'member_id' => $memberId,
]);
}
public function accountsPayable(): Response
{
$this->authorize('accounting.reports.ap');
$supplierId = !empty($_GET['supplier_id']) ? (int) $_GET['supplier_id'] : null;
$outstanding = AccountPayable::getOutstanding($supplierId);
$aging = AccountPayable::getAgingSummary();
return $this->view('Accounting/Views/reports/accounts_payable', [
'outstanding' => $outstanding,
'aging' => $aging,
'supplier_id' => $supplierId,
]);
}
public function memberStatement(): Response
{
$this->authorize('accounting.reports.member_statement');
$memberId = (int) ($_GET['member_id'] ?? 0);
$dateFrom = $_GET['date_from'] ?? date('Y-01-01');
$dateTo = $_GET['date_to'] ?? date('Y-m-d');
$entries = [];
$member = null;
if ($memberId > 0) {
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT id, full_name_ar, form_number FROM members WHERE id = ?", [$memberId]);
$entries = LedgerService::getMemberStatement($memberId, $dateFrom, $dateTo);
}
return $this->view('Accounting/Views/reports/member_statement', [
'member' => $member,
'entries' => $entries,
'member_id' => $memberId,
'date_from' => $dateFrom,
'date_to' => $dateTo,
]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Models;
use App\Core\Model;
use App\Core\App;
class Account extends Model
{
protected static string $table = 'chart_of_accounts';
protected static array $fillable = [
'account_code', 'name_ar', 'name_en', 'account_type', 'account_nature',
'parent_id', 'level', 'is_header', 'is_active', 'is_system',
'description_ar', 'description_en', 'opening_balance', 'current_balance',
'currency', 'cost_center_id', 'is_bank_account', 'bank_account_id',
];
protected static bool $timestamps = true;
protected static bool $softDelete = true;
public static function findByCode(string $code): ?static
{
$row = static::query()->where('account_code', '=', $code)->where('is_archived', '=', 0)->first();
if ($row === null) {
return null;
}
$instance = new static($row);
$instance->exists = true;
return $instance;
}
public static function getByType(string $type): array
{
return static::query()
->where('account_type', '=', $type)
->where('is_active', '=', 1)
->where('is_archived', '=', 0)
->orderBy('account_code', 'ASC')
->get();
}
public static function getPostableAccounts(): array
{
return static::query()
->where('is_header', '=', 0)
->where('is_active', '=', 1)
->where('is_archived', '=', 0)
->orderBy('account_code', 'ASC')
->get();
}
public function children(): array
{
return static::query()
->where('parent_id', '=', $this->id)
->where('is_archived', '=', 0)
->orderBy('account_code', 'ASC')
->get();
}
public function parent(): ?array
{
if ($this->parent_id === null) {
return null;
}
return static::query()->where('id', '=', $this->parent_id)->first();
}
public static function getTree(): array
{
$all = static::query()
->where('is_archived', '=', 0)
->orderBy('account_code', 'ASC')
->get();
return self::buildTree($all, null);
}
private static function buildTree(array $accounts, ?int $parentId): array
{
$tree = [];
foreach ($accounts as $account) {
$pid = $account['parent_id'] !== null ? (int) $account['parent_id'] : null;
if ($pid === $parentId) {
$account['children'] = self::buildTree($accounts, (int) $account['id']);
$tree[] = $account;
}
}
return $tree;
}
public static function search(string $term): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM chart_of_accounts
WHERE is_archived = 0
AND (account_code LIKE ? OR name_ar LIKE ? OR name_en LIKE ?)
ORDER BY account_code ASC
LIMIT 50",
["%{$term}%", "%{$term}%", "%{$term}%"]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Models;
use App\Core\Model;
use App\Core\App;
class AccountBalance extends Model
{
protected static string $table = 'account_balances';
protected static array $fillable = [
'account_id', 'fiscal_year_id', 'period',
'opening_debit', 'opening_credit', 'period_debit', 'period_credit',
'closing_debit', 'closing_credit', 'cost_center_id', 'branch_id',
'is_closed',
];
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
protected static bool $dispatchEvents = false;
public static function getForPeriod(int $fiscalYearId, string $period, ?int $costCenterId = null, ?int $branchId = null): array
{
$db = App::getInstance()->db();
$where = 'ab.fiscal_year_id = ? AND ab.period = ?';
$params = [$fiscalYearId, $period];
if ($costCenterId !== null) {
$where .= ' AND ab.cost_center_id = ?';
$params[] = $costCenterId;
}
if ($branchId !== null) {
$where .= ' AND ab.branch_id = ?';
$params[] = $branchId;
}
return $db->select(
"SELECT ab.*, coa.account_code, coa.name_ar, coa.name_en,
coa.account_type, coa.account_nature, coa.level, coa.is_header
FROM account_balances ab
JOIN chart_of_accounts coa ON coa.id = ab.account_id
WHERE {$where}
ORDER BY coa.account_code ASC",
$params
);
}
public static function upsert(array $data): void
{
$db = App::getInstance()->db();
$db->execute(
"INSERT INTO account_balances
(account_id, fiscal_year_id, period, opening_debit, opening_credit,
period_debit, period_credit, closing_debit, closing_credit,
cost_center_id, branch_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
ON DUPLICATE KEY UPDATE
period_debit = VALUES(period_debit),
period_credit = VALUES(period_credit),
closing_debit = VALUES(closing_debit),
closing_credit = VALUES(closing_credit),
updated_at = NOW()",
[
$data['account_id'], $data['fiscal_year_id'], $data['period'],
$data['opening_debit'] ?? '0.00', $data['opening_credit'] ?? '0.00',
$data['period_debit'] ?? '0.00', $data['period_credit'] ?? '0.00',
$data['closing_debit'] ?? '0.00', $data['closing_credit'] ?? '0.00',
$data['cost_center_id'] ?? null, $data['branch_id'] ?? null,
]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Models;
use App\Core\Model;
use App\Core\App;
class AccountPayable extends Model
{
protected static string $table = 'accounts_payable';
protected static array $fillable = [
'supplier_id', 'invoice_number', 'invoice_date', 'due_date',
'description_ar', 'description_en', 'total_amount', 'paid_amount',
'balance', 'currency', 'status', 'journal_entry_id', 'payment_entry_id',
'purchase_order_id', 'cost_center_id', 'branch_id', 'notes',
];
protected static bool $timestamps = true;
protected static bool $softDelete = true;
public static function getOutstanding(?int $supplierId = null): array
{
$db = App::getInstance()->db();
$where = "ap.is_archived = 0 AND ap.status IN ('pending','partial','overdue')";
$params = [];
if ($supplierId !== null) {
$where .= ' AND ap.supplier_id = ?';
$params[] = $supplierId;
}
return $db->select(
"SELECT ap.*, s.name as supplier_name
FROM accounts_payable ap
LEFT JOIN suppliers s ON s.id = ap.supplier_id
WHERE {$where}
ORDER BY ap.due_date ASC",
$params
);
}
public static function getAgingSummary(): array
{
$db = App::getInstance()->db();
$today = date('Y-m-d');
return $db->selectOne(
"SELECT
SUM(CASE WHEN due_date >= ? THEN balance ELSE 0 END) as current_amount,
SUM(CASE WHEN due_date < ? AND due_date >= DATE_SUB(?, INTERVAL 30 DAY) THEN balance ELSE 0 END) as days_30,
SUM(CASE WHEN due_date < DATE_SUB(?, INTERVAL 30 DAY) AND due_date >= DATE_SUB(?, INTERVAL 60 DAY) THEN balance ELSE 0 END) as days_60,
SUM(CASE WHEN due_date < DATE_SUB(?, INTERVAL 60 DAY) AND due_date >= DATE_SUB(?, INTERVAL 90 DAY) THEN balance ELSE 0 END) as days_90,
SUM(CASE WHEN due_date < DATE_SUB(?, INTERVAL 90 DAY) THEN balance ELSE 0 END) as over_90,
SUM(balance) as total
FROM accounts_payable
WHERE is_archived = 0 AND status IN ('pending','partial','overdue')",
[$today, $today, $today, $today, $today, $today, $today, $today]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Models;
use App\Core\Model;
use App\Core\App;
class AccountReceivable extends Model
{
protected static string $table = 'accounts_receivable';
protected static array $fillable = [
'member_id', 'document_type', 'document_id', 'document_number',
'document_date', 'due_date', 'description_ar', 'description_en',
'total_amount', 'paid_amount', 'balance', 'currency', 'status',
'journal_entry_id', 'payment_entry_id', 'cost_center_id', 'branch_id', 'notes',
];
protected static bool $timestamps = true;
protected static bool $softDelete = true;
public static function getOutstanding(?int $memberId = null): array
{
$db = App::getInstance()->db();
$where = "ar.is_archived = 0 AND ar.status IN ('pending','partial','overdue')";
$params = [];
if ($memberId !== null) {
$where .= ' AND ar.member_id = ?';
$params[] = $memberId;
}
return $db->select(
"SELECT ar.*, m.full_name_ar as member_name, m.form_number
FROM accounts_receivable ar
LEFT JOIN members m ON m.id = ar.member_id
WHERE {$where}
ORDER BY ar.due_date ASC",
$params
);
}
public static function getAgingSummary(): array
{
$db = App::getInstance()->db();
$today = date('Y-m-d');
return $db->selectOne(
"SELECT
SUM(CASE WHEN due_date >= ? THEN balance ELSE 0 END) as current_amount,
SUM(CASE WHEN due_date < ? AND due_date >= DATE_SUB(?, INTERVAL 30 DAY) THEN balance ELSE 0 END) as days_30,
SUM(CASE WHEN due_date < DATE_SUB(?, INTERVAL 30 DAY) AND due_date >= DATE_SUB(?, INTERVAL 60 DAY) THEN balance ELSE 0 END) as days_60,
SUM(CASE WHEN due_date < DATE_SUB(?, INTERVAL 60 DAY) AND due_date >= DATE_SUB(?, INTERVAL 90 DAY) THEN balance ELSE 0 END) as days_90,
SUM(CASE WHEN due_date < DATE_SUB(?, INTERVAL 90 DAY) THEN balance ELSE 0 END) as over_90,
SUM(balance) as total
FROM accounts_receivable
WHERE is_archived = 0 AND status IN ('pending','partial','overdue')",
[$today, $today, $today, $today, $today, $today, $today, $today]
);
}
public static function findByDocument(string $type, int $documentId): ?static
{
$row = static::query()
->where('document_type', '=', $type)
->where('document_id', '=', $documentId)
->where('is_archived', '=', 0)
->first();
if ($row === null) {
return null;
}
$instance = new static($row);
$instance->exists = true;
return $instance;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Models;
use App\Core\Model;
class BankAccount extends Model
{
protected static string $table = 'bank_accounts';
protected static array $fillable = [
'account_name_ar', 'account_name_en', 'bank_name_ar', 'bank_name_en',
'account_number', 'iban', 'swift_code', 'branch_name', 'currency',
'gl_account_id', 'opening_balance', 'current_balance',
'is_default', 'is_active', 'notes',
];
protected static bool $timestamps = true;
protected static bool $softDelete = true;
public static function getDefault(): ?static
{
$row = static::query()
->where('is_default', '=', 1)
->where('is_active', '=', 1)
->where('is_archived', '=', 0)
->first();
if ($row === null) {
return null;
}
$instance = new static($row);
$instance->exists = true;
return $instance;
}
public static function getActive(): array
{
return static::query()
->where('is_active', '=', 1)
->where('is_archived', '=', 0)
->orderBy('account_name_ar', 'ASC')
->get();
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Models;
use App\Core\Model;
class BankReconciliation extends Model
{
protected static string $table = 'bank_reconciliations';
protected static array $fillable = [
'bank_account_id', 'reconciliation_date', 'statement_date',
'statement_balance', 'book_balance', 'adjusted_balance', 'difference',
'status', 'approved_at', 'approved_by', 'notes',
];
protected static bool $timestamps = true;
protected static bool $softDelete = true;
public function items(): array
{
return BankReconciliationItem::query()
->where('reconciliation_id', '=', $this->id)
->orderBy('id', 'ASC')
->get();
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Models;
use App\Core\Model;
class BankReconciliationItem extends Model
{
protected static string $table = 'bank_reconciliation_items';
protected static array $fillable = [
'reconciliation_id', 'item_type', 'description_ar', 'description_en',
'amount', 'transaction_date', 'reference_number', 'journal_entry_id',
'is_cleared', 'cleared_date',
];
protected static bool $timestamps = false;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Models;
use App\Core\Model;
class CostCenter extends Model
{
protected static string $table = 'cost_centers';
protected static array $fillable = [
'code', 'name_ar', 'name_en', 'type', 'branch_id',
'parent_id', 'is_active', 'description',
];
protected static bool $timestamps = true;
protected static bool $softDelete = true;
public static function findByCode(string $code): ?static
{
$row = static::query()->where('code', '=', $code)->where('is_archived', '=', 0)->first();
if ($row === null) {
return null;
}
$instance = new static($row);
$instance->exists = true;
return $instance;
}
public static function getActive(): array
{
return static::query()
->where('is_active', '=', 1)
->where('is_archived', '=', 0)
->orderBy('code', 'ASC')
->get();
}
public static function findByBranch(int $branchId): ?static
{
$row = static::query()
->where('branch_id', '=', $branchId)
->where('is_archived', '=', 0)
->first();
if ($row === null) {
return null;
}
$instance = new static($row);
$instance->exists = true;
return $instance;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Models;
use App\Core\Model;
class FiscalYear extends Model
{
protected static string $table = 'fiscal_years';
protected static array $fillable = [
'name_ar', 'name_en', 'start_date', 'end_date',
'status', 'is_current', 'closed_at', 'closed_by', 'notes',
];
protected static bool $timestamps = true;
protected static bool $softDelete = true;
public static function current(): ?static
{
$row = static::query()->where('is_current', '=', 1)->where('is_archived', '=', 0)->first();
if ($row === null) {
return null;
}
$instance = new static($row);
$instance->exists = true;
return $instance;
}
public static function findByDate(string $date): ?static
{
$row = static::query()
->where('start_date', '<=', $date)
->where('end_date', '>=', $date)
->where('is_archived', '=', 0)
->first();
if ($row === null) {
return null;
}
$instance = new static($row);
$instance->exists = true;
return $instance;
}
public function isOpen(): bool
{
return $this->status === 'open';
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Models;
use App\Core\Model;
use App\Core\App;
class JournalEntry extends Model
{
protected static string $table = 'journal_entries';
protected static array $fillable = [
'entry_number', 'fiscal_year_id', 'entry_date', 'reference_type',
'reference_id', 'reference_number', 'description_ar', 'description_en',
'total_debit', 'total_credit', 'currency', 'status', 'posted_at',
'posted_by', 'reversed_entry_id', 'reversal_of_id', 'is_auto_generated',
'source_module', 'branch_id', 'cost_center_id', 'period_closed',
'attachments', 'notes',
];
protected static bool $timestamps = true;
protected static bool $softDelete = true;
public function lines(): array
{
return JournalEntryLine::query()
->where('journal_entry_id', '=', $this->id)
->orderBy('line_number', 'ASC')
->get();
}
public static function generateEntryNumber(): string
{
$db = App::getInstance()->db();
$year = date('Y');
$prefix = 'JE-' . $year . '-';
$last = $db->selectOne(
"SELECT entry_number FROM journal_entries WHERE entry_number LIKE ? ORDER BY id DESC LIMIT 1",
[$prefix . '%']
);
if ($last) {
$parts = explode('-', $last['entry_number']);
$seq = (int) end($parts) + 1;
} else {
$seq = 1;
}
return $prefix . str_pad((string) $seq, 6, '0', STR_PAD_LEFT);
}
public function isPosted(): bool
{
return $this->status === 'posted';
}
public function isDraft(): bool
{
return $this->status === 'draft';
}
public function isReversed(): bool
{
return $this->status === 'reversed';
}
public static function findByReference(string $type, int $id): ?static
{
$row = static::query()
->where('reference_type', '=', $type)
->where('reference_id', '=', $id)
->where('is_archived', '=', 0)
->first();
if ($row === null) {
return null;
}
$instance = new static($row);
$instance->exists = true;
return $instance;
}
public static function search(array $filters = [], int $page = 1, int $perPage = 25): array
{
$db = App::getInstance()->db();
$where = ['je.is_archived = 0'];
$params = [];
if (!empty($filters['status'])) {
$where[] = 'je.status = ?';
$params[] = $filters['status'];
}
if (!empty($filters['date_from'])) {
$where[] = 'je.entry_date >= ?';
$params[] = $filters['date_from'];
}
if (!empty($filters['date_to'])) {
$where[] = 'je.entry_date <= ?';
$params[] = $filters['date_to'];
}
if (!empty($filters['source_module'])) {
$where[] = 'je.source_module = ?';
$params[] = $filters['source_module'];
}
if (!empty($filters['reference_type'])) {
$where[] = 'je.reference_type = ?';
$params[] = $filters['reference_type'];
}
if (!empty($filters['fiscal_year_id'])) {
$where[] = 'je.fiscal_year_id = ?';
$params[] = $filters['fiscal_year_id'];
}
if (!empty($filters['search'])) {
$where[] = '(je.entry_number LIKE ? OR je.description_ar LIKE ? OR je.description_en LIKE ? OR je.reference_number LIKE ?)';
$term = '%' . $filters['search'] . '%';
$params = array_merge($params, [$term, $term, $term, $term]);
}
$whereClause = implode(' AND ', $where);
$offset = ($page - 1) * $perPage;
$countRow = $db->selectOne("SELECT COUNT(*) as total FROM journal_entries je WHERE {$whereClause}", $params);
$total = (int) ($countRow['total'] ?? 0);
$rows = $db->select(
"SELECT je.*, fy.name_ar as fiscal_year_name
FROM journal_entries je
LEFT JOIN fiscal_years fy ON fy.id = je.fiscal_year_id
WHERE {$whereClause}
ORDER BY je.entry_date DESC, je.id DESC
LIMIT {$perPage} OFFSET {$offset}",
$params
);
return [
'data' => $rows,
'total' => $total,
'page' => $page,
'per_page' => $perPage,
'pages' => (int) ceil($total / $perPage),
];
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Models;
use App\Core\Model;
use App\Core\App;
class JournalEntryLine extends Model
{
protected static string $table = 'journal_entry_lines';
protected static array $fillable = [
'journal_entry_id', 'line_number', 'account_id', 'description_ar',
'description_en', 'debit', 'credit', 'currency', 'cost_center_id',
'branch_id', 'member_id', 'employee_id', 'supplier_id',
'reference_type', 'reference_id', 'notes',
];
protected static bool $timestamps = false;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
public function account(): ?array
{
return App::getInstance()->db()->selectOne(
"SELECT * FROM chart_of_accounts WHERE id = ?",
[$this->account_id]
);
}
public static function getByJournalEntry(int $journalEntryId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT jel.*, coa.account_code, coa.name_ar as account_name_ar, coa.name_en as account_name_en
FROM journal_entry_lines jel
JOIN chart_of_accounts coa ON coa.id = jel.account_id
WHERE jel.journal_entry_id = ?
ORDER BY jel.line_number ASC",
[$journalEntryId]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Models;
use App\Core\Model;
use App\Core\App;
class PeriodClosing extends Model
{
protected static string $table = 'period_closings';
protected static array $fillable = [
'fiscal_year_id', 'period', 'period_start', 'period_end',
'closing_type', 'status', 'total_debit', 'total_credit',
'journal_count', 'closing_entry_id', 'closed_at', 'closed_by',
'reopened_at', 'reopened_by', 'reopen_reason', 'notes',
];
protected static bool $timestamps = true;
protected static bool $softDelete = true;
public static function findByPeriod(int $fiscalYearId, string $period): ?static
{
$row = static::query()
->where('fiscal_year_id', '=', $fiscalYearId)
->where('period', '=', $period)
->where('is_archived', '=', 0)
->first();
if ($row === null) {
return null;
}
$instance = new static($row);
$instance->exists = true;
return $instance;
}
public static function isPeriodClosed(int $fiscalYearId, string $period): bool
{
$pc = self::findByPeriod($fiscalYearId, $period);
return $pc !== null && $pc->status === 'closed';
}
public static function getForFiscalYear(int $fiscalYearId): array
{
return static::query()
->where('fiscal_year_id', '=', $fiscalYearId)
->where('is_archived', '=', 0)
->orderBy('period', 'ASC')
->get();
}
}
<?php
declare(strict_types=1);
return [
// ── Dashboard ────────────────────────────────────────────
['GET', '/accounting', 'Accounting\Controllers\ReportController@dashboard', ['auth'], 'accounting.reports.view'],
// ── Fiscal Years ─────────────────────────────────────────
['GET', '/accounting/fiscal-years', 'Accounting\Controllers\FiscalYearController@index', ['auth'], 'accounting.fiscal_year.view'],
['GET', '/accounting/fiscal-years/create', 'Accounting\Controllers\FiscalYearController@create', ['auth'], 'accounting.fiscal_year.manage'],
['POST', '/accounting/fiscal-years', 'Accounting\Controllers\FiscalYearController@store', ['auth'], 'accounting.fiscal_year.manage'],
['GET', '/accounting/fiscal-years/{id:\d+}', 'Accounting\Controllers\FiscalYearController@show', ['auth'], 'accounting.fiscal_year.view'],
['POST', '/accounting/fiscal-years/{id:\d+}/close', 'Accounting\Controllers\FiscalYearController@close', ['auth'], 'accounting.fiscal_year.close'],
// ── Chart of Accounts ────────────────────────────────────
['GET', '/accounting/chart-of-accounts', 'Accounting\Controllers\ChartOfAccountsController@index', ['auth'], 'accounting.coa.view'],
['GET', '/accounting/chart-of-accounts/create', 'Accounting\Controllers\ChartOfAccountsController@create', ['auth'], 'accounting.coa.manage'],
['POST', '/accounting/chart-of-accounts', 'Accounting\Controllers\ChartOfAccountsController@store', ['auth'], 'accounting.coa.manage'],
['GET', '/accounting/chart-of-accounts/{id:\d+}/edit', 'Accounting\Controllers\ChartOfAccountsController@edit', ['auth'], 'accounting.coa.manage'],
['POST', '/accounting/chart-of-accounts/{id:\d+}', 'Accounting\Controllers\ChartOfAccountsController@update', ['auth'], 'accounting.coa.manage'],
['GET', '/accounting/chart-of-accounts/search', 'Accounting\Controllers\ChartOfAccountsController@search', ['auth'], 'accounting.coa.view'],
// ── Cost Centers ─────────────────────────────────────────
['GET', '/accounting/cost-centers', 'Accounting\Controllers\CostCenterController@index', ['auth'], 'accounting.cost_center.view'],
['GET', '/accounting/cost-centers/create', 'Accounting\Controllers\CostCenterController@create', ['auth'], 'accounting.cost_center.manage'],
['POST', '/accounting/cost-centers', 'Accounting\Controllers\CostCenterController@store', ['auth'], 'accounting.cost_center.manage'],
['GET', '/accounting/cost-centers/{id:\d+}/edit', 'Accounting\Controllers\CostCenterController@edit', ['auth'], 'accounting.cost_center.manage'],
['POST', '/accounting/cost-centers/{id:\d+}', 'Accounting\Controllers\CostCenterController@update', ['auth'], 'accounting.cost_center.manage'],
// ── Bank Accounts ────────────────────────────────────────
['GET', '/accounting/bank-accounts', 'Accounting\Controllers\BankAccountController@index', ['auth'], 'accounting.bank_account.view'],
['GET', '/accounting/bank-accounts/create', 'Accounting\Controllers\BankAccountController@create', ['auth'], 'accounting.bank_account.manage'],
['POST', '/accounting/bank-accounts', 'Accounting\Controllers\BankAccountController@store', ['auth'], 'accounting.bank_account.manage'],
['GET', '/accounting/bank-accounts/{id:\d+}/edit', 'Accounting\Controllers\BankAccountController@edit', ['auth'], 'accounting.bank_account.manage'],
['POST', '/accounting/bank-accounts/{id:\d+}', 'Accounting\Controllers\BankAccountController@update', ['auth'], 'accounting.bank_account.manage'],
// ── Journal Entries ──────────────────────────────────────
['GET', '/accounting/journal-entries', 'Accounting\Controllers\JournalEntryController@index', ['auth'], 'accounting.journal.view'],
['GET', '/accounting/journal-entries/create', 'Accounting\Controllers\JournalEntryController@create', ['auth'], 'accounting.journal.create'],
['POST', '/accounting/journal-entries', 'Accounting\Controllers\JournalEntryController@store', ['auth'], 'accounting.journal.create'],
['GET', '/accounting/journal-entries/{id:\d+}', 'Accounting\Controllers\JournalEntryController@show', ['auth'], 'accounting.journal.view'],
['POST', '/accounting/journal-entries/{id:\d+}/post', 'Accounting\Controllers\JournalEntryController@post', ['auth'], 'accounting.journal.post'],
['POST', '/accounting/journal-entries/{id:\d+}/reverse', 'Accounting\Controllers\JournalEntryController@reverse', ['auth'], 'accounting.journal.reverse'],
['GET', '/accounting/journal-entries/search-accounts', 'Accounting\Controllers\JournalEntryController@searchAccounts', ['auth'], 'accounting.journal.view'],
// ── Bank Reconciliation ──────────────────────────────────
['GET', '/accounting/bank-reconciliation', 'Accounting\Controllers\BankReconciliationController@index', ['auth'], 'accounting.bank_recon.view'],
['GET', '/accounting/bank-reconciliation/create', 'Accounting\Controllers\BankReconciliationController@create', ['auth'], 'accounting.bank_recon.manage'],
['POST', '/accounting/bank-reconciliation', 'Accounting\Controllers\BankReconciliationController@store', ['auth'], 'accounting.bank_recon.manage'],
['GET', '/accounting/bank-reconciliation/{id:\d+}', 'Accounting\Controllers\BankReconciliationController@show', ['auth'], 'accounting.bank_recon.view'],
['POST', '/accounting/bank-reconciliation/{id:\d+}/add-item', 'Accounting\Controllers\BankReconciliationController@addItem', ['auth'], 'accounting.bank_recon.manage'],
['POST', '/accounting/bank-reconciliation/{id:\d+}/complete', 'Accounting\Controllers\BankReconciliationController@complete', ['auth'], 'accounting.bank_recon.manage'],
// ── Period Closing ───────────────────────────────────────
['GET', '/accounting/period-closing', 'Accounting\Controllers\PeriodClosingController@index', ['auth'], 'accounting.period.view'],
['POST', '/accounting/period-closing/close-month', 'Accounting\Controllers\PeriodClosingController@closeMonth', ['auth'], 'accounting.period.close'],
['POST', '/accounting/period-closing/reopen-month', 'Accounting\Controllers\PeriodClosingController@reopenMonth', ['auth'], 'accounting.period.reopen'],
// ── Reports ──────────────────────────────────────────────
['GET', '/accounting/reports/trial-balance', 'Accounting\Controllers\ReportController@trialBalance', ['auth'], 'accounting.reports.trial_balance'],
['GET', '/accounting/reports/general-ledger', 'Accounting\Controllers\ReportController@generalLedger', ['auth'], 'accounting.reports.general_ledger'],
['GET', '/accounting/reports/income-statement', 'Accounting\Controllers\ReportController@incomeStatement', ['auth'], 'accounting.reports.income_statement'],
['GET', '/accounting/reports/balance-sheet', 'Accounting\Controllers\ReportController@balanceSheet', ['auth'], 'accounting.reports.balance_sheet'],
['GET', '/accounting/reports/consolidated-balance-sheet', 'Accounting\Controllers\ReportController@consolidatedBalanceSheet', ['auth'], 'accounting.reports.consolidated'],
['GET', '/accounting/reports/accounts-receivable', 'Accounting\Controllers\ReportController@accountsReceivable', ['auth'], 'accounting.reports.ar'],
['GET', '/accounting/reports/accounts-payable', 'Accounting\Controllers\ReportController@accountsPayable', ['auth'], 'accounting.reports.ap'],
['GET', '/accounting/reports/member-statement', 'Accounting\Controllers\ReportController@memberStatement', ['auth'], 'accounting.reports.member_statement'],
];
This diff is collapsed.
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Services;
use App\Core\App;
/**
* Generates income statement (profit & loss) and balance sheet
* from posted journal entries.
*/
final class FinancialReportService
{
/**
* Income Statement (قائمة الدخل)
* Revenue - Expenses = Net Income
*/
public static function getIncomeStatement(
string $dateFrom,
string $dateTo,
?int $costCenterId = null,
?int $branchId = null
): array {
$db = App::getInstance()->db();
$extraWhere = '';
$params = [$dateFrom, $dateTo];
if ($costCenterId !== null) {
$extraWhere .= ' AND jel.cost_center_id = ?';
$params[] = $costCenterId;
}
if ($branchId !== null) {
$extraWhere .= ' AND jel.branch_id = ?';
$params[] = $branchId;
}
// Revenue accounts (credit-nature)
$revenue = $db->select(
"SELECT coa.id, coa.account_code, coa.name_ar, coa.name_en,
COALESCE(SUM(jel.credit), 0) - COALESCE(SUM(jel.debit), 0) as balance
FROM chart_of_accounts coa
LEFT JOIN journal_entry_lines jel ON jel.account_id = coa.id
AND jel.journal_entry_id IN (
SELECT id FROM journal_entries
WHERE status = 'posted' AND entry_date >= ? AND entry_date <= ? AND is_archived = 0
)
{$extraWhere}
WHERE coa.account_type = 'revenue' AND coa.is_archived = 0 AND coa.is_header = 0
GROUP BY coa.id, coa.account_code, coa.name_ar, coa.name_en
HAVING balance != 0
ORDER BY coa.account_code ASC",
$params
);
// Expense accounts (debit-nature)
$expenses = $db->select(
"SELECT coa.id, coa.account_code, coa.name_ar, coa.name_en,
COALESCE(SUM(jel.debit), 0) - COALESCE(SUM(jel.credit), 0) as balance
FROM chart_of_accounts coa
LEFT JOIN journal_entry_lines jel ON jel.account_id = coa.id
AND jel.journal_entry_id IN (
SELECT id FROM journal_entries
WHERE status = 'posted' AND entry_date >= ? AND entry_date <= ? AND is_archived = 0
)
{$extraWhere}
WHERE coa.account_type = 'expense' AND coa.is_archived = 0 AND coa.is_header = 0
GROUP BY coa.id, coa.account_code, coa.name_ar, coa.name_en
HAVING balance != 0
ORDER BY coa.account_code ASC",
$params
);
$totalRevenue = '0.00';
foreach ($revenue as $r) {
$totalRevenue = bcadd($totalRevenue, (string) $r['balance'], 2);
}
$totalExpenses = '0.00';
foreach ($expenses as $e) {
$totalExpenses = bcadd($totalExpenses, (string) $e['balance'], 2);
}
$netIncome = bcsub($totalRevenue, $totalExpenses, 2);
return [
'revenue' => $revenue,
'expenses' => $expenses,
'total_revenue' => $totalRevenue,
'total_expenses' => $totalExpenses,
'net_income' => $netIncome,
'date_from' => $dateFrom,
'date_to' => $dateTo,
];
}
/**
* Balance Sheet (الميزانية العمومية)
* Assets = Liabilities + Equity
*/
public static function getBalanceSheet(
string $asOfDate,
?int $costCenterId = null,
?int $branchId = null
): array {
$db = App::getInstance()->db();
$extraWhere = '';
$params = [$asOfDate];
if ($costCenterId !== null) {
$extraWhere .= ' AND jel.cost_center_id = ?';
$params[] = $costCenterId;
}
if ($branchId !== null) {
$extraWhere .= ' AND jel.branch_id = ?';
$params[] = $branchId;
}
// Build balance for each account type up to asOfDate
$allAccounts = $db->select(
"SELECT coa.id, coa.account_code, coa.name_ar, coa.name_en,
coa.account_type, coa.account_nature, coa.opening_balance,
COALESCE(SUM(jel.debit), 0) as total_debit,
COALESCE(SUM(jel.credit), 0) as total_credit
FROM chart_of_accounts coa
LEFT JOIN journal_entry_lines jel ON jel.account_id = coa.id
AND jel.journal_entry_id IN (
SELECT id FROM journal_entries
WHERE status = 'posted' AND entry_date <= ? AND is_archived = 0
)
{$extraWhere}
WHERE coa.is_archived = 0 AND coa.is_header = 0
AND coa.account_type IN ('asset','liability','equity')
GROUP BY coa.id, coa.account_code, coa.name_ar, coa.name_en,
coa.account_type, coa.account_nature, coa.opening_balance
ORDER BY coa.account_code ASC",
$params
);
$assets = [];
$liabilities = [];
$equity = [];
$totalAssets = '0.00';
$totalLiabilities = '0.00';
$totalEquity = '0.00';
foreach ($allAccounts as $acc) {
$opening = (string) $acc['opening_balance'];
if ($acc['account_nature'] === 'debit') {
$balance = bcadd($opening, bcsub((string) $acc['total_debit'], (string) $acc['total_credit'], 2), 2);
} else {
$balance = bcadd($opening, bcsub((string) $acc['total_credit'], (string) $acc['total_debit'], 2), 2);
}
if (bccomp($balance, '0.00', 2) === 0) {
continue;
}
$acc['balance'] = $balance;
switch ($acc['account_type']) {
case 'asset':
$assets[] = $acc;
$totalAssets = bcadd($totalAssets, $balance, 2);
break;
case 'liability':
$liabilities[] = $acc;
$totalLiabilities = bcadd($totalLiabilities, $balance, 2);
break;
case 'equity':
$equity[] = $acc;
$totalEquity = bcadd($totalEquity, $balance, 2);
break;
}
}
// Calculate current-period net income and add to equity
$fiscalYearStart = self::getFiscalYearStart($asOfDate);
$incomeStatement = self::getIncomeStatement($fiscalYearStart, $asOfDate, $costCenterId, $branchId);
$netIncome = $incomeStatement['net_income'];
if (bccomp($netIncome, '0.00', 2) !== 0) {
$equity[] = [
'account_code' => '---',
'name_ar' => 'صافي ربح/خسارة الفترة',
'name_en' => 'Current Period Net Income',
'balance' => $netIncome,
];
$totalEquity = bcadd($totalEquity, $netIncome, 2);
}
$totalLiabilitiesAndEquity = bcadd($totalLiabilities, $totalEquity, 2);
return [
'assets' => $assets,
'liabilities' => $liabilities,
'equity' => $equity,
'total_assets' => $totalAssets,
'total_liabilities' => $totalLiabilities,
'total_equity' => $totalEquity,
'total_liabilities_and_equity' => $totalLiabilitiesAndEquity,
'is_balanced' => bccomp($totalAssets, $totalLiabilitiesAndEquity, 2) === 0,
'as_of_date' => $asOfDate,
];
}
/**
* Multi-branch consolidation — aggregate all branches into one balance sheet.
*/
public static function getConsolidatedBalanceSheet(string $asOfDate): array
{
$db = App::getInstance()->db();
$branches = $db->select("SELECT id, name_ar, name_en FROM branches WHERE is_archived = 0 ORDER BY id");
$branchSheets = [];
foreach ($branches as $branch) {
$branchSheets[] = [
'branch' => $branch,
'balance_sheet' => self::getBalanceSheet($asOfDate, null, (int) $branch['id']),
];
}
// Consolidated (all branches)
$consolidated = self::getBalanceSheet($asOfDate);
return [
'consolidated' => $consolidated,
'branch_sheets' => $branchSheets,
'as_of_date' => $asOfDate,
];
}
private static function getFiscalYearStart(string $date): string
{
$config = App::getInstance()->config('app');
$startMonth = (int) ($config['financial_year_start_month'] ?? 7);
$startDay = (int) ($config['financial_year_start_day'] ?? 1);
$year = (int) date('Y', strtotime($date));
$month = (int) date('m', strtotime($date));
if ($month < $startMonth) {
$year--;
}
return sprintf('%04d-%02d-%02d', $year, $startMonth, $startDay);
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
<?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;">
<h3 style="margin:0;">إضافة حساب بنكي جديد</h3>
</div>
<div style="padding:20px;">
<form method="POST" action="/accounting/bank-accounts">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div>
<label class="form-label">اسم الحساب (عربي) <span style="color:#DC2626;">*</span></label>
<input type="text" name="account_name_ar" class="form-input" required>
</div>
<div>
<label class="form-label">اسم الحساب (إنجليزي)</label>
<input type="text" name="account_name_en" class="form-input" dir="ltr">
</div>
<div>
<label class="form-label">اسم البنك (عربي) <span style="color:#DC2626;">*</span></label>
<input type="text" name="bank_name_ar" class="form-input" required>
</div>
<div>
<label class="form-label">اسم البنك (إنجليزي)</label>
<input type="text" name="bank_name_en" class="form-input" dir="ltr">
</div>
<div>
<label class="form-label">رقم الحساب <span style="color:#DC2626;">*</span></label>
<input type="text" name="account_number" class="form-input" required dir="ltr">
</div>
<div>
<label class="form-label">IBAN</label>
<input type="text" name="iban" class="form-input" dir="ltr">
</div>
<div>
<label class="form-label">SWIFT Code</label>
<input type="text" name="swift_code" class="form-input" dir="ltr">
</div>
<div>
<label class="form-label">فرع البنك</label>
<input type="text" name="branch_name" class="form-input">
</div>
<div>
<label class="form-label">حساب دفتر الأستاذ</label>
<select name="gl_account_id" class="form-select">
<option value="">— ربط بحساب —</option>
<?php foreach ($gl_accounts as $gl): ?>
<option value="<?= (int)$gl['id'] ?>"><?= e($gl['account_code']) ?><?= e($gl['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">الرصيد الافتتاحي</label>
<input type="number" name="opening_balance" class="form-input" value="0.00" step="0.01" dir="ltr">
</div>
<div>
<label class="form-label" style="display:flex;align-items:center;gap:8px;">
<input type="checkbox" name="is_default" value="1"> حساب افتراضي
</label>
</div>
<div style="grid-column:1/-1;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-textarea" rows="2"></textarea>
</div>
</div>
<div style="margin-top:20px;display:flex;gap:8px;">
<button type="submit" class="btn btn-primary">حفظ</button>
<a href="/accounting/bank-accounts" 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('content'); ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;">تعديل: <?= e($account['account_name_ar']) ?></h3>
</div>
<div style="padding:20px;">
<form method="POST" action="/accounting/bank-accounts/<?= (int)$account['id'] ?>">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div>
<label class="form-label">اسم الحساب (عربي) <span style="color:#DC2626;">*</span></label>
<input type="text" name="account_name_ar" class="form-input" value="<?= e($account['account_name_ar']) ?>" required>
</div>
<div>
<label class="form-label">اسم الحساب (إنجليزي)</label>
<input type="text" name="account_name_en" class="form-input" value="<?= e($account['account_name_en'] ?? '') ?>" dir="ltr">
</div>
<div>
<label class="form-label">اسم البنك (عربي) <span style="color:#DC2626;">*</span></label>
<input type="text" name="bank_name_ar" class="form-input" value="<?= e($account['bank_name_ar']) ?>" required>
</div>
<div>
<label class="form-label">اسم البنك (إنجليزي)</label>
<input type="text" name="bank_name_en" class="form-input" value="<?= e($account['bank_name_en'] ?? '') ?>" dir="ltr">
</div>
<div>
<label class="form-label">IBAN</label>
<input type="text" name="iban" class="form-input" value="<?= e($account['iban'] ?? '') ?>" dir="ltr">
</div>
<div>
<label class="form-label">SWIFT Code</label>
<input type="text" name="swift_code" class="form-input" value="<?= e($account['swift_code'] ?? '') ?>" dir="ltr">
</div>
<div>
<label class="form-label" style="display:flex;align-items:center;gap:8px;">
<input type="checkbox" name="is_active" value="1" <?= (int)$account['is_active'] ? 'checked' : '' ?>> نشط
</label>
</div>
<div style="grid-column:1/-1;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-textarea" rows="2"><?= e($account['notes'] ?? '') ?></textarea>
</div>
</div>
<div style="margin-top:20px;display:flex;gap:8px;">
<button type="submit" class="btn btn-primary">حفظ</button>
<a href="/accounting/bank-accounts" 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/bank-accounts/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 ($accounts as $acc): ?>
<tr>
<td style="font-weight:600;"><?= e($acc['account_name_ar']) ?><?= (int)$acc['is_default'] ? ' <span style="color:#0D7377;font-size:11px;">(افتراضي)</span>' : '' ?></td>
<td><?= e($acc['bank_name_ar']) ?></td>
<td style="direction:ltr;text-align:right;"><?= e($acc['account_number']) ?></td>
<td><?= e($acc['currency']) ?></td>
<td style="direction:ltr;text-align:right;font-weight:600;"><?= money($acc['current_balance']) ?></td>
<td><span style="color:<?= (int)$acc['is_active'] ? '#059669' : '#DC2626' ?>;font-weight:600;"><?= (int)$acc['is_active'] ? 'نشط' : 'موقف' ?></span></td>
<td><a href="/accounting/bank-accounts/<?= (int)$acc['id'] ?>/edit" class="btn btn-sm btn-outline">تعديل</a></td>
</tr>
<?php endforeach; ?>
<?php if (empty($accounts)): ?>
<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'); ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;">إنشاء مطابقة بنكية</h3>
</div>
<div style="padding:20px;">
<form method="POST" action="/accounting/bank-reconciliation">
<?= 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 ($bank_accounts as $ba): ?>
<option value="<?= (int)$ba['id'] ?>"><?= e($ba['account_name_ar']) ?><?= e($ba['bank_name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">رصيد كشف البنك <span style="color:#DC2626;">*</span></label>
<input type="number" name="statement_balance" class="form-input" step="0.01" required dir="ltr">
</div>
<div>
<label class="form-label">تاريخ كشف البنك <span style="color:#DC2626;">*</span></label>
<input type="date" name="statement_date" class="form-input" value="<?= date('Y-m-d') ?>" required>
</div>
<div>
<label class="form-label">تاريخ المطابقة</label>
<input type="date" name="reconciliation_date" class="form-input" value="<?= date('Y-m-d') ?>">
</div>
<div style="grid-column:1/-1;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-textarea" rows="2"></textarea>
</div>
</div>
<div style="margin-top:20px;display:flex;gap:8px;">
<button type="submit" class="btn btn-primary">إنشاء المطابقة</button>
<a href="/accounting/bank-reconciliation" 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/bank-reconciliation/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 ($reconciliations as $r): ?>
<?php
$statusColor = match($r['status']) {
'draft' => '#F59E0B',
'in_progress' => '#0284C7',
'completed' => '#059669',
'approved' => '#0D7377',
default => '#6B7280',
};
$statusLabel = match($r['status']) {
'draft' => 'مسودة',
'in_progress' => 'جاري العمل',
'completed' => 'مكتملة',
'approved' => 'معتمدة',
default => $r['status'],
};
?>
<tr>
<td><?= e($r['bank_account_name'] ?? '—') ?></td>
<td><?= e($r['reconciliation_date']) ?></td>
<td style="direction:ltr;text-align:right;"><?= money($r['statement_balance']) ?></td>
<td style="direction:ltr;text-align:right;"><?= money($r['book_balance']) ?></td>
<td style="direction:ltr;text-align:right;font-weight:600;color:<?= bccomp((string)$r['difference'], '0.00', 2) === 0 ? '#059669' : '#DC2626' ?>;"><?= money($r['difference']) ?></td>
<td><span style="color:<?= $statusColor ?>;font-weight:600;"><?= $statusLabel ?></span></td>
<td><a href="/accounting/bank-reconciliation/<?= (int)$r['id'] ?>" class="btn btn-sm btn-outline">عرض</a></td>
</tr>
<?php endforeach; ?>
<?php if (empty($reconciliations)): ?>
<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
$statusLabel = match($recon['status']) {
'draft' => 'مسودة',
'in_progress' => 'جاري العمل',
'completed' => 'مكتملة',
'approved' => 'معتمدة',
default => $recon['status'],
};
?>
<!-- Summary -->
<div class="card" style="margin-bottom:15px;">
<div style="padding:20px;">
<h2 style="margin:0 0 15px;">مطابقة: <?= e($bank_account['account_name_ar'] ?? '—') ?></h2>
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:15px;">
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">رصيد كشف البنك</div>
<div style="font-size:20px;font-weight:700;direction:ltr;"><?= money($recon['statement_balance']) ?></div>
</div>
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">رصيد الدفاتر</div>
<div style="font-size:20px;font-weight:700;direction:ltr;"><?= money($recon['book_balance']) ?></div>
</div>
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">الرصيد المعدل</div>
<div style="font-size:20px;font-weight:700;direction:ltr;"><?= money($recon['adjusted_balance']) ?></div>
</div>
<div class="card" style="padding:15px;text-align:center;background:<?= bccomp((string)$recon['difference'], '0.00', 2) === 0 ? '#F0FDF4' : '#FEF2F2' ?>;">
<div style="font-size:12px;color:#6B7280;">الفرق</div>
<div style="font-size:20px;font-weight:700;color:<?= bccomp((string)$recon['difference'], '0.00', 2) === 0 ? '#059669' : '#DC2626' ?>;direction:ltr;"><?= money($recon['difference']) ?></div>
</div>
<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;"><?= $statusLabel ?></div>
</div>
</div>
<?php if ($recon['status'] !== 'completed' && $recon['status'] !== 'approved'): ?>
<div style="margin-top:15px;display:flex;gap:8px;">
<?php if (bccomp((string)$recon['difference'], '0.00', 2) === 0): ?>
<form method="POST" action="/accounting/bank-reconciliation/<?= (int)$recon['id'] ?>/complete" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-primary" onclick="return confirm('هل تريد إكمال المطابقة؟')">إكمال المطابقة</button>
</form>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
<!-- Reconciliation Items -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<!-- Items List -->
<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>
</tr>
</thead>
<tbody>
<?php foreach ($items as $item): ?>
<?php
$typeLabel = match($item['item_type']) {
'outstanding_check' => 'شيك معلق',
'deposit_in_transit' => 'إيداع في الطريق',
'bank_charge' => 'مصاريف بنكية',
'bank_interest' => 'فوائد بنكية',
'book_error' => 'خطأ دفتري',
'bank_error' => 'خطأ بنكي',
default => $item['item_type'],
};
?>
<tr>
<td style="font-size:12px;"><?= $typeLabel ?></td>
<td style="font-size:13px;"><?= e($item['description_ar']) ?></td>
<td style="direction:ltr;text-align:right;font-weight:600;"><?= money($item['amount']) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($items)): ?>
<tr><td colspan="3" style="text-align:center;color:#6B7280;padding:20px;">لا توجد بنود</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- Add Item Form -->
<?php if ($recon['status'] !== 'completed' && $recon['status'] !== 'approved'): ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;">إضافة بند</h3>
</div>
<div style="padding:20px;">
<form method="POST" action="/accounting/bank-reconciliation/<?= (int)$recon['id'] ?>/add-item">
<?= csrf_field() ?>
<div style="display:grid;gap:12px;">
<div>
<label class="form-label">النوع <span style="color:#DC2626;">*</span></label>
<select name="item_type" class="form-select" required>
<option value="">— اختر —</option>
<option value="outstanding_check">شيك معلق</option>
<option value="deposit_in_transit">إيداع في الطريق</option>
<option value="bank_charge">مصاريف بنكية</option>
<option value="bank_interest">فوائد بنكية</option>
<option value="book_error">خطأ دفتري</option>
<option value="bank_error">خطأ بنكي</option>
</select>
</div>
<div>
<label class="form-label">الوصف <span style="color:#DC2626;">*</span></label>
<input type="text" name="description_ar" class="form-input" required>
</div>
<div>
<label class="form-label">المبلغ <span style="color:#DC2626;">*</span></label>
<input type="number" name="amount" class="form-input" step="0.01" required dir="ltr">
</div>
<div>
<label class="form-label">رقم المرجع</label>
<input type="text" name="reference_number" class="form-input" dir="ltr">
</div>
<button type="submit" class="btn btn-primary">إضافة</button>
</div>
</form>
</div>
</div>
<?php endif; ?>
</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;">
<h3 style="margin:0;">إضافة حساب جديد لدليل الحسابات</h3>
</div>
<div style="padding:20px;">
<form method="POST" action="/accounting/chart-of-accounts">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div>
<label class="form-label">كود الحساب <span style="color:#DC2626;">*</span></label>
<input type="text" name="account_code" class="form-input" placeholder="مثال: 1101" required dir="ltr">
</div>
<div>
<label class="form-label">الحساب الأب</label>
<select name="parent_id" class="form-select">
<option value="">— بدون (حساب رئيسي) —</option>
<?php foreach ($parents as $p): ?>
<option value="<?= (int)$p['id'] ?>"><?= e($p['account_code']) ?><?= e($p['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">اسم الحساب (عربي) <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" class="form-input" required>
</div>
<div>
<label class="form-label">اسم الحساب (إنجليزي) <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_en" class="form-input" required dir="ltr">
</div>
<div>
<label class="form-label">نوع الحساب <span style="color:#DC2626;">*</span></label>
<select name="account_type" class="form-select" required>
<option value="">— اختر —</option>
<option value="asset">أصول</option>
<option value="liability">خصوم</option>
<option value="equity">حقوق ملكية</option>
<option value="revenue">إيرادات</option>
<option value="expense">مصروفات</option>
</select>
</div>
<div>
<label class="form-label">طبيعة الحساب <span style="color:#DC2626;">*</span></label>
<select name="account_nature" class="form-select" required>
<option value="">— اختر —</option>
<option value="debit">مدين</option>
<option value="credit">دائن</option>
</select>
</div>
<div>
<label class="form-label">رصيد افتتاحي</label>
<input type="number" name="opening_balance" class="form-input" value="0.00" step="0.01" dir="ltr">
</div>
<div>
<label class="form-label">مركز التكلفة الافتراضي</label>
<select name="cost_center_id" class="form-select">
<option value="">— بدون —</option>
<?php foreach ($cost_centers as $cc): ?>
<option value="<?= (int)$cc['id'] ?>"><?= e($cc['code']) ?><?= e($cc['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label" style="display:flex;align-items:center;gap:8px;">
<input type="checkbox" name="is_header" value="1"> حساب رئيسي (لا يُرحّل إليه)
</label>
</div>
<div style="grid-column:1/-1;">
<label class="form-label">وصف (عربي)</label>
<textarea name="description_ar" class="form-textarea" rows="2"></textarea>
</div>
</div>
<div style="margin-top:20px;display:flex;gap:8px;">
<button type="submit" class="btn btn-primary">حفظ الحساب</button>
<a href="/accounting/chart-of-accounts" class="btn btn-outline">إلغاء</a>
</div>
</form>
</div>
</div>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تعديل حساب — <?= e($account['account_code']) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;">تعديل حساب: <?= e($account['account_code']) ?><?= e($account['name_ar']) ?></h3>
</div>
<div style="padding:20px;">
<form method="POST" action="/accounting/chart-of-accounts/<?= (int)$account['id'] ?>">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div>
<label class="form-label">كود الحساب</label>
<input type="text" class="form-input" value="<?= e($account['account_code']) ?>" disabled dir="ltr" style="background:#F3F4F6;">
</div>
<div>
<label class="form-label">الحساب الأب</label>
<select name="parent_id" class="form-select" disabled>
<option value="">— بدون —</option>
<?php foreach ($parents as $p): ?>
<option value="<?= (int)$p['id'] ?>" <?= (int)$p['id'] === (int)($account['parent_id'] ?? 0) ? 'selected' : '' ?>><?= e($p['account_code']) ?><?= e($p['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">اسم الحساب (عربي) <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" class="form-input" value="<?= e($account['name_ar']) ?>" required>
</div>
<div>
<label class="form-label">اسم الحساب (إنجليزي) <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_en" class="form-input" value="<?= e($account['name_en']) ?>" required dir="ltr">
</div>
<div>
<label class="form-label">نوع الحساب <span style="color:#DC2626;">*</span></label>
<select name="account_type" class="form-select" required>
<option value="asset" <?= $account['account_type'] === 'asset' ? 'selected' : '' ?>>أصول</option>
<option value="liability" <?= $account['account_type'] === 'liability' ? 'selected' : '' ?>>خصوم</option>
<option value="equity" <?= $account['account_type'] === 'equity' ? 'selected' : '' ?>>حقوق ملكية</option>
<option value="revenue" <?= $account['account_type'] === 'revenue' ? 'selected' : '' ?>>إيرادات</option>
<option value="expense" <?= $account['account_type'] === 'expense' ? 'selected' : '' ?>>مصروفات</option>
</select>
</div>
<div>
<label class="form-label">طبيعة الحساب <span style="color:#DC2626;">*</span></label>
<select name="account_nature" class="form-select" required>
<option value="debit" <?= $account['account_nature'] === 'debit' ? 'selected' : '' ?>>مدين</option>
<option value="credit" <?= $account['account_nature'] === 'credit' ? 'selected' : '' ?>>دائن</option>
</select>
</div>
<div>
<label class="form-label">مركز التكلفة الافتراضي</label>
<select name="cost_center_id" class="form-select">
<option value="">— بدون —</option>
<?php foreach ($cost_centers as $cc): ?>
<option value="<?= (int)$cc['id'] ?>" <?= (int)$cc['id'] === (int)($account['cost_center_id'] ?? 0) ? 'selected' : '' ?>><?= e($cc['code']) ?><?= e($cc['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label" style="display:flex;align-items:center;gap:8px;">
<input type="checkbox" name="is_active" value="1" <?= (int)$account['is_active'] ? 'checked' : '' ?>> حساب نشط
</label>
<label class="form-label" style="display:flex;align-items:center;gap:8px;margin-top:8px;">
<input type="checkbox" name="is_header" value="1" <?= (int)$account['is_header'] ? 'checked' : '' ?>> حساب رئيسي
</label>
</div>
<div style="grid-column:1/-1;">
<label class="form-label">وصف (عربي)</label>
<textarea name="description_ar" class="form-textarea" rows="2"><?= e($account['description_ar'] ?? '') ?></textarea>
</div>
</div>
<div style="margin-top:20px;display:flex;gap:8px;">
<button type="submit" class="btn btn-primary">حفظ التعديلات</button>
<a href="/accounting/chart-of-accounts" 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/chart-of-accounts/create" class="btn btn-primary">+ حساب جديد</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;">دليل الحسابات (شجرة الحسابات)</h3>
</div>
<div style="padding:15px 20px;">
<?php if (empty($tree)): ?>
<p style="color:#6B7280;text-align:center;padding:40px 0;">لا توجد حسابات — <a href="/accounting/chart-of-accounts/create">أضف حساباً جديداً</a></p>
<?php else: ?>
<?php
function renderAccountTree(array $accounts, int $depth = 0): void {
foreach ($accounts as $acc) {
$indent = $depth * 25;
$isHeader = (int)($acc['is_header'] ?? 0);
$weight = $isHeader ? '700' : '400';
$color = match($acc['account_type']) {
'asset' => '#0D7377',
'liability' => '#DC2626',
'equity' => '#7C3AED',
'revenue' => '#059669',
'expense' => '#F59E0B',
default => '#374151',
};
$typeLabel = match($acc['account_type']) {
'asset' => 'أصول',
'liability' => 'خصوم',
'equity' => 'حقوق ملكية',
'revenue' => 'إيرادات',
'expense' => 'مصروفات',
default => '',
};
?>
<div style="display:flex;align-items:center;padding:8px 0;border-bottom:1px solid #F3F4F6;margin-right:<?= $indent ?>px;">
<span style="width:80px;direction:ltr;text-align:right;color:<?= $color ?>;font-weight:600;font-size:13px;"><?= e($acc['account_code']) ?></span>
<span style="flex:1;margin:0 15px;font-weight:<?= $weight ?>;"><?= e($acc['name_ar']) ?></span>
<span style="width:80px;font-size:12px;color:#6B7280;"><?= $typeLabel ?></span>
<span style="width:100px;direction:ltr;text-align:left;font-size:13px;"><?= !$isHeader ? money($acc['current_balance'] ?? '0.00') : '' ?></span>
<a href="/accounting/chart-of-accounts/<?= (int)$acc['id'] ?>/edit" style="font-size:12px;color:#0D7377;">تعديل</a>
</div>
<?php
if (!empty($acc['children'])) {
renderAccountTree($acc['children'], $depth + 1);
}
}
}
renderAccountTree($tree);
?>
<?php endif; ?>
</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;">
<h3 style="margin:0;">إضافة مركز تكلفة / ربحية</h3>
</div>
<div style="padding:20px;">
<form method="POST" action="/accounting/cost-centers">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div>
<label class="form-label">الكود <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" class="form-input" required dir="ltr">
</div>
<div>
<label class="form-label">النوع <span style="color:#DC2626;">*</span></label>
<select name="type" class="form-select" required>
<option value="cost_center">مركز تكلفة</option>
<option value="profit_center">مركز ربحية</option>
</select>
</div>
<div>
<label class="form-label">الاسم (عربي) <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" class="form-input" required>
</div>
<div>
<label class="form-label">الاسم (إنجليزي) <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_en" class="form-input" required dir="ltr">
</div>
<div>
<label class="form-label">الفرع</label>
<select name="branch_id" class="form-select">
<option value="">— بدون —</option>
<?php foreach ($branches as $b): ?>
<option value="<?= (int)$b['id'] ?>"><?= e($b['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">المركز الأب</label>
<select name="parent_id" class="form-select">
<option value="">— بدون —</option>
<?php foreach ($parents as $p): ?>
<option value="<?= (int)$p['id'] ?>"><?= e($p['code']) ?><?= e($p['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="grid-column:1/-1;">
<label class="form-label">وصف</label>
<textarea name="description" class="form-textarea" rows="2"></textarea>
</div>
</div>
<div style="margin-top:20px;display:flex;gap:8px;">
<button type="submit" class="btn btn-primary">حفظ</button>
<a href="/accounting/cost-centers" 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('content'); ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;">تعديل: <?= e($center['code']) ?><?= e($center['name_ar']) ?></h3>
</div>
<div style="padding:20px;">
<form method="POST" action="/accounting/cost-centers/<?= (int)$center['id'] ?>">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div>
<label class="form-label">الكود</label>
<input type="text" class="form-input" value="<?= e($center['code']) ?>" disabled dir="ltr" style="background:#F3F4F6;">
</div>
<div>
<label class="form-label">النوع <span style="color:#DC2626;">*</span></label>
<select name="type" class="form-select" required>
<option value="cost_center" <?= $center['type'] === 'cost_center' ? 'selected' : '' ?>>مركز تكلفة</option>
<option value="profit_center" <?= $center['type'] === 'profit_center' ? 'selected' : '' ?>>مركز ربحية</option>
</select>
</div>
<div>
<label class="form-label">الاسم (عربي) <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" class="form-input" value="<?= e($center['name_ar']) ?>" required>
</div>
<div>
<label class="form-label">الاسم (إنجليزي) <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_en" class="form-input" value="<?= e($center['name_en']) ?>" required dir="ltr">
</div>
<div>
<label class="form-label">الفرع</label>
<select name="branch_id" class="form-select">
<option value="">— بدون —</option>
<?php foreach ($branches as $b): ?>
<option value="<?= (int)$b['id'] ?>" <?= (int)$b['id'] === (int)($center['branch_id'] ?? 0) ? 'selected' : '' ?>><?= e($b['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label" style="display:flex;align-items:center;gap:8px;">
<input type="checkbox" name="is_active" value="1" <?= (int)$center['is_active'] ? 'checked' : '' ?>> نشط
</label>
</div>
<div style="grid-column:1/-1;">
<label class="form-label">وصف</label>
<textarea name="description" class="form-textarea" rows="2"><?= e($center['description'] ?? '') ?></textarea>
</div>
</div>
<div style="margin-top:20px;display:flex;gap:8px;">
<button type="submit" class="btn btn-primary">حفظ</button>
<a href="/accounting/cost-centers" class="btn btn-outline">إلغاء</a>
</div>
</form>
</div>
</div>
<?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.
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