Commit 0618b998 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add branch switcher in topbar with session-based branch scoping

- BranchSwitcher component in topbar: select branch or "all branches"
- UsesBranchScope trait: all components read active branch from session
- Dashboard/Reports: filter by selected branch, show all when "all"
- Receptionist wizards: use session branch instead of user->branch_id
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 4e5b3052
......@@ -13,29 +13,34 @@
class ReportService
{
public function financialSummary(string $from, string $to): array
public function financialSummary(string $from, string $to, ?int $branchId = null): array
{
return [
'total_invoiced' => Invoice::whereBetween('created_at', [$from, $to])->sum('total_amount'),
'total_invoiced' => Invoice::whereBetween('created_at', [$from, $to])
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))->sum('total_amount'),
'total_collected' => Payment::where('status', 'confirmed')
->whereBetween('created_at', [$from, $to])->sum('amount'),
->whereBetween('created_at', [$from, $to])
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))->sum('amount'),
'total_outstanding' => Invoice::whereIn('status', ['sent', 'partially_paid', 'overdue'])
->sum('due_amount'),
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))->sum('due_amount'),
'pos_revenue' => POSTransaction::whereBetween('processed_at', [$from, $to])
->sum('total_amount'),
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))->sum('total_amount'),
'payment_methods' => Payment::where('status', 'confirmed')
->whereBetween('created_at', [$from, $to])
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->select('method', DB::raw('SUM(amount) as total'), DB::raw('COUNT(*) as count'))
->groupBy('method')
->get()->toArray(),
];
}
public function attendanceReport(string $from, string $to, ?int $groupId = null): array
public function attendanceReport(string $from, string $to, ?int $groupId = null, ?int $branchId = null): array
{
$query = AttendanceRecord::whereBetween('created_at', [$from, $to]);
if ($groupId) {
$query->whereHas('session', fn ($q) => $q->where('training_group_id', $groupId));
} elseif ($branchId) {
$query->whereHas('session.group', fn ($q) => $q->where('branch_id', $branchId));
}
$total = (clone $query)->count();
......@@ -53,16 +58,20 @@ public function attendanceReport(string $from, string $to, ?int $groupId = null)
];
}
public function enrollmentReport(string $from, string $to): array
public function enrollmentReport(string $from, string $to, ?int $branchId = null): array
{
return [
'new_enrollments' => Enrollment::whereBetween('enrollment_date', [$from, $to])->count(),
'new_enrollments' => Enrollment::whereBetween('enrollment_date', [$from, $to])
->when($branchId, fn ($q) => $q->whereHas('group', fn ($g) => $g->where('branch_id', $branchId)))->count(),
'by_status' => Enrollment::whereBetween('enrollment_date', [$from, $to])
->when($branchId, fn ($q) => $q->whereHas('group', fn ($g) => $g->where('branch_id', $branchId)))
->select('status', DB::raw('COUNT(*) as count'))
->groupBy('status')
->pluck('count', 'status')->toArray(),
'active_participants' => Participant::where('status', 'active')->count(),
'active_participants' => Participant::where('status', 'active')
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))->count(),
'by_membership_type' => Participant::where('status', 'active')
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereNotNull('membership_type')
->select('membership_type', DB::raw('COUNT(*) as count'))
->groupBy('membership_type')
......@@ -74,6 +83,7 @@ public function participantList(array $filters = []): \Illuminate\Support\Collec
{
$query = Participant::with(['person', 'enrollments.group'])
->when($filters['status'] ?? null, fn ($q, $s) => $q->where('status', $s))
->when($filters['branch_id'] ?? null, fn ($q, $b) => $q->where('branch_id', $b))
->when($filters['group_id'] ?? null, fn ($q, $g) => $q->whereHas(
'enrollments',
fn ($eq) => $eq->where('training_group_id', $g)->where('status', 'active')
......
<?php
namespace App\Domain\Shared\Traits;
use App\Domain\Identity\Models\Branch;
trait UsesBranchScope
{
public function getActiveBranchId(): ?int
{
return session('active_branch_id', auth()->user()->branch_id);
}
public function getActiveBranchIdOrFail(): int
{
$id = $this->getActiveBranchId();
if (!$id) {
$id = Branch::where('is_active', true)->first()?->id;
if ($id) {
session(['active_branch_id' => $id]);
}
}
return $id;
}
public function isAllBranches(): bool
{
return session('active_branch_id') === null && !session()->has('active_branch_id');
}
}
<?php
namespace App\Livewire;
use App\Domain\Identity\Models\Branch;
use Livewire\Component;
class BranchSwitcher extends Component
{
public ?int $activeBranchId = null;
public function mount(): void
{
$this->activeBranchId = session('active_branch_id', auth()->user()->branch_id);
if (!$this->activeBranchId) {
$first = Branch::where('is_active', true)->first();
if ($first) {
$this->activeBranchId = $first->id;
session(['active_branch_id' => $first->id]);
}
}
}
public function updatedActiveBranchId($value): void
{
if ($value === 'all') {
session(['active_branch_id' => null]);
$this->activeBranchId = null;
} else {
$branchId = (int) $value;
session(['active_branch_id' => $branchId]);
$this->activeBranchId = $branchId;
}
$this->dispatch('branch-switched', branchId: $this->activeBranchId);
$this->redirect(request()->header('Referer', '/'), navigate: true);
}
public function render()
{
return view('livewire.branch-switcher', [
'branches' => Branch::where('is_active', true)->get(['id', 'name_ar', 'code']),
]);
}
}
......@@ -10,6 +10,7 @@
use App\Domain\Inventory\Models\Product;
use App\Domain\Participant\Models\Participant;
use App\Domain\POS\Models\POSTransaction;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Models\Enrollment;
use App\Domain\Training\Models\TrainingGroup;
use App\Domain\Training\Models\TrainingSession;
......@@ -21,32 +22,42 @@
#[Title('لوحة التحكم')]
class Dashboard extends Component
{
use UsesBranchScope;
public function render()
{
$today = now()->toDateString();
$thisMonth = now()->startOfMonth()->toDateString();
$branchId = $this->getActiveBranchId();
// Key metrics
$stats = [
'active_participants' => Participant::where('status', 'active')->count(),
'active_enrollments' => Enrollment::where('status', 'active')->count(),
'active_groups' => TrainingGroup::whereIn('status', ['active', 'full'])->count(),
'today_sessions' => TrainingSession::where('session_date', $today)->count(),
'active_participants' => Participant::where('status', 'active')
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))->count(),
'active_enrollments' => Enrollment::where('status', 'active')
->when($branchId, fn ($q) => $q->whereHas('group', fn ($g) => $g->where('branch_id', $branchId)))->count(),
'active_groups' => TrainingGroup::whereIn('status', ['active', 'full'])
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))->count(),
'today_sessions' => TrainingSession::where('session_date', $today)
->when($branchId, fn ($q) => $q->whereHas('group', fn ($g) => $g->where('branch_id', $branchId)))->count(),
];
// Financial summary (this month)
$financial = [
'revenue_this_month' => Payment::where('status', 'confirmed')
->whereDate('created_at', '>=', $thisMonth)->sum('amount'),
->whereDate('created_at', '>=', $thisMonth)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))->sum('amount'),
'outstanding_invoices' => Invoice::whereIn('status', [
InvoiceStatus::Sent, InvoiceStatus::PartiallyPaid, InvoiceStatus::Overdue,
])->sum('due_amount'),
'pos_today' => POSTransaction::whereDate('processed_at', $today)->sum('total_amount'),
'pos_today_count' => POSTransaction::whereDate('processed_at', $today)->count(),
])->when($branchId, fn ($q) => $q->where('branch_id', $branchId))->sum('due_amount'),
'pos_today' => POSTransaction::whereDate('processed_at', $today)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))->sum('total_amount'),
'pos_today_count' => POSTransaction::whereDate('processed_at', $today)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))->count(),
];
// Attendance today
$todaySessions = TrainingSession::where('session_date', $today)->pluck('id');
$todaySessions = TrainingSession::where('session_date', $today)
->when($branchId, fn ($q) => $q->whereHas('group', fn ($g) => $g->where('branch_id', $branchId)))
->pluck('id');
$attendance = [
'expected' => AttendanceRecord::whereIn('training_session_id', $todaySessions)->count(),
'present' => AttendanceRecord::whereIn('training_session_id', $todaySessions)
......@@ -58,23 +69,23 @@ public function render()
? round(($attendance['present'] / $attendance['expected']) * 100, 0)
: 0;
// Low stock alerts
$lowStockCount = Product::where('track_inventory', true)
->where('is_active', true)
->whereNotNull('min_stock_level')
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereHas('inventoryLevels', function ($q) {
$q->whereRaw('quantity_on_hand < (SELECT min_stock_level FROM products WHERE products.id = inventory_levels.product_id)');
})->count();
// Groups near capacity
$nearFullGroups = TrainingGroup::where('status', 'active')
->whereRaw('current_count >= max_capacity * 0.9')
->where('max_capacity', '>', 0)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->count();
// Recent enrollments (last 7 days)
$recentEnrollments = Enrollment::where('status', 'active')
->where('created_at', '>=', now()->subDays(7))
->when($branchId, fn ($q) => $q->whereHas('group', fn ($g) => $g->where('branch_id', $branchId)))
->count();
return view('livewire.dashboard', [
......
......@@ -7,6 +7,7 @@
use App\Domain\Financial\Services\PaymentService;
use App\Domain\Participant\Models\Participant;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
......@@ -15,6 +16,8 @@
#[Title('تحصيل مدفوعات')]
class CollectPaymentWizard extends Component
{
use UsesBranchScope;
public ?int $branchId = null;
public int $currentStep = 1;
......@@ -41,13 +44,7 @@ class CollectPaymentWizard extends Component
public function mount(): void
{
$this->authorize('invoices.create');
$user = auth()->user();
$this->branchId = $user->branch_id;
if (!$this->branchId) {
$this->branchId = \App\Domain\Identity\Models\Branch::where('is_active', true)->first()?->id;
}
$this->branchId = $this->getActiveBranchIdOrFail();
}
public function rules(): array
......
......@@ -4,6 +4,7 @@
use App\Domain\Participant\Models\Participant;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Models\Activity;
use App\Domain\Training\Models\Enrollment;
use App\Domain\Training\Models\TrainingProgram;
......@@ -15,6 +16,8 @@
#[Title('تسجيل في برنامج')]
class EnrollExistingWizard extends Component
{
use UsesBranchScope;
public ?int $branchId = null;
public int $currentStep = 1;
......@@ -39,13 +42,7 @@ class EnrollExistingWizard extends Component
public function mount(): void
{
$this->authorize('enrollments.create');
$user = auth()->user();
$this->branchId = $user->branch_id;
if (!$this->branchId) {
$this->branchId = \App\Domain\Identity\Models\Branch::where('is_active', true)->first()?->id;
}
$this->branchId = $this->getActiveBranchIdOrFail();
}
public function rules(): array
......
......@@ -5,6 +5,7 @@
use App\Domain\Financial\Enums\PaymentMethod;
use App\Domain\Participant\Services\ParticipantService;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Models\Activity;
use App\Domain\Training\Models\TrainingProgram;
use Livewire\Attributes\Layout;
......@@ -15,6 +16,8 @@
#[Title('تسجيل جديد')]
class NewRegistrationWizard extends Component
{
use UsesBranchScope;
public ?int $branchId = null;
public int $currentStep = 1;
......@@ -51,13 +54,7 @@ class NewRegistrationWizard extends Component
public function mount(): void
{
$this->authorize('participants.create');
$user = auth()->user();
$this->branchId = $user->branch_id;
if (!$this->branchId) {
$this->branchId = \App\Domain\Identity\Models\Branch::where('is_active', true)->first()?->id;
}
$this->branchId = $this->getActiveBranchIdOrFail();
}
public function rules(): array
......
......@@ -7,6 +7,7 @@
use App\Domain\Financial\Models\Payment;
use App\Domain\Participant\Models\Participant;
use App\Domain\Training\Models\TrainingSession;
use App\Domain\Shared\Traits\UsesBranchScope;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
......@@ -15,24 +16,16 @@
#[Title('مكتب الاستقبال')]
class ReceptionistDashboard extends Component
{
use UsesBranchScope;
public ?int $branchId = null;
public string $branchName = '';
public function mount(): void
{
$this->authorize('participants.list');
$user = auth()->user();
$this->branchId = $user->branch_id;
if (!$this->branchId) {
$firstBranch = \App\Domain\Identity\Models\Branch::where('is_active', true)->first();
$this->branchId = $firstBranch?->id;
}
$this->branchName = $this->branchId
? (\App\Domain\Identity\Models\Branch::find($this->branchId)?->name_ar ?? '')
: '';
$this->branchId = $this->getActiveBranchIdOrFail();
$this->branchName = \App\Domain\Identity\Models\Branch::find($this->branchId)?->name_ar ?? '';
}
public function render()
......
......@@ -3,6 +3,7 @@
namespace App\Livewire\Reports;
use App\Domain\Shared\Services\ReportService;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Models\TrainingGroup;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
......@@ -13,6 +14,8 @@
#[Title('التقارير')]
class ReportsPage extends Component
{
use UsesBranchScope;
#[Url]
public string $reportType = 'financial';
......@@ -38,16 +41,19 @@ public function setReportType(string $type): void
public function render(ReportService $reportService)
{
$branchId = $this->getActiveBranchId();
$reportData = match ($this->reportType) {
'financial' => $reportService->financialSummary($this->dateFrom, $this->dateTo),
'attendance' => $reportService->attendanceReport($this->dateFrom, $this->dateTo, $this->groupId),
'enrollment' => $reportService->enrollmentReport($this->dateFrom, $this->dateTo),
'financial' => $reportService->financialSummary($this->dateFrom, $this->dateTo, $branchId),
'attendance' => $reportService->attendanceReport($this->dateFrom, $this->dateTo, $this->groupId, $branchId),
'enrollment' => $reportService->enrollmentReport($this->dateFrom, $this->dateTo, $branchId),
default => [],
};
return view('livewire.reports.reports-page', [
'reportData' => $reportData,
'groups' => TrainingGroup::orderBy('name_ar')->get(),
'groups' => TrainingGroup::orderBy('name_ar')
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))->get(),
]);
}
}
......@@ -14,6 +14,9 @@
<!-- Right side -->
<div class="flex items-center gap-4">
<!-- Branch Switcher -->
@livewire('branch-switcher')
<!-- Notifications bell (placeholder) -->
<button class="relative text-gray-500 hover:text-gray-700">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/></svg>
......
<div class="relative">
<select wire:model.live="activeBranchId"
class="appearance-none bg-gray-50 border border-gray-200 rounded-lg px-3 py-1.5 pe-8 text-sm font-medium text-gray-700 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 cursor-pointer">
<option value="all">{{ __('كل الفروع') }}</option>
@foreach($branches as $branch)
<option value="{{ $branch->id }}">{{ $branch->name_ar }}</option>
@endforeach
</select>
<div class="pointer-events-none absolute inset-y-0 start-0 flex items-center ps-2.5">
<svg class="w-4 h-4 text-gray-500" 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>
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