Commit 0c5becba authored by Mahmoud Aglan's avatar Mahmoud Aglan

fgjddhkdhjk

parent 60e1e70f
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\App;
use App\Core\Response;
use App\Modules\Accounting\Models\AccountBudget;
use App\Modules\Accounting\Models\CostCenterBudget;
use App\Modules\Accounting\Services\BudgetService;
class BudgetController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('accounting.budget.view');
$db = App::getInstance()->db();
$fiscalYears = $db->select("SELECT id, name_ar, start_date, end_date, status FROM fiscal_years WHERE is_archived = 0 ORDER BY start_date DESC");
$currentYear = $db->selectOne("SELECT id FROM fiscal_years WHERE is_current = 1 AND is_archived = 0");
$selectedYearId = (int)($request->get('fiscal_year_id') ?? ($currentYear['id'] ?? 0));
$accounts = $db->select("
SELECT id, code, name_ar, account_type, is_header
FROM chart_of_accounts
WHERE is_archived = 0 AND is_header = 0
ORDER BY code
");
$budgets = [];
if ($selectedYearId) {
$rows = AccountBudget::getForYear($selectedYearId);
foreach ($rows as $row) {
$budgets[(int)$row['account_id']][$row['period']] = $row;
}
}
return $this->view('Accounting/Views/budgets/index', [
'fiscalYears' => $fiscalYears,
'selectedYearId' => $selectedYearId,
'accounts' => $accounts,
'budgets' => $budgets,
]);
}
public function store(Request $request): Response
{
$this->authorize('accounting.budget.manage');
$fiscalYearId = (int)$request->post('fiscal_year_id');
$accountId = (int)$request->post('account_id');
$session = App::getInstance()->session();
$userId = (int)($session->get('employee_id') ?? 0);
if (!$fiscalYearId || !$accountId) {
return $this->redirect('/accounting/budgets')
->withError('بيانات غير مكتملة');
}
$amounts = $request->post('amounts', []);
$notes = $request->post('budget_notes', '');
foreach ($amounts as $period => $amount) {
$amount = (float)$amount;
if ($amount == 0) {
continue;
}
AccountBudget::upsert($fiscalYearId, $accountId, $period, $amount, $notes ?: null, $userId ?: null);
}
return $this->redirect('/accounting/budgets?fiscal_year_id=' . $fiscalYearId)
->withSuccess('تم حفظ الموازنة بنجاح');
}
public function costCenters(Request $request): Response
{
$this->authorize('accounting.budget.view');
$db = App::getInstance()->db();
$fiscalYears = $db->select("SELECT id, name_ar, start_date, end_date, status FROM fiscal_years WHERE is_archived = 0 ORDER BY start_date DESC");
$currentYear = $db->selectOne("SELECT id FROM fiscal_years WHERE is_current = 1 AND is_archived = 0");
$selectedYearId = (int)($request->get('fiscal_year_id') ?? ($currentYear['id'] ?? 0));
$costCenters = $db->select("SELECT id, code, name_ar FROM cost_centers WHERE is_active = 1 ORDER BY code");
$budgets = [];
if ($selectedYearId) {
$rows = CostCenterBudget::getForYear($selectedYearId);
foreach ($rows as $row) {
$budgets[(int)$row['cost_center_id']][$row['period']] = $row;
}
}
return $this->view('Accounting/Views/budgets/cost_centers', [
'fiscalYears' => $fiscalYears,
'selectedYearId' => $selectedYearId,
'costCenters' => $costCenters,
'budgets' => $budgets,
]);
}
public function storeCostCenter(Request $request): Response
{
$this->authorize('accounting.budget.manage');
$fiscalYearId = (int)$request->post('fiscal_year_id');
$costCenterId = (int)$request->post('cost_center_id');
$accountId = $request->post('account_id') ? (int)$request->post('account_id') : null;
$session = App::getInstance()->session();
$userId = (int)($session->get('employee_id') ?? 0);
if (!$fiscalYearId || !$costCenterId) {
return $this->redirect('/accounting/budgets/cost-centers')
->withError('بيانات غير مكتملة');
}
$amounts = $request->post('amounts', []);
$notes = $request->post('budget_notes', '');
foreach ($amounts as $period => $amount) {
$amount = (float)$amount;
if ($amount == 0) {
continue;
}
CostCenterBudget::upsert($fiscalYearId, $costCenterId, $accountId, $period, $amount, $notes ?: null, $userId ?: null);
}
return $this->redirect('/accounting/budgets/cost-centers?fiscal_year_id=' . $fiscalYearId)
->withSuccess('تم حفظ موازنة مركز التكلفة بنجاح');
}
public function varianceReport(Request $request): Response
{
$this->authorize('accounting.budget.view');
$db = App::getInstance()->db();
$fiscalYears = $db->select("SELECT id, name_ar, start_date, end_date FROM fiscal_years WHERE is_archived = 0 ORDER BY start_date DESC");
$currentYear = $db->selectOne("SELECT id FROM fiscal_years WHERE is_current = 1 AND is_archived = 0");
$selectedYearId = (int)($request->get('fiscal_year_id') ?? ($currentYear['id'] ?? 0));
$fromPeriod = $request->get('from_period');
$toPeriod = $request->get('to_period');
$report = [];
if ($selectedYearId) {
$report = BudgetService::getVarianceReport($selectedYearId, $fromPeriod, $toPeriod);
}
return $this->view('Accounting/Views/budgets/variance_report', [
'fiscalYears' => $fiscalYears,
'selectedYearId' => $selectedYearId,
'fromPeriod' => $fromPeriod,
'toPeriod' => $toPeriod,
'report' => $report,
]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\App;
use App\Core\Response;
use App\Modules\Accounting\Models\DailyTransaction;
use App\Modules\Accounting\Models\JournalType;
use App\Modules\Accounting\Services\DailyTransactionService;
class DailyTransactionController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('accounting.daily_tx.view');
$date = $request->get('date', date('Y-m-d'));
$transactions = DailyTransaction::getByDate($date);
$db = App::getInstance()->db();
$accounts = $db->select("SELECT id, code, name_ar FROM chart_of_accounts WHERE is_archived = 0 AND is_header = 0 ORDER BY code");
$journalTypes = JournalType::getActive();
$costCenters = $db->select("SELECT id, code, name_ar FROM cost_centers WHERE is_active = 1 ORDER BY code");
$accountMap = [];
foreach ($accounts as $a) $accountMap[(int)$a['id']] = $a;
$typeMap = [];
foreach ($journalTypes as $jt) $typeMap[(int)$jt['id']] = $jt;
return $this->view('Accounting/Views/daily_transactions/index', [
'date' => $date,
'transactions' => $transactions,
'accounts' => $accounts,
'journalTypes' => $journalTypes,
'costCenters' => $costCenters,
'accountMap' => $accountMap,
'typeMap' => $typeMap,
]);
}
public function store(Request $request): Response
{
$this->authorize('accounting.daily_tx.manage');
$data = $this->validate($request->all(), [
'transaction_date' => 'required',
'account_id' => 'required',
'amount' => 'required',
'direction' => 'required',
]);
$session = App::getInstance()->session();
DailyTransaction::create([
'transaction_date' => $data['transaction_date'],
'journal_type_id' => $request->post('journal_type_id') ? (int)$request->post('journal_type_id') : null,
'account_id' => (int)$data['account_id'],
'cost_center_id' => $request->post('cost_center_id') ? (int)$request->post('cost_center_id') : null,
'amount' => (float)$data['amount'],
'direction' => $data['direction'],
'description' => $request->post('description'),
'document_number' => $request->post('document_number'),
'currency' => $request->post('currency', 'EGP'),
'branch_id' => $request->post('branch_id') ? (int)$request->post('branch_id') : null,
'created_by' => (int)($session->get('employee_id') ?? 0) ?: null,
]);
return $this->redirect('/accounting/daily-transactions?date=' . $data['transaction_date'])
->withSuccess('تم تسجيل الحركة');
}
public function consolidate(Request $request): Response
{
$this->authorize('accounting.daily_tx.manage');
$date = $request->post('date', date('Y-m-d'));
$journalTypeId = $request->post('journal_type_id') ? (int)$request->post('journal_type_id') : null;
$entryId = DailyTransactionService::consolidate($date, $journalTypeId);
if ($entryId === null) {
return $this->redirect('/accounting/daily-transactions?date=' . $date)
->withError('لا توجد حركات غير مجمعة لهذا التاريخ');
}
return $this->redirect('/accounting/daily-transactions?date=' . $date)
->withSuccess('تم إنشاء القيد المجمع رقم #' . $entryId);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Modules\Accounting\Models\AccountingDimension;
use App\Modules\Accounting\Models\AccountingDimensionValue;
class DimensionController extends Controller
{
public function index(): Response
{
$this->authorize('accounting.dimensions.view');
$dimensions = AccountingDimension::query()->orderBy('code', 'ASC')->get();
return $this->view('Accounting/Views/dimensions/index', [
'dimensions' => $dimensions,
]);
}
public function store(Request $request): Response
{
$this->authorize('accounting.dimensions.manage');
$data = $this->validate($request->all(), [
'code' => 'required',
'name_ar' => 'required',
'name_en' => 'required',
]);
AccountingDimension::create([
'code' => strtoupper($data['code']),
'name_ar' => $data['name_ar'],
'name_en' => $data['name_en'],
'is_required' => (int)($request->post('is_required', 0)),
'is_active' => 1,
]);
return $this->redirect('/accounting/dimensions')
->withSuccess('تم إنشاء البُعد المحاسبي');
}
public function show(Request $request, string $id): Response
{
$this->authorize('accounting.dimensions.view');
$dimension = AccountingDimension::findOrFail((int)$id);
$values = AccountingDimension::getValuesForDimension((int)$id);
return $this->view('Accounting/Views/dimensions/show', [
'dimension' => $dimension->toArray(),
'values' => $values,
]);
}
public function storeValue(Request $request, string $id): Response
{
$this->authorize('accounting.dimensions.manage');
$dimension = AccountingDimension::findOrFail((int)$id);
$data = $this->validate($request->all(), [
'code' => 'required',
'name_ar' => 'required',
'name_en' => 'required',
]);
AccountingDimensionValue::create([
'dimension_id' => (int)$dimension->id,
'code' => strtoupper($data['code']),
'name_ar' => $data['name_ar'],
'name_en' => $data['name_en'],
'parent_id' => $request->post('parent_id') ? (int)$request->post('parent_id') : null,
'is_active' => 1,
]);
return $this->redirect('/accounting/dimensions/' . $id)
->withSuccess('تم إضافة القيمة');
}
public function toggleActive(Request $request, string $id): Response
{
$this->authorize('accounting.dimensions.manage');
$dimension = AccountingDimension::findOrFail((int)$id);
$dimension->update([
'is_active' => $dimension->is_active ? 0 : 1,
]);
return $this->redirect('/accounting/dimensions')
->withSuccess('تم تحديث الحالة');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\App;
use App\Core\Response;
use App\Modules\Accounting\Models\InstrumentPortfolio;
use App\Modules\Accounting\Models\NegotiableInstrument;
class InstrumentPortfolioController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('accounting.instruments.view');
$type = $request->get('type', '');
$qb = InstrumentPortfolio::query();
if ($type) {
$qb->where('portfolio_type', '=', $type);
}
$portfolios = $qb->orderBy('portfolio_date', 'DESC')->get();
return $this->view('Accounting/Views/portfolios/index', [
'portfolios' => $portfolios,
'type' => $type,
]);
}
public function create(Request $request): Response
{
$this->authorize('accounting.instruments.manage');
$db = App::getInstance()->db();
$bankAccounts = $db->select("SELECT id, account_name_ar FROM bank_accounts WHERE is_active = 1 ORDER BY account_name_ar");
$availableInstruments = NegotiableInstrument::query()
->where('status', '=', 'in_hand')
->where('direction', '=', 'receivable')
->orderBy('due_date', 'ASC')
->get();
return $this->view('Accounting/Views/portfolios/create', [
'bankAccounts' => $bankAccounts,
'availableInstruments' => $availableInstruments,
]);
}
public function store(Request $request): Response
{
$this->authorize('accounting.instruments.manage');
$data = $this->validate($request->all(), [
'portfolio_type' => 'required',
'portfolio_date' => 'required',
]);
$session = App::getInstance()->session();
$portfolioNumber = InstrumentPortfolio::generateNumber($data['portfolio_type']);
$portfolio = InstrumentPortfolio::create([
'portfolio_number' => $portfolioNumber,
'portfolio_type' => $data['portfolio_type'],
'portfolio_date' => $data['portfolio_date'],
'bank_account_id' => $request->post('bank_account_id') ? (int)$request->post('bank_account_id') : null,
'description' => $request->post('description'),
'status' => 'draft',
'total_amount' => 0,
'instrument_count' => 0,
'created_by' => (int)($session->get('employee_id') ?? 0) ?: null,
]);
$instrumentIds = $request->post('instrument_ids', []);
if (is_array($instrumentIds)) {
foreach ($instrumentIds as $instId) {
$portfolio->addItem((int)$instId);
}
}
return $this->redirect('/accounting/portfolios')
->withSuccess('تم إنشاء الحافظة: ' . $portfolioNumber);
}
public function show(Request $request, string $id): Response
{
$this->authorize('accounting.instruments.view');
$portfolio = InstrumentPortfolio::findOrFail((int)$id);
$items = $portfolio->getItems();
return $this->view('Accounting/Views/portfolios/show', [
'portfolio' => $portfolio->toArray(),
'items' => $items,
]);
}
public function confirm(Request $request, string $id): Response
{
$this->authorize('accounting.instruments.manage');
$portfolio = InstrumentPortfolio::findOrFail((int)$id);
if ($portfolio->status !== 'draft') {
return $this->redirect('/accounting/portfolios/' . $id)
->withError('الحافظة مؤكدة بالفعل');
}
$items = $portfolio->getItems();
$newStatus = $this->mapPortfolioTypeToInstrumentStatus($portfolio->portfolio_type);
foreach ($items as $item) {
$instrument = NegotiableInstrument::find((int)$item['id']);
if ($instrument && $newStatus) {
$extra = [];
if ($portfolio->bank_account_id) {
$extra['bank_account_id'] = (int)$portfolio->bank_account_id;
}
$instrument->transitionTo($newStatus, $extra ?: null);
}
}
$portfolio->update(['status' => 'confirmed']);
return $this->redirect('/accounting/portfolios/' . $id)
->withSuccess('تم تأكيد الحافظة وتحديث حالة الأوراق');
}
private function mapPortfolioTypeToInstrumentStatus(string $portfolioType): ?string
{
$map = [
'deposit' => 'under_collection',
'collection' => 'under_collection',
'payment' => 'paid',
'endorsement' => 'endorsed',
'return' => 'returned',
'bounce' => 'bounced',
];
return $map[$portfolioType] ?? null;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\App;
use App\Core\Response;
use App\Modules\Accounting\Models\JournalType;
class JournalTypeController extends Controller
{
public function index(): Response
{
$this->authorize('accounting.journal_type.view');
$types = JournalType::query()
->orderBy('code', 'ASC')
->get();
return $this->view('Accounting/Views/journal_types/index', [
'types' => $types,
]);
}
public function store(Request $request): Response
{
$this->authorize('accounting.journal_type.manage');
$data = $this->validate($request->all(), [
'code' => 'required',
'name_ar' => 'required',
'name_en' => 'required',
]);
if (JournalType::findByCode($data['code'])) {
return $this->redirect('/accounting/journal-types')
->withError('كود نوع اليومية مستخدم بالفعل');
}
JournalType::create([
'code' => strtoupper($data['code']),
'name_ar' => $data['name_ar'],
'name_en' => $data['name_en'],
'short_name_ar' => $request->post('short_name_ar'),
'short_name_en' => $request->post('short_name_en'),
'numbering_method' => $request->post('numbering_method', 'yearly'),
'number_prefix' => $request->post('number_prefix'),
'number_length' => (int) $request->post('number_length', 6),
'is_auto_generated' => (int) ($request->post('is_auto_generated', 0)),
'is_default' => (int) ($request->post('is_default', 0)),
'is_active' => 1,
]);
return $this->redirect('/accounting/journal-types')
->withSuccess('تم إنشاء نوع اليومية بنجاح');
}
public function edit(Request $request, string $id): Response
{
$this->authorize('accounting.journal_type.manage');
$type = JournalType::findOrFail((int) $id);
return $this->view('Accounting/Views/journal_types/edit', [
'type' => $type->toArray(),
]);
}
public function update(Request $request, string $id): Response
{
$this->authorize('accounting.journal_type.manage');
$type = JournalType::findOrFail((int) $id);
$data = $this->validate($request->all(), [
'name_ar' => 'required',
'name_en' => 'required',
]);
$type->update([
'name_ar' => $data['name_ar'],
'name_en' => $data['name_en'],
'short_name_ar' => $request->post('short_name_ar'),
'short_name_en' => $request->post('short_name_en'),
'numbering_method' => $request->post('numbering_method', 'yearly'),
'number_prefix' => $request->post('number_prefix'),
'number_length' => (int) $request->post('number_length', 6),
'is_auto_generated' => (int) ($request->post('is_auto_generated', 0)),
'is_default' => (int) ($request->post('is_default', 0)),
'is_active' => (int) ($request->post('is_active', 1)),
]);
return $this->redirect('/accounting/journal-types')
->withSuccess('تم تحديث نوع اليومية بنجاح');
}
public function toggleActive(Request $request, string $id): Response
{
$this->authorize('accounting.journal_type.manage');
$type = JournalType::findOrFail((int) $id);
$type->update([
'is_active' => $type->is_active ? 0 : 1,
]);
return $this->redirect('/accounting/journal-types')
->withSuccess('تم تحديث الحالة بنجاح');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\App;
use App\Core\Response;
use App\Modules\Accounting\Models\NegotiableInstrument;
class NegotiableInstrumentController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('accounting.instruments.view');
$direction = $request->get('direction', 'receivable');
$status = $request->get('status', '');
$qb = NegotiableInstrument::query()
->where('direction', '=', $direction);
if ($status) {
$qb->where('status', '=', $status);
}
$instruments = $qb->orderBy('due_date', 'ASC')->get();
$db = App::getInstance()->db();
$bankAccounts = $db->select("SELECT id, account_name_ar FROM bank_accounts WHERE is_active = 1 ORDER BY account_name_ar");
return $this->view('Accounting/Views/instruments/index', [
'instruments' => $instruments,
'direction' => $direction,
'status' => $status,
'bankAccounts' => $bankAccounts,
]);
}
public function store(Request $request): Response
{
$this->authorize('accounting.instruments.manage');
$data = $this->validate($request->all(), [
'instrument_type' => 'required',
'direction' => 'required',
'instrument_number' => 'required',
'amount' => 'required',
'issue_date' => 'required',
'due_date' => 'required',
]);
$session = App::getInstance()->session();
NegotiableInstrument::create([
'instrument_type' => $data['instrument_type'],
'direction' => $data['direction'],
'instrument_number' => $data['instrument_number'],
'amount' => (float)$data['amount'],
'currency' => $request->post('currency', 'EGP'),
'issue_date' => $data['issue_date'],
'due_date' => $data['due_date'],
'drawer_name' => $request->post('drawer_name'),
'drawer_bank' => $request->post('drawer_bank'),
'beneficiary_name' => $request->post('beneficiary_name'),
'member_id' => $request->post('member_id') ? (int)$request->post('member_id') : null,
'vendor_id' => $request->post('vendor_id') ? (int)$request->post('vendor_id') : null,
'bank_account_id' => $request->post('bank_account_id') ? (int)$request->post('bank_account_id') : null,
'notes' => $request->post('notes'),
'status' => 'in_hand',
'current_location' => 'safe',
'created_by' => (int)($session->get('employee_id') ?? 0) ?: null,
]);
return $this->redirect('/accounting/instruments?direction=' . $data['direction'])
->withSuccess('تم تسجيل الورقة التجارية بنجاح');
}
public function show(Request $request, string $id): Response
{
$this->authorize('accounting.instruments.view');
$instrument = NegotiableInstrument::findOrFail((int)$id);
return $this->view('Accounting/Views/instruments/show', [
'instrument' => $instrument->toArray(),
]);
}
public function changeStatus(Request $request, string $id): Response
{
$this->authorize('accounting.instruments.manage');
$instrument = NegotiableInstrument::findOrFail((int)$id);
$newStatus = $request->post('new_status');
if (!$instrument->canTransitionTo($newStatus)) {
return $this->redirect('/accounting/instruments/' . $id)
->withError('لا يمكن تغيير الحالة إلى: ' . (NegotiableInstrument::$statusLabels[$newStatus] ?? $newStatus));
}
$extra = [];
if ($request->post('bank_account_id')) {
$extra['bank_account_id'] = (int)$request->post('bank_account_id');
}
if ($request->post('endorsee_name')) {
$extra['endorsee_name'] = $request->post('endorsee_name');
}
$instrument->transitionTo($newStatus, $extra ?: null);
return $this->redirect('/accounting/instruments/' . $id)
->withSuccess('تم تغيير الحالة إلى: ' . (NegotiableInstrument::$statusLabels[$newStatus] ?? $newStatus));
}
public function dueSoon(Request $request): Response
{
$this->authorize('accounting.instruments.view');
$days = (int)($request->get('days') ?? 7);
$instruments = NegotiableInstrument::getDueWithin($days);
return $this->view('Accounting/Views/instruments/due_soon', [
'instruments' => $instruments,
'days' => $days,
]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\App;
use App\Core\Response;
use App\Modules\Accounting\Models\HistoricalBalance;
class OpeningEntryController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('accounting.opening_entry.view');
$db = App::getInstance()->db();
$fiscalYears = $db->select("SELECT id, name_ar, start_date, end_date, status FROM fiscal_years WHERE is_archived = 0 ORDER BY start_date DESC");
$selectedYearId = (int)($request->get('fiscal_year_id') ?? 0);
$historicalBalances = [];
if ($selectedYearId) {
$historicalBalances = $db->select("
SELECT hb.*, coa.code AS account_code, coa.name_ar AS account_name
FROM historical_balances hb
JOIN chart_of_accounts coa ON coa.id = hb.account_id
WHERE hb.fiscal_year_id = ?
ORDER BY coa.code
", [$selectedYearId]);
}
return $this->view('Accounting/Views/opening_entries/index', [
'fiscalYears' => $fiscalYears,
'selectedYearId' => $selectedYearId,
'historicalBalances' => $historicalBalances,
]);
}
public function create(Request $request): Response
{
$this->authorize('accounting.opening_entry.manage');
$db = App::getInstance()->db();
$fiscalYears = $db->select("SELECT id, name_ar, start_date, end_date, status FROM fiscal_years WHERE is_archived = 0 AND status = 'open' ORDER BY start_date DESC");
$accounts = $db->select("SELECT id, code, name_ar, account_type FROM chart_of_accounts WHERE is_archived = 0 AND is_header = 0 ORDER BY code");
return $this->view('Accounting/Views/opening_entries/create', [
'fiscalYears' => $fiscalYears,
'accounts' => $accounts,
]);
}
public function store(Request $request): Response
{
$this->authorize('accounting.opening_entry.manage');
$fiscalYearId = (int)$request->post('fiscal_year_id');
$entries = $request->post('entries', []);
if (!$fiscalYearId || empty($entries)) {
return $this->redirect('/accounting/opening-entries/create')
->withError('بيانات غير مكتملة');
}
$db = App::getInstance()->db();
$session = App::getInstance()->session();
$fy = $db->selectOne("SELECT * FROM fiscal_years WHERE id = ? AND status = 'open'", [$fiscalYearId]);
if (!$fy) {
return $this->redirect('/accounting/opening-entries/create')
->withError('السنة المالية غير مفتوحة');
}
$totalDebit = 0;
$totalCredit = 0;
$lines = [];
foreach ($entries as $entry) {
$accountId = (int)($entry['account_id'] ?? 0);
$debit = (float)($entry['debit'] ?? 0);
$credit = (float)($entry['credit'] ?? 0);
if (!$accountId || ($debit == 0 && $credit == 0)) {
continue;
}
$totalDebit += $debit;
$totalCredit += $credit;
$lines[] = [
'account_id' => $accountId,
'debit' => $debit,
'credit' => $credit,
'description' => $entry['description'] ?? 'قيد افتتاحي',
];
}
if (abs($totalDebit - $totalCredit) > 0.01) {
return $this->redirect('/accounting/opening-entries/create')
->withError('القيد غير متوازن — المدين: ' . number_format($totalDebit, 2) . ' الدائن: ' . number_format($totalCredit, 2));
}
$now = date('Y-m-d H:i:s');
$entryNumber = 'OPN-' . substr($fy['start_date'], 0, 4) . '-001';
$db->insert('journal_entries', [
'entry_number' => $entryNumber,
'entry_date' => $fy['start_date'],
'fiscal_year_id' => $fiscalYearId,
'reference_type' => 'opening',
'description' => 'قيد افتتاحي — ' . ($fy['name_ar'] ?? ''),
'total_debit' => $totalDebit,
'total_credit' => $totalCredit,
'status' => 'posted',
'posted_at' => $now,
'created_by' => (int)($session->get('employee_id') ?? 0) ?: null,
'created_at' => $now,
'updated_at' => $now,
]);
$journalEntryId = (int)$db->lastInsertId();
foreach ($lines as $line) {
$db->insert('journal_entry_lines', [
'journal_entry_id' => $journalEntryId,
'account_id' => $line['account_id'],
'debit_amount' => $line['debit'],
'credit_amount' => $line['credit'],
'description' => $line['description'],
'created_at' => $now,
'updated_at' => $now,
]);
}
return $this->redirect('/accounting/opening-entries?fiscal_year_id=' . $fiscalYearId)
->withSuccess('تم إنشاء القيد الافتتاحي بنجاح');
}
public function snapshot(Request $request): Response
{
$this->authorize('accounting.opening_entry.manage');
$fiscalYearId = (int)$request->post('fiscal_year_id');
if (!$fiscalYearId) {
return $this->redirect('/accounting/opening-entries')
->withError('اختر سنة مالية');
}
$count = HistoricalBalance::snapshotYear($fiscalYearId);
return $this->redirect('/accounting/opening-entries?fiscal_year_id=' . $fiscalYearId)
->withSuccess("تم حفظ أرصدة {$count} حساب كأرصدة تاريخية");
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Models;
use App\Core\Model;
class AccountBudget extends Model
{
protected static string $table = 'account_budgets';
protected static array $fillable = [
'fiscal_year_id', 'account_id', 'period',
'budgeted_amount', 'notes', 'created_by',
];
protected static bool $timestamps = true;
protected static bool $softDelete = false;
public static function getForYear(int $fiscalYearId): array
{
return static::query()
->where('fiscal_year_id', '=', $fiscalYearId)
->orderBy('account_id', 'ASC')
->get();
}
public static function getForAccount(int $accountId, int $fiscalYearId): array
{
return static::query()
->where('account_id', '=', $accountId)
->where('fiscal_year_id', '=', $fiscalYearId)
->orderBy('period', 'ASC')
->get();
}
public static function upsert(int $fiscalYearId, int $accountId, string $period, float $amount, ?string $notes = null, ?int $createdBy = null): void
{
$existing = static::query()
->where('fiscal_year_id', '=', $fiscalYearId)
->where('account_id', '=', $accountId)
->where('period', '=', $period)
->first();
if ($existing) {
$instance = new static($existing);
$instance->exists = true;
$instance->update([
'budgeted_amount' => $amount,
'notes' => $notes,
]);
} else {
static::create([
'fiscal_year_id' => $fiscalYearId,
'account_id' => $accountId,
'period' => $period,
'budgeted_amount' => $amount,
'notes' => $notes,
'created_by' => $createdBy,
]);
}
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Models;
use App\Core\Model;
use App\Core\App;
class AccountingDimension extends Model
{
protected static string $table = 'accounting_dimensions';
protected static array $fillable = [
'code', 'name_ar', 'name_en', 'is_required', 'is_active',
];
protected static bool $timestamps = true;
protected static bool $softDelete = false;
public static function getActive(): array
{
return static::query()
->where('is_active', '=', 1)
->orderBy('code', 'ASC')
->get();
}
public function getValues(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM accounting_dimension_values WHERE dimension_id = ? AND is_active = 1 ORDER BY code",
[(int)$this->id]
);
}
public static function getValuesForDimension(int $dimensionId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM accounting_dimension_values WHERE dimension_id = ? AND is_active = 1 ORDER BY code",
[$dimensionId]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Models;
use App\Core\Model;
class AccountingDimensionValue extends Model
{
protected static string $table = 'accounting_dimension_values';
protected static array $fillable = [
'dimension_id', 'code', 'name_ar', 'name_en', 'parent_id', 'is_active',
];
protected static bool $timestamps = true;
protected static bool $softDelete = false;
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Models;
use App\Core\Model;
class CostCenterBudget extends Model
{
protected static string $table = 'cost_center_budgets';
protected static array $fillable = [
'fiscal_year_id', 'cost_center_id', 'account_id', 'period',
'budgeted_amount', 'notes', 'created_by',
];
protected static bool $timestamps = true;
protected static bool $softDelete = false;
public static function getForYear(int $fiscalYearId): array
{
return static::query()
->where('fiscal_year_id', '=', $fiscalYearId)
->orderBy('cost_center_id', 'ASC')
->get();
}
public static function getForCostCenter(int $costCenterId, int $fiscalYearId): array
{
return static::query()
->where('cost_center_id', '=', $costCenterId)
->where('fiscal_year_id', '=', $fiscalYearId)
->orderBy('period', 'ASC')
->get();
}
public static function upsert(int $fiscalYearId, int $costCenterId, ?int $accountId, string $period, float $amount, ?string $notes = null, ?int $createdBy = null): void
{
$qb = static::query()
->where('fiscal_year_id', '=', $fiscalYearId)
->where('cost_center_id', '=', $costCenterId)
->where('period', '=', $period);
if ($accountId !== null) {
$qb->where('account_id', '=', $accountId);
} else {
$qb->whereNull('account_id');
}
$existing = $qb->first();
if ($existing) {
$instance = new static($existing);
$instance->exists = true;
$instance->update([
'budgeted_amount' => $amount,
'notes' => $notes,
]);
} else {
static::create([
'fiscal_year_id' => $fiscalYearId,
'cost_center_id' => $costCenterId,
'account_id' => $accountId,
'period' => $period,
'budgeted_amount' => $amount,
'notes' => $notes,
'created_by' => $createdBy,
]);
}
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Models;
use App\Core\Model;
class DailyTransaction extends Model
{
protected static string $table = 'daily_transactions';
protected static array $fillable = [
'transaction_date', 'journal_type_id', 'account_id', 'cost_center_id',
'amount', 'direction', 'description', 'document_number', 'currency',
'is_consolidated', 'consolidated_entry_id', 'branch_id', 'created_by',
];
protected static bool $timestamps = false;
protected static bool $softDelete = false;
public static function getUnconsolidated(?string $date = null): array
{
$qb = static::query()->where('is_consolidated', '=', 0);
if ($date) {
$qb->where('transaction_date', '=', $date);
}
return $qb->orderBy('transaction_date', 'ASC')->get();
}
public static function getByDate(string $date): array
{
return static::query()
->where('transaction_date', '=', $date)
->orderBy('id', 'ASC')
->get();
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Models;
use App\Core\Model;
use App\Core\App;
class HistoricalBalance extends Model
{
protected static string $table = 'historical_balances';
protected static array $fillable = [
'account_id', 'fiscal_year_id', 'period',
'debit_total', 'credit_total', 'closing_balance',
];
protected static bool $timestamps = false;
protected static bool $softDelete = false;
public static function getForYear(int $fiscalYearId): array
{
return static::query()
->where('fiscal_year_id', '=', $fiscalYearId)
->orderBy('account_id', 'ASC')
->get();
}
public static function snapshotYear(int $fiscalYearId): int
{
$db = App::getInstance()->db();
$balances = $db->select("
SELECT
jel.account_id,
SUM(jel.debit_amount) AS debit_total,
SUM(jel.credit_amount) AS credit_total,
SUM(jel.debit_amount) - SUM(jel.credit_amount) AS closing_balance
FROM journal_entry_lines jel
JOIN journal_entries je ON je.id = jel.journal_entry_id
WHERE je.fiscal_year_id = ? AND je.status = 'posted'
GROUP BY jel.account_id
", [$fiscalYearId]);
$now = date('Y-m-d H:i:s');
$fy = $db->selectOne("SELECT start_date FROM fiscal_years WHERE id = ?", [$fiscalYearId]);
$year = $fy ? substr($fy['start_date'], 0, 4) : date('Y');
$count = 0;
foreach ($balances as $bal) {
$existing = $db->selectOne(
"SELECT id FROM historical_balances WHERE account_id = ? AND fiscal_year_id = ? AND period = ?",
[(int)$bal['account_id'], $fiscalYearId, $year]
);
if ($existing) {
$db->update('historical_balances', [
'debit_total' => (float)$bal['debit_total'],
'credit_total' => (float)$bal['credit_total'],
'closing_balance' => (float)$bal['closing_balance'],
], 'id = ?', [(int)$existing['id']]);
} else {
$db->insert('historical_balances', [
'account_id' => (int)$bal['account_id'],
'fiscal_year_id' => $fiscalYearId,
'period' => $year,
'debit_total' => (float)$bal['debit_total'],
'credit_total' => (float)$bal['credit_total'],
'closing_balance' => (float)$bal['closing_balance'],
'created_at' => $now,
]);
}
$count++;
}
return $count;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Models;
use App\Core\Model;
use App\Core\App;
class InstrumentPortfolio extends Model
{
protected static string $table = 'instrument_portfolios';
protected static array $fillable = [
'portfolio_number', 'portfolio_type', 'portfolio_date',
'bank_account_id', 'total_amount', 'instrument_count',
'description', 'branch_id', 'journal_entry_id', 'status', 'created_by',
];
protected static bool $timestamps = true;
protected static bool $softDelete = false;
public static array $typeLabels = [
'deposit' => 'حافظة إيداع',
'collection' => 'حافظة تحصيل',
'payment' => 'حافظة دفع',
'endorsement' => 'حافظة تظهير',
'return' => 'حافظة مرتجعات',
'bounce' => 'حافظة مرتدات',
];
public static function generateNumber(string $type): string
{
$db = App::getInstance()->db();
$prefix = strtoupper(substr($type, 0, 3)) . '-' . date('Y');
$last = $db->selectOne(
"SELECT portfolio_number FROM instrument_portfolios WHERE portfolio_number LIKE ? ORDER BY id DESC LIMIT 1",
[$prefix . '%']
);
if ($last) {
$num = (int)substr($last['portfolio_number'], -4) + 1;
} else {
$num = 1;
}
return $prefix . '-' . str_pad((string)$num, 4, '0', STR_PAD_LEFT);
}
public function getItems(): array
{
$db = App::getInstance()->db();
return $db->select("
SELECT ni.*
FROM negotiable_instruments ni
JOIN instrument_portfolio_items ipi ON ipi.instrument_id = ni.id
WHERE ipi.portfolio_id = ?
ORDER BY ni.due_date ASC
", [(int)$this->id]);
}
public function addItem(int $instrumentId): void
{
$db = App::getInstance()->db();
$db->insert('instrument_portfolio_items', [
'portfolio_id' => (int)$this->id,
'instrument_id' => $instrumentId,
]);
$this->recalculate();
}
public function recalculate(): void
{
$db = App::getInstance()->db();
$stats = $db->selectOne("
SELECT COUNT(*) as cnt, COALESCE(SUM(ni.amount), 0) as total
FROM negotiable_instruments ni
JOIN instrument_portfolio_items ipi ON ipi.instrument_id = ni.id
WHERE ipi.portfolio_id = ?
", [(int)$this->id]);
$this->update([
'instrument_count' => (int)$stats['cnt'],
'total_amount' => (float)$stats['total'],
]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Models;
use App\Core\Model;
use App\Core\App;
class JournalType extends Model
{
protected static string $table = 'journal_types';
protected static array $fillable = [
'code', 'name_ar', 'name_en', 'short_name_ar', 'short_name_en',
'numbering_method', 'number_prefix', 'number_length',
'is_auto_generated', 'is_default', 'is_active',
];
protected static bool $timestamps = true;
protected static bool $softDelete = false;
public static function findByCode(string $code): ?static
{
$row = static::query()->where('code', '=', $code)->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)
->orderBy('code', 'ASC')
->get();
}
public static function getDefault(): ?static
{
$row = static::query()
->where('is_default', '=', 1)
->where('is_active', '=', 1)
->first();
if ($row === null) {
return null;
}
$instance = new static($row);
$instance->exists = true;
return $instance;
}
public function generateNextNumber(?int $fiscalYearId = null): string
{
$db = App::getInstance()->db();
$prefix = $this->number_prefix ?? $this->code . '-';
$length = (int) ($this->number_length ?? 6);
if ($this->numbering_method === 'monthly') {
$period = date('Ym');
$likePattern = $prefix . $period . '%';
} else {
$year = date('Y');
$likePattern = $prefix . $year . '%';
}
$last = $db->selectOne(
"SELECT entry_number FROM journal_entries
WHERE entry_number LIKE ?
ORDER BY entry_number DESC LIMIT 1",
[$likePattern]
);
if ($last) {
$lastNum = (int) substr($last['entry_number'], -$length);
$nextNum = $lastNum + 1;
} else {
$nextNum = 1;
}
if ($this->numbering_method === 'monthly') {
return $prefix . date('Ym') . str_pad((string) $nextNum, $length, '0', STR_PAD_LEFT);
}
return $prefix . date('Y') . str_pad((string) $nextNum, $length, '0', STR_PAD_LEFT);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Models;
use App\Core\Model;
class NegotiableInstrument extends Model
{
protected static string $table = 'negotiable_instruments';
protected static array $fillable = [
'instrument_type', 'direction', 'instrument_number', 'amount', 'currency',
'issue_date', 'due_date', 'drawer_name', 'drawer_bank',
'beneficiary_name', 'endorsee_name', 'status', 'current_location',
'bank_account_id', 'member_id', 'vendor_id', 'payment_id',
'journal_entry_id', 'notes', 'is_archived', 'created_by',
];
protected static bool $timestamps = true;
protected static bool $softDelete = true;
public const STATUS_IN_HAND = 'in_hand';
public const STATUS_UNDER_COLLECTION = 'under_collection';
public const STATUS_COLLECTED = 'collected';
public const STATUS_BOUNCED = 'bounced';
public const STATUS_ENDORSED = 'endorsed';
public const STATUS_CANCELLED = 'cancelled';
public const STATUS_PAID = 'paid';
public const STATUS_RETURNED = 'returned';
public static array $statusLabels = [
'in_hand' => 'في الخزينة',
'under_collection' => 'تحت التحصيل',
'collected' => 'محصّل',
'bounced' => 'مرتد',
'endorsed' => 'مظهّر',
'cancelled' => 'ملغي',
'paid' => 'مدفوع',
'returned' => 'مرتجع',
];
public static array $directionLabels = [
'receivable' => 'أوراق قبض',
'payable' => 'أوراق دفع',
];
public static function getReceivables(string $status = ''): array
{
$qb = static::query()->where('direction', '=', 'receivable');
if ($status) {
$qb->where('status', '=', $status);
}
return $qb->orderBy('due_date', 'ASC')->get();
}
public static function getPayables(string $status = ''): array
{
$qb = static::query()->where('direction', '=', 'payable');
if ($status) {
$qb->where('status', '=', $status);
}
return $qb->orderBy('due_date', 'ASC')->get();
}
public static function getDueWithin(int $days): array
{
$future = date('Y-m-d', strtotime("+{$days} days"));
return static::query()
->where('status', '=', 'in_hand')
->where('due_date', '<=', $future)
->where('due_date', '>=', date('Y-m-d'))
->orderBy('due_date', 'ASC')
->get();
}
public function canTransitionTo(string $newStatus): bool
{
$allowed = $this->getAllowedTransitions();
return in_array($newStatus, $allowed, true);
}
public function getAllowedTransitions(): array
{
$map = [
'in_hand' => ['under_collection', 'endorsed', 'cancelled', 'paid', 'returned'],
'under_collection' => ['collected', 'bounced', 'returned'],
'collected' => [],
'bounced' => ['in_hand', 'cancelled'],
'endorsed' => ['returned'],
'cancelled' => [],
'paid' => [],
'returned' => ['in_hand'],
];
return $map[$this->status] ?? [];
}
public function transitionTo(string $newStatus, ?array $extra = null): bool
{
if (!$this->canTransitionTo($newStatus)) {
return false;
}
$data = ['status' => $newStatus];
if ($newStatus === 'under_collection' || $newStatus === 'collected') {
$data['current_location'] = 'bank';
} elseif ($newStatus === 'endorsed') {
$data['current_location'] = 'endorsed_to';
if (isset($extra['endorsee_name'])) {
$data['endorsee_name'] = $extra['endorsee_name'];
}
} elseif ($newStatus === 'returned') {
$data['current_location'] = 'returned_to_drawer';
} elseif ($newStatus === 'in_hand') {
$data['current_location'] = 'safe';
}
if ($extra) {
if (isset($extra['bank_account_id'])) {
$data['bank_account_id'] = $extra['bank_account_id'];
}
if (isset($extra['journal_entry_id'])) {
$data['journal_entry_id'] = $extra['journal_entry_id'];
}
}
$this->update($data);
return true;
}
}
This diff is collapsed.
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Services;
use App\Core\App;
class BudgetService
{
public static function getVarianceReport(int $fiscalYearId, ?string $fromPeriod = null, ?string $toPeriod = null): array
{
$db = App::getInstance()->db();
$params = [$fiscalYearId];
$periodFilter = '';
if ($fromPeriod && $toPeriod) {
$periodFilter = " AND ab.period BETWEEN ? AND ?";
$params[] = $fromPeriod;
$params[] = $toPeriod;
}
$budgets = $db->select("
SELECT
ab.account_id,
coa.code AS account_code,
coa.name_ar AS account_name,
coa.account_type,
SUM(ab.budgeted_amount) AS total_budget
FROM account_budgets ab
JOIN chart_of_accounts coa ON coa.id = ab.account_id
WHERE ab.fiscal_year_id = ?
{$periodFilter}
GROUP BY ab.account_id, coa.code, coa.name_ar, coa.account_type
ORDER BY coa.code
", $params);
$fy = $db->selectOne("SELECT start_date, end_date FROM fiscal_years WHERE id = ?", [$fiscalYearId]);
if (!$fy) {
return [];
}
$actualParams = [$fiscalYearId];
$actualPeriodFilter = '';
if ($fromPeriod && $toPeriod) {
$actualPeriodFilter = " AND je.entry_date BETWEEN ? AND ?";
$actualParams[] = $fromPeriod . '-01';
$actualParams[] = $toPeriod . '-31';
}
$actuals = $db->select("
SELECT
jel.account_id,
SUM(jel.debit_amount) AS total_debit,
SUM(jel.credit_amount) AS total_credit
FROM journal_entry_lines jel
JOIN journal_entries je ON je.id = jel.journal_entry_id
WHERE je.fiscal_year_id = ?
AND je.status = 'posted'
{$actualPeriodFilter}
GROUP BY jel.account_id
", $actualParams);
$actualMap = [];
foreach ($actuals as $a) {
$actualMap[(int)$a['account_id']] = $a;
}
$report = [];
foreach ($budgets as $b) {
$accountId = (int)$b['account_id'];
$actual = $actualMap[$accountId] ?? ['total_debit' => 0, 'total_credit' => 0];
$debit = (float)$actual['total_debit'];
$credit = (float)$actual['total_credit'];
if (in_array($b['account_type'], ['expense', 'asset'])) {
$actualAmount = $debit - $credit;
} else {
$actualAmount = $credit - $debit;
}
$budget = (float)$b['total_budget'];
$variance = $budget - $actualAmount;
$variancePct = $budget != 0 ? ($variance / $budget) * 100 : 0;
$report[] = [
'account_id' => $accountId,
'account_code' => $b['account_code'],
'account_name' => $b['account_name'],
'account_type' => $b['account_type'],
'budgeted' => $budget,
'actual' => $actualAmount,
'variance' => $variance,
'variance_pct' => round($variancePct, 1),
];
}
return $report;
}
public static function getCostCenterVariance(int $fiscalYearId, int $costCenterId): array
{
$db = App::getInstance()->db();
$budgets = $db->select("
SELECT
ccb.account_id,
coa.code AS account_code,
coa.name_ar AS account_name,
coa.account_type,
SUM(ccb.budgeted_amount) AS total_budget
FROM cost_center_budgets ccb
LEFT JOIN chart_of_accounts coa ON coa.id = ccb.account_id
WHERE ccb.fiscal_year_id = ? AND ccb.cost_center_id = ?
GROUP BY ccb.account_id, coa.code, coa.name_ar, coa.account_type
ORDER BY coa.code
", [$fiscalYearId, $costCenterId]);
$actuals = $db->select("
SELECT
jel.account_id,
SUM(jel.debit_amount) AS total_debit,
SUM(jel.credit_amount) AS total_credit
FROM journal_entry_lines jel
JOIN journal_entries je ON je.id = jel.journal_entry_id
WHERE je.fiscal_year_id = ?
AND je.status = 'posted'
AND jel.cost_center_id = ?
GROUP BY jel.account_id
", [$fiscalYearId, $costCenterId]);
$actualMap = [];
foreach ($actuals as $a) {
$actualMap[(int)$a['account_id']] = $a;
}
$report = [];
foreach ($budgets as $b) {
$accountId = (int)($b['account_id'] ?? 0);
$actual = $actualMap[$accountId] ?? ['total_debit' => 0, 'total_credit' => 0];
$debit = (float)$actual['total_debit'];
$credit = (float)$actual['total_credit'];
$accountType = $b['account_type'] ?? 'expense';
if (in_array($accountType, ['expense', 'asset'])) {
$actualAmount = $debit - $credit;
} else {
$actualAmount = $credit - $debit;
}
$budget = (float)$b['total_budget'];
$variance = $budget - $actualAmount;
$variancePct = $budget != 0 ? ($variance / $budget) * 100 : 0;
$report[] = [
'account_id' => $accountId,
'account_code' => $b['account_code'] ?? '—',
'account_name' => $b['account_name'] ?? 'إجمالي المركز',
'account_type' => $accountType,
'budgeted' => $budget,
'actual' => $actualAmount,
'variance' => $variance,
'variance_pct' => round($variancePct, 1),
];
}
return $report;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Services;
use App\Core\App;
use App\Modules\Accounting\Models\DailyTransaction;
class DailyTransactionService
{
public static function consolidate(string $date, ?int $journalTypeId = null): ?int
{
$db = App::getInstance()->db();
$qb = DailyTransaction::query()
->where('is_consolidated', '=', 0)
->where('transaction_date', '=', $date);
if ($journalTypeId) {
$qb->where('journal_type_id', '=', $journalTypeId);
}
$transactions = $qb->get();
if (empty($transactions)) {
return null;
}
$lines = [];
$totalDebit = 0;
$totalCredit = 0;
foreach ($transactions as $tx) {
$accountId = (int)$tx['account_id'];
$amount = (float)$tx['amount'];
$direction = $tx['direction'];
$debit = $direction === 'debit' ? $amount : 0;
$credit = $direction === 'credit' ? $amount : 0;
$totalDebit += $debit;
$totalCredit += $credit;
$lines[] = [
'account_id' => $accountId,
'debit_amount' => $debit,
'credit_amount' => $credit,
'cost_center_id' => $tx['cost_center_id'] ? (int)$tx['cost_center_id'] : null,
'description' => $tx['description'] ?? 'حركة يومية مجمعة',
];
}
$fy = $db->selectOne("SELECT id FROM fiscal_years WHERE is_current = 1 AND is_archived = 0");
if (!$fy) {
return null;
}
$now = date('Y-m-d H:i:s');
$entryNumber = 'DT-' . str_replace('-', '', $date) . '-001';
$existing = $db->selectOne("SELECT id FROM journal_entries WHERE entry_number = ?", [$entryNumber]);
if ($existing) {
$suffix = $db->selectOne(
"SELECT COUNT(*) as cnt FROM journal_entries WHERE entry_number LIKE ?",
['DT-' . str_replace('-', '', $date) . '%']
);
$num = ((int)($suffix['cnt'] ?? 0)) + 1;
$entryNumber = 'DT-' . str_replace('-', '', $date) . '-' . str_pad((string)$num, 3, '0', STR_PAD_LEFT);
}
$db->insert('journal_entries', [
'entry_number' => $entryNumber,
'entry_date' => $date,
'fiscal_year_id' => (int)$fy['id'],
'reference_type' => 'daily_consolidated',
'description' => 'قيد مجمع — حركات يومية ' . $date,
'total_debit' => $totalDebit,
'total_credit' => $totalCredit,
'status' => 'posted',
'posted_at' => $now,
'created_at' => $now,
'updated_at' => $now,
]);
$journalEntryId = (int)$db->lastInsertId();
foreach ($lines as $line) {
$db->insert('journal_entry_lines', [
'journal_entry_id' => $journalEntryId,
'account_id' => $line['account_id'],
'debit_amount' => $line['debit_amount'],
'credit_amount' => $line['credit_amount'],
'cost_center_id' => $line['cost_center_id'],
'description' => $line['description'],
'created_at' => $now,
'updated_at' => $now,
]);
}
$txIds = array_column($transactions, 'id');
if (!empty($txIds)) {
$placeholders = implode(',', array_fill(0, count($txIds), '?'));
$db->query(
"UPDATE daily_transactions SET is_consolidated = 1, consolidated_entry_id = ? WHERE id IN ({$placeholders})",
array_merge([$journalEntryId], $txIds)
);
}
return $journalEntryId;
}
}
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>موازنات مراكز التكلفة<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<h2 style="margin:0;">الموازنات التقديرية — مراكز التكلفة</h2>
<a href="/accounting/budgets<?= $selectedYearId ? '?fiscal_year_id='.$selectedYearId : '' ?>" class="btn btn-outline">موازنات الحسابات</a>
</div>
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;">
<form method="GET" style="display:flex;gap:10px;align-items:end;">
<div style="flex:1;">
<label class="form-label" style="font-size:12px;">السنة المالية</label>
<select name="fiscal_year_id" class="form-select" onchange="this.form.submit()">
<option value="">— اختر —</option>
<?php foreach ($fiscalYears as $fy): ?>
<option value="<?= (int)$fy['id'] ?>" <?= (int)$fy['id'] === $selectedYearId ? 'selected' : '' ?>><?= e($fy['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
</form>
</div>
</div>
<?php if ($selectedYearId): ?>
<div class="card" style="margin-bottom:15px;">
<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/budgets/cost-centers">
<?= csrf_field() ?>
<input type="hidden" name="fiscal_year_id" value="<?= $selectedYearId ?>">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:15px;">
<div>
<label class="form-label">مركز التكلفة <span style="color:#DC2626;">*</span></label>
<select name="cost_center_id" class="form-select" required>
<option value="">— اختر —</option>
<?php foreach ($costCenters as $cc): ?>
<option value="<?= (int)$cc['id'] ?>"><?= e($cc['code']) ?><?= e($cc['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">ملاحظات</label>
<input type="text" name="budget_notes" class="form-input" placeholder="ملاحظات اختيارية">
</div>
</div>
<div style="overflow-x:auto;">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<?php for ($m = 1; $m <= 12; $m++): ?>
<?php $period = date('Y') . '-' . str_pad((string)$m, 2, '0', STR_PAD_LEFT); ?>
<th style="text-align:center;font-size:12px;"><?= $period ?></th>
<?php endfor; ?>
</tr>
</thead>
<tbody>
<tr>
<?php for ($m = 1; $m <= 12; $m++): ?>
<?php $period = date('Y') . '-' . str_pad((string)$m, 2, '0', STR_PAD_LEFT); ?>
<td><input type="number" name="amounts[<?= $period ?>]" class="form-input" style="width:100px;text-align:center;" step="0.01" min="0" value="0"></td>
<?php endfor; ?>
</tr>
</tbody>
</table>
</div>
<div style="margin-top:15px;">
<button type="submit" class="btn btn-primary">حفظ الموازنة</button>
</div>
</form>
</div>
</div>
<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 style="text-align:center;">إجمالي الموازنة</th>
<th style="text-align:center;">عدد الشهور</th>
</tr>
</thead>
<tbody>
<?php
$ccMap = [];
foreach ($costCenters as $cc) $ccMap[(int)$cc['id']] = $cc;
?>
<?php if (!empty($budgets)): ?>
<?php foreach ($budgets as $ccId => $periods): ?>
<?php
$cc = $ccMap[$ccId] ?? null;
$total = array_sum(array_column($periods, 'budgeted_amount'));
?>
<tr>
<td><?= $cc ? e($cc['code'] . ' — ' . $cc['name_ar']) : 'مركز #' . $ccId ?></td>
<td style="text-align:center;font-weight:600;"><?= number_format($total, 2) ?></td>
<td style="text-align:center;"><?= count($periods) ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="3" style="text-align:center;color:#6B7280;padding:30px;">لا توجد موازنات مسجلة</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>الموازنات التقديرية<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<h2 style="margin:0;">الموازنات التقديرية — حسابات</h2>
<a href="/accounting/budgets/cost-centers<?= $selectedYearId ? '?fiscal_year_id='.$selectedYearId : '' ?>" class="btn btn-outline">موازنات مراكز التكلفة</a>
</div>
<!-- Year Selector -->
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;">
<form method="GET" style="display:flex;gap:10px;align-items:end;">
<div style="flex:1;">
<label class="form-label" style="font-size:12px;">السنة المالية</label>
<select name="fiscal_year_id" class="form-select" onchange="this.form.submit()">
<option value="">— اختر —</option>
<?php foreach ($fiscalYears as $fy): ?>
<option value="<?= (int)$fy['id'] ?>" <?= (int)$fy['id'] === $selectedYearId ? 'selected' : '' ?>><?= e($fy['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
</form>
</div>
</div>
<?php if ($selectedYearId): ?>
<!-- Budget Entry Form -->
<div class="card" style="margin-bottom:15px;">
<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/budgets">
<?= csrf_field() ?>
<input type="hidden" name="fiscal_year_id" value="<?= $selectedYearId ?>">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:15px;">
<div>
<label class="form-label">الحساب <span style="color:#DC2626;">*</span></label>
<select name="account_id" class="form-select" required>
<option value="">— اختر حساب —</option>
<?php foreach ($accounts as $acc): ?>
<option value="<?= (int)$acc['id'] ?>"><?= e($acc['code']) ?><?= e($acc['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">ملاحظات</label>
<input type="text" name="budget_notes" class="form-input" placeholder="ملاحظات اختيارية">
</div>
</div>
<div style="overflow-x:auto;">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<?php for ($m = 1; $m <= 12; $m++): ?>
<?php $period = date('Y') . '-' . str_pad((string)$m, 2, '0', STR_PAD_LEFT); ?>
<th style="text-align:center;font-size:12px;"><?= $period ?></th>
<?php endfor; ?>
</tr>
</thead>
<tbody>
<tr>
<?php for ($m = 1; $m <= 12; $m++): ?>
<?php $period = date('Y') . '-' . str_pad((string)$m, 2, '0', STR_PAD_LEFT); ?>
<td><input type="number" name="amounts[<?= $period ?>]" class="form-input" style="width:100px;text-align:center;" step="0.01" min="0" value="0"></td>
<?php endfor; ?>
</tr>
</tbody>
</table>
</div>
<div style="margin-top:15px;">
<button type="submit" class="btn btn-primary">حفظ الموازنة</button>
</div>
</form>
</div>
</div>
<!-- Existing Budgets Summary -->
<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 style="text-align:center;">إجمالي الموازنة</th>
<th style="text-align:center;">عدد الشهور</th>
</tr>
</thead>
<tbody>
<?php
$accountMap = [];
foreach ($accounts as $acc) $accountMap[(int)$acc['id']] = $acc;
?>
<?php if (!empty($budgets)): ?>
<?php foreach ($budgets as $accountId => $periods): ?>
<?php
$acc = $accountMap[$accountId] ?? null;
$total = array_sum(array_column($periods, 'budgeted_amount'));
?>
<tr>
<td><?= $acc ? e($acc['code'] . ' — ' . $acc['name_ar']) : 'حساب #' . $accountId ?></td>
<td style="text-align:center;font-weight:600;"><?= number_format($total, 2) ?></td>
<td style="text-align:center;"><?= count($periods) ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="3" style="text-align:center;color:#6B7280;padding:30px;">لا توجد موازنات مسجلة لهذه السنة</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<div style="margin-top:15px;">
<a href="/accounting/budgets/variance<?= $selectedYearId ? '?fiscal_year_id='.$selectedYearId : '' ?>" class="btn btn-outline">تقرير الانحرافات</a>
</div>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تقرير الانحرافات<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<h2 style="margin:0;">تقرير انحرافات الموازنة</h2>
<a href="/accounting/budgets<?= $selectedYearId ? '?fiscal_year_id='.$selectedYearId : '' ?>" class="btn btn-outline">العودة للموازنات</a>
</div>
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;">
<form method="GET" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
<div>
<label class="form-label" style="font-size:12px;">السنة المالية</label>
<select name="fiscal_year_id" class="form-select">
<?php foreach ($fiscalYears as $fy): ?>
<option value="<?= (int)$fy['id'] ?>" <?= (int)$fy['id'] === $selectedYearId ? 'selected' : '' ?>><?= e($fy['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label" style="font-size:12px;">من فترة</label>
<input type="month" name="from_period" class="form-input" value="<?= e($fromPeriod ?? '') ?>" dir="ltr">
</div>
<div>
<label class="form-label" style="font-size:12px;">إلى فترة</label>
<input type="month" name="to_period" class="form-input" value="<?= e($toPeriod ?? '') ?>" dir="ltr">
</div>
<div>
<button type="submit" class="btn btn-primary">عرض</button>
</div>
</form>
</div>
</div>
<?php if (!empty($report)): ?>
<div class="card">
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>كود الحساب</th>
<th>اسم الحساب</th>
<th>النوع</th>
<th style="text-align:center;">الموازنة</th>
<th style="text-align:center;">الفعلي</th>
<th style="text-align:center;">الانحراف</th>
<th style="text-align:center;">النسبة %</th>
</tr>
</thead>
<tbody>
<?php
$totalBudget = 0;
$totalActual = 0;
$totalVariance = 0;
$typeLabels = ['expense' => 'مصروف', 'revenue' => 'إيراد', 'asset' => 'أصل', 'liability' => 'التزام', 'equity' => 'حقوق ملكية'];
?>
<?php foreach ($report as $row): ?>
<?php
$totalBudget += $row['budgeted'];
$totalActual += $row['actual'];
$totalVariance += $row['variance'];
$isOverrun = $row['variance'] < 0;
?>
<tr>
<td style="direction:ltr;text-align:right;font-weight:600;"><?= e($row['account_code']) ?></td>
<td><?= e($row['account_name']) ?></td>
<td><?= $typeLabels[$row['account_type']] ?? $row['account_type'] ?></td>
<td style="text-align:center;"><?= number_format($row['budgeted'], 2) ?></td>
<td style="text-align:center;"><?= number_format($row['actual'], 2) ?></td>
<td style="text-align:center;color:<?= $isOverrun ? '#DC2626' : '#059669' ?>;font-weight:600;">
<?= number_format($row['variance'], 2) ?>
</td>
<td style="text-align:center;color:<?= $isOverrun ? '#DC2626' : '#059669' ?>;">
<?= $row['variance_pct'] ?>%
</td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr style="font-weight:700;background:#F9FAFB;">
<td colspan="3">الإجمالي</td>
<td style="text-align:center;"><?= number_format($totalBudget, 2) ?></td>
<td style="text-align:center;"><?= number_format($totalActual, 2) ?></td>
<td style="text-align:center;color:<?= $totalVariance < 0 ? '#DC2626' : '#059669' ?>;"><?= number_format($totalVariance, 2) ?></td>
<td style="text-align:center;"></td>
</tr>
</tfoot>
</table>
</div>
</div>
<?php elseif ($selectedYearId): ?>
<div class="card">
<div style="padding:30px;text-align:center;color:#6B7280;">
لا توجد بيانات موازنة لهذه السنة المالية
</div>
</div>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>الحركات اليومية<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<h2 style="margin:0;">الحركات اليومية — <?= e($date) ?></h2>
</div>
<!-- Date Selector -->
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;">
<form method="GET" style="display:flex;gap:10px;align-items:end;">
<div>
<label class="form-label" style="font-size:12px;">التاريخ</label>
<input type="date" name="date" class="form-input" value="<?= e($date) ?>" dir="ltr" onchange="this.form.submit()">
</div>
</form>
</div>
</div>
<!-- Quick Entry Form -->
<div class="card" style="margin-bottom:15px;">
<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/daily-transactions">
<?= csrf_field() ?>
<input type="hidden" name="transaction_date" value="<?= e($date) ?>">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:15px;">
<div>
<label class="form-label">الحساب <span style="color:#DC2626;">*</span></label>
<select name="account_id" class="form-select" required>
<option value="">— اختر —</option>
<?php foreach ($accounts as $acc): ?>
<option value="<?= (int)$acc['id'] ?>"><?= e($acc['code']) ?><?= e($acc['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">المبلغ <span style="color:#DC2626;">*</span></label>
<input type="number" name="amount" class="form-input" required step="0.01" min="0.01" dir="ltr">
</div>
<div>
<label class="form-label">الاتجاه <span style="color:#DC2626;">*</span></label>
<select name="direction" class="form-select" required>
<option value="debit">مدين</option>
<option value="credit">دائن</option>
</select>
</div>
<div>
<label class="form-label">نوع اليومية</label>
<select name="journal_type_id" class="form-select">
<option value="">— بدون —</option>
<?php foreach ($journalTypes as $jt): ?>
<option value="<?= (int)$jt['id'] ?>"><?= e($jt['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">مركز التكلفة</label>
<select name="cost_center_id" class="form-select">
<option value="">— بدون —</option>
<?php foreach ($costCenters as $cc): ?>
<option value="<?= (int)$cc['id'] ?>"><?= e($cc['code']) ?><?= e($cc['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">رقم المستند</label>
<input type="text" name="document_number" class="form-input" dir="ltr">
</div>
<div style="grid-column:span 2;">
<label class="form-label">بيان</label>
<input type="text" name="description" class="form-input">
</div>
</div>
<div style="margin-top:15px;">
<button type="submit" class="btn btn-primary">تسجيل</button>
</div>
</form>
</div>
</div>
<!-- Transactions List -->
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0;">حركات اليوم</h3>
<?php
$unconsolidated = array_filter($transactions, fn($t) => !(int)$t['is_consolidated']);
?>
<?php if (!empty($unconsolidated)): ?>
<form method="POST" action="/accounting/daily-transactions/consolidate" style="display:inline;">
<?= csrf_field() ?>
<input type="hidden" name="date" value="<?= e($date) ?>">
<button type="submit" class="btn btn-primary" onclick="return confirm('سيتم تجميع كل الحركات غير المجمعة في قيد يومية واحد. متابعة؟');">
تجميع في قيد (<?= count($unconsolidated) ?> حركة)
</button>
</form>
<?php endif; ?>
</div>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>#</th>
<th>الحساب</th>
<th>مدين</th>
<th>دائن</th>
<th>نوع اليومية</th>
<th>بيان</th>
<th>مستند</th>
<th>الحالة</th>
</tr>
</thead>
<tbody>
<?php
$totalDebit = 0; $totalCredit = 0;
foreach ($transactions as $i => $tx):
$isDebit = $tx['direction'] === 'debit';
$amount = (float)$tx['amount'];
if ($isDebit) $totalDebit += $amount; else $totalCredit += $amount;
$acc = $accountMap[(int)$tx['account_id']] ?? null;
$jt = isset($tx['journal_type_id']) ? ($typeMap[(int)$tx['journal_type_id']] ?? null) : null;
?>
<tr>
<td><?= $i + 1 ?></td>
<td><?= $acc ? e($acc['code'] . ' — ' . $acc['name_ar']) : '#' . $tx['account_id'] ?></td>
<td style="text-align:center;<?= $isDebit ? 'font-weight:600;' : 'color:#9CA3AF;' ?>"><?= $isDebit ? number_format($amount, 2) : '—' ?></td>
<td style="text-align:center;<?= !$isDebit ? 'font-weight:600;' : 'color:#9CA3AF;' ?>"><?= !$isDebit ? number_format($amount, 2) : '—' ?></td>
<td><?= $jt ? e($jt['name_ar']) : '—' ?></td>
<td><?= e($tx['description'] ?? '—') ?></td>
<td style="direction:ltr;text-align:right;"><?= e($tx['document_number'] ?? '—') ?></td>
<td>
<?php if ((int)$tx['is_consolidated']): ?>
<span style="color:#059669;font-weight:600;">مجمّع</span>
<?php else: ?>
<span style="color:#D97706;">معلّق</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($transactions)): ?>
<tr><td colspan="8" style="text-align:center;color:#6B7280;padding:30px;">لا توجد حركات لهذا اليوم</td></tr>
<?php endif; ?>
</tbody>
<?php if (!empty($transactions)): ?>
<tfoot>
<tr style="font-weight:700;background:#F9FAFB;">
<td colspan="2">الإجمالي</td>
<td style="text-align:center;"><?= number_format($totalDebit, 2) ?></td>
<td style="text-align:center;"><?= number_format($totalCredit, 2) ?></td>
<td colspan="4"></td>
</tr>
</tfoot>
<?php endif; ?>
</table>
</div>
</div>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>الأبعاد المحاسبية<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<h2 style="margin:0;">الأبعاد المحاسبية</h2>
<button type="button" id="btn-new-dim" class="btn btn-primary">+ بُعد جديد</button>
</div>
<div class="card" id="create-form" style="margin-bottom:15px;display:none;">
<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/dimensions">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:15px;">
<div>
<label class="form-label">الكود <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" class="form-input" required maxlength="20" dir="ltr" style="text-transform:uppercase;">
</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 style="display:flex;align-items:end;">
<label class="form-label" style="display:flex;align-items:center;gap:6px;margin:0;">
<input type="checkbox" name="is_required" value="1"> إلزامي
</label>
</div>
</div>
<div style="margin-top:15px;display:flex;gap:8px;">
<button type="submit" class="btn btn-primary">حفظ</button>
<button type="button" id="btn-cancel-dim" class="btn btn-outline">إلغاء</button>
</div>
</form>
</div>
</div>
<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>
</tr>
</thead>
<tbody>
<?php foreach ($dimensions as $dim): ?>
<tr>
<td style="font-weight:600;direction:ltr;text-align:right;"><?= e($dim['code']) ?></td>
<td><?= e($dim['name_ar']) ?></td>
<td dir="ltr" style="text-align:right;"><?= e($dim['name_en']) ?></td>
<td><?= (int)$dim['is_required'] ? '<span style="color:#D97706;">نعم</span>' : '—' ?></td>
<td>
<form method="POST" action="/accounting/dimensions/<?= (int)$dim['id'] ?>/toggle-active" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" style="background:none;border:none;cursor:pointer;color:<?= (int)$dim['is_active'] ? '#059669' : '#DC2626' ?>;font-weight:600;">
<?= (int)$dim['is_active'] ? 'نشط' : 'موقف' ?>
</button>
</form>
</td>
<td><a href="/accounting/dimensions/<?= (int)$dim['id'] ?>" class="btn btn-sm btn-outline">القيم</a></td>
</tr>
<?php endforeach; ?>
<?php if (empty($dimensions)): ?>
<tr><td colspan="6" style="text-align:center;color:#6B7280;padding:30px;">لا توجد أبعاد محاسبية</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<script>
(function(){
var btn = document.getElementById('btn-new-dim');
var form = document.getElementById('create-form');
var cancel = document.getElementById('btn-cancel-dim');
if(btn) btn.addEventListener('click', function(){ form.style.display='block'; btn.style.display='none'; });
if(cancel) cancel.addEventListener('click', function(){ form.style.display='none'; btn.style.display=''; });
})();
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>قيم البُعد المحاسبي<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<h2 style="margin:0;"><?= e($dimension['name_ar']) ?> (<?= e($dimension['code']) ?>)</h2>
<a href="/accounting/dimensions" class="btn btn-outline">العودة</a>
</div>
<div class="card" style="margin-bottom:15px;">
<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/dimensions/<?= (int)$dimension['id'] ?>/values">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:15px;">
<div>
<label class="form-label">الكود <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" class="form-input" required maxlength="20" dir="ltr" style="text-transform:uppercase;">
</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="parent_id" class="form-select">
<option value="">— بدون —</option>
<?php foreach ($values as $v): ?>
<option value="<?= (int)$v['id'] ?>"><?= e($v['code']) ?><?= e($v['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div style="margin-top:15px;">
<button type="submit" class="btn btn-primary">إضافة</button>
</div>
</form>
</div>
</div>
<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>
</tr>
</thead>
<tbody>
<?php
$valMap = [];
foreach ($values as $v) $valMap[(int)$v['id']] = $v;
?>
<?php foreach ($values as $v): ?>
<tr>
<td style="font-weight:600;direction:ltr;text-align:right;"><?= e($v['code']) ?></td>
<td><?= e($v['name_ar']) ?></td>
<td dir="ltr" style="text-align:right;"><?= e($v['name_en']) ?></td>
<td><?= isset($v['parent_id']) && isset($valMap[(int)$v['parent_id']]) ? e($valMap[(int)$v['parent_id']]['name_ar']) : '—' ?></td>
<td><span style="color:<?= (int)$v['is_active'] ? '#059669' : '#DC2626' ?>;"><?= (int)$v['is_active'] ? 'نشط' : 'موقف' ?></span></td>
</tr>
<?php endforeach; ?>
<?php if (empty($values)): ?>
<tr><td colspan="5" style="text-align:center;color:#6B7280;padding:30px;">لا توجد قيم — أضف قيمًا أعلاه</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Accounting\Models\NegotiableInstrument;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>أوراق مستحقة قريبًا<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<h2 style="margin:0;">أوراق مستحقة خلال <?= $days ?> يوم</h2>
<a href="/accounting/instruments" class="btn btn-outline">العودة</a>
</div>
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;">
<form method="GET" style="display:flex;gap:10px;align-items:end;">
<div>
<label class="form-label" style="font-size:12px;">عدد الأيام</label>
<select name="days" class="form-select" onchange="this.form.submit()">
<option value="3" <?= $days === 3 ? 'selected' : '' ?>>3 أيام</option>
<option value="7" <?= $days === 7 ? 'selected' : '' ?>>7 أيام</option>
<option value="14" <?= $days === 14 ? 'selected' : '' ?>>14 يوم</option>
<option value="30" <?= $days === 30 ? 'selected' : '' ?>>30 يوم</option>
</select>
</div>
</form>
</div>
</div>
<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 ($instruments as $inst): ?>
<tr>
<td style="font-weight:600;direction:ltr;text-align:right;"><?= e($inst['instrument_number']) ?></td>
<td><?= $inst['instrument_type'] === 'check' ? 'شيك' : 'كمبيالة' ?></td>
<td><?= NegotiableInstrument::$directionLabels[$inst['direction']] ?? $inst['direction'] ?></td>
<td style="font-weight:600;"><?= number_format((float)$inst['amount'], 2) ?></td>
<td style="direction:ltr;text-align:right;color:#D97706;font-weight:600;"><?= e($inst['due_date']) ?></td>
<td><?= e($inst['drawer_name'] ?? '—') ?></td>
<td><a href="/accounting/instruments/<?= (int)$inst['id'] ?>" class="btn btn-sm btn-outline">عرض</a></td>
</tr>
<?php endforeach; ?>
<?php if (empty($instruments)): ?>
<tr><td colspan="7" style="text-align:center;color:#6B7280;padding:30px;">لا توجد أوراق مستحقة خلال هذه الفترة</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Accounting\Models\NegotiableInstrument;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>الأوراق التجارية<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<h2 style="margin:0;"><?= $direction === 'receivable' ? 'أوراق القبض' : 'أوراق الدفع' ?></h2>
<div style="display:flex;gap:8px;">
<a href="/accounting/instruments/due-soon" class="btn btn-outline">مستحقة قريبًا</a>
<a href="/accounting/portfolios" class="btn btn-outline">الحوافظ</a>
<button type="button" id="btn-new-inst" class="btn btn-primary">+ ورقة جديدة</button>
</div>
</div>
<!-- Filters -->
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;">
<form method="GET" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
<div>
<label class="form-label" style="font-size:12px;">الاتجاه</label>
<select name="direction" class="form-select" onchange="this.form.submit()">
<option value="receivable" <?= $direction === 'receivable' ? 'selected' : '' ?>>أوراق قبض</option>
<option value="payable" <?= $direction === 'payable' ? 'selected' : '' ?>>أوراق دفع</option>
</select>
</div>
<div>
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-select" onchange="this.form.submit()">
<option value="">الكل</option>
<?php foreach (NegotiableInstrument::$statusLabels as $k => $v): ?>
<option value="<?= $k ?>" <?= $status === $k ? 'selected' : '' ?>><?= $v ?></option>
<?php endforeach; ?>
</select>
</div>
</form>
</div>
</div>
<!-- Create Form -->
<div class="card" id="create-form" style="margin-bottom:15px;display:none;">
<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/instruments">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:15px;">
<div>
<label class="form-label">النوع <span style="color:#DC2626;">*</span></label>
<select name="instrument_type" class="form-select" required>
<option value="check">شيك</option>
<option value="promissory_note">كمبيالة</option>
</select>
</div>
<div>
<label class="form-label">الاتجاه <span style="color:#DC2626;">*</span></label>
<select name="direction" class="form-select" required>
<option value="receivable" <?= $direction === 'receivable' ? 'selected' : '' ?>>ورقة قبض</option>
<option value="payable" <?= $direction === 'payable' ? 'selected' : '' ?>>ورقة دفع</option>
</select>
</div>
<div>
<label class="form-label">رقم الورقة <span style="color:#DC2626;">*</span></label>
<input type="text" name="instrument_number" class="form-input" required dir="ltr">
</div>
<div>
<label class="form-label">المبلغ <span style="color:#DC2626;">*</span></label>
<input type="number" name="amount" class="form-input" required step="0.01" min="0.01" dir="ltr">
</div>
<div>
<label class="form-label">تاريخ الإصدار <span style="color:#DC2626;">*</span></label>
<input type="date" name="issue_date" class="form-input" required dir="ltr" value="<?= date('Y-m-d') ?>">
</div>
<div>
<label class="form-label">تاريخ الاستحقاق <span style="color:#DC2626;">*</span></label>
<input type="date" name="due_date" class="form-input" required dir="ltr">
</div>
<div>
<label class="form-label">اسم الساحب</label>
<input type="text" name="drawer_name" class="form-input">
</div>
<div>
<label class="form-label">بنك الساحب</label>
<input type="text" name="drawer_bank" class="form-input">
</div>
<div>
<label class="form-label">المستفيد</label>
<input type="text" name="beneficiary_name" class="form-input">
</div>
<div>
<label class="form-label">الحساب البنكي</label>
<select name="bank_account_id" class="form-select">
<option value="">— بدون —</option>
<?php foreach ($bankAccounts as $ba): ?>
<option value="<?= (int)$ba['id'] ?>"><?= e($ba['account_name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="grid-column:1/-1;">
<label class="form-label">ملاحظات</label>
<input type="text" name="notes" class="form-input">
</div>
</div>
<div style="margin-top:15px;display:flex;gap:8px;">
<button type="submit" class="btn btn-primary">حفظ</button>
<button type="button" id="btn-cancel-inst" class="btn btn-outline">إلغاء</button>
</div>
</form>
</div>
</div>
<!-- List -->
<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 ($instruments as $inst): ?>
<tr>
<td style="font-weight:600;direction:ltr;text-align:right;"><?= e($inst['instrument_number']) ?></td>
<td><?= $inst['instrument_type'] === 'check' ? 'شيك' : 'كمبيالة' ?></td>
<td style="font-weight:600;"><?= number_format((float)$inst['amount'], 2) ?></td>
<td style="direction:ltr;text-align:right;"><?= e($inst['due_date']) ?></td>
<td><?= e($inst['drawer_name'] ?? '—') ?></td>
<td>
<?php
$statusColor = match($inst['status']) {
'collected', 'paid' => '#059669',
'bounced' => '#DC2626',
'under_collection' => '#D97706',
'endorsed' => '#2563EB',
'cancelled' => '#6B7280',
default => '#374151',
};
?>
<span style="color:<?= $statusColor ?>;font-weight:600;"><?= NegotiableInstrument::$statusLabels[$inst['status']] ?? $inst['status'] ?></span>
</td>
<td><a href="/accounting/instruments/<?= (int)$inst['id'] ?>" class="btn btn-sm btn-outline">عرض</a></td>
</tr>
<?php endforeach; ?>
<?php if (empty($instruments)): ?>
<tr><td colspan="7" style="text-align:center;color:#6B7280;padding:30px;">لا توجد أوراق تجارية</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<script>
(function(){
var btn = document.getElementById('btn-new-inst');
var form = document.getElementById('create-form');
var cancel = document.getElementById('btn-cancel-inst');
if(btn) btn.addEventListener('click', function(){ form.style.display='block'; btn.style.display='none'; });
if(cancel) cancel.addEventListener('click', function(){ form.style.display='none'; btn.style.display=''; });
})();
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Accounting\Models\NegotiableInstrument;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>تفاصيل الورقة التجارية<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<h2 style="margin:0;">ورقة #<?= e($instrument['instrument_number']) ?></h2>
<a href="/accounting/instruments?direction=<?= e($instrument['direction']) ?>" class="btn btn-outline">العودة</a>
</div>
<div class="card" style="margin-bottom:15px;">
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:15px;">
<div>
<span style="font-size:12px;color:#6B7280;">النوع</span>
<p style="margin:4px 0;font-weight:600;"><?= $instrument['instrument_type'] === 'check' ? 'شيك' : 'كمبيالة' ?></p>
</div>
<div>
<span style="font-size:12px;color:#6B7280;">الاتجاه</span>
<p style="margin:4px 0;font-weight:600;"><?= NegotiableInstrument::$directionLabels[$instrument['direction']] ?? $instrument['direction'] ?></p>
</div>
<div>
<span style="font-size:12px;color:#6B7280;">الحالة</span>
<p style="margin:4px 0;font-weight:700;color:<?= $instrument['status'] === 'bounced' ? '#DC2626' : '#059669' ?>;">
<?= NegotiableInstrument::$statusLabels[$instrument['status']] ?? $instrument['status'] ?>
</p>
</div>
<div>
<span style="font-size:12px;color:#6B7280;">المبلغ</span>
<p style="margin:4px 0;font-weight:700;font-size:18px;"><?= number_format((float)$instrument['amount'], 2) ?> <?= e($instrument['currency']) ?></p>
</div>
<div>
<span style="font-size:12px;color:#6B7280;">تاريخ الإصدار</span>
<p style="margin:4px 0;"><?= e($instrument['issue_date']) ?></p>
</div>
<div>
<span style="font-size:12px;color:#6B7280;">تاريخ الاستحقاق</span>
<p style="margin:4px 0;font-weight:600;"><?= e($instrument['due_date']) ?></p>
</div>
<div>
<span style="font-size:12px;color:#6B7280;">الساحب</span>
<p style="margin:4px 0;"><?= e($instrument['drawer_name'] ?? '—') ?></p>
</div>
<div>
<span style="font-size:12px;color:#6B7280;">بنك الساحب</span>
<p style="margin:4px 0;"><?= e($instrument['drawer_bank'] ?? '—') ?></p>
</div>
<div>
<span style="font-size:12px;color:#6B7280;">المستفيد</span>
<p style="margin:4px 0;"><?= e($instrument['beneficiary_name'] ?? '—') ?></p>
</div>
<?php if ($instrument['endorsee_name']): ?>
<div>
<span style="font-size:12px;color:#6B7280;">المظهّر إليه</span>
<p style="margin:4px 0;"><?= e($instrument['endorsee_name']) ?></p>
</div>
<?php endif; ?>
<div>
<span style="font-size:12px;color:#6B7280;">الموقع الحالي</span>
<p style="margin:4px 0;"><?php
$locations = ['safe' => 'الخزينة', 'bank' => 'البنك', 'endorsed_to' => 'مظهّر', 'returned_to_drawer' => 'مرتجع للساحب'];
echo $locations[$instrument['current_location']] ?? $instrument['current_location'];
?></p>
</div>
<?php if ($instrument['notes']): ?>
<div style="grid-column:1/-1;">
<span style="font-size:12px;color:#6B7280;">ملاحظات</span>
<p style="margin:4px 0;"><?= e($instrument['notes']) ?></p>
</div>
<?php endif; ?>
</div>
</div>
</div>
<!-- Status Change -->
<?php
$inst = new NegotiableInstrument($instrument);
$inst->exists = true;
$allowed = $inst->getAllowedTransitions();
?>
<?php if (!empty($allowed)): ?>
<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/instruments/<?= (int)$instrument['id'] ?>/change-status">
<?= csrf_field() ?>
<div style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
<div>
<label class="form-label">الحالة الجديدة</label>
<select name="new_status" class="form-select" required>
<?php foreach ($allowed as $s): ?>
<option value="<?= $s ?>"><?= NegotiableInstrument::$statusLabels[$s] ?? $s ?></option>
<?php endforeach; ?>
</select>
</div>
<div id="endorsee-field" style="display:none;">
<label class="form-label">المظهّر إليه</label>
<input type="text" name="endorsee_name" class="form-input">
</div>
<div>
<button type="submit" class="btn btn-primary">تنفيذ</button>
</div>
</div>
</form>
</div>
</div>
<script>
(function(){
var sel = document.querySelector('[name="new_status"]');
var ef = document.getElementById('endorsee-field');
if(sel && ef) sel.addEventListener('change', function(){ ef.style.display = this.value==='endorsed'?'block':'none'; });
})();
</script>
<?php endif; ?>
<?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($type['code']) ?><?= e($type['name_ar']) ?></h3>
</div>
<div style="padding:20px;">
<form method="POST" action="/accounting/journal-types/<?= (int)$type['id'] ?>">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:15px;">
<div>
<label class="form-label">الكود</label>
<input type="text" class="form-input" value="<?= e($type['code']) ?>" disabled dir="ltr" style="background:#F3F4F6;">
</div>
<div>
<label class="form-label">الاسم (عربي) <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" class="form-input" value="<?= e($type['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($type['name_en']) ?>" required dir="ltr">
</div>
<div>
<label class="form-label">الاسم المختصر (عربي)</label>
<input type="text" name="short_name_ar" class="form-input" value="<?= e($type['short_name_ar'] ?? '') ?>" maxlength="20">
</div>
<div>
<label class="form-label">الاسم المختصر (إنجليزي)</label>
<input type="text" name="short_name_en" class="form-input" value="<?= e($type['short_name_en'] ?? '') ?>" dir="ltr" maxlength="20">
</div>
<div>
<label class="form-label">طريقة الترقيم</label>
<select name="numbering_method" class="form-select">
<option value="yearly" <?= ($type['numbering_method'] ?? 'yearly') === 'yearly' ? 'selected' : '' ?>>سنوي</option>
<option value="monthly" <?= ($type['numbering_method'] ?? '') === 'monthly' ? 'selected' : '' ?>>شهري</option>
</select>
</div>
<div>
<label class="form-label">بادئة الرقم</label>
<input type="text" name="number_prefix" class="form-input" value="<?= e($type['number_prefix'] ?? '') ?>" dir="ltr" maxlength="10">
</div>
<div>
<label class="form-label">طول الرقم</label>
<input type="number" name="number_length" class="form-input" value="<?= (int)($type['number_length'] ?? 6) ?>" min="3" max="10" dir="ltr">
</div>
<div style="display:flex;align-items:end;gap:20px;">
<label class="form-label" style="display:flex;align-items:center;gap:6px;margin:0;">
<input type="checkbox" name="is_auto_generated" value="1" <?= (int)($type['is_auto_generated'] ?? 0) ? 'checked' : '' ?>> آلي فقط
</label>
<label class="form-label" style="display:flex;align-items:center;gap:6px;margin:0;">
<input type="checkbox" name="is_default" value="1" <?= (int)($type['is_default'] ?? 0) ? 'checked' : '' ?>> افتراضي
</label>
<label class="form-label" style="display:flex;align-items:center;gap:6px;margin:0;">
<input type="checkbox" name="is_active" value="1" <?= (int)($type['is_active'] ?? 1) ? 'checked' : '' ?>> نشط
</label>
</div>
</div>
<div style="margin-top:20px;display:flex;gap:8px;">
<button type="submit" class="btn btn-primary">حفظ</button>
<a href="/accounting/journal-types" 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 style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<h2 style="margin:0;">أنواع اليومية</h2>
<button type="button" id="btn-new-type" class="btn btn-primary">+ نوع يومية جديد</button>
</div>
<!-- Create Form (hidden by default) -->
<div class="card" id="create-form" style="margin-bottom:15px;display:none;">
<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/journal-types">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:15px;">
<div>
<label class="form-label">الكود <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" class="form-input" required maxlength="10" dir="ltr" placeholder="GEN" style="text-transform:uppercase;">
</div>
<div>
<label class="form-label">الاسم (عربي) <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" class="form-input" required placeholder="يومية عامة">
</div>
<div>
<label class="form-label">الاسم (إنجليزي) <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_en" class="form-input" required dir="ltr" placeholder="General Journal">
</div>
<div>
<label class="form-label">الاسم المختصر (عربي)</label>
<input type="text" name="short_name_ar" class="form-input" maxlength="20">
</div>
<div>
<label class="form-label">الاسم المختصر (إنجليزي)</label>
<input type="text" name="short_name_en" class="form-input" dir="ltr" maxlength="20">
</div>
<div>
<label class="form-label">طريقة الترقيم</label>
<select name="numbering_method" class="form-select">
<option value="yearly">سنوي</option>
<option value="monthly">شهري</option>
</select>
</div>
<div>
<label class="form-label">بادئة الرقم</label>
<input type="text" name="number_prefix" class="form-input" dir="ltr" maxlength="10" placeholder="GEN-">
</div>
<div>
<label class="form-label">طول الرقم</label>
<input type="number" name="number_length" class="form-input" value="6" min="3" max="10" dir="ltr">
</div>
<div style="display:flex;align-items:end;gap:20px;">
<label class="form-label" style="display:flex;align-items:center;gap:6px;margin:0;">
<input type="checkbox" name="is_auto_generated" value="1"> آلي فقط
</label>
<label class="form-label" style="display:flex;align-items:center;gap:6px;margin:0;">
<input type="checkbox" name="is_default" value="1"> افتراضي
</label>
</div>
</div>
<div style="margin-top:15px;display:flex;gap:8px;">
<button type="submit" class="btn btn-primary">حفظ</button>
<button type="button" id="btn-cancel-create" class="btn btn-outline">إلغاء</button>
</div>
</form>
</div>
</div>
<!-- List -->
<div class="card">
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>الكود</th>
<th>الاسم</th>
<th>طريقة الترقيم</th>
<th>البادئة</th>
<th>آلي</th>
<th>افتراضي</th>
<th>الحالة</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($types as $t): ?>
<tr>
<td style="font-weight:600;direction:ltr;text-align:right;"><?= e($t['code']) ?></td>
<td><?= e($t['name_ar']) ?><br><small style="color:#6B7280;"><?= e($t['name_en']) ?></small></td>
<td><?= $t['numbering_method'] === 'yearly' ? 'سنوي' : 'شهري' ?></td>
<td style="direction:ltr;text-align:right;"><?= e($t['number_prefix'] ?? '—') ?></td>
<td><?= (int)$t['is_auto_generated'] ? '<span style="color:#059669;">&#10003;</span>' : '—' ?></td>
<td><?= (int)$t['is_default'] ? '<span style="color:#2563EB;">&#10003;</span>' : '—' ?></td>
<td>
<form method="POST" action="/accounting/journal-types/<?= (int)$t['id'] ?>/toggle-active" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" style="background:none;border:none;cursor:pointer;color:<?= (int)$t['is_active'] ? '#059669' : '#DC2626' ?>;font-weight:600;">
<?= (int)$t['is_active'] ? 'نشط' : 'موقف' ?>
</button>
</form>
</td>
<td><a href="/accounting/journal-types/<?= (int)$t['id'] ?>/edit" class="btn btn-sm btn-outline">تعديل</a></td>
</tr>
<?php endforeach; ?>
<?php if (empty($types)): ?>
<tr><td colspan="8" style="text-align:center;color:#6B7280;padding:30px;">لا توجد أنواع يومية — أضف نوعًا جديدًا</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<script>
(function(){
var btn = document.getElementById('btn-new-type');
var form = document.getElementById('create-form');
var cancel = document.getElementById('btn-cancel-create');
if(btn) btn.addEventListener('click', function(){ form.style.display='block'; btn.style.display='none'; });
if(cancel) cancel.addEventListener('click', function(){ form.style.display='none'; btn.style.display=''; });
})();
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إنشاء قيد افتتاحي<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<h2 style="margin:0;">إنشاء قيد افتتاحي</h2>
<a href="/accounting/opening-entries" class="btn btn-outline">العودة</a>
</div>
<div class="card">
<div style="padding:20px;">
<form method="POST" action="/accounting/opening-entries" id="opening-form">
<?= csrf_field() ?>
<div style="margin-bottom:20px;">
<label class="form-label">السنة المالية <span style="color:#DC2626;">*</span></label>
<select name="fiscal_year_id" class="form-select" required style="max-width:300px;">
<option value="">— اختر —</option>
<?php foreach ($fiscalYears as $fy): ?>
<option value="<?= (int)$fy['id'] ?>"><?= e($fy['name_ar']) ?> (<?= e($fy['start_date']) ?>)</option>
<?php endforeach; ?>
</select>
</div>
<div class="table-responsive">
<table class="data-table" style="width:100%;" id="lines-table">
<thead>
<tr>
<th style="width:35%;">الحساب</th>
<th style="width:20%;">مدين</th>
<th style="width:20%;">دائن</th>
<th style="width:20%;">بيان</th>
<th style="width:5%;"></th>
</tr>
</thead>
<tbody id="lines-body">
<tr>
<td>
<select name="entries[0][account_id]" class="form-select" required>
<option value="">— اختر حساب —</option>
<?php foreach ($accounts as $acc): ?>
<option value="<?= (int)$acc['id'] ?>"><?= e($acc['code']) ?><?= e($acc['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</td>
<td><input type="number" name="entries[0][debit]" class="form-input line-debit" step="0.01" min="0" value="0" dir="ltr"></td>
<td><input type="number" name="entries[0][credit]" class="form-input line-credit" step="0.01" min="0" value="0" dir="ltr"></td>
<td><input type="text" name="entries[0][description]" class="form-input" value="قيد افتتاحي"></td>
<td></td>
</tr>
</tbody>
<tfoot>
<tr style="font-weight:700;background:#F9FAFB;">
<td>الإجمالي</td>
<td id="total-debit" style="text-align:center;">0.00</td>
<td id="total-credit" style="text-align:center;">0.00</td>
<td colspan="2" id="balance-msg"></td>
</tr>
</tfoot>
</table>
</div>
<div style="margin-top:15px;display:flex;gap:8px;">
<button type="button" id="btn-add-line" class="btn btn-outline">+ سطر</button>
<button type="submit" class="btn btn-primary">حفظ القيد الافتتاحي</button>
</div>
</form>
</div>
</div>
<script>
(function(){
var idx = 1;
var body = document.getElementById('lines-body');
var btn = document.getElementById('btn-add-line');
var accountOptions = body.querySelector('select').innerHTML;
btn.addEventListener('click', function(){
var tr = document.createElement('tr');
tr.innerHTML = '<td><select name="entries['+idx+'][account_id]" class="form-select" required>'+accountOptions+'</select></td>'
+'<td><input type="number" name="entries['+idx+'][debit]" class="form-input line-debit" step="0.01" min="0" value="0" dir="ltr"></td>'
+'<td><input type="number" name="entries['+idx+'][credit]" class="form-input line-credit" step="0.01" min="0" value="0" dir="ltr"></td>'
+'<td><input type="text" name="entries['+idx+'][description]" class="form-input" value="قيد افتتاحي"></td>'
+'<td><button type="button" class="btn btn-sm btn-outline" onclick="this.closest(\'tr\').remove();calcTotals();">×</button></td>';
body.appendChild(tr);
idx++;
bindCalc();
});
function calcTotals(){
var d=0,c=0;
document.querySelectorAll('.line-debit').forEach(function(i){d+=parseFloat(i.value)||0;});
document.querySelectorAll('.line-credit').forEach(function(i){c+=parseFloat(i.value)||0;});
document.getElementById('total-debit').textContent=d.toFixed(2);
document.getElementById('total-credit').textContent=c.toFixed(2);
var diff=Math.abs(d-c);
var msg=document.getElementById('balance-msg');
if(diff<0.01){msg.textContent='متوازن';msg.style.color='#059669';}
else{msg.textContent='فرق: '+diff.toFixed(2);msg.style.color='#DC2626';}
}
function bindCalc(){
document.querySelectorAll('.line-debit,.line-credit').forEach(function(i){i.removeEventListener('input',calcTotals);i.addEventListener('input',calcTotals);});
}
bindCalc();
})();
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>القيود الافتتاحية والأرصدة التاريخية<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<h2 style="margin:0;">القيود الافتتاحية والأرصدة التاريخية</h2>
<a href="/accounting/opening-entries/create" class="btn btn-primary">+ قيد افتتاحي جديد</a>
</div>
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;">
<form method="GET" style="display:flex;gap:10px;align-items:end;">
<div style="flex:1;">
<label class="form-label" style="font-size:12px;">السنة المالية</label>
<select name="fiscal_year_id" class="form-select" onchange="this.form.submit()">
<option value="">— اختر —</option>
<?php foreach ($fiscalYears as $fy): ?>
<option value="<?= (int)$fy['id'] ?>" <?= (int)$fy['id'] === $selectedYearId ? 'selected' : '' ?>><?= e($fy['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php if ($selectedYearId): ?>
<div>
<form method="POST" action="/accounting/opening-entries/snapshot" style="display:inline;">
<?= csrf_field() ?>
<input type="hidden" name="fiscal_year_id" value="<?= $selectedYearId ?>">
<button type="submit" class="btn btn-outline" onclick="return confirm('سيتم حفظ الأرصدة الحالية كأرصدة تاريخية. متابعة؟');">حفظ كأرصدة تاريخية</button>
</form>
</div>
<?php endif; ?>
</form>
</div>
</div>
<?php if ($selectedYearId && !empty($historicalBalances)): ?>
<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 style="text-align:center;">إجمالي المدين</th>
<th style="text-align:center;">إجمالي الدائن</th>
<th style="text-align:center;">الرصيد الختامي</th>
</tr>
</thead>
<tbody>
<?php
$sumDebit = 0; $sumCredit = 0; $sumBalance = 0;
foreach ($historicalBalances as $hb):
$sumDebit += (float)$hb['debit_total'];
$sumCredit += (float)$hb['credit_total'];
$sumBalance += (float)$hb['closing_balance'];
?>
<tr>
<td style="font-weight:600;direction:ltr;text-align:right;"><?= e($hb['account_code']) ?></td>
<td><?= e($hb['account_name']) ?></td>
<td style="text-align:center;"><?= number_format((float)$hb['debit_total'], 2) ?></td>
<td style="text-align:center;"><?= number_format((float)$hb['credit_total'], 2) ?></td>
<td style="text-align:center;font-weight:600;color:<?= (float)$hb['closing_balance'] >= 0 ? '#059669' : '#DC2626' ?>;">
<?= number_format((float)$hb['closing_balance'], 2) ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr style="font-weight:700;background:#F9FAFB;">
<td colspan="2">الإجمالي</td>
<td style="text-align:center;"><?= number_format($sumDebit, 2) ?></td>
<td style="text-align:center;"><?= number_format($sumCredit, 2) ?></td>
<td style="text-align:center;"><?= number_format($sumBalance, 2) ?></td>
</tr>
</tfoot>
</table>
</div>
</div>
<?php elseif ($selectedYearId): ?>
<div class="card">
<div style="padding:30px;text-align:center;color:#6B7280;">
لا توجد أرصدة تاريخية محفوظة لهذه السنة. اضغط "حفظ كأرصدة تاريخية" لإنشائها.
</div>
</div>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Accounting\Models\InstrumentPortfolio;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>إنشاء حافظة<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<h2 style="margin:0;">إنشاء حافظة جديدة</h2>
<a href="/accounting/portfolios" class="btn btn-outline">العودة</a>
</div>
<div class="card">
<div style="padding:20px;">
<form method="POST" action="/accounting/portfolios">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:15px;margin-bottom:20px;">
<div>
<label class="form-label">نوع الحافظة <span style="color:#DC2626;">*</span></label>
<select name="portfolio_type" class="form-select" required>
<?php foreach (InstrumentPortfolio::$typeLabels as $k => $v): ?>
<option value="<?= $k ?>"><?= $v ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">التاريخ <span style="color:#DC2626;">*</span></label>
<input type="date" name="portfolio_date" class="form-input" required value="<?= date('Y-m-d') ?>" dir="ltr">
</div>
<div>
<label class="form-label">الحساب البنكي</label>
<select name="bank_account_id" class="form-select">
<option value="">— بدون —</option>
<?php foreach ($bankAccounts as $ba): ?>
<option value="<?= (int)$ba['id'] ?>"><?= e($ba['account_name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="grid-column:1/-1;">
<label class="form-label">وصف</label>
<input type="text" name="description" class="form-input" placeholder="وصف الحافظة (اختياري)">
</div>
</div>
<h4 style="margin:0 0 10px;">اختر الأوراق</h4>
<?php if (!empty($availableInstruments)): ?>
<div class="table-responsive" style="max-height:400px;overflow-y:auto;">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th style="width:40px;"><input type="checkbox" id="select-all"></th>
<th>رقم الورقة</th>
<th>المبلغ</th>
<th>تاريخ الاستحقاق</th>
<th>الساحب</th>
</tr>
</thead>
<tbody>
<?php foreach ($availableInstruments as $inst): ?>
<tr>
<td><input type="checkbox" name="instrument_ids[]" value="<?= (int)$inst['id'] ?>"></td>
<td style="direction:ltr;text-align:right;"><?= e($inst['instrument_number']) ?></td>
<td style="font-weight:600;"><?= number_format((float)$inst['amount'], 2) ?></td>
<td style="direction:ltr;text-align:right;"><?= e($inst['due_date']) ?></td>
<td><?= e($inst['drawer_name'] ?? '—') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<p style="color:#6B7280;text-align:center;padding:20px;">لا توجد أوراق متاحة (في الخزينة)</p>
<?php endif; ?>
<div style="margin-top:20px;display:flex;gap:8px;">
<button type="submit" class="btn btn-primary">إنشاء الحافظة</button>
<a href="/accounting/portfolios" class="btn btn-outline">إلغاء</a>
</div>
</form>
</div>
</div>
<script>
(function(){
var sa = document.getElementById('select-all');
if(sa) sa.addEventListener('change', function(){
document.querySelectorAll('[name="instrument_ids[]"]').forEach(function(cb){ cb.checked = sa.checked; });
});
})();
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Accounting\Models\InstrumentPortfolio;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>حوافظ الأوراق التجارية<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<h2 style="margin:0;">الحوافظ</h2>
<div style="display:flex;gap:8px;">
<a href="/accounting/instruments" class="btn btn-outline">الأوراق التجارية</a>
<a href="/accounting/portfolios/create" class="btn btn-primary">+ حافظة جديدة</a>
</div>
</div>
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;">
<form method="GET" style="display:flex;gap:10px;align-items:end;">
<div>
<label class="form-label" style="font-size:12px;">نوع الحافظة</label>
<select name="type" class="form-select" onchange="this.form.submit()">
<option value="">الكل</option>
<?php foreach (InstrumentPortfolio::$typeLabels as $k => $v): ?>
<option value="<?= $k ?>" <?= $type === $k ? 'selected' : '' ?>><?= $v ?></option>
<?php endforeach; ?>
</select>
</div>
</form>
</div>
</div>
<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 ($portfolios as $p): ?>
<tr>
<td style="font-weight:600;direction:ltr;text-align:right;"><?= e($p['portfolio_number']) ?></td>
<td><?= InstrumentPortfolio::$typeLabels[$p['portfolio_type']] ?? $p['portfolio_type'] ?></td>
<td style="direction:ltr;text-align:right;"><?= e($p['portfolio_date']) ?></td>
<td style="text-align:center;"><?= (int)$p['instrument_count'] ?></td>
<td style="font-weight:600;"><?= number_format((float)$p['total_amount'], 2) ?></td>
<td>
<span style="color:<?= $p['status'] === 'confirmed' ? '#059669' : '#D97706' ?>;font-weight:600;">
<?= $p['status'] === 'confirmed' ? 'مؤكدة' : 'مسودة' ?>
</span>
</td>
<td><a href="/accounting/portfolios/<?= (int)$p['id'] ?>" class="btn btn-sm btn-outline">عرض</a></td>
</tr>
<?php endforeach; ?>
<?php if (empty($portfolios)): ?>
<tr><td colspan="7" style="text-align:center;color:#6B7280;padding:30px;">لا توجد حوافظ</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Accounting\Models\InstrumentPortfolio;
use App\Modules\Accounting\Models\NegotiableInstrument;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>تفاصيل الحافظة<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<h2 style="margin:0;">حافظة: <?= e($portfolio['portfolio_number']) ?></h2>
<a href="/accounting/portfolios" class="btn btn-outline">العودة</a>
</div>
<div class="card" style="margin-bottom:15px;">
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:15px;">
<div>
<span style="font-size:12px;color:#6B7280;">النوع</span>
<p style="margin:4px 0;font-weight:600;"><?= InstrumentPortfolio::$typeLabels[$portfolio['portfolio_type']] ?? $portfolio['portfolio_type'] ?></p>
</div>
<div>
<span style="font-size:12px;color:#6B7280;">التاريخ</span>
<p style="margin:4px 0;"><?= e($portfolio['portfolio_date']) ?></p>
</div>
<div>
<span style="font-size:12px;color:#6B7280;">عدد الأوراق</span>
<p style="margin:4px 0;font-weight:600;"><?= (int)$portfolio['instrument_count'] ?></p>
</div>
<div>
<span style="font-size:12px;color:#6B7280;">الإجمالي</span>
<p style="margin:4px 0;font-weight:700;font-size:18px;"><?= number_format((float)$portfolio['total_amount'], 2) ?></p>
</div>
<div>
<span style="font-size:12px;color:#6B7280;">الحالة</span>
<p style="margin:4px 0;font-weight:600;color:<?= $portfolio['status'] === 'confirmed' ? '#059669' : '#D97706' ?>;">
<?= $portfolio['status'] === 'confirmed' ? 'مؤكدة' : 'مسودة' ?>
</p>
</div>
<?php if ($portfolio['description']): ?>
<div style="grid-column:span 3;">
<span style="font-size:12px;color:#6B7280;">وصف</span>
<p style="margin:4px 0;"><?= e($portfolio['description']) ?></p>
</div>
<?php endif; ?>
</div>
</div>
</div>
<!-- Items -->
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;">أوراق الحافظة</h3>
</div>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>رقم الورقة</th>
<th>النوع</th>
<th>المبلغ</th>
<th>تاريخ الاستحقاق</th>
<th>الساحب</th>
<th>الحالة</th>
</tr>
</thead>
<tbody>
<?php foreach ($items as $item): ?>
<tr>
<td style="direction:ltr;text-align:right;"><?= e($item['instrument_number']) ?></td>
<td><?= $item['instrument_type'] === 'check' ? 'شيك' : 'كمبيالة' ?></td>
<td style="font-weight:600;"><?= number_format((float)$item['amount'], 2) ?></td>
<td style="direction:ltr;text-align:right;"><?= e($item['due_date']) ?></td>
<td><?= e($item['drawer_name'] ?? '—') ?></td>
<td><?= NegotiableInstrument::$statusLabels[$item['status']] ?? $item['status'] ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($items)): ?>
<tr><td colspan="6" style="text-align:center;color:#6B7280;padding:20px;">لا توجد أوراق في هذه الحافظة</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if ($portfolio['status'] === 'draft'): ?>
<form method="POST" action="/accounting/portfolios/<?= (int)$portfolio['id'] ?>/confirm">
<?= csrf_field() ?>
<button type="submit" class="btn btn-primary" onclick="return confirm('هل أنت متأكد من تأكيد الحافظة؟ سيتم تحديث حالة جميع الأوراق.');">تأكيد الحافظة</button>
</form>
<?php endif; ?>
<?php $__template->endSection(); ?>
...@@ -33,24 +33,48 @@ PermissionRegistry::register('accounting', [ ...@@ -33,24 +33,48 @@ PermissionRegistry::register('accounting', [
'accounting.coa.view' => ['ar' => 'عرض دليل الحسابات', 'en' => 'View Chart of Accounts'], 'accounting.coa.view' => ['ar' => 'عرض دليل الحسابات', 'en' => 'View Chart of Accounts'],
'accounting.coa.manage' => ['ar' => 'إدارة دليل الحسابات', 'en' => 'Manage Chart of Accounts'], 'accounting.coa.manage' => ['ar' => 'إدارة دليل الحسابات', 'en' => 'Manage Chart of Accounts'],
// Journal Types
'accounting.journal_type.view' => ['ar' => 'عرض أنواع اليومية', 'en' => 'View Journal Types'],
'accounting.journal_type.manage' => ['ar' => 'إدارة أنواع اليومية', 'en' => 'Manage Journal Types'],
// Cost Centers // Cost Centers
'accounting.cost_center.view' => ['ar' => 'عرض مراكز التكلفة', 'en' => 'View Cost Centers'], 'accounting.cost_center.view' => ['ar' => 'عرض مراكز التكلفة', 'en' => 'View Cost Centers'],
'accounting.cost_center.manage' => ['ar' => 'إدارة مراكز التكلفة', 'en' => 'Manage Cost Centers'], 'accounting.cost_center.manage' => ['ar' => 'إدارة مراكز التكلفة', 'en' => 'Manage Cost Centers'],
// Budgets
'accounting.budget.view' => ['ar' => 'عرض الموازنات التقديرية', 'en' => 'View Budgets'],
'accounting.budget.manage' => ['ar' => 'إدارة الموازنات التقديرية', 'en' => 'Manage Budgets'],
// Bank Accounts // Bank Accounts
'accounting.bank_account.view' => ['ar' => 'عرض الحسابات البنكية', 'en' => 'View Bank Accounts'], 'accounting.bank_account.view' => ['ar' => 'عرض الحسابات البنكية', 'en' => 'View Bank Accounts'],
'accounting.bank_account.manage' => ['ar' => 'إدارة الحسابات البنكية', 'en' => 'Manage Bank Accounts'], 'accounting.bank_account.manage' => ['ar' => 'إدارة الحسابات البنكية', 'en' => 'Manage Bank Accounts'],
// Negotiable Instruments
'accounting.instruments.view' => ['ar' => 'عرض الأوراق التجارية', 'en' => 'View Negotiable Instruments'],
'accounting.instruments.manage' => ['ar' => 'إدارة الأوراق التجارية', 'en' => 'Manage Negotiable Instruments'],
// Journal Entries // Journal Entries
'accounting.journal.view' => ['ar' => 'عرض قيود اليومية', 'en' => 'View Journal Entries'], 'accounting.journal.view' => ['ar' => 'عرض قيود اليومية', 'en' => 'View Journal Entries'],
'accounting.journal.create' => ['ar' => 'إنشاء قيد يومية', 'en' => 'Create Journal Entry'], 'accounting.journal.create' => ['ar' => 'إنشاء قيد يومية', 'en' => 'Create Journal Entry'],
'accounting.journal.post' => ['ar' => 'ترحيل قيود اليومية', 'en' => 'Post Journal Entries'], 'accounting.journal.post' => ['ar' => 'ترحيل قيود اليومية', 'en' => 'Post Journal Entries'],
'accounting.journal.reverse' => ['ar' => 'عكس قيود اليومية', 'en' => 'Reverse Journal Entries'], 'accounting.journal.reverse' => ['ar' => 'عكس قيود اليومية', 'en' => 'Reverse Journal Entries'],
// Accounting Dimensions
'accounting.dimensions.view' => ['ar' => 'عرض الأبعاد المحاسبية', 'en' => 'View Accounting Dimensions'],
'accounting.dimensions.manage' => ['ar' => 'إدارة الأبعاد المحاسبية', 'en' => 'Manage Accounting Dimensions'],
// Bank Reconciliation // Bank Reconciliation
'accounting.bank_recon.view' => ['ar' => 'عرض المطابقات البنكية', 'en' => 'View Bank Reconciliations'], 'accounting.bank_recon.view' => ['ar' => 'عرض المطابقات البنكية', 'en' => 'View Bank Reconciliations'],
'accounting.bank_recon.manage' => ['ar' => 'إدارة المطابقات البنكية', 'en' => 'Manage Bank Reconciliations'], 'accounting.bank_recon.manage' => ['ar' => 'إدارة المطابقات البنكية', 'en' => 'Manage Bank Reconciliations'],
// Daily Transactions
'accounting.daily_tx.view' => ['ar' => 'عرض الحركات اليومية', 'en' => 'View Daily Transactions'],
'accounting.daily_tx.manage' => ['ar' => 'إدارة الحركات اليومية', 'en' => 'Manage Daily Transactions'],
// Opening Entries & History
'accounting.opening_entry.view' => ['ar' => 'عرض القيود الافتتاحية', 'en' => 'View Opening Entries'],
'accounting.opening_entry.manage' => ['ar' => 'إدارة القيود الافتتاحية', 'en' => 'Manage Opening Entries'],
// Period Closing // Period Closing
'accounting.period.view' => ['ar' => 'عرض إقفال الفترات', 'en' => 'View Period Closings'], 'accounting.period.view' => ['ar' => 'عرض إقفال الفترات', 'en' => 'View Period Closings'],
'accounting.period.close' => ['ar' => 'إقفال فترة', 'en' => 'Close Period'], 'accounting.period.close' => ['ar' => 'إقفال فترة', 'en' => 'Close Period'],
...@@ -73,11 +97,17 @@ MenuRegistry::register('accounting', [ ...@@ -73,11 +97,17 @@ MenuRegistry::register('accounting', [
['label_ar' => 'لوحة التحكم', 'label_en' => 'Dashboard', 'route' => '/accounting', 'permission' => 'accounting.reports.view', 'order' => 1], ['label_ar' => 'لوحة التحكم', 'label_en' => 'Dashboard', 'route' => '/accounting', 'permission' => 'accounting.reports.view', 'order' => 1],
['label_ar' => 'دليل الحسابات', 'label_en' => 'Chart of Accounts', 'route' => '/accounting/chart-of-accounts', 'permission' => 'accounting.coa.view', 'order' => 2], ['label_ar' => 'دليل الحسابات', 'label_en' => 'Chart of Accounts', 'route' => '/accounting/chart-of-accounts', 'permission' => 'accounting.coa.view', 'order' => 2],
['label_ar' => 'قيود اليومية', 'label_en' => 'Journal Entries', 'route' => '/accounting/journal-entries', 'permission' => 'accounting.journal.view', 'order' => 3], ['label_ar' => 'قيود اليومية', 'label_en' => 'Journal Entries', 'route' => '/accounting/journal-entries', 'permission' => 'accounting.journal.view', 'order' => 3],
['label_ar' => 'السنوات المالية', 'label_en' => 'Fiscal Years', 'route' => '/accounting/fiscal-years', 'permission' => 'accounting.fiscal_year.view', 'order' => 4], ['label_ar' => 'أنواع اليومية', 'label_en' => 'Journal Types', 'route' => '/accounting/journal-types', 'permission' => 'accounting.journal_type.view', 'order' => 4],
['label_ar' => 'السنوات المالية', 'label_en' => 'Fiscal Years', 'route' => '/accounting/fiscal-years', 'permission' => 'accounting.fiscal_year.view', 'order' => 5],
['label_ar' => 'مراكز التكلفة', 'label_en' => 'Cost Centers', 'route' => '/accounting/cost-centers', 'permission' => 'accounting.cost_center.view', 'order' => 5], ['label_ar' => 'مراكز التكلفة', 'label_en' => 'Cost Centers', 'route' => '/accounting/cost-centers', 'permission' => 'accounting.cost_center.view', 'order' => 5],
['label_ar' => 'الحسابات البنكية', 'label_en' => 'Bank Accounts', 'route' => '/accounting/bank-accounts', 'permission' => 'accounting.bank_account.view', 'order' => 6], ['label_ar' => 'الموازنات التقديرية', 'label_en' => 'Budgets', 'route' => '/accounting/budgets', 'permission' => 'accounting.budget.view', 'order' => 6],
['label_ar' => 'المطابقة البنكية', 'label_en' => 'Bank Reconciliation', 'route' => '/accounting/bank-reconciliation', 'permission' => 'accounting.bank_recon.view', 'order' => 7], ['label_ar' => 'الحسابات البنكية', 'label_en' => 'Bank Accounts', 'route' => '/accounting/bank-accounts', 'permission' => 'accounting.bank_account.view', 'order' => 7],
['label_ar' => 'إقفال الفترات', 'label_en' => 'Period Closing', 'route' => '/accounting/period-closing', 'permission' => 'accounting.period.view', 'order' => 8], ['label_ar' => 'الأوراق التجارية', 'label_en' => 'Instruments', 'route' => '/accounting/instruments', 'permission' => 'accounting.instruments.view', 'order' => 8],
['label_ar' => 'الأبعاد المحاسبية', 'label_en' => 'Dimensions', 'route' => '/accounting/dimensions', 'permission' => 'accounting.dimensions.view', 'order' => 9],
['label_ar' => 'المطابقة البنكية', 'label_en' => 'Bank Reconciliation', 'route' => '/accounting/bank-reconciliation', 'permission' => 'accounting.bank_recon.view', 'order' => 10],
['label_ar' => 'الحركات اليومية', 'label_en' => 'Daily Transactions', 'route' => '/accounting/daily-transactions', 'permission' => 'accounting.daily_tx.view', 'order' => 11],
['label_ar' => 'القيود الافتتاحية', 'label_en' => 'Opening Entries', 'route' => '/accounting/opening-entries', 'permission' => 'accounting.opening_entry.view', 'order' => 12],
['label_ar' => 'إقفال الفترات', 'label_en' => 'Period Closing', 'route' => '/accounting/period-closing', 'permission' => 'accounting.period.view', 'order' => 12],
['label_ar' => 'ميزان المراجعة', 'label_en' => 'Trial Balance', 'route' => '/accounting/reports/trial-balance', 'permission' => 'accounting.reports.trial_balance', 'order' => 9], ['label_ar' => 'ميزان المراجعة', 'label_en' => 'Trial Balance', 'route' => '/accounting/reports/trial-balance', 'permission' => 'accounting.reports.trial_balance', 'order' => 9],
['label_ar' => 'دفتر الأستاذ', 'label_en' => 'General Ledger', 'route' => '/accounting/reports/general-ledger', 'permission' => 'accounting.reports.general_ledger', 'order' => 10], ['label_ar' => 'دفتر الأستاذ', 'label_en' => 'General Ledger', 'route' => '/accounting/reports/general-ledger', 'permission' => 'accounting.reports.general_ledger', 'order' => 10],
['label_ar' => 'قائمة الدخل', 'label_en' => 'Income Statement', 'route' => '/accounting/reports/income-statement', 'permission' => 'accounting.reports.income_statement','order' => 11], ['label_ar' => 'قائمة الدخل', 'label_en' => 'Income Statement', 'route' => '/accounting/reports/income-statement', 'permission' => 'accounting.reports.income_statement','order' => 11],
......
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `journal_types` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`code` VARCHAR(10) NOT NULL COMMENT 'Unique short code e.g. GEN, RCV, PAY',
`name_ar` VARCHAR(100) NOT NULL,
`name_en` VARCHAR(100) NOT NULL,
`short_name_ar` VARCHAR(20) NULL,
`short_name_en` VARCHAR(20) NULL,
`numbering_method` ENUM('yearly','monthly') NOT NULL DEFAULT 'yearly',
`number_prefix` VARCHAR(10) NULL COMMENT 'Prefix for entry numbers e.g. RCV-',
`number_length` TINYINT UNSIGNED NOT NULL DEFAULT 6,
`is_auto_generated` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'System-generated entries only',
`is_default` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Default journal type for manual entries',
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE INDEX `idx_jt_code` (`code`),
INDEX `idx_jt_active` (`is_active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `journal_types`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `account_budgets` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`fiscal_year_id` BIGINT UNSIGNED NOT NULL,
`account_id` BIGINT UNSIGNED NOT NULL,
`period` VARCHAR(7) NOT NULL COMMENT 'YYYY-MM',
`budgeted_amount` DECIMAL(18,2) NOT NULL DEFAULT 0.00,
`notes` TEXT NULL,
`created_by` BIGINT UNSIGNED NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `idx_budget_unique` (`fiscal_year_id`, `account_id`, `period`),
INDEX `idx_budget_account` (`account_id`),
INDEX `idx_budget_year` (`fiscal_year_id`),
FOREIGN KEY (`fiscal_year_id`) REFERENCES `fiscal_years`(`id`),
FOREIGN KEY (`account_id`) REFERENCES `chart_of_accounts`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `account_budgets`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `cost_center_budgets` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`fiscal_year_id` BIGINT UNSIGNED NOT NULL,
`cost_center_id` BIGINT UNSIGNED NOT NULL,
`account_id` BIGINT UNSIGNED NULL COMMENT 'Optional: budget per account within cost center',
`period` VARCHAR(7) NOT NULL COMMENT 'YYYY-MM',
`budgeted_amount` DECIMAL(18,2) NOT NULL DEFAULT 0.00,
`notes` TEXT NULL,
`created_by` BIGINT UNSIGNED NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `idx_cc_budget_unique` (`fiscal_year_id`, `cost_center_id`, `account_id`, `period`),
INDEX `idx_cc_budget_center` (`cost_center_id`),
INDEX `idx_cc_budget_year` (`fiscal_year_id`),
FOREIGN KEY (`fiscal_year_id`) REFERENCES `fiscal_years`(`id`),
FOREIGN KEY (`cost_center_id`) REFERENCES `cost_centers`(`id`),
FOREIGN KEY (`account_id`) REFERENCES `chart_of_accounts`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `cost_center_budgets`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `negotiable_instruments` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`instrument_type` ENUM('check','promissory_note') NOT NULL DEFAULT 'check',
`direction` ENUM('receivable','payable') NOT NULL,
`instrument_number` VARCHAR(50) NOT NULL,
`amount` DECIMAL(18,2) NOT NULL,
`currency` VARCHAR(3) NOT NULL DEFAULT 'EGP',
`issue_date` DATE NOT NULL,
`due_date` DATE NOT NULL,
`drawer_name` VARCHAR(200) NULL COMMENT 'الساحب',
`drawer_bank` VARCHAR(200) NULL,
`beneficiary_name` VARCHAR(200) NULL COMMENT 'المستفيد',
`endorsee_name` VARCHAR(200) NULL COMMENT 'المظهر إليه',
`status` ENUM('in_hand','under_collection','collected','bounced','endorsed','cancelled','paid','returned') NOT NULL DEFAULT 'in_hand',
`current_location` ENUM('safe','bank','endorsed_to','returned_to_drawer') NOT NULL DEFAULT 'safe',
`bank_account_id` BIGINT UNSIGNED NULL,
`member_id` BIGINT UNSIGNED NULL,
`vendor_id` BIGINT UNSIGNED NULL,
`payment_id` BIGINT UNSIGNED NULL COMMENT 'Source payment record',
`journal_entry_id` BIGINT UNSIGNED NULL,
`notes` TEXT NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
INDEX `idx_ni_status` (`status`),
INDEX `idx_ni_due_date` (`due_date`),
INDEX `idx_ni_direction` (`direction`),
INDEX `idx_ni_member` (`member_id`),
INDEX `idx_ni_vendor` (`vendor_id`),
INDEX `idx_ni_bank` (`bank_account_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `negotiable_instruments`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `instrument_portfolios` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`portfolio_number` VARCHAR(30) NOT NULL,
`portfolio_type` ENUM('deposit','collection','payment','endorsement','return','bounce') NOT NULL,
`portfolio_date` DATE NOT NULL,
`bank_account_id` BIGINT UNSIGNED NULL,
`total_amount` DECIMAL(18,2) NOT NULL DEFAULT 0.00,
`instrument_count` INT UNSIGNED NOT NULL DEFAULT 0,
`description` TEXT NULL,
`branch_id` BIGINT UNSIGNED NULL,
`journal_entry_id` BIGINT UNSIGNED NULL,
`status` ENUM('draft','confirmed') NOT NULL DEFAULT 'draft',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
INDEX `idx_port_type` (`portfolio_type`),
INDEX `idx_port_date` (`portfolio_date`),
INDEX `idx_port_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `instrument_portfolio_items` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`portfolio_id` BIGINT UNSIGNED NOT NULL,
`instrument_id` BIGINT UNSIGNED NOT NULL,
FOREIGN KEY (`portfolio_id`) REFERENCES `instrument_portfolios`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`instrument_id`) REFERENCES `negotiable_instruments`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "
DROP TABLE IF EXISTS `instrument_portfolio_items`;
DROP TABLE IF EXISTS `instrument_portfolios`
",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `accounting_dimensions` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`code` VARCHAR(20) NOT NULL,
`name_ar` VARCHAR(100) NOT NULL,
`name_en` VARCHAR(100) NOT NULL,
`is_required` TINYINT(1) NOT NULL DEFAULT 0,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE INDEX `idx_dim_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `accounting_dimension_values` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`dimension_id` BIGINT UNSIGNED NOT NULL,
`code` VARCHAR(20) NOT NULL,
`name_ar` VARCHAR(100) NOT NULL,
`name_en` VARCHAR(100) NOT NULL,
`parent_id` BIGINT UNSIGNED NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `idx_dim_val_unique` (`dimension_id`, `code`),
FOREIGN KEY (`dimension_id`) REFERENCES `accounting_dimensions`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`parent_id`) REFERENCES `accounting_dimension_values`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "
DROP TABLE IF EXISTS `accounting_dimension_values`;
DROP TABLE IF EXISTS `accounting_dimensions`
",
];
<?php
declare(strict_types=1);
return [
'up' => "
ALTER TABLE `journal_entry_lines`
ADD COLUMN `dimensions` JSON NULL COMMENT 'Flexible dimension tagging e.g. {\"project\":\"PRJ001\"}' AFTER `cost_center_id`
",
'down' => "
ALTER TABLE `journal_entry_lines` DROP COLUMN `dimensions`
",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `historical_balances` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`account_id` BIGINT UNSIGNED NOT NULL,
`fiscal_year_id` BIGINT UNSIGNED NOT NULL,
`period` VARCHAR(7) NOT NULL COMMENT 'YYYY-MM or YYYY for annual',
`debit_total` DECIMAL(18,2) NOT NULL DEFAULT 0.00,
`credit_total` DECIMAL(18,2) NOT NULL DEFAULT 0.00,
`closing_balance` DECIMAL(18,2) NOT NULL DEFAULT 0.00,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `idx_hist_unique` (`account_id`, `fiscal_year_id`, `period`),
INDEX `idx_hist_year` (`fiscal_year_id`),
FOREIGN KEY (`account_id`) REFERENCES `chart_of_accounts`(`id`),
FOREIGN KEY (`fiscal_year_id`) REFERENCES `fiscal_years`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `historical_balances`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `daily_transactions` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`transaction_date` DATE NOT NULL,
`journal_type_id` BIGINT UNSIGNED NULL,
`account_id` BIGINT UNSIGNED NOT NULL,
`cost_center_id` BIGINT UNSIGNED NULL,
`amount` DECIMAL(18,2) NOT NULL,
`direction` ENUM('debit','credit') NOT NULL,
`description` VARCHAR(500) NULL,
`document_number` VARCHAR(50) NULL,
`currency` VARCHAR(3) NOT NULL DEFAULT 'EGP',
`is_consolidated` TINYINT(1) NOT NULL DEFAULT 0,
`consolidated_entry_id` BIGINT UNSIGNED NULL,
`branch_id` BIGINT UNSIGNED NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
INDEX `idx_dt_date` (`transaction_date`),
INDEX `idx_dt_consolidated` (`is_consolidated`),
INDEX `idx_dt_type` (`journal_type_id`),
FOREIGN KEY (`account_id`) REFERENCES `chart_of_accounts`(`id`),
FOREIGN KEY (`journal_type_id`) REFERENCES `journal_types`(`id`),
FOREIGN KEY (`consolidated_entry_id`) REFERENCES `journal_entries`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `daily_transactions`",
];
<?php
declare(strict_types=1);
return function (\App\Core\Database $db): void {
$types = [
['code' => 'GEN', 'name_ar' => 'يومية عامة', 'name_en' => 'General Journal', 'short_name_ar' => 'عامة', 'short_name_en' => 'GEN', 'number_prefix' => 'GEN-', 'is_default' => 1],
['code' => 'RCV', 'name_ar' => 'يومية مقبوضات', 'name_en' => 'Receipts Journal', 'short_name_ar' => 'مقبوضات', 'short_name_en' => 'RCV', 'number_prefix' => 'RCV-', 'is_auto_generated' => 1],
['code' => 'PAY', 'name_ar' => 'يومية مدفوعات', 'name_en' => 'Payments Journal', 'short_name_ar' => 'مدفوعات', 'short_name_en' => 'PAY', 'number_prefix' => 'PAY-', 'is_auto_generated' => 1],
['code' => 'PUR', 'name_ar' => 'يومية مشتريات', 'name_en' => 'Purchases Journal', 'short_name_ar' => 'مشتريات', 'short_name_en' => 'PUR', 'number_prefix' => 'PUR-', 'is_auto_generated' => 1],
['code' => 'SAL', 'name_ar' => 'يومية مبيعات', 'name_en' => 'Sales Journal', 'short_name_ar' => 'مبيعات', 'short_name_en' => 'SAL', 'number_prefix' => 'SAL-', 'is_auto_generated' => 1],
['code' => 'HR', 'name_ar' => 'يومية رواتب', 'name_en' => 'Payroll Journal', 'short_name_ar' => 'رواتب', 'short_name_en' => 'HR', 'number_prefix' => 'HR-', 'is_auto_generated' => 1],
['code' => 'ADJ', 'name_ar' => 'يومية تسويات', 'name_en' => 'Adjustments Journal', 'short_name_ar' => 'تسويات', 'short_name_en' => 'ADJ', 'number_prefix' => 'ADJ-'],
['code' => 'OPN', 'name_ar' => 'قيد افتتاحي', 'name_en' => 'Opening Entry', 'short_name_ar' => 'افتتاحي', 'short_name_en' => 'OPN', 'number_prefix' => 'OPN-', 'is_auto_generated' => 1],
['code' => 'CLS', 'name_ar' => 'قيد إقفال', 'name_en' => 'Closing Entry', 'short_name_ar' => 'إقفال', 'short_name_en' => 'CLS', 'number_prefix' => 'CLS-', 'is_auto_generated' => 1],
];
$now = date('Y-m-d H:i:s');
foreach ($types as $type) {
$existing = $db->selectOne("SELECT id FROM journal_types WHERE code = ?", [$type['code']]);
if ($existing) {
continue;
}
$db->insert('journal_types', [
'code' => $type['code'],
'name_ar' => $type['name_ar'],
'name_en' => $type['name_en'],
'short_name_ar' => $type['short_name_ar'] ?? null,
'short_name_en' => $type['short_name_en'] ?? null,
'numbering_method' => 'yearly',
'number_prefix' => $type['number_prefix'] ?? null,
'number_length' => 6,
'is_auto_generated' => (int) ($type['is_auto_generated'] ?? 0),
'is_default' => (int) ($type['is_default'] ?? 0),
'is_active' => 1,
'created_at' => $now,
'updated_at' => $now,
]);
}
};
#!/bin/bash
#
# One-time CapRover setup: adds persistent directory for uploads.
# Run this ONCE from your local machine:
# bash docker/caprover-setup.sh
#
# After this, uploads survive force deploys.
#
set -e
# ── Config (edit these) ──
CAPROVER_URL="${CAPROVER_URL:-https://captain.yourdomain.com}"
CAPROVER_APP="${CAPROVER_APP:-club-erp}"
CAPROVER_PASSWORD="${CAPROVER_PASSWORD:-}"
echo "=== CapRover Persistent Storage Setup ==="
echo ""
if [ -z "$CAPROVER_URL" ] || [ "$CAPROVER_URL" = "https://captain.yourdomain.com" ]; then
read -p "CapRover URL (e.g. https://captain.yourdomain.com): " CAPROVER_URL
fi
if [ -z "$CAPROVER_APP" ] || [ "$CAPROVER_APP" = "club-erp" ]; then
read -p "App name in CapRover: " CAPROVER_APP
fi
if [ -z "$CAPROVER_PASSWORD" ]; then
read -sp "CapRover password: " CAPROVER_PASSWORD
echo ""
fi
echo ""
echo "URL: $CAPROVER_URL"
echo "App: $CAPROVER_APP"
echo ""
# ── Get auth token ──
echo "Authenticating..."
TOKEN=$(curl -s -X POST "$CAPROVER_URL/api/v2/login" \
-H "Content-Type: application/json" \
-H "x-namespace: captain" \
-d "{\"password\":\"$CAPROVER_PASSWORD\"}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('data',{}).get('token',''))" 2>/dev/null)
if [ -z "$TOKEN" ]; then
echo "ERROR: Failed to authenticate. Check URL and password."
exit 1
fi
echo "Authenticated!"
# ── Get current app definition ──
echo "Fetching app definition..."
APP_DEF=$(curl -s -X GET "$CAPROVER_URL/api/v2/user/apps/appDefinitions" \
-H "Content-Type: application/json" \
-H "x-namespace: captain" \
-H "x-captain-auth: $TOKEN")
# ── Add persistent directory ──
echo "Adding persistent directory: /var/www/html/storage/uploads"
UPDATE_RESULT=$(curl -s -X POST "$CAPROVER_URL/api/v2/user/apps/appDefinitions/update" \
-H "Content-Type: application/json" \
-H "x-namespace: captain" \
-H "x-captain-auth: $TOKEN" \
-d "{
\"appName\": \"$CAPROVER_APP\",
\"volumes\": [
{
\"containerPath\": \"/var/www/html/storage/uploads\",
\"volumeName\": \"${CAPROVER_APP}-uploads\"
}
]
}")
# Check result
STATUS=$(echo "$UPDATE_RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status',0))" 2>/dev/null)
if [ "$STATUS" = "100" ]; then
echo ""
echo "=== SUCCESS ==="
echo "Persistent volume '${CAPROVER_APP}-uploads' mapped to /var/www/html/storage/uploads"
echo ""
echo "Next force deploy will use this volume. Uploads will persist across deploys."
else
echo ""
echo "=== RESPONSE ==="
echo "$UPDATE_RESULT" | python3 -m json.tool 2>/dev/null || echo "$UPDATE_RESULT"
echo ""
echo "If this failed, try the alternative method below."
echo ""
echo "=== ALTERNATIVE: Manual API call ==="
echo "curl -X POST '$CAPROVER_URL/api/v2/user/apps/appDefinitions/update' \\"
echo " -H 'Content-Type: application/json' \\"
echo " -H 'x-namespace: captain' \\"
echo " -H 'x-captain-auth: YOUR_TOKEN' \\"
echo " -d '{\"appName\":\"$CAPROVER_APP\",\"volumes\":[{\"containerPath\":\"/var/www/html/storage/uploads\",\"volumeName\":\"${CAPROVER_APP}-uploads\"}]}'"
fi
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