Commit 047d2619 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add expenses (مصروفات) statistics to dashboard and financial overview

- Dashboard: new "مصروفات اليوم" stat card with monthly total
- Financial overview: includes direct expenses from expenses table in
  totals, expense breakdown by category, recent expenses list
- Monthly P&L chart now includes direct expenses in calculation
- Expense breakdown section links to expenses list and create form

The /expenses/create form already records double-entry transactions
via ExpenseService — this commit ensures those records are reflected
in all financial statistics and reporting views.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 0fb3cd87
......@@ -8,6 +8,7 @@
use App\Domain\Document\Enums\DocumentType;
use App\Domain\Document\Models\Document;
use App\Domain\Financial\Enums\InvoiceStatus;
use App\Domain\Financial\Models\Expense;
use App\Domain\Financial\Models\Invoice;
use App\Domain\Financial\Models\Payment;
use App\Domain\HR\Enums\PayslipStatus;
......@@ -69,6 +70,14 @@ public function render()
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->sum('amount');
$expensesToday = Expense::whereDate('expense_date', $today)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->sum('amount');
$expensesThisMonth = Expense::whereBetween('expense_date', [now()->startOfMonth(), now()->endOfMonth()])
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->sum('amount');
$activeParticipants = Participant::where('status', 'active')
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->count();
......@@ -157,6 +166,8 @@ public function render()
'sessionsToday' => $sessionsToday,
'trainersPresent' => $trainersPresent,
'paymentsToday' => $paymentsToday,
'expensesToday' => $expensesToday,
'expensesThisMonth' => $expensesThisMonth,
'activeParticipants' => $activeParticipants,
// Row 2
'overdueInvoicesCount' => $overdueInvoicesCount,
......
......@@ -4,6 +4,8 @@
use App\Domain\Facility\Models\Facility;
use App\Domain\Financial\Enums\AccountType;
use App\Domain\Financial\Enums\ExpenseCategory;
use App\Domain\Financial\Models\Expense;
use App\Domain\Financial\Models\FinancialAccount;
use App\Domain\Financial\Models\Invoice;
use App\Domain\Financial\Models\Payment;
......@@ -169,11 +171,42 @@ private function getExpenses($from, $to): array
$facilityCostsThisPeriod = $this->calculateProRatedFacilityCost($from, $to);
// Direct expenses from the expenses table
$directExpensesQuery = Expense::whereBetween('expense_date', [$from, $to]);
if ($this->branch_id) {
$directExpensesQuery->where('branch_id', $this->branch_id);
}
$directExpensesTotal = (clone $directExpensesQuery)->sum('amount');
// Breakdown by category
$byCategory = (clone $directExpensesQuery)
->select('category', DB::raw('SUM(amount) as total'), DB::raw('COUNT(*) as count'))
->groupBy('category')
->orderByDesc('total')
->get()
->map(fn ($e) => [
'category' => $e->category,
'label' => $e->category instanceof ExpenseCategory ? $e->category->label() : $e->category,
'total' => (int) $e->total,
'count' => (int) $e->count,
])->toArray();
// Recent expenses
$recentExpenses = Expense::whereBetween('expense_date', [$from, $to])
->when($this->branch_id, fn ($q) => $q->where('branch_id', $this->branch_id))
->with('creator')
->orderByDesc('expense_date')
->limit(10)
->get();
return [
'outbound_payments' => $outboundPayments,
'purchase_orders' => $purchaseOrders,
'facility_costs' => $facilityCostsThisPeriod,
'total' => $outboundPayments + $purchaseOrders + $facilityCostsThisPeriod,
'direct_expenses' => $directExpensesTotal,
'by_category' => $byCategory,
'recent_expenses' => $recentExpenses,
'total' => $outboundPayments + $purchaseOrders + $facilityCostsThisPeriod + $directExpensesTotal,
];
}
......@@ -219,7 +252,11 @@ private function getMonthlyPL(): array
}
$facilityCost = $facilityQuery->sum('monthly_rental_cost');
$totalExpenses = $outbound + $purchases + $facilityCost;
$directExpenses = Expense::whereBetween('expense_date', [$start, $end])
->when($this->branch_id, fn ($q) => $q->where('branch_id', $this->branch_id))
->sum('amount');
$totalExpenses = $outbound + $purchases + $facilityCost + $directExpenses;
$months[] = [
'label' => $month->translatedFormat('M Y'),
......
......@@ -5,8 +5,8 @@
<p class="text-gray-500 text-sm mt-1">{{ now()->translatedFormat('l j F Y') }} &mdash; {{ __('مرحبا') }}، {{ auth()->user()->name_ar ?? auth()->user()->name }}</p>
</div>
<!-- Row 1: Today's Overview (4 stat cards) — auto-refresh every 30s -->
<div wire:poll.30s class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<!-- Row 1: Today's Overview (5 stat cards) — auto-refresh every 30s -->
<div wire:poll.30s class="grid grid-cols-2 lg:grid-cols-5 gap-4 mb-6">
<!-- Sessions Today -->
<div class="bg-white rounded-xl shadow-sm border border-blue-200 p-4 sm:p-5">
<div class="flex items-center gap-3">
......@@ -52,6 +52,24 @@
</div>
</div>
<!-- Expenses Today -->
<div class="bg-white rounded-xl shadow-sm border border-red-200 p-4 sm:p-5">
<div class="flex items-center gap-3">
<div class="shrink-0 w-11 h-11 bg-red-50 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 17h8m0 0V9m0 8l-8-8-4 4-6-6"/>
</svg>
</div>
<div class="min-w-0">
<p class="text-2xl sm:text-3xl font-bold text-red-600" dir="ltr">{{ number_format($expensesToday / 100, 2) }} <span class="text-sm">{{ __('ج.م') }}</span></p>
<p class="text-xs sm:text-sm text-gray-500">{{ __('مصروفات اليوم') }}</p>
</div>
</div>
@if($expensesThisMonth > 0)
<p class="text-xs text-gray-400 mt-2" dir="ltr">{{ number_format($expensesThisMonth / 100, 0) }} {{ __('ج.م هذا الشهر') }}</p>
@endif
</div>
<!-- Active Participants -->
<div class="bg-white rounded-xl shadow-sm border border-purple-200 p-4 sm:p-5">
<div class="flex items-center gap-3">
......
......@@ -160,11 +160,31 @@
{{-- Expense Breakdown --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6">
<h3 class="text-sm sm:text-base font-semibold text-gray-800 mb-4">{{ __('تفاصيل المصروفات') }}</h3>
<div class="flex items-center justify-between mb-4">
<h3 class="text-sm sm:text-base font-semibold text-gray-800">{{ __('تفاصيل المصروفات') }}</h3>
@can('expenses.create')
<a href="{{ route('expenses.create') }}" wire:navigate class="text-xs text-blue-600 hover:text-blue-700 font-medium">+ {{ __('تسجيل مصروف') }}</a>
@endcan
</div>
@if($expenses['total'] > 0)
<div class="space-y-4">
{{-- Direct Expenses (from expenses table) --}}
@if($expenses['direct_expenses'] > 0)
@php $pct = round(($expenses['direct_expenses'] / $expenses['total']) * 100); @endphp
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600 font-medium">{{ __('مصروفات مسجلة') }}</span>
<span class="font-medium" dir="ltr">{{ number_format($expenses['direct_expenses'] / 100, 0) }} {{ __('ج.م') }} <span class="text-gray-400">({{ $pct }}%)</span></span>
</div>
<div class="w-full bg-gray-100 rounded-full h-2.5">
<div class="bg-red-500 h-2.5 rounded-full" style="width: {{ $pct }}%"></div>
</div>
</div>
@endif
{{-- Facility Costs --}}
@php $pct = $expenses['total'] > 0 ? round(($expenses['facility_costs'] / $expenses['total']) * 100) : 0; @endphp
@if($expenses['facility_costs'] > 0)
@php $pct = round(($expenses['facility_costs'] / $expenses['total']) * 100); @endphp
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">{{ __('إيجارات المنشآت') }}</span>
......@@ -174,9 +194,11 @@
<div class="bg-amber-500 h-2.5 rounded-full" style="width: {{ $pct }}%"></div>
</div>
</div>
@endif
{{-- Purchase Orders --}}
@php $pct = $expenses['total'] > 0 ? round(($expenses['purchase_orders'] / $expenses['total']) * 100) : 0; @endphp
@if($expenses['purchase_orders'] > 0)
@php $pct = round(($expenses['purchase_orders'] / $expenses['total']) * 100); @endphp
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">{{ __('مشتريات المخزون') }}</span>
......@@ -186,9 +208,11 @@
<div class="bg-blue-500 h-2.5 rounded-full" style="width: {{ $pct }}%"></div>
</div>
</div>
@endif
{{-- Outbound Payments --}}
@php $pct = $expenses['total'] > 0 ? round(($expenses['outbound_payments'] / $expenses['total']) * 100) : 0; @endphp
@if($expenses['outbound_payments'] > 0)
@php $pct = round(($expenses['outbound_payments'] / $expenses['total']) * 100); @endphp
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">{{ __('مدفوعات أخرى') }}</span>
......@@ -198,6 +222,7 @@
<div class="bg-purple-500 h-2.5 rounded-full" style="width: {{ $pct }}%"></div>
</div>
</div>
@endif
{{-- Total Bar --}}
<div class="pt-3 border-t border-gray-100">
......@@ -350,7 +375,72 @@
</div>
</div>
{{-- Row 5: Collection Rate Progress --}}
{{-- Row 5: Expense Categories + Recent Expenses --}}
@if(count($expenses['by_category']) > 0 || $expenses['recent_expenses']->isNotEmpty())
<div class="grid grid-cols-1 lg:grid-cols-2 gap-3 sm:gap-4 mb-4 sm:mb-6">
{{-- Expense by Category --}}
@if(count($expenses['by_category']) > 0)
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-sm sm:text-base font-semibold text-gray-800">{{ __('المصروفات حسب الفئة') }}</h3>
<a href="{{ route('expenses.list') }}" wire:navigate class="text-xs text-blue-600 hover:text-blue-700 font-medium">{{ __('عرض الكل') }}</a>
</div>
@php
$maxCat = max(1, collect($expenses['by_category'])->max('total'));
$catColors = ['maintenance' => 'bg-orange-500', 'supplies' => 'bg-cyan-500', 'transport' => 'bg-indigo-500', 'food_beverage' => 'bg-yellow-500', 'sports_equipment' => 'bg-green-500', 'printing' => 'bg-pink-500', 'cleaning' => 'bg-teal-500', 'medical' => 'bg-red-500', 'utilities' => 'bg-violet-500', 'other' => 'bg-gray-500'];
@endphp
<div class="space-y-3">
@foreach($expenses['by_category'] as $cat)
@php $catKey = $cat['category'] instanceof \App\Domain\Financial\Enums\ExpenseCategory ? $cat['category']->value : $cat['category']; @endphp
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">{{ $cat['label'] }}</span>
<span class="font-medium" dir="ltr">{{ number_format($cat['total'] / 100, 0) }} {{ __('ج.م') }} <span class="text-gray-400">({{ $cat['count'] }})</span></span>
</div>
<div class="w-full bg-gray-100 rounded-full h-2">
<div class="{{ $catColors[$catKey] ?? 'bg-gray-500' }} h-2 rounded-full" style="width: {{ ($cat['total'] / $maxCat) * 100 }}%"></div>
</div>
</div>
@endforeach
</div>
</div>
@endif
{{-- Recent Expenses --}}
@if($expenses['recent_expenses']->isNotEmpty())
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-sm sm:text-base font-semibold text-gray-800">{{ __('آخر المصروفات المسجلة') }}</h3>
@can('expenses.create')
<a href="{{ route('expenses.create') }}" wire:navigate class="text-xs text-blue-600 hover:text-blue-700 font-medium">+ {{ __('جديد') }}</a>
@endcan
</div>
<div class="space-y-2 max-h-[350px] overflow-y-auto">
@foreach($expenses['recent_expenses'] as $exp)
<div class="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
<div class="shrink-0 w-9 h-9 bg-red-100 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 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 class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-800 truncate">{{ $exp->description }}</p>
<p class="text-xs text-gray-500">
{{ $exp->category->label() }}
@if($exp->recipient_name) — {{ $exp->recipient_name }} @endif
&middot; {{ $exp->expense_date->translatedFormat('j M') }}
</p>
</div>
<span class="text-sm font-bold text-red-600 whitespace-nowrap" dir="ltr">{{ number_format($exp->amount / 100, 2) }} {{ __('ج.م') }}</span>
</div>
@endforeach
</div>
</div>
@endif
</div>
@endif
{{-- Row 6: Collection Rate Progress --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6">
<h3 class="text-sm sm:text-base font-semibold text-gray-800 mb-4">{{ __('معدل تحصيل الفواتير') }}</h3>
<div class="grid grid-cols-2 md:grid-cols-3 gap-4 sm:gap-6">
......
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