Commit 92001ac6 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add facility rent tracking and miscellaneous expenses

- New tables: facility_rent_payments, expenses with double-entry transactions
- ExpenseService handles both rent and misc expenses with account resolution
- Livewire CRUD: FacilityRentForm/List, ExpenseForm/List
- Dashboard quick action buttons for recording expenses
- Facility show page: new "Rent" tab with payment history
- Sidebar nav links under Financial section
- Reports: expense_summary and facility_rent_summary added to ReportsHub
- New permission: expenses.create/list/view + seeded account 5050
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 390d55df
<?php
namespace App\Domain\Financial\Enums;
enum ExpenseCategory: string
{
case Maintenance = 'maintenance';
case Supplies = 'supplies';
case Transport = 'transport';
case FoodBeverage = 'food_beverage';
case SportsEquipment = 'sports_equipment';
case Printing = 'printing';
case Cleaning = 'cleaning';
case Medical = 'medical';
case Utilities = 'utilities';
case Other = 'other';
public function label(): string
{
return match ($this) {
self::Maintenance => 'صيانة',
self::Supplies => 'مستلزمات',
self::Transport => 'نقل ومواصلات',
self::FoodBeverage => 'طعام ومشروبات',
self::SportsEquipment => 'أدوات رياضية',
self::Printing => 'طباعة',
self::Cleaning => 'نظافة',
self::Medical => 'طبي',
self::Utilities => 'مرافق وخدمات',
self::Other => 'أخرى',
};
}
}
<?php
namespace App\Domain\Financial\Models;
use App\Domain\Financial\Enums\ExpenseCategory;
use App\Domain\Financial\Enums\PaymentMethod;
use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\Shared\Traits\HasUuid;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class Expense extends Model
{
use HasUuid, BelongsToAcademy, SoftDeletes;
protected $fillable = [
'academy_id',
'branch_id',
'category',
'amount',
'description',
'recipient_name',
'payment_method',
'receipt_reference',
'expense_date',
'notes',
'created_by',
];
protected function casts(): array
{
return [
'category' => ExpenseCategory::class,
'amount' => 'integer',
'payment_method' => PaymentMethod::class,
'expense_date' => 'date',
];
}
public function branch(): BelongsTo
{
return $this->belongsTo(\App\Domain\Identity\Models\Branch::class);
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
}
<?php
namespace App\Domain\Financial\Models;
use App\Domain\Facility\Models\Facility;
use App\Domain\Financial\Enums\PaymentMethod;
use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\Shared\Traits\HasUuid;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class FacilityRentPayment extends Model
{
use HasUuid, BelongsToAcademy, SoftDeletes;
protected $fillable = [
'academy_id',
'branch_id',
'facility_id',
'period',
'amount',
'payment_method',
'cheque_number',
'cheque_date',
'bank_name',
'recipient_name',
'payment_date',
'notes',
'created_by',
];
protected function casts(): array
{
return [
'amount' => 'integer',
'payment_method' => PaymentMethod::class,
'cheque_date' => 'date',
'payment_date' => 'date',
];
}
public function facility(): BelongsTo
{
return $this->belongsTo(Facility::class);
}
public function branch(): BelongsTo
{
return $this->belongsTo(\App\Domain\Identity\Models\Branch::class);
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
}
<?php
namespace App\Domain\Financial\Services;
use App\Domain\Financial\Enums\TransactionType;
use App\Domain\Financial\Models\Expense;
use App\Domain\Financial\Models\FacilityRentPayment;
use App\Domain\Financial\Models\FinancialAccount;
use App\Domain\Financial\Models\Transaction;
use App\Domain\Shared\Exceptions\DomainException;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class ExpenseService
{
public function recordRentPayment(array $data, User $actor): FacilityRentPayment
{
return DB::transaction(function () use ($data, $actor) {
$academyId = app('current_academy')?->id ?? $actor->academy_id;
$rentPayment = FacilityRentPayment::create([
'academy_id' => $academyId,
'branch_id' => $data['branch_id'] ?? null,
'facility_id' => $data['facility_id'],
'period' => $data['period'],
'amount' => $data['amount'],
'payment_method' => $data['payment_method'],
'cheque_number' => $data['cheque_number'] ?? null,
'cheque_date' => $data['cheque_date'] ?? null,
'bank_name' => $data['bank_name'] ?? null,
'recipient_name' => $data['recipient_name'] ?? null,
'payment_date' => $data['payment_date'],
'notes' => $data['notes'] ?? null,
'created_by' => $actor->id,
]);
$this->createExpenseTransaction(
academyId: $academyId,
expenseAccountCode: '5010',
paymentMethod: $data['payment_method'],
amount: $data['amount'],
description: "إيجار منشأة: {$data['period']}",
referenceModel: $rentPayment,
actor: $actor,
transactionDate: $data['payment_date'],
);
return $rentPayment;
});
}
public function recordExpense(array $data, User $actor): Expense
{
return DB::transaction(function () use ($data, $actor) {
$academyId = app('current_academy')?->id ?? $actor->academy_id;
$expense = Expense::create([
'academy_id' => $academyId,
'branch_id' => $data['branch_id'] ?? null,
'category' => $data['category'],
'amount' => $data['amount'],
'description' => $data['description'],
'recipient_name' => $data['recipient_name'] ?? null,
'payment_method' => $data['payment_method'],
'receipt_reference' => $data['receipt_reference'] ?? null,
'expense_date' => $data['expense_date'],
'notes' => $data['notes'] ?? null,
'created_by' => $actor->id,
]);
$expenseAccountCode = $this->resolveExpenseAccountCode($data['category']);
$this->createExpenseTransaction(
academyId: $academyId,
expenseAccountCode: $expenseAccountCode,
paymentMethod: $data['payment_method'],
amount: $data['amount'],
description: $data['description'],
referenceModel: $expense,
actor: $actor,
transactionDate: $data['expense_date'],
);
return $expense;
});
}
private function createExpenseTransaction(
int $academyId,
string $expenseAccountCode,
string $paymentMethod,
int $amount,
string $description,
$referenceModel,
User $actor,
string $transactionDate,
): void {
$debitAccount = FinancialAccount::where('academy_id', $academyId)
->where('code', $expenseAccountCode)
->first();
if (!$debitAccount) {
throw new DomainException('حساب المصروفات غير موجود — يرجى إعداد شجرة الحسابات');
}
$creditAccountCode = match ($paymentMethod) {
'cash' => '1000',
'bank_transfer', 'cheque' => '1010',
default => '1000',
};
$creditAccount = FinancialAccount::where('academy_id', $academyId)
->where('code', $creditAccountCode)
->first();
if (!$creditAccount) {
throw new DomainException('حساب النقدية/البنك غير موجود — يرجى إعداد شجرة الحسابات');
}
Transaction::create([
'academy_id' => $academyId,
'debit_account_id' => $debitAccount->id,
'credit_account_id' => $creditAccount->id,
'reference_type' => get_class($referenceModel),
'reference_id' => $referenceModel->id,
'amount' => $amount,
'currency' => 'EGP',
'type' => TransactionType::PaymentMade,
'description' => $description,
'transaction_date' => $transactionDate,
'created_by' => $actor->id,
]);
}
private function resolveExpenseAccountCode(string $category): string
{
return match ($category) {
'maintenance' => '5030',
'utilities' => '5030',
'sports_equipment' => '5020',
'supplies' => '5020',
default => '5050',
};
}
}
......@@ -697,6 +697,42 @@ public function enrollmentReport(string $from, string $to, ?int $branchId = null
];
}
public function expenseSummary(string $from, string $to, ?int $branchId = null): Collection
{
return \App\Domain\Financial\Models\Expense::with(['creator'])
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereBetween('expense_date', [$from, $to])
->orderByDesc('expense_date')
->get()
->map(fn ($e) => [
'date' => $e->expense_date?->format('Y-m-d'),
'category' => $e->category->label(),
'description' => $e->description,
'amount' => $e->amount,
'recipient' => $e->recipient_name ?? '—',
'payment_method' => $e->payment_method->value,
'created_by' => $e->creator?->name,
]);
}
public function facilityRentSummary(string $from, string $to, ?int $branchId = null): Collection
{
return \App\Domain\Financial\Models\FacilityRentPayment::with(['facility', 'creator'])
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereBetween('payment_date', [$from, $to])
->orderByDesc('payment_date')
->get()
->map(fn ($r) => [
'facility' => $r->facility?->name_ar,
'period' => $r->period,
'amount' => $r->amount,
'payment_method' => $r->payment_method->value,
'cheque_number' => $r->cheque_number ?? '—',
'payment_date' => $r->payment_date?->format('Y-m-d'),
'created_by' => $r->creator?->name,
]);
}
public function participantList(array $filters = []): Collection
{
$query = Participant::with(['person', 'enrollments.group'])
......
<?php
namespace App\Livewire\Financial;
use App\Domain\Financial\Enums\ExpenseCategory;
use App\Domain\Financial\Services\ExpenseService;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('تسجيل مصروف')]
class ExpenseForm extends Component
{
use UsesBranchScope;
public string $category = '';
public string $amount_display = '';
public string $description = '';
public string $recipient_name = '';
public string $payment_method = 'cash';
public string $receipt_reference = '';
public ?string $expense_date = null;
public string $notes = '';
public function mount(): void
{
$this->authorize('expenses.create');
$this->expense_date = now()->toDateString();
}
public function rules(): array
{
return [
'category' => 'required|in:' . implode(',', array_column(ExpenseCategory::cases(), 'value')),
'amount_display' => 'required|numeric|min:0.01',
'description' => 'required|string|max:500',
'recipient_name' => 'nullable|string|max:255',
'payment_method' => 'required|in:cash,card,bank_transfer,wallet,online,cheque,other',
'receipt_reference' => 'nullable|string|max:100',
'expense_date' => 'required|date',
'notes' => 'nullable|string',
];
}
public function messages(): array
{
return [
'category.required' => 'اختر فئة المصروف',
'amount_display.required' => 'المبلغ مطلوب',
'amount_display.min' => 'المبلغ يجب أن يكون أكبر من صفر',
'description.required' => 'وصف المصروف مطلوب',
'description.max' => 'الوصف يجب ألا يتجاوز 500 حرف',
'payment_method.required' => 'اختر طريقة الدفع',
'expense_date.required' => 'تاريخ المصروف مطلوب',
];
}
public function save(ExpenseService $service): void
{
$this->validate();
try {
$service->recordExpense([
'branch_id' => $this->getActiveBranchId(),
'category' => $this->category,
'amount' => (int) round((float) $this->amount_display * 100),
'description' => $this->description,
'recipient_name' => $this->recipient_name ?: null,
'payment_method' => $this->payment_method,
'receipt_reference' => $this->receipt_reference ?: null,
'expense_date' => $this->expense_date,
'notes' => $this->notes ?: null,
], auth()->user());
session()->flash('success', __('تم تسجيل المصروف بنجاح'));
$this->redirect(route('expenses.list'), navigate: true);
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
}
}
public function render()
{
return view('livewire.financial.expense-form', [
'categories' => ExpenseCategory::cases(),
]);
}
}
<?php
namespace App\Livewire\Financial;
use App\Domain\Financial\Enums\ExpenseCategory;
use App\Domain\Financial\Models\Expense;
use App\Domain\Shared\Traits\UsesBranchScope;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
#[Layout('layouts.app')]
#[Title('سجل المصروفات')]
class ExpenseList extends Component
{
use WithPagination, UsesBranchScope;
#[Url]
public string $search = '';
#[Url]
public string $category_filter = '';
#[Url]
public ?string $from_date = null;
#[Url]
public ?string $to_date = null;
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedCategoryFilter(): void
{
$this->resetPage();
}
public function render()
{
$branchId = $this->getActiveBranchId();
$expenses = Expense::with(['creator'])
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->when($this->category_filter, fn ($q) => $q->where('category', $this->category_filter))
->when($this->from_date, fn ($q) => $q->where('expense_date', '>=', $this->from_date))
->when($this->to_date, fn ($q) => $q->where('expense_date', '<=', $this->to_date))
->when($this->search, fn ($q) => $q->where(function ($q) {
$q->where('description', 'ilike', "%{$this->search}%")
->orWhere('recipient_name', 'ilike', "%{$this->search}%");
}))
->orderByDesc('expense_date')
->paginate(20);
$totalAmount = Expense::query()
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->when($this->category_filter, fn ($q) => $q->where('category', $this->category_filter))
->when($this->from_date, fn ($q) => $q->where('expense_date', '>=', $this->from_date))
->when($this->to_date, fn ($q) => $q->where('expense_date', '<=', $this->to_date))
->sum('amount');
return view('livewire.financial.expense-list', [
'expenses' => $expenses,
'totalAmount' => $totalAmount,
'categories' => ExpenseCategory::cases(),
]);
}
}
<?php
namespace App\Livewire\Financial;
use App\Domain\Facility\Models\Facility;
use App\Domain\Financial\Services\ExpenseService;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('تسجيل إيجار منشأة')]
class FacilityRentForm extends Component
{
use UsesBranchScope;
public ?int $facility_id = null;
public string $period = '';
public string $amount_display = '';
public string $payment_method = 'cheque';
public string $cheque_number = '';
public ?string $cheque_date = null;
public string $bank_name = '';
public string $recipient_name = '';
public ?string $payment_date = null;
public string $notes = '';
public function mount(?int $facility = null): void
{
$this->authorize('facilities.update');
$this->facility_id = $facility;
$this->payment_date = now()->toDateString();
$this->period = now()->format('Y-m');
if ($facility) {
$f = Facility::find($facility);
if ($f && $f->monthly_rental_cost) {
$this->amount_display = number_format($f->monthly_rental_cost / 100, 2, '.', '');
}
}
}
public function rules(): array
{
$rules = [
'facility_id' => 'required|exists:facilities,id',
'period' => 'required|string|size:7',
'amount_display' => 'required|numeric|min:0.01',
'payment_method' => 'required|in:cash,card,bank_transfer,wallet,online,cheque,other',
'recipient_name' => 'nullable|string|max:255',
'payment_date' => 'required|date',
'notes' => 'nullable|string',
];
if ($this->payment_method === 'cheque') {
$rules['cheque_number'] = 'required|string|max:50';
$rules['cheque_date'] = 'required|date';
$rules['bank_name'] = 'required|string|max:100';
}
return $rules;
}
public function messages(): array
{
return [
'facility_id.required' => 'اختر المنشأة',
'period.required' => 'حدد الشهر',
'amount_display.required' => 'المبلغ مطلوب',
'amount_display.min' => 'المبلغ يجب أن يكون أكبر من صفر',
'payment_method.required' => 'اختر طريقة الدفع',
'cheque_number.required' => 'رقم الشيك مطلوب',
'cheque_date.required' => 'تاريخ الشيك مطلوب',
'bank_name.required' => 'اسم البنك مطلوب',
'payment_date.required' => 'تاريخ الدفع مطلوب',
];
}
public function save(ExpenseService $service): void
{
$this->validate();
try {
$service->recordRentPayment([
'branch_id' => $this->getActiveBranchId(),
'facility_id' => $this->facility_id,
'period' => $this->period,
'amount' => (int) round((float) $this->amount_display * 100),
'payment_method' => $this->payment_method,
'cheque_number' => $this->cheque_number ?: null,
'cheque_date' => $this->cheque_date ?: null,
'bank_name' => $this->bank_name ?: null,
'recipient_name' => $this->recipient_name ?: null,
'payment_date' => $this->payment_date,
'notes' => $this->notes ?: null,
], auth()->user());
session()->flash('success', __('تم تسجيل دفعة الإيجار بنجاح'));
$this->redirect(route('expenses.rent.list'), navigate: true);
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
}
}
public function render()
{
$branchId = $this->getActiveBranchId();
return view('livewire.financial.facility-rent-form', [
'facilities' => Facility::where('status', 'active')
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->orderBy('name_ar')
->get(['id', 'name_ar', 'monthly_rental_cost']),
]);
}
}
<?php
namespace App\Livewire\Financial;
use App\Domain\Financial\Models\FacilityRentPayment;
use App\Domain\Shared\Traits\UsesBranchScope;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
#[Layout('layouts.app')]
#[Title('سجل إيجارات المنشآت')]
class FacilityRentList extends Component
{
use WithPagination, UsesBranchScope;
#[Url]
public string $search = '';
#[Url]
public ?int $facility_filter = null;
#[Url]
public string $period_filter = '';
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedFacilityFilter(): void
{
$this->resetPage();
}
public function render()
{
$branchId = $this->getActiveBranchId();
$payments = FacilityRentPayment::with(['facility', 'creator'])
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->when($this->facility_filter, fn ($q) => $q->where('facility_id', $this->facility_filter))
->when($this->period_filter, fn ($q) => $q->where('period', $this->period_filter))
->when($this->search, fn ($q) => $q->where(function ($q) {
$q->where('cheque_number', 'ilike', "%{$this->search}%")
->orWhere('recipient_name', 'ilike', "%{$this->search}%")
->orWhere('bank_name', 'ilike', "%{$this->search}%");
}))
->orderByDesc('payment_date')
->paginate(20);
$totalAmount = FacilityRentPayment::query()
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->when($this->facility_filter, fn ($q) => $q->where('facility_id', $this->facility_filter))
->when($this->period_filter, fn ($q) => $q->where('period', $this->period_filter))
->sum('amount');
return view('livewire.financial.facility-rent-list', [
'payments' => $payments,
'totalAmount' => $totalAmount,
]);
}
}
......@@ -279,6 +279,22 @@ public function getReportConfig(): array
'money_cols' => ['total', 'discounts'],
'uses_dates' => true,
],
'expense_summary' => [
'name' => 'تقرير المصروفات',
'method' => 'expenseSummary',
'headers' => ['التاريخ', 'الفئة', 'الوصف', 'المبلغ', 'المستلم', 'طريقة الدفع', 'بواسطة'],
'columns' => ['date', 'category', 'description', 'amount', 'recipient', 'payment_method', 'created_by'],
'money_cols' => ['amount'],
'uses_dates' => true,
],
'facility_rent_summary' => [
'name' => 'تقرير إيجارات المنشآت',
'method' => 'facilityRentSummary',
'headers' => ['المنشأة', 'الشهر', 'المبلغ', 'طريقة الدفع', 'رقم الشيك', 'تاريخ الدفع', 'بواسطة'],
'columns' => ['facility', 'period', 'amount', 'payment_method', 'cheque_number', 'payment_date', 'created_by'],
'money_cols' => ['amount'],
'uses_dates' => true,
],
];
}
......
......@@ -31,6 +31,8 @@ public function getReportsProperty(): array
['key' => 'cash_sessions', 'name' => 'ملخص الورديات', 'desc' => 'كل وردية نقدية مع الفرق بين المتوقع والفعلي'],
['key' => 'overdue_aging', 'name' => 'تقادم الفواتير', 'desc' => 'الفواتير المتأخرة مصنفة بالأيام (30/60/90+)'],
['key' => 'revenue_by_activity', 'name' => 'الإيرادات حسب النشاط', 'desc' => 'مقارنة إيرادات كل نشاط رياضي'],
['key' => 'expense_summary', 'name' => 'المصروفات', 'desc' => 'كل المصروفات النثرية حسب الفئة والتاريخ'],
['key' => 'facility_rent_summary', 'name' => 'إيجارات المنشآت', 'desc' => 'دفعات الإيجار الشهرية لكل منشأة'],
],
],
'participants' => [
......
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('facility_rent_payments', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('academy_id')->constrained('organizations');
$table->foreignId('branch_id')->nullable()->constrained('branches');
$table->foreignId('facility_id')->constrained('facilities');
$table->string('period', 7); // YYYY-MM
$table->bigInteger('amount');
$table->string('payment_method', 30)->default('cheque');
$table->string('cheque_number', 50)->nullable();
$table->date('cheque_date')->nullable();
$table->string('bank_name', 100)->nullable();
$table->string('recipient_name', 255)->nullable();
$table->date('payment_date');
$table->text('notes')->nullable();
$table->foreignId('created_by')->constrained('users');
$table->timestamps();
$table->softDeletes();
$table->index(['academy_id', 'facility_id', 'period']);
$table->unique(['academy_id', 'facility_id', 'period']);
});
DB::statement("ALTER TABLE facility_rent_payments ADD CONSTRAINT facility_rent_payments_payment_method_check CHECK (payment_method IN ('cash', 'card', 'bank_transfer', 'wallet', 'online', 'cheque', 'other'))");
}
public function down(): void
{
Schema::dropIfExists('facility_rent_payments');
}
};
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('expenses', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('academy_id')->constrained('organizations');
$table->foreignId('branch_id')->nullable()->constrained('branches');
$table->string('category', 30);
$table->bigInteger('amount');
$table->string('description', 500);
$table->string('recipient_name', 255)->nullable();
$table->string('payment_method', 30)->default('cash');
$table->string('receipt_reference', 100)->nullable();
$table->date('expense_date');
$table->text('notes')->nullable();
$table->foreignId('created_by')->constrained('users');
$table->timestamps();
$table->softDeletes();
$table->index(['academy_id', 'category', 'expense_date']);
$table->index(['academy_id', 'expense_date']);
});
DB::statement("ALTER TABLE expenses ADD CONSTRAINT expenses_category_check CHECK (category IN ('maintenance', 'supplies', 'transport', 'food_beverage', 'sports_equipment', 'printing', 'cleaning', 'medical', 'utilities', 'other'))");
DB::statement("ALTER TABLE expenses ADD CONSTRAINT expenses_payment_method_check CHECK (payment_method IN ('cash', 'card', 'bank_transfer', 'wallet', 'online', 'cheque', 'other'))");
}
public function down(): void
{
Schema::dropIfExists('expenses');
}
};
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
$permissions = ['expenses.create', 'expenses.list', 'expenses.view'];
foreach ($permissions as $perm) {
DB::table('permissions')->insertOrIgnore([
'name' => $perm,
'guard_name' => 'web',
'created_at' => now(),
'updated_at' => now(),
]);
}
// Also seed the 5050 account if missing
$academies = DB::table('organizations')->pluck('id');
foreach ($academies as $academyId) {
DB::table('financial_accounts')->insertOrIgnore([
'academy_id' => $academyId,
'code' => '5050',
'name' => 'Miscellaneous Expenses',
'name_ar' => 'مصروفات نثرية',
'type' => 'expense',
'category' => 'operating',
'is_system' => true,
'is_active' => true,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
public function down(): void
{
DB::table('permissions')->whereIn('name', ['expenses.create', 'expenses.list', 'expenses.view'])->delete();
DB::table('financial_accounts')->where('code', '5050')->delete();
}
};
......@@ -38,6 +38,7 @@ public function run(): void
['code' => '5020', 'name' => 'Equipment Purchases', 'name_ar' => 'مشتريات المعدات', 'type' => 'expense', 'category' => 'operating', 'is_system' => true],
['code' => '5030', 'name' => 'Utilities', 'name_ar' => 'المرافق', 'type' => 'expense', 'category' => 'operating', 'is_system' => true],
['code' => '5040', 'name' => 'Marketing', 'name_ar' => 'التسويق', 'type' => 'expense', 'category' => 'operating', 'is_system' => true],
['code' => '5050', 'name' => 'Miscellaneous Expenses', 'name_ar' => 'مصروفات نثرية', 'type' => 'expense', 'category' => 'operating', 'is_system' => true],
// Liability
['code' => '2000', 'name' => 'Accounts Payable', 'name_ar' => 'الدائنون', 'type' => 'liability', 'category' => 'current_liability', 'is_system' => true],
......
......@@ -125,6 +125,7 @@ private function getPermissionsList(): array
'cash_sessions.open', 'cash_sessions.close', 'cash_sessions.list', 'cash_sessions.manage',
'refunds.initiate', 'refunds.approve',
'daily_closing.create', 'daily_closing.view',
'expenses.create', 'expenses.list', 'expenses.view',
// Pricing
'pricing.list', 'pricing.create', 'pricing.update', 'pricing.delete',
......@@ -372,6 +373,7 @@ private function accountantPermissions(): array
'cash_sessions.open', 'cash_sessions.close', 'cash_sessions.list', 'cash_sessions.manage',
'refunds.initiate', 'refunds.approve',
'daily_closing.create', 'daily_closing.view',
'expenses.create', 'expenses.list', 'expenses.view',
'reports.financial', 'reports.view', 'reports.export_pdf', 'reports.export_excel',
];
foreach ($financialPerms as $perm) {
......
......@@ -38,6 +38,8 @@
['section' => 'المالية', 'items' => [
['label' => 'النظرة المالية', 'route' => 'financial.overview', 'icon' => 'chart-bar', 'permission' => 'invoices.list'],
['label' => 'الفواتير', 'route' => 'invoices.list', 'icon' => 'document', 'permission' => 'invoices.list'],
['label' => 'المصروفات', 'route' => 'expenses.list', 'icon' => 'banknotes', 'permission' => 'expenses.create'],
['label' => 'إيجارات المنشآت', 'route' => 'expenses.rent.list', 'icon' => 'building-office', 'permission' => 'facilities.update'],
['label' => 'المحافظ', 'route' => 'wallets.list', 'icon' => 'wallet', 'permission' => 'wallets.list'],
['label' => 'جلسات الكاشير', 'route' => 'cash-sessions.list', 'icon' => 'calculator', 'permission' => 'cash_sessions.list'],
]],
......
......@@ -174,6 +174,24 @@
</div>
@endcan
<!-- Quick Actions -->
@can('expenses.create')
<div class="mb-6 flex flex-wrap gap-3">
<a href="{{ route('expenses.create') }}" wire:navigate
class="inline-flex items-center gap-2 px-4 py-2.5 bg-red-50 border border-red-200 text-red-700 rounded-xl hover:bg-red-100 transition text-sm font-medium">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
{{ __('تسجيل مصروف') }}
</a>
@can('facilities.update')
<a href="{{ route('expenses.rent.create') }}" wire:navigate
class="inline-flex items-center gap-2 px-4 py-2.5 bg-emerald-50 border border-emerald-200 text-emerald-700 rounded-xl hover:bg-emerald-100 transition text-sm font-medium">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
{{ __('تسجيل إيجار منشأة') }}
</a>
@endcan
</div>
@endcan
<!-- Row 3: Two columns — Today's Schedule + Recent Payments -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
<!-- Left: Today's Schedule -->
......
......@@ -211,6 +211,13 @@ class="whitespace-nowrap py-3 px-6 border-b-2 font-medium text-sm transition-col
class="whitespace-nowrap py-3 px-6 border-b-2 font-medium text-sm transition-colors">
{{ __('جدول اليوم') }}
</button>
@can('facilities.update')
<button @click="activeTab = 'rent'"
:class="activeTab === 'rent' ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
class="whitespace-nowrap py-3 px-6 border-b-2 font-medium text-sm transition-colors">
{{ __('الإيجار') }}
</button>
@endcan
</nav>
</div>
......@@ -541,6 +548,67 @@ class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white rounded-lg ho
@endif
</div>
{{-- Rent Tab --}}
@can('facilities.update')
<div x-show="activeTab === 'rent'" x-cloak>
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-800">{{ __('سجل الإيجار') }}</h3>
<a href="{{ route('expenses.rent.create', ['facility' => $facility->id]) }}" wire:navigate
class="inline-flex items-center gap-2 px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition text-sm">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
{{ __('تسجيل دفعة') }}
</a>
</div>
@if($facility->monthly_rental_cost)
<div class="mb-4 p-3 bg-blue-50 rounded-lg text-sm text-blue-700">
{{ __('الإيجار الشهري المتفق عليه') }}: <strong dir="ltr">{{ format_money($facility->monthly_rental_cost) }}</strong>
</div>
@endif
@php
$rentPayments = \App\Domain\Financial\Models\FacilityRentPayment::where('facility_id', $facility->id)
->orderByDesc('period')
->limit(12)
->get();
@endphp
@if($rentPayments->isNotEmpty())
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50 border-b">
<tr>
<th class="px-4 py-2 text-start text-xs font-medium text-gray-500">{{ __('الشهر') }}</th>
<th class="px-4 py-2 text-start text-xs font-medium text-gray-500">{{ __('المبلغ') }}</th>
<th class="px-4 py-2 text-start text-xs font-medium text-gray-500">{{ __('طريقة الدفع') }}</th>
<th class="px-4 py-2 text-start text-xs font-medium text-gray-500">{{ __('رقم الشيك') }}</th>
<th class="px-4 py-2 text-start text-xs font-medium text-gray-500">{{ __('تاريخ الدفع') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@foreach($rentPayments as $rp)
<tr class="hover:bg-gray-50">
<td class="px-4 py-2 text-gray-600" dir="ltr">{{ $rp->period }}</td>
<td class="px-4 py-2 font-semibold text-gray-800" dir="ltr">{{ format_money($rp->amount) }}</td>
<td class="px-4 py-2 text-gray-600">{{ $rp->payment_method->value === 'cheque' ? 'شيك' : ($rp->payment_method->value === 'cash' ? 'نقدي' : $rp->payment_method->value) }}</td>
<td class="px-4 py-2 text-gray-600" dir="ltr">{{ $rp->cheque_number ?? '—' }}</td>
<td class="px-4 py-2 text-gray-600" dir="ltr">{{ $rp->payment_date?->format('Y-m-d') }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<div class="text-center py-8">
<svg class="w-12 h-12 text-gray-300 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
</svg>
<p class="text-sm text-gray-500">{{ __('لا توجد دفعات إيجار مسجلة لهذه المنشأة') }}</p>
</div>
@endif
</div>
@endcan
{{-- Schedule Tab (Today) --}}
<div x-show="activeTab === 'schedule'" x-cloak>
<div class="mb-4">
......
<div>
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4 sm:mb-6">
<h1 class="text-xl sm:text-2xl font-bold text-gray-800">{{ __('تسجيل مصروف') }}</h1>
<a href="{{ route('expenses.list') }}" wire:navigate
class="inline-flex items-center gap-1 text-sm text-gray-600 hover:text-gray-800">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 17l-5-5m0 0l5-5m-5 5h12"/></svg>
{{ __('سجل المصروفات') }}
</a>
</div>
@if(session('error'))
<div class="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">{{ session('error') }}</div>
@endif
<form wire:submit="save" class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6">
{{-- Category --}}
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('فئة المصروف') }} <span class="text-red-500">*</span></label>
<select wire:model="category" class="w-full rounded-lg border-gray-300 text-sm py-2.5">
<option value="">{{ __('اختر الفئة') }}</option>
@foreach($categories as $cat)
<option value="{{ $cat->value }}">{{ $cat->label() }}</option>
@endforeach
</select>
@error('category') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
{{-- Amount --}}
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('المبلغ (ج.م)') }} <span class="text-red-500">*</span></label>
<input type="number" wire:model="amount_display" dir="ltr" step="0.01" min="0"
class="w-full rounded-lg border-gray-300 text-sm py-2.5" placeholder="0.00">
@error('amount_display') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
{{-- Description --}}
<div class="md:col-span-2">
<label class="block text-sm text-gray-600 mb-1">{{ __('الوصف') }} <span class="text-red-500">*</span></label>
<input type="text" wire:model="description"
class="w-full rounded-lg border-gray-300 text-sm py-2.5"
placeholder="{{ __('مثال: شراء أدوات تدريب للفريق الأول') }}">
@error('description') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
{{-- Recipient --}}
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('المستلم / راحت لمين') }}</label>
<input type="text" wire:model="recipient_name"
class="w-full rounded-lg border-gray-300 text-sm py-2.5"
placeholder="{{ __('اسم الشخص أو المحل') }}">
@error('recipient_name') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
{{-- Payment Method --}}
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('طريقة الدفع') }} <span class="text-red-500">*</span></label>
<select wire:model="payment_method" class="w-full rounded-lg border-gray-300 text-sm py-2.5">
<option value="cash">{{ __('نقدي') }}</option>
<option value="card">{{ __('بطاقة') }}</option>
<option value="bank_transfer">{{ __('تحويل بنكي') }}</option>
<option value="cheque">{{ __('شيك') }}</option>
<option value="other">{{ __('أخرى') }}</option>
</select>
@error('payment_method') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
{{-- Receipt Reference --}}
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('رقم الإيصال') }}</label>
<input type="text" wire:model="receipt_reference" dir="ltr"
class="w-full rounded-lg border-gray-300 text-sm py-2.5">
@error('receipt_reference') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
{{-- Date --}}
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('تاريخ المصروف') }} <span class="text-red-500">*</span></label>
<input type="date" wire:model="expense_date" dir="ltr"
class="w-full rounded-lg border-gray-300 text-sm py-2.5">
@error('expense_date') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
{{-- Notes --}}
<div class="md:col-span-2">
<label class="block text-sm text-gray-600 mb-1">{{ __('ملاحظات') }}</label>
<textarea wire:model="notes" rows="2"
class="w-full rounded-lg border-gray-300 text-sm py-2.5"></textarea>
</div>
</div>
{{-- Submit --}}
<div class="mt-6 flex justify-end">
<button type="submit" wire:loading.attr="disabled" wire:target="save"
class="inline-flex items-center gap-2 px-6 py-2.5 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition text-sm font-medium disabled:opacity-50">
<span wire:loading.remove wire:target="save">{{ __('تسجيل المصروف') }}</span>
<span wire:loading wire:target="save">{{ __('جارٍ الحفظ...') }}</span>
</button>
</div>
</form>
</div>
<div>
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4 sm:mb-6">
<h1 class="text-xl sm:text-2xl font-bold text-gray-800">{{ __('سجل المصروفات') }}</h1>
@can('expenses.create')
<a href="{{ route('expenses.create') }}" wire:navigate
class="inline-flex items-center gap-2 px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition text-sm">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
{{ __('مصروف جديد') }}
</a>
@endcan
</div>
@if(session('success'))
<div class="mb-4 p-3 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm">{{ session('success') }}</div>
@endif
{{-- Summary Card --}}
<div class="mb-4 p-4 bg-gradient-to-l from-red-50 to-white border border-red-200 rounded-xl">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center">
<svg class="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
</div>
<div>
<p class="text-sm text-gray-500">{{ __('إجمالي المصروفات') }}</p>
<p class="text-lg font-bold text-red-700" dir="ltr">{{ format_money($totalAmount) }}</p>
</div>
</div>
</div>
{{-- Filters --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-4">
<div class="grid grid-cols-1 sm:grid-cols-4 gap-3">
<input type="text" wire:model.live.debounce.300ms="search"
class="w-full rounded-lg border-gray-300 text-sm py-2" placeholder="{{ __('بحث بالوصف أو المستلم...') }}">
<select wire:model.live="category_filter" class="w-full rounded-lg border-gray-300 text-sm py-2">
<option value="">{{ __('كل الفئات') }}</option>
@foreach($categories as $cat)
<option value="{{ $cat->value }}">{{ $cat->label() }}</option>
@endforeach
</select>
<input type="date" wire:model.live="from_date" dir="ltr"
class="w-full rounded-lg border-gray-300 text-sm py-2" placeholder="{{ __('من') }}">
<input type="date" wire:model.live="to_date" dir="ltr"
class="w-full rounded-lg border-gray-300 text-sm py-2" placeholder="{{ __('إلى') }}">
</div>
</div>
{{-- Table --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div wire:loading.class="opacity-50 pointer-events-none" class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50 border-b">
<tr>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500">{{ __('التاريخ') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500">{{ __('الفئة') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500">{{ __('الوصف') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500">{{ __('المبلغ') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500">{{ __('المستلم') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500">{{ __('طريقة الدفع') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500">{{ __('بواسطة') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@forelse($expenses as $expense)
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 text-gray-600" dir="ltr">{{ $expense->expense_date?->format('Y-m-d') }}</td>
<td class="px-4 py-3">
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-700">
{{ $expense->category->label() }}
</span>
</td>
<td class="px-4 py-3 text-gray-800 max-w-xs truncate">{{ $expense->description }}</td>
<td class="px-4 py-3 font-semibold text-red-700" dir="ltr">{{ format_money($expense->amount) }}</td>
<td class="px-4 py-3 text-gray-600">{{ $expense->recipient_name ?? '—' }}</td>
<td class="px-4 py-3 text-gray-600">
@php
$methodLabels = [
'cash' => 'نقدي',
'card' => 'بطاقة',
'bank_transfer' => 'تحويل بنكي',
'cheque' => 'شيك',
'other' => 'أخرى',
];
@endphp
{{ $methodLabels[$expense->payment_method->value ?? $expense->payment_method] ?? $expense->payment_method }}
</td>
<td class="px-4 py-3 text-gray-500 text-xs">{{ $expense->creator?->name }}</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-4 py-8 text-center text-gray-400">{{ __('لا توجد مصروفات مسجلة') }}</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($expenses->hasPages())
<div class="px-4 py-3 border-t">{{ $expenses->links() }}</div>
@endif
</div>
</div>
<div>
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4 sm:mb-6">
<h1 class="text-xl sm:text-2xl font-bold text-gray-800">{{ __('تسجيل دفعة إيجار منشأة') }}</h1>
<a href="{{ route('expenses.rent.list') }}" wire:navigate
class="inline-flex items-center gap-1 text-sm text-gray-600 hover:text-gray-800">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 17l-5-5m0 0l5-5m-5 5h12"/></svg>
{{ __('سجل الإيجارات') }}
</a>
</div>
@if(session('error'))
<div class="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">{{ session('error') }}</div>
@endif
<form wire:submit="save" class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6">
{{-- Facility --}}
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('المنشأة') }} <span class="text-red-500">*</span></label>
<select wire:model.live="facility_id" class="w-full rounded-lg border-gray-300 text-sm py-2.5">
<option value="">{{ __('اختر المنشأة') }}</option>
@foreach($facilities as $f)
<option value="{{ $f->id }}">
{{ $f->name_ar }}
@if($f->monthly_rental_cost)
({{ format_money($f->monthly_rental_cost) }}/{{ __('شهر') }})
@endif
</option>
@endforeach
</select>
@error('facility_id') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
{{-- Period --}}
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('الشهر') }} <span class="text-red-500">*</span></label>
<input type="month" wire:model="period" dir="ltr"
class="w-full rounded-lg border-gray-300 text-sm py-2.5">
@error('period') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
{{-- Amount --}}
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('المبلغ (ج.م)') }} <span class="text-red-500">*</span></label>
<input type="number" wire:model="amount_display" dir="ltr" step="0.01" min="0"
class="w-full rounded-lg border-gray-300 text-sm py-2.5" placeholder="0.00">
@error('amount_display') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
{{-- Payment Method --}}
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('طريقة الدفع') }} <span class="text-red-500">*</span></label>
<select wire:model.live="payment_method" class="w-full rounded-lg border-gray-300 text-sm py-2.5">
<option value="cheque">{{ __('شيك') }}</option>
<option value="cash">{{ __('نقدي') }}</option>
<option value="bank_transfer">{{ __('تحويل بنكي') }}</option>
<option value="card">{{ __('بطاقة') }}</option>
<option value="other">{{ __('أخرى') }}</option>
</select>
@error('payment_method') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
{{-- Cheque fields (shown when payment_method is cheque) --}}
@if($payment_method === 'cheque')
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('رقم الشيك') }} <span class="text-red-500">*</span></label>
<input type="text" wire:model="cheque_number" dir="ltr"
class="w-full rounded-lg border-gray-300 text-sm py-2.5">
@error('cheque_number') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('تاريخ الشيك') }} <span class="text-red-500">*</span></label>
<input type="date" wire:model="cheque_date" dir="ltr"
class="w-full rounded-lg border-gray-300 text-sm py-2.5">
@error('cheque_date') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('البنك') }} <span class="text-red-500">*</span></label>
<input type="text" wire:model="bank_name"
class="w-full rounded-lg border-gray-300 text-sm py-2.5" placeholder="{{ __('مثال: البنك الأهلي') }}">
@error('bank_name') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
@endif
{{-- Recipient --}}
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('المستلم') }}</label>
<input type="text" wire:model="recipient_name"
class="w-full rounded-lg border-gray-300 text-sm py-2.5" placeholder="{{ __('اسم المؤجر / المالك') }}">
@error('recipient_name') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
{{-- Payment Date --}}
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('تاريخ الدفع') }} <span class="text-red-500">*</span></label>
<input type="date" wire:model="payment_date" dir="ltr"
class="w-full rounded-lg border-gray-300 text-sm py-2.5">
@error('payment_date') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
{{-- Notes --}}
<div class="md:col-span-2">
<label class="block text-sm text-gray-600 mb-1">{{ __('ملاحظات') }}</label>
<textarea wire:model="notes" rows="2"
class="w-full rounded-lg border-gray-300 text-sm py-2.5"></textarea>
</div>
</div>
{{-- Submit --}}
<div class="mt-6 flex justify-end">
<button type="submit" wire:loading.attr="disabled" wire:target="save"
class="inline-flex items-center gap-2 px-6 py-2.5 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition text-sm font-medium disabled:opacity-50">
<span wire:loading.remove wire:target="save">{{ __('تسجيل الدفعة') }}</span>
<span wire:loading wire:target="save">{{ __('جارٍ الحفظ...') }}</span>
</button>
</div>
</form>
</div>
<div>
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4 sm:mb-6">
<h1 class="text-xl sm:text-2xl font-bold text-gray-800">{{ __('سجل إيجارات المنشآت') }}</h1>
@can('facilities.update')
<a href="{{ route('expenses.rent.create') }}" wire:navigate
class="inline-flex items-center gap-2 px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition text-sm">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
{{ __('تسجيل دفعة') }}
</a>
@endcan
</div>
@if(session('success'))
<div class="mb-4 p-3 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm">{{ session('success') }}</div>
@endif
{{-- Summary Card --}}
<div class="mb-4 p-4 bg-gradient-to-l from-emerald-50 to-white border border-emerald-200 rounded-xl">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-emerald-100 rounded-lg flex items-center justify-center">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
</div>
<div>
<p class="text-sm text-gray-500">{{ __('إجمالي المدفوع') }}</p>
<p class="text-lg font-bold text-emerald-700" dir="ltr">{{ format_money($totalAmount) }}</p>
</div>
</div>
</div>
{{-- Filters --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-4">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<input type="text" wire:model.live.debounce.300ms="search"
class="w-full rounded-lg border-gray-300 text-sm py-2" placeholder="{{ __('بحث برقم شيك أو مستلم...') }}">
<input type="month" wire:model.live="period_filter" dir="ltr"
class="w-full rounded-lg border-gray-300 text-sm py-2">
</div>
</div>
{{-- Table --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div wire:loading.class="opacity-50 pointer-events-none" class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50 border-b">
<tr>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500">{{ __('المنشأة') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500">{{ __('الشهر') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500">{{ __('المبلغ') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500">{{ __('طريقة الدفع') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500">{{ __('رقم الشيك') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500">{{ __('المستلم') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500">{{ __('تاريخ الدفع') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500">{{ __('بواسطة') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@forelse($payments as $payment)
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 font-medium text-gray-800">{{ $payment->facility?->name_ar }}</td>
<td class="px-4 py-3 text-gray-600" dir="ltr">{{ $payment->period }}</td>
<td class="px-4 py-3 font-semibold text-gray-800" dir="ltr">{{ format_money($payment->amount) }}</td>
<td class="px-4 py-3 text-gray-600">
@php
$methodLabels = [
'cheque' => 'شيك',
'cash' => 'نقدي',
'bank_transfer' => 'تحويل بنكي',
'card' => 'بطاقة',
'other' => 'أخرى',
];
@endphp
{{ $methodLabels[$payment->payment_method->value ?? $payment->payment_method] ?? $payment->payment_method }}
</td>
<td class="px-4 py-3 text-gray-600" dir="ltr">{{ $payment->cheque_number ?? '—' }}</td>
<td class="px-4 py-3 text-gray-600">{{ $payment->recipient_name ?? '—' }}</td>
<td class="px-4 py-3 text-gray-600" dir="ltr">{{ $payment->payment_date?->format('Y-m-d') }}</td>
<td class="px-4 py-3 text-gray-500 text-xs">{{ $payment->creator?->name }}</td>
</tr>
@empty
<tr>
<td colspan="8" class="px-4 py-8 text-center text-gray-400">{{ __('لا توجد دفعات إيجار مسجلة') }}</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($payments->hasPages())
<div class="px-4 py-3 border-t">{{ $payments->links() }}</div>
@endif
</div>
</div>
......@@ -236,6 +236,16 @@
Route::get('/financial-overview', FinancialOverview::class)->name('financial.overview')
->middleware('permission:invoices.list');
// Expenses
Route::get('/expenses', \App\Livewire\Financial\ExpenseList::class)->name('expenses.list')
->middleware('permission:expenses.create');
Route::get('/expenses/create', \App\Livewire\Financial\ExpenseForm::class)->name('expenses.create')
->middleware('permission:expenses.create');
Route::get('/expenses/rent', \App\Livewire\Financial\FacilityRentList::class)->name('expenses.rent.list')
->middleware('permission:facilities.update');
Route::get('/expenses/rent/create', \App\Livewire\Financial\FacilityRentForm::class)->name('expenses.rent.create')
->middleware('permission:facilities.update');
// Invoices
Route::get('/invoices', InvoiceList::class)->name('invoices.list')
->middleware('permission:invoices.list');
......
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