Commit f282f7bc authored by Claude's avatar Claude

Filter every management screen by the active branch

The system predates branches, and adoption of the branch switcher was
partial: 44 of 186 Livewire components used UsesBranchScope, and several
that imported it never actually called it. The dashboard was the worst
case — half its widgets were branch-aware and half silently reported
academy-wide totals next to them, so the numbers on one screen were not
comparable with each other.

OC-Sport runs 7 active branches, so every unscoped widget was showing
six other branches' data.

Dashboard: scoped trainers-present, pending payslips, pending documents,
low stock and expiring medical certificates, which were academy-wide.
All six dashboard widgets (revenue, product revenue, subscription
revenue, enrolment trends, overdue renewals, trainer dues) now filter by
branch, including the raw-SQL CTEs in the revenue breakdowns.

Lists and reports: events, evaluations, base prices, pricing rules,
promotions, stock counts, kits, document approvals, trainers, trainer
advances, payroll, essential deliveries and the financial report.

Pickers: participant, group, program, facility, warehouse, product and
employee selectors now offer only the active branch's records, so a
transfer or invoice cannot silently reference another branch.

POS and InvoiceShow used auth()->user()->branch_id directly, ignoring
the switcher entirely — a user who switched branch still transacted
against their home branch. Both now read the active branch.

Deliberately left unscoped: parent- and guardian-facing screens, which
are scoped to their own children and have no branch switcher, and
single-record detail screens, which are already scoped by the record and
would hide legitimately related history for participants who moved
between branches.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
parent 60eabb70
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
namespace App\Livewire\Activities; namespace App\Livewire\Activities;
use App\Domain\HR\Models\Trainer; use App\Domain\HR\Models\Trainer;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Models\Activity; use App\Domain\Training\Models\Activity;
use App\Domain\Training\Services\ActivityService; use App\Domain\Training\Services\ActivityService;
use App\Livewire\Concerns\WithSorting; use App\Livewire\Concerns\WithSorting;
...@@ -15,7 +16,7 @@ ...@@ -15,7 +16,7 @@
#[Title('الأنشطة')] #[Title('الأنشطة')]
class ActivityList extends Component class ActivityList extends Component
{ {
use WithSorting; use WithSorting, UsesBranchScope;
#[Url] #[Url]
public string $search = ''; public string $search = '';
...@@ -57,6 +58,7 @@ public function delete(string $uuid, ActivityService $service): void ...@@ -57,6 +58,7 @@ public function delete(string $uuid, ActivityService $service): void
public function render() public function render()
{ {
$branchId = $this->getActiveBranchId();
$query = Activity::query() $query = Activity::query()
->withCount('participants') ->withCount('participants')
->when($this->search, fn ($q) => $q->where('name_ar', 'ilike', "%{$this->search}%") ->when($this->search, fn ($q) => $q->where('name_ar', 'ilike', "%{$this->search}%")
...@@ -65,7 +67,9 @@ public function render() ...@@ -65,7 +67,9 @@ public function render()
->orderBy($this->sortBy, $this->sortDir); ->orderBy($this->sortBy, $this->sortDir);
$trainersByActivity = []; $trainersByActivity = [];
foreach (Trainer::with('employee.person')->where('status', 'active')->get() as $trainer) { foreach (Trainer::with('employee.person')->where('status', 'active')
->when($branchId, fn ($q) => $q->whereHas('employee', fn ($e) => $e->forBranch($branchId)))
->get() as $trainer) {
if (!empty($trainer->sports)) { if (!empty($trainer->sports)) {
foreach ($trainer->sports as $actId) { foreach ($trainer->sports as $actId) {
$trainersByActivity[$actId][] = $trainer->employee->person->name_ar ?? ''; $trainersByActivity[$actId][] = $trainer->employee->person->name_ar ?? '';
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
use App\Domain\Financial\Services\PaymentService; use App\Domain\Financial\Services\PaymentService;
use App\Domain\Participant\Models\Participant; use App\Domain\Participant\Models\Participant;
use App\Domain\Shared\Exceptions\DomainException; use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
...@@ -19,6 +20,8 @@ ...@@ -19,6 +20,8 @@
#[Title('تصحيح فاتورة')] #[Title('تصحيح فاتورة')]
class InvoiceCorrectionWizard extends Component class InvoiceCorrectionWizard extends Component
{ {
use UsesBranchScope;
public int $currentStep = 1; public int $currentStep = 1;
// Step 1: Find participant // Step 1: Find participant
...@@ -322,7 +325,9 @@ public function render() ...@@ -322,7 +325,9 @@ public function render()
{ {
$searchResults = collect(); $searchResults = collect();
if (strlen($this->search) >= 2 && !$this->selectedParticipantId) { if (strlen($this->search) >= 2 && !$this->selectedParticipantId) {
$branchId = $this->getActiveBranchId();
$searchResults = Participant::query() $searchResults = Participant::query()
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->with('person') ->with('person')
->where(function ($q) { ->where(function ($q) {
$search = $this->search; $search = $this->search;
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
use App\Domain\Scheduling\Enums\AssignmentStatus; use App\Domain\Scheduling\Enums\AssignmentStatus;
use App\Domain\Scheduling\Services\AssignmentService; use App\Domain\Scheduling\Services\AssignmentService;
use App\Domain\Shared\Exceptions\DomainException; use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Models\TrainingGroup; use App\Domain\Training\Models\TrainingGroup;
use App\Domain\Training\Models\TrainingSession; use App\Domain\Training\Models\TrainingSession;
use App\Models\User; use App\Models\User;
...@@ -17,6 +18,8 @@ ...@@ -17,6 +18,8 @@
#[Title('إنشاء تكليف')] #[Title('إنشاء تكليف')]
class AssignmentForm extends Component class AssignmentForm extends Component
{ {
use UsesBranchScope;
public string $user_id = ''; public string $user_id = '';
public string $assignable_type = 'group'; public string $assignable_type = 'group';
public string $assignable_id = ''; public string $assignable_id = '';
...@@ -122,12 +125,15 @@ public function render() ...@@ -122,12 +125,15 @@ public function render()
$assignableOptions = []; $assignableOptions = [];
if ($this->assignable_type === 'group') { if ($this->assignable_type === 'group') {
$assignableOptions = TrainingGroup::orderBy('name_ar') $assignableOptions = TrainingGroup::query()
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->orderBy('name_ar')
->get(['id', 'name_ar', 'name']) ->get(['id', 'name_ar', 'name'])
->mapWithKeys(fn ($g) => [$g->id => $g->name_ar]) ->mapWithKeys(fn ($g) => [$g->id => $g->name_ar])
->toArray(); ->toArray();
} elseif ($this->assignable_type === 'session') { } elseif ($this->assignable_type === 'session') {
$assignableOptions = TrainingSession::with('group') $assignableOptions = TrainingSession::with('group')
->when($branchId, fn ($q) => $q->whereHas('group', fn ($g) => $g->where('branch_id', $branchId)))
->orderByDesc('session_date') ->orderByDesc('session_date')
->limit(100) ->limit(100)
->get() ->get()
......
...@@ -131,7 +131,10 @@ public function render() ...@@ -131,7 +131,10 @@ public function render()
->where('assignable_type', TrainingGroup::class) ->where('assignable_type', TrainingGroup::class)
->pluck('assignable_id'); ->pluck('assignable_id');
$branchId = $this->getActiveBranchId();
$headTrainerGroupIds = TrainingGroup::where('head_trainer_id', $user->id) $headTrainerGroupIds = TrainingGroup::where('head_trainer_id', $user->id)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereIn('status', ['active', 'forming']) ->whereIn('status', ['active', 'forming'])
->pluck('id'); ->pluck('id');
...@@ -139,6 +142,7 @@ public function render() ...@@ -139,6 +142,7 @@ public function render()
// Today's sessions for assigned groups // Today's sessions for assigned groups
$todaySessions = TrainingSession::whereIn('training_group_id', $allGroupIds) $todaySessions = TrainingSession::whereIn('training_group_id', $allGroupIds)
->when($branchId, fn ($q) => $q->whereHas('group', fn ($g) => $g->where('branch_id', $branchId)))
->where('session_date', $today) ->where('session_date', $today)
->whereIn('status', [SessionStatus::Scheduled, SessionStatus::InProgress, SessionStatus::Completed]) ->whereIn('status', [SessionStatus::Scheduled, SessionStatus::InProgress, SessionStatus::Completed])
->with('group') ->with('group')
......
...@@ -60,7 +60,10 @@ public function render() ...@@ -60,7 +60,10 @@ public function render()
->when($branchId, fn ($q) => $q->whereHas('group', fn ($g) => $g->where('branch_id', $branchId))) ->when($branchId, fn ($q) => $q->whereHas('group', fn ($g) => $g->where('branch_id', $branchId)))
->count(); ->count();
$trainersPresent = AttendanceRecord::whereHas('session', fn ($q) => $q->where('session_date', $today)) $trainersPresent = AttendanceRecord::whereHas('session', function ($q) use ($today, $branchId) {
$q->where('session_date', $today)
->when($branchId, fn ($s) => $s->whereHas('group', fn ($g) => $g->where('branch_id', $branchId)));
})
->where('subject_type', \App\Models\User::class) ->where('subject_type', \App\Models\User::class)
->whereIn('status', [AttendanceStatus::Present, AttendanceStatus::Late]) ->whereIn('status', [AttendanceStatus::Present, AttendanceStatus::Late])
->count(); ->count();
...@@ -101,16 +104,22 @@ public function render() ...@@ -101,16 +104,22 @@ public function render()
->count(); ->count();
$pendingPayslips = Payslip::whereIn('status', [PayslipStatus::Draft, PayslipStatus::PendingApproval]) $pendingPayslips = Payslip::whereIn('status', [PayslipStatus::Draft, PayslipStatus::PendingApproval])
->when($branchId, fn ($q) => $q->whereHas('trainer.employee', fn ($e) => $e->forBranch($branchId)))
->count(); ->count();
$pendingDocuments = Document::where('status', DocumentStatus::Pending) $pendingDocuments = Document::where('status', DocumentStatus::Pending)
->when($branchId, fn ($q) => $q->whereHasMorph('documentable', [Participant::class], fn ($p) => $p->where('branch_id', $branchId)))
->count(); ->count();
$lowStockItems = Product::where('track_inventory', true) $lowStockItems = Product::where('track_inventory', true)
->where('is_active', true) ->where('is_active', true)
->whereNotNull('min_stock_level') ->whereNotNull('min_stock_level')
->where('min_stock_level', '>', 0) ->where('min_stock_level', '>', 0)
->whereHas('inventoryLevels', fn ($q) => $q->whereRaw('quantity_on_hand <= (SELECT min_stock_level FROM products WHERE products.id = inventory_levels.product_id)')) ->when($branchId, fn ($q) => $q->where(fn ($b) => $b->whereNull('branch_id')->orWhere('branch_id', $branchId)))
->whereHas('inventoryLevels', function ($q) use ($branchId) {
$q->whereRaw('quantity_on_hand <= (SELECT min_stock_level FROM products WHERE products.id = inventory_levels.product_id)')
->when($branchId, fn ($l) => $l->where('branch_id', $branchId));
})
->count(); ->count();
$expiringMedicalCerts = Document::where('document_type', DocumentType::MedicalCertificate) $expiringMedicalCerts = Document::where('document_type', DocumentType::MedicalCertificate)
...@@ -118,6 +127,7 @@ public function render() ...@@ -118,6 +127,7 @@ public function render()
->whereNotNull('expires_at') ->whereNotNull('expires_at')
->where('expires_at', '<=', now()->addDays(7)) ->where('expires_at', '<=', now()->addDays(7))
->where('expires_at', '>=', $today) ->where('expires_at', '>=', $today)
->when($branchId, fn ($q) => $q->whereHasMorph('documentable', [Participant::class], fn ($p) => $p->where('branch_id', $branchId)))
->count(); ->count();
// Sessions that ended but attendance not taken // Sessions that ended but attendance not taken
......
...@@ -2,20 +2,25 @@ ...@@ -2,20 +2,25 @@
namespace App\Livewire\Dashboard; namespace App\Livewire\Dashboard;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Models\Enrollment; use App\Domain\Training\Models\Enrollment;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Livewire\Component; use Livewire\Component;
class EnrollmentTrends extends Component class EnrollmentTrends extends Component
{ {
use UsesBranchScope;
public string $period = '30'; public string $period = '30';
public function render() public function render()
{ {
$branchId = $this->getActiveBranchId();
$days = (int) $this->period; $days = (int) $this->period;
$startDate = now()->subDays($days)->toDateString(); $startDate = now()->subDays($days)->toDateString();
$dailyEnrollments = Enrollment::where('created_at', '>=', $startDate) $dailyEnrollments = Enrollment::where('created_at', '>=', $startDate)
->when($branchId, fn ($q) => $q->whereHas('group', fn ($g) => $g->where('branch_id', $branchId)))
->select(DB::raw('DATE(created_at) as date'), DB::raw('COUNT(*) as count')) ->select(DB::raw('DATE(created_at) as date'), DB::raw('COUNT(*) as count'))
->groupBy('date') ->groupBy('date')
->orderBy('date') ->orderBy('date')
...@@ -25,9 +30,12 @@ public function render() ...@@ -25,9 +30,12 @@ public function render()
$totalNew = array_sum($dailyEnrollments); $totalNew = array_sum($dailyEnrollments);
$cancelled = Enrollment::where('status', 'cancelled') $cancelled = Enrollment::where('status', 'cancelled')
->where('updated_at', '>=', $startDate) ->where('updated_at', '>=', $startDate)
->when($branchId, fn ($q) => $q->whereHas('group', fn ($g) => $g->where('branch_id', $branchId)))
->count(); ->count();
$active = Enrollment::where('status', 'active')->count(); $active = Enrollment::where('status', 'active')
->when($branchId, fn ($q) => $q->whereHas('group', fn ($g) => $g->where('branch_id', $branchId)))
->count();
return view('livewire.dashboard.enrollment-trends', [ return view('livewire.dashboard.enrollment-trends', [
'dailyEnrollments' => $dailyEnrollments, 'dailyEnrollments' => $dailyEnrollments,
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
use App\Domain\Financial\Enums\InvoiceStatus; use App\Domain\Financial\Enums\InvoiceStatus;
use App\Domain\Financial\Models\Invoice; use App\Domain\Financial\Models\Invoice;
use App\Domain\Participant\Models\Participant; use App\Domain\Participant\Models\Participant;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Enums\EnrollmentStatus; use App\Domain\Training\Enums\EnrollmentStatus;
use App\Domain\Training\Enums\RenewalPolicy; use App\Domain\Training\Enums\RenewalPolicy;
use App\Domain\Training\Models\Enrollment; use App\Domain\Training\Models\Enrollment;
...@@ -12,6 +13,8 @@ ...@@ -12,6 +13,8 @@
class OverdueRenewalsAlert extends Component class OverdueRenewalsAlert extends Component
{ {
use UsesBranchScope;
public bool $showList = false; public bool $showList = false;
public function toggleList(): void public function toggleList(): void
...@@ -22,11 +25,13 @@ public function toggleList(): void ...@@ -22,11 +25,13 @@ public function toggleList(): void
public function render() public function render()
{ {
$today = now()->toDateString(); $today = now()->toDateString();
$branchId = $this->getActiveBranchId();
$unpaidInvoices = Invoice::whereIn('status', [InvoiceStatus::Sent, InvoiceStatus::PartiallyPaid, InvoiceStatus::Overdue]) $unpaidInvoices = Invoice::whereIn('status', [InvoiceStatus::Sent, InvoiceStatus::PartiallyPaid, InvoiceStatus::Overdue])
->where('due_amount', '>', 0) ->where('due_amount', '>', 0)
->where('billable_type', Participant::class) ->where('billable_type', Participant::class)
->where('notes', 'like', '%تجديد%') ->where('notes', 'like', '%تجديد%')
->when($branchId, fn ($q) => $q->whereHasMorph('billable', [Participant::class], fn ($p) => $p->where('branch_id', $branchId)))
->with(['billable.person']) ->with(['billable.person'])
->orderBy('due_date') ->orderBy('due_date')
->get(); ->get();
...@@ -38,6 +43,7 @@ public function render() ...@@ -38,6 +43,7 @@ public function render()
RenewalPolicy::AutoRenew->value, RenewalPolicy::AutoRenew->value,
RenewalPolicy::ManualRenew->value, RenewalPolicy::ManualRenew->value,
])) ]))
->when($branchId, fn ($q) => $q->whereHas('group', fn ($g) => $g->where('branch_id', $branchId)))
->with(['participant.person', 'program', 'group.branch']) ->with(['participant.person', 'program', 'group.branch'])
->get() ->get()
->filter(function ($enrollment) { ->filter(function ($enrollment) {
......
...@@ -3,11 +3,14 @@ ...@@ -3,11 +3,14 @@
namespace App\Livewire\Dashboard; namespace App\Livewire\Dashboard;
use App\Domain\Financial\Models\Payment; use App\Domain\Financial\Models\Payment;
use App\Domain\Shared\Traits\UsesBranchScope;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Livewire\Component; use Livewire\Component;
class ProductRevenueWidget extends Component class ProductRevenueWidget extends Component
{ {
use UsesBranchScope;
public string $period = 'month'; public string $period = 'month';
public function render() public function render()
...@@ -29,25 +32,27 @@ public function render() ...@@ -29,25 +32,27 @@ public function render()
}; };
$academyId = app('current_academy')?->id; $academyId = app('current_academy')?->id;
$branchId = $this->getActiveBranchId();
$productType = 'App\\Domain\\Inventory\\Models\\Product'; $productType = 'App\\Domain\\Inventory\\Models\\Product';
// Current period product revenue (pro-rata) // Current period product revenue (pro-rata)
$totalProductRevenue = $this->getProductRevenue($startDate, null, $academyId, $productType); $totalProductRevenue = $this->getProductRevenue($startDate, null, $academyId, $branchId, $productType);
// Previous period // Previous period
$previousProductRevenue = $this->getProductRevenue($previousStart, $startDate, $academyId, $productType); $previousProductRevenue = $this->getProductRevenue($previousStart, $startDate, $academyId, $branchId, $productType);
$change = $previousProductRevenue > 0 $change = $previousProductRevenue > 0
? round(($totalProductRevenue - $previousProductRevenue) / $previousProductRevenue * 100, 1) ? round(($totalProductRevenue - $previousProductRevenue) / $previousProductRevenue * 100, 1)
: ($totalProductRevenue > 0 ? 100 : 0); : ($totalProductRevenue > 0 ? 100 : 0);
// Revenue per product // Revenue per product
$byProduct = $this->getRevenueByProduct($startDate, $academyId, $productType); $byProduct = $this->getRevenueByProduct($startDate, $academyId, $branchId, $productType);
// Total confirmed payments this period // Total confirmed payments this period
$totalRevenue = Payment::where('status', 'confirmed') $totalRevenue = Payment::where('status', 'confirmed')
->where('created_at', '>=', $startDate) ->where('created_at', '>=', $startDate)
->when($academyId, fn ($q) => $q->where('academy_id', $academyId)) ->when($academyId, fn ($q) => $q->where('academy_id', $academyId))
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->sum('amount'); ->sum('amount');
$productPercent = $totalRevenue > 0 $productPercent = $totalRevenue > 0
...@@ -62,6 +67,7 @@ public function render() ...@@ -62,6 +67,7 @@ public function render()
->where('payments.status', 'confirmed') ->where('payments.status', 'confirmed')
->where('payments.created_at', '>=', $startDate) ->where('payments.created_at', '>=', $startDate)
->when($academyId, fn ($q) => $q->where('payments.academy_id', $academyId)) ->when($academyId, fn ($q) => $q->where('payments.academy_id', $academyId))
->when($branchId, fn ($q) => $q->where('payments.branch_id', $branchId))
->sum('invoice_items.quantity'); ->sum('invoice_items.quantity');
return view('livewire.dashboard.product-revenue-widget', [ return view('livewire.dashboard.product-revenue-widget', [
...@@ -74,7 +80,7 @@ public function render() ...@@ -74,7 +80,7 @@ public function render()
]); ]);
} }
private function getProductRevenue($from, $to, $academyId, string $productType): int private function getProductRevenue($from, $to, $academyId, ?int $branchId, string $productType): int
{ {
$result = DB::table('payments') $result = DB::table('payments')
->join('invoices', 'payments.invoice_id', '=', 'invoices.id') ->join('invoices', 'payments.invoice_id', '=', 'invoices.id')
...@@ -91,6 +97,7 @@ private function getProductRevenue($from, $to, $academyId, string $productType): ...@@ -91,6 +97,7 @@ private function getProductRevenue($from, $to, $academyId, string $productType):
->where('payments.created_at', '>=', $from) ->where('payments.created_at', '>=', $from)
->when($to, fn ($q) => $q->where('payments.created_at', '<', $to)) ->when($to, fn ($q) => $q->where('payments.created_at', '<', $to))
->when($academyId, fn ($q) => $q->where('payments.academy_id', $academyId)) ->when($academyId, fn ($q) => $q->where('payments.academy_id', $academyId))
->when($branchId, fn ($q) => $q->where('payments.branch_id', $branchId))
->select( ->select(
DB::raw('SUM(payments.amount * prod_items.product_total / invoices.total_amount) as product_revenue') DB::raw('SUM(payments.amount * prod_items.product_total / invoices.total_amount) as product_revenue')
) )
...@@ -99,7 +106,7 @@ private function getProductRevenue($from, $to, $academyId, string $productType): ...@@ -99,7 +106,7 @@ private function getProductRevenue($from, $to, $academyId, string $productType):
return (int) ($result ?? 0); return (int) ($result ?? 0);
} }
private function getRevenueByProduct($from, $academyId, string $productType) private function getRevenueByProduct($from, $academyId, ?int $branchId, string $productType)
{ {
return DB::table('payments') return DB::table('payments')
->join('invoices', 'payments.invoice_id', '=', 'invoices.id') ->join('invoices', 'payments.invoice_id', '=', 'invoices.id')
...@@ -112,6 +119,7 @@ private function getRevenueByProduct($from, $academyId, string $productType) ...@@ -112,6 +119,7 @@ private function getRevenueByProduct($from, $academyId, string $productType)
->where('invoices.total_amount', '>', 0) ->where('invoices.total_amount', '>', 0)
->where('payments.created_at', '>=', $from) ->where('payments.created_at', '>=', $from)
->when($academyId, fn ($q) => $q->where('payments.academy_id', $academyId)) ->when($academyId, fn ($q) => $q->where('payments.academy_id', $academyId))
->when($branchId, fn ($q) => $q->where('payments.branch_id', $branchId))
->select( ->select(
'products.id as product_id', 'products.id as product_id',
'products.name_ar as product_name', 'products.name_ar as product_name',
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
namespace App\Livewire\Dashboard; namespace App\Livewire\Dashboard;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Enums\EnrollmentStatus; use App\Domain\Training\Enums\EnrollmentStatus;
use App\Domain\Training\Enums\RenewalPolicy; use App\Domain\Training\Enums\RenewalPolicy;
use App\Domain\Training\Models\Enrollment; use App\Domain\Training\Models\Enrollment;
...@@ -10,8 +11,11 @@ ...@@ -10,8 +11,11 @@
class RenewalsDueWidget extends Component class RenewalsDueWidget extends Component
{ {
use UsesBranchScope;
public function render() public function render()
{ {
$branchId = $this->getActiveBranchId();
$today = now()->toDateString(); $today = now()->toDateString();
$upcoming3Days = now()->addDays(3)->toDateString(); $upcoming3Days = now()->addDays(3)->toDateString();
...@@ -21,6 +25,7 @@ public function render() ...@@ -21,6 +25,7 @@ public function render()
RenewalPolicy::AutoRenew->value, RenewalPolicy::AutoRenew->value,
RenewalPolicy::ManualRenew->value, RenewalPolicy::ManualRenew->value,
])) ]))
->when($branchId, fn ($q) => $q->whereHas('group', fn ($g) => $g->where('branch_id', $branchId)))
->count(); ->count();
$dueThisWeek = Enrollment::where('status', EnrollmentStatus::Active) $dueThisWeek = Enrollment::where('status', EnrollmentStatus::Active)
...@@ -29,6 +34,7 @@ public function render() ...@@ -29,6 +34,7 @@ public function render()
RenewalPolicy::AutoRenew->value, RenewalPolicy::AutoRenew->value,
RenewalPolicy::ManualRenew->value, RenewalPolicy::ManualRenew->value,
])) ]))
->when($branchId, fn ($q) => $q->whereHas('group', fn ($g) => $g->where('branch_id', $branchId)))
->count(); ->count();
$overdue = Enrollment::where('status', EnrollmentStatus::Active) $overdue = Enrollment::where('status', EnrollmentStatus::Active)
...@@ -37,6 +43,7 @@ public function render() ...@@ -37,6 +43,7 @@ public function render()
RenewalPolicy::AutoRenew->value, RenewalPolicy::AutoRenew->value,
RenewalPolicy::ManualRenew->value, RenewalPolicy::ManualRenew->value,
])) ]))
->when($branchId, fn ($q) => $q->whereHas('group', fn ($g) => $g->where('branch_id', $branchId)))
->count(); ->count();
$byBranch = Enrollment::where('enrollments.status', EnrollmentStatus::Active) $byBranch = Enrollment::where('enrollments.status', EnrollmentStatus::Active)
...@@ -45,6 +52,7 @@ public function render() ...@@ -45,6 +52,7 @@ public function render()
RenewalPolicy::AutoRenew->value, RenewalPolicy::AutoRenew->value,
RenewalPolicy::ManualRenew->value, RenewalPolicy::ManualRenew->value,
])) ]))
->when($branchId, fn ($q) => $q->where('training_groups.branch_id', $branchId))
->join('training_groups', 'enrollments.training_group_id', '=', 'training_groups.id') ->join('training_groups', 'enrollments.training_group_id', '=', 'training_groups.id')
->join('branches', 'training_groups.branch_id', '=', 'branches.id') ->join('branches', 'training_groups.branch_id', '=', 'branches.id')
->select('branches.name_ar as branch_name', DB::raw('COUNT(*) as count')) ->select('branches.name_ar as branch_name', DB::raw('COUNT(*) as count'))
......
...@@ -3,15 +3,20 @@ ...@@ -3,15 +3,20 @@
namespace App\Livewire\Dashboard; namespace App\Livewire\Dashboard;
use App\Domain\Financial\Models\Payment; use App\Domain\Financial\Models\Payment;
use App\Domain\Shared\Traits\UsesBranchScope;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Livewire\Component; use Livewire\Component;
class RevenueWidget extends Component class RevenueWidget extends Component
{ {
use UsesBranchScope;
public string $period = 'month'; public string $period = 'month';
public function render() public function render()
{ {
$branchId = $this->getActiveBranchId();
$startDate = match ($this->period) { $startDate = match ($this->period) {
'today' => now()->startOfDay(), 'today' => now()->startOfDay(),
'week' => now()->startOfWeek(), 'week' => now()->startOfWeek(),
...@@ -22,6 +27,7 @@ public function render() ...@@ -22,6 +27,7 @@ public function render()
$revenue = Payment::where('status', 'confirmed') $revenue = Payment::where('status', 'confirmed')
->where('created_at', '>=', $startDate) ->where('created_at', '>=', $startDate)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->sum('amount'); ->sum('amount');
$previousStart = match ($this->period) { $previousStart = match ($this->period) {
...@@ -34,6 +40,7 @@ public function render() ...@@ -34,6 +40,7 @@ public function render()
$previousRevenue = Payment::where('status', 'confirmed') $previousRevenue = Payment::where('status', 'confirmed')
->whereBetween('created_at', [$previousStart, $startDate]) ->whereBetween('created_at', [$previousStart, $startDate])
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->sum('amount'); ->sum('amount');
$change = $previousRevenue > 0 $change = $previousRevenue > 0
...@@ -42,6 +49,7 @@ public function render() ...@@ -42,6 +49,7 @@ public function render()
$byMethod = Payment::where('status', 'confirmed') $byMethod = Payment::where('status', 'confirmed')
->where('created_at', '>=', $startDate) ->where('created_at', '>=', $startDate)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->select('method', DB::raw('SUM(amount) as total')) ->select('method', DB::raw('SUM(amount) as total'))
->groupBy('method') ->groupBy('method')
->pluck('total', 'method') ->pluck('total', 'method')
......
...@@ -3,12 +3,15 @@ ...@@ -3,12 +3,15 @@
namespace App\Livewire\Dashboard; namespace App\Livewire\Dashboard;
use App\Domain\Financial\Models\Payment; use App\Domain\Financial\Models\Payment;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Models\Enrollment; use App\Domain\Training\Models\Enrollment;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Livewire\Component; use Livewire\Component;
class SubscriptionRevenueWidget extends Component class SubscriptionRevenueWidget extends Component
{ {
use UsesBranchScope;
public string $period = 'month'; public string $period = 'month';
public function render() public function render()
...@@ -30,26 +33,28 @@ public function render() ...@@ -30,26 +33,28 @@ public function render()
}; };
$academyId = app('current_academy')?->id; $academyId = app('current_academy')?->id;
$branchId = $this->getActiveBranchId();
// Current period subscription revenue (pro-rata from invoice items) // Current period subscription revenue (pro-rata from invoice items)
$totalSubscriptionRevenue = $this->getSubscriptionRevenue($startDate, null, $academyId); $totalSubscriptionRevenue = $this->getSubscriptionRevenue($startDate, null, $academyId, $branchId);
// Previous period // Previous period
$previousSubscriptionRevenue = $this->getSubscriptionRevenue($previousStart, $startDate, $academyId); $previousSubscriptionRevenue = $this->getSubscriptionRevenue($previousStart, $startDate, $academyId, $branchId);
$change = $previousSubscriptionRevenue > 0 $change = $previousSubscriptionRevenue > 0
? round(($totalSubscriptionRevenue - $previousSubscriptionRevenue) / $previousSubscriptionRevenue * 100, 1) ? round(($totalSubscriptionRevenue - $previousSubscriptionRevenue) / $previousSubscriptionRevenue * 100, 1)
: ($totalSubscriptionRevenue > 0 ? 100 : 0); : ($totalSubscriptionRevenue > 0 ? 100 : 0);
// Revenue by activity // Revenue by activity
$byActivity = collect($this->getRevenueByActivity($startDate, $academyId)); $byActivity = collect($this->getRevenueByActivity($startDate, $academyId, $branchId));
// Top programs by subscription revenue // Top programs by subscription revenue
$topPrograms = collect($this->getTopPrograms($startDate, $academyId)); $topPrograms = collect($this->getTopPrograms($startDate, $academyId, $branchId));
// Enrollment counts // Enrollment counts
$enrollmentQuery = Enrollment::where('created_at', '>=', $startDate) $enrollmentQuery = Enrollment::where('created_at', '>=', $startDate)
->when($academyId, fn ($q) => $q->where('academy_id', $academyId)); ->when($academyId, fn ($q) => $q->where('academy_id', $academyId))
->when($branchId, fn ($q) => $q->whereHas('group', fn ($g) => $g->where('branch_id', $branchId)));
$newEnrollments = (clone $enrollmentQuery)->count(); $newEnrollments = (clone $enrollmentQuery)->count();
$activeEnrollments = (clone $enrollmentQuery)->where('status', 'active')->count(); $activeEnrollments = (clone $enrollmentQuery)->where('status', 'active')->count();
...@@ -58,12 +63,14 @@ public function render() ...@@ -58,12 +63,14 @@ public function render()
$previousEnrollments = Enrollment::whereBetween('created_at', [$previousStart, $startDate]) $previousEnrollments = Enrollment::whereBetween('created_at', [$previousStart, $startDate])
->when($academyId, fn ($q) => $q->where('academy_id', $academyId)) ->when($academyId, fn ($q) => $q->where('academy_id', $academyId))
->when($branchId, fn ($q) => $q->whereHas('group', fn ($g) => $g->where('branch_id', $branchId)))
->count(); ->count();
// Total confirmed payments this period // Total confirmed payments this period
$totalRevenue = Payment::where('status', 'confirmed') $totalRevenue = Payment::where('status', 'confirmed')
->where('created_at', '>=', $startDate) ->where('created_at', '>=', $startDate)
->when($academyId, fn ($q) => $q->where('academy_id', $academyId)) ->when($academyId, fn ($q) => $q->where('academy_id', $academyId))
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->sum('amount'); ->sum('amount');
$subscriptionPercent = $totalRevenue > 0 $subscriptionPercent = $totalRevenue > 0
...@@ -90,7 +97,7 @@ public function render() ...@@ -90,7 +97,7 @@ public function render()
* Subscription items = invoice_items WHERE itemable_type IS NULL. * Subscription items = invoice_items WHERE itemable_type IS NULL.
* For each payment: payment.amount * (invoice_sub_total / invoice.total_amount) * For each payment: payment.amount * (invoice_sub_total / invoice.total_amount)
*/ */
private function getSubscriptionRevenue($from, $to, $academyId): int private function getSubscriptionRevenue($from, $to, $academyId, ?int $branchId): int
{ {
$result = DB::table('payments') $result = DB::table('payments')
->join('invoices', 'payments.invoice_id', '=', 'invoices.id') ->join('invoices', 'payments.invoice_id', '=', 'invoices.id')
...@@ -107,6 +114,7 @@ private function getSubscriptionRevenue($from, $to, $academyId): int ...@@ -107,6 +114,7 @@ private function getSubscriptionRevenue($from, $to, $academyId): int
->where('payments.created_at', '>=', $from) ->where('payments.created_at', '>=', $from)
->when($to, fn ($q) => $q->where('payments.created_at', '<', $to)) ->when($to, fn ($q) => $q->where('payments.created_at', '<', $to))
->when($academyId, fn ($q) => $q->where('payments.academy_id', $academyId)) ->when($academyId, fn ($q) => $q->where('payments.academy_id', $academyId))
->when($branchId, fn ($q) => $q->where('payments.branch_id', $branchId))
->select( ->select(
DB::raw('SUM(payments.amount * sub_items.sub_total / invoices.total_amount) as subscription_revenue') DB::raw('SUM(payments.amount * sub_items.sub_total / invoices.total_amount) as subscription_revenue')
) )
...@@ -120,7 +128,7 @@ private function getSubscriptionRevenue($from, $to, $academyId): int ...@@ -120,7 +128,7 @@ private function getSubscriptionRevenue($from, $to, $academyId): int
* Uses the participant's latest enrollment to determine which activity the subscription belongs to. * Uses the participant's latest enrollment to determine which activity the subscription belongs to.
* For participants with multiple activities, revenue is attributed to each proportionally. * For participants with multiple activities, revenue is attributed to each proportionally.
*/ */
private function getRevenueByActivity($from, $academyId) private function getRevenueByActivity($from, $academyId, ?int $branchId)
{ {
return DB::select(" return DB::select("
WITH payment_sub_revenue AS ( WITH payment_sub_revenue AS (
...@@ -141,6 +149,7 @@ private function getRevenueByActivity($from, $academyId) ...@@ -141,6 +149,7 @@ private function getRevenueByActivity($from, $academyId)
AND i.billable_type = 'App\\Domain\\Participant\\Models\\Participant' AND i.billable_type = 'App\\Domain\\Participant\\Models\\Participant'
AND p.created_at >= ? AND p.created_at >= ?
" . ($academyId ? "AND p.academy_id = {$academyId}" : "") . " " . ($academyId ? "AND p.academy_id = {$academyId}" : "") . "
" . ($branchId ? "AND p.branch_id = " . (int) $branchId : "") . "
), ),
participant_activity AS ( participant_activity AS (
SELECT DISTINCT ON (e.participant_id) SELECT DISTINCT ON (e.participant_id)
...@@ -165,6 +174,7 @@ private function getRevenueByActivity($from, $academyId) ...@@ -165,6 +174,7 @@ private function getRevenueByActivity($from, $academyId)
WHERE e.status IN ('active', 'completed') WHERE e.status IN ('active', 'completed')
AND e.created_at >= ? AND e.created_at >= ?
" . ($academyId ? "AND e.academy_id = {$academyId}" : "") . " " . ($academyId ? "AND e.academy_id = {$academyId}" : "") . "
" . ($branchId ? "AND e.training_group_id IN (SELECT id FROM training_groups WHERE branch_id = " . (int) $branchId . ")" : "") . "
GROUP BY a.id GROUP BY a.id
) )
SELECT SELECT
...@@ -185,7 +195,7 @@ private function getRevenueByActivity($from, $academyId) ...@@ -185,7 +195,7 @@ private function getRevenueByActivity($from, $academyId)
/** /**
* Top programs by subscription revenue collected. * Top programs by subscription revenue collected.
*/ */
private function getTopPrograms($from, $academyId) private function getTopPrograms($from, $academyId, ?int $branchId)
{ {
return DB::select(" return DB::select("
WITH payment_sub_revenue AS ( WITH payment_sub_revenue AS (
...@@ -206,6 +216,7 @@ private function getTopPrograms($from, $academyId) ...@@ -206,6 +216,7 @@ private function getTopPrograms($from, $academyId)
AND i.billable_type = 'App\\Domain\\Participant\\Models\\Participant' AND i.billable_type = 'App\\Domain\\Participant\\Models\\Participant'
AND p.created_at >= ? AND p.created_at >= ?
" . ($academyId ? "AND p.academy_id = {$academyId}" : "") . " " . ($academyId ? "AND p.academy_id = {$academyId}" : "") . "
" . ($branchId ? "AND p.branch_id = " . (int) $branchId : "") . "
), ),
participant_program AS ( participant_program AS (
SELECT DISTINCT ON (e.participant_id) SELECT DISTINCT ON (e.participant_id)
......
...@@ -5,13 +5,17 @@ ...@@ -5,13 +5,17 @@
use App\Domain\HR\Models\Employee; use App\Domain\HR\Models\Employee;
use App\Domain\HR\Models\Trainer; use App\Domain\HR\Models\Trainer;
use App\Domain\HR\Models\TrainerCompensation; use App\Domain\HR\Models\TrainerCompensation;
use App\Domain\Shared\Traits\UsesBranchScope;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Livewire\Component; use Livewire\Component;
class TrainerDuesWidget extends Component class TrainerDuesWidget extends Component
{ {
use UsesBranchScope;
public function render() public function render()
{ {
$branchId = $this->getActiveBranchId();
$totalDues = 0; $totalDues = 0;
$trainerTotals = []; $trainerTotals = [];
$topTrainers = collect(); $topTrainers = collect();
...@@ -22,6 +26,7 @@ public function render() ...@@ -22,6 +26,7 @@ public function render()
$compensations = TrainerCompensation::whereIn('status', ['pending', 'approved']) $compensations = TrainerCompensation::whereIn('status', ['pending', 'approved'])
->forPeriod($startOfMonth, $endOfMonth) ->forPeriod($startOfMonth, $endOfMonth)
->when($branchId, fn ($q) => $q->whereHas('trainer.employee', fn ($e) => $e->forBranch($branchId)))
->select('trainer_id', DB::raw('SUM(amount) as total_amount'), DB::raw('COUNT(*) as records_count')) ->select('trainer_id', DB::raw('SUM(amount) as total_amount'), DB::raw('COUNT(*) as records_count'))
->groupBy('trainer_id') ->groupBy('trainer_id')
->get() ->get()
...@@ -29,6 +34,7 @@ public function render() ...@@ -29,6 +34,7 @@ public function render()
$salaryTrainers = Trainer::whereIn('compensation_model', ['salary', 'hybrid']) $salaryTrainers = Trainer::whereIn('compensation_model', ['salary', 'hybrid'])
->where('status', 'active') ->where('status', 'active')
->when($branchId, fn ($q) => $q->whereHas('employee', fn ($e) => $e->forBranch($branchId)))
->with('employee:id,salary_amount') ->with('employee:id,salary_amount')
->get(); ->get();
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
use App\Domain\Document\Enums\DocumentStatus; use App\Domain\Document\Enums\DocumentStatus;
use App\Domain\Document\Enums\DocumentType; use App\Domain\Document\Enums\DocumentType;
use App\Domain\Document\Models\Document; use App\Domain\Document\Models\Document;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Livewire\Concerns\WithSorting; use App\Livewire\Concerns\WithSorting;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
...@@ -16,7 +17,7 @@ ...@@ -16,7 +17,7 @@
#[Title('اعتماد المستندات')] #[Title('اعتماد المستندات')]
class DocumentApprovalList extends Component class DocumentApprovalList extends Component
{ {
use WithPagination, WithSorting; use WithPagination, WithSorting, UsesBranchScope;
#[Url] #[Url]
public string $search = ''; public string $search = '';
...@@ -46,7 +47,9 @@ public function render() ...@@ -46,7 +47,9 @@ public function render()
{ {
$this->authorize('documents.approve'); $this->authorize('documents.approve');
$branchId = $this->getActiveBranchId();
$query = Document::with(['documentable', 'uploader']) $query = Document::with(['documentable', 'uploader'])
->when($branchId, fn ($q) => $q->whereHasMorph('documentable', [\App\Domain\Participant\Models\Participant::class], fn ($p) => $p->where('branch_id', $branchId)))
->orderBy($this->sortBy, $this->sortDir); ->orderBy($this->sortBy, $this->sortDir);
if ($this->status) { if ($this->status) {
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
use App\Domain\Participant\Models\Participant; use App\Domain\Participant\Models\Participant;
use App\Domain\Pricing\Services\PricingService; use App\Domain\Pricing\Services\PricingService;
use App\Domain\Shared\Exceptions\DomainException; use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Models\Enrollment; use App\Domain\Training\Models\Enrollment;
use App\Domain\Training\Models\TrainingGroup; use App\Domain\Training\Models\TrainingGroup;
use App\Domain\Training\Models\TrainingProgram; use App\Domain\Training\Models\TrainingProgram;
...@@ -18,6 +19,8 @@ ...@@ -18,6 +19,8 @@
#[Title('نقل مشتركين')] #[Title('نقل مشتركين')]
class TransferParticipantWizard extends Component class TransferParticipantWizard extends Component
{ {
use UsesBranchScope;
public int $currentStep = 1; public int $currentStep = 1;
public int $totalSteps = 4; public int $totalSteps = 4;
public bool $completed = false; public bool $completed = false;
...@@ -312,7 +315,9 @@ public function render() ...@@ -312,7 +315,9 @@ public function render()
// Search results for single mode // Search results for single mode
$searchResults = collect(); $searchResults = collect();
if ($this->mode === 'single' && strlen($this->participantSearch) >= 2 && !$this->singleParticipantInfo) { if ($this->mode === 'single' && strlen($this->participantSearch) >= 2 && !$this->singleParticipantInfo) {
$branchId = $this->getActiveBranchId();
$searchResults = Participant::with('person') $searchResults = Participant::with('person')
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereHas('person', function ($q) { ->whereHas('person', function ($q) {
$q->where('name_ar', 'ilike', "%{$this->participantSearch}%") $q->where('name_ar', 'ilike', "%{$this->participantSearch}%")
->orWhere('name', 'ilike', "%{$this->participantSearch}%") ->orWhere('name', 'ilike', "%{$this->participantSearch}%")
...@@ -327,6 +332,7 @@ public function render() ...@@ -327,6 +332,7 @@ public function render()
$sourceGroups = collect(); $sourceGroups = collect();
if ($this->mode === 'bulk' && !$this->sourceGroupId) { if ($this->mode === 'bulk' && !$this->sourceGroupId) {
$sourceGroups = TrainingGroup::whereIn('status', ['active', 'forming']) $sourceGroups = TrainingGroup::whereIn('status', ['active', 'forming'])
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->where('current_count', '>', 0) ->where('current_count', '>', 0)
->with('program') ->with('program')
->orderBy('name_ar') ->orderBy('name_ar')
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
use App\Domain\Participant\Models\Participant; use App\Domain\Participant\Models\Participant;
use App\Domain\Shared\Exceptions\DomainException; use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Models\Evaluation; use App\Domain\Training\Models\Evaluation;
use App\Domain\Training\Models\EvaluationCriterion; use App\Domain\Training\Models\EvaluationCriterion;
use App\Domain\Training\Models\TrainingGroup; use App\Domain\Training\Models\TrainingGroup;
...@@ -16,6 +17,8 @@ ...@@ -16,6 +17,8 @@
#[Title('تقييم جديد')] #[Title('تقييم جديد')]
class EvaluationForm extends Component class EvaluationForm extends Component
{ {
use UsesBranchScope;
public ?Evaluation $evaluation = null; public ?Evaluation $evaluation = null;
public string $participant_id = ''; public string $participant_id = '';
...@@ -154,8 +157,13 @@ private function saveEvaluation(EvaluationService $service, bool $shouldSubmit): ...@@ -154,8 +157,13 @@ private function saveEvaluation(EvaluationService $service, bool $shouldSubmit):
public function render() public function render()
{ {
$participants = Participant::with('person')->get(); $branchId = $this->getActiveBranchId();
$groups = TrainingGroup::orderBy('name_ar')->get(['id', 'name_ar']); $participants = Participant::with('person')
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->get();
$groups = TrainingGroup::orderBy('name_ar')
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->get(['id', 'name_ar']);
$criteria = EvaluationCriterion::active()->orderBy('sort_order')->get(); $criteria = EvaluationCriterion::active()->orderBy('sort_order')->get();
return view('livewire.evaluations.evaluation-form', [ return view('livewire.evaluations.evaluation-form', [
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
namespace App\Livewire\Evaluations; namespace App\Livewire\Evaluations;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Enums\EvaluationStatus; use App\Domain\Training\Enums\EvaluationStatus;
use App\Domain\Training\Models\Evaluation; use App\Domain\Training\Models\Evaluation;
use App\Domain\Training\Models\TrainingGroup; use App\Domain\Training\Models\TrainingGroup;
...@@ -18,7 +19,7 @@ ...@@ -18,7 +19,7 @@
#[Title('التقييمات')] #[Title('التقييمات')]
class EvaluationList extends Component class EvaluationList extends Component
{ {
use WithPagination, AppliesRoleScope, WithSorting; use WithPagination, AppliesRoleScope, WithSorting, UsesBranchScope;
protected string $scopePermission = 'evaluations.list'; protected string $scopePermission = 'evaluations.list';
...@@ -68,7 +69,9 @@ public function updatedGroupId(): void ...@@ -68,7 +69,9 @@ public function updatedGroupId(): void
public function render() public function render()
{ {
$branchId = $this->getActiveBranchId();
$query = Evaluation::query() $query = Evaluation::query()
->when($branchId, fn ($q) => $q->whereHas('group', fn ($g) => $g->where('branch_id', $branchId)))
->with(['participant.person', 'group.program', 'evaluator']) ->with(['participant.person', 'group.program', 'evaluator'])
->when($this->search, fn ($q) => $q->whereHas('participant.person', fn ($pq) => $pq->where('name_ar', 'ilike', "%{$this->search}%"))) ->when($this->search, fn ($q) => $q->whereHas('participant.person', fn ($pq) => $pq->where('name_ar', 'ilike', "%{$this->search}%")))
->when($this->status, fn ($q) => $q->where('status', $this->status)) ->when($this->status, fn ($q) => $q->where('status', $this->status))
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
use App\Domain\Event\Services\EventService; use App\Domain\Event\Services\EventService;
use App\Domain\Facility\Models\Facility; use App\Domain\Facility\Models\Facility;
use App\Domain\Shared\Exceptions\DomainException; use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Website\Enums\MediaCollection; use App\Domain\Website\Enums\MediaCollection;
use App\Domain\Website\Services\MediaService; use App\Domain\Website\Services\MediaService;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
...@@ -20,7 +21,7 @@ ...@@ -20,7 +21,7 @@
#[Title('إنشاء حدث')] #[Title('إنشاء حدث')]
class CreateEventWizard extends Component class CreateEventWizard extends Component
{ {
use WithFileUploads; use WithFileUploads, UsesBranchScope;
public ?Event $event = null; public ?Event $event = null;
public bool $editing = false; public bool $editing = false;
...@@ -365,11 +366,14 @@ private function sanitizeFormFields(): void ...@@ -365,11 +366,14 @@ private function sanitizeFormFields(): void
public function render() public function render()
{ {
$branchId = $this->getActiveBranchId();
return view('livewire.events.create-event-wizard', [ return view('livewire.events.create-event-wizard', [
'eventTypes' => EventType::cases(), 'eventTypes' => EventType::cases(),
'locationTypes' => LocationType::cases(), 'locationTypes' => LocationType::cases(),
'fieldTypes' => FormFieldType::cases(), 'fieldTypes' => FormFieldType::cases(),
'facilities' => Facility::where('status', 'active')->orderBy('name_ar')->get(['id', 'name_ar']), 'facilities' => Facility::where('status', 'active')
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->orderBy('name_ar')->get(['id', 'name_ar']),
]); ]);
} }
} }
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
use App\Domain\Event\Enums\EventStatus; use App\Domain\Event\Enums\EventStatus;
use App\Domain\Event\Enums\EventType; use App\Domain\Event\Enums\EventType;
use App\Domain\Event\Models\Event; use App\Domain\Event\Models\Event;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Livewire\Concerns\WithSorting; use App\Livewire\Concerns\WithSorting;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
...@@ -16,7 +17,7 @@ ...@@ -16,7 +17,7 @@
#[Title('الأحداث')] #[Title('الأحداث')]
class EventList extends Component class EventList extends Component
{ {
use WithPagination, WithSorting; use WithPagination, WithSorting, UsesBranchScope;
#[Url] #[Url]
public string $search = ''; public string $search = '';
...@@ -53,7 +54,9 @@ public function updatedType(): void ...@@ -53,7 +54,9 @@ public function updatedType(): void
public function render() public function render()
{ {
$branchId = $this->getActiveBranchId();
$events = Event::query() $events = Event::query()
->when($branchId, fn ($q) => $q->whereHas('facility', fn ($f) => $f->where('branch_id', $branchId)))
->when($this->search, fn ($q) => $q->where('title', 'ilike', "%{$this->search}%")) ->when($this->search, fn ($q) => $q->where('title', 'ilike', "%{$this->search}%"))
->when($this->status, fn ($q) => $q->where('status', $this->status)) ->when($this->status, fn ($q) => $q->where('status', $this->status))
->when($this->type, fn ($q) => $q->where('type', $this->type)) ->when($this->type, fn ($q) => $q->where('type', $this->type))
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
use App\Domain\Facility\Models\SpaceSegment; use App\Domain\Facility\Models\SpaceSegment;
use App\Domain\Facility\Services\SpaceCollisionService; use App\Domain\Facility\Services\SpaceCollisionService;
use App\Domain\Facility\Services\SpaceLayoutService; use App\Domain\Facility\Services\SpaceLayoutService;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Models\TrainingGroup; use App\Domain\Training\Models\TrainingGroup;
use App\Domain\Training\Models\TrainingSchedule; use App\Domain\Training\Models\TrainingSchedule;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
...@@ -18,6 +19,8 @@ ...@@ -18,6 +19,8 @@
#[Title('تعيين المجموعات على الملاعب')] #[Title('تعيين المجموعات على الملاعب')]
class SpaceAssignmentWizard extends Component class SpaceAssignmentWizard extends Component
{ {
use UsesBranchScope;
public int $step = 1; public int $step = 1;
// Step 1: Select Group // Step 1: Select Group
...@@ -50,6 +53,7 @@ public function mount(): void ...@@ -50,6 +53,7 @@ public function mount(): void
{ {
$this->authorize('facilities.manage_layouts'); $this->authorize('facilities.manage_layouts');
$this->facilities = Facility::where('status', 'active') $this->facilities = Facility::where('status', 'active')
->when($this->getActiveBranchId(), fn ($q) => $q->where('branch_id', $this->getActiveBranchId()))
->orderBy('name_ar') ->orderBy('name_ar')
->get(['id', 'name', 'name_ar', 'type']) ->get(['id', 'name', 'name_ar', 'type'])
->toArray(); ->toArray();
...@@ -301,6 +305,7 @@ public function render() ...@@ -301,6 +305,7 @@ public function render()
$searchResults = collect(); $searchResults = collect();
if (strlen($this->groupSearch) >= 2) { if (strlen($this->groupSearch) >= 2) {
$searchResults = TrainingGroup::with('program') $searchResults = TrainingGroup::with('program')
->when($this->getActiveBranchId(), fn ($q) => $q->where('branch_id', $this->getActiveBranchId()))
->where(function ($q) { ->where(function ($q) {
$q->where('name_ar', 'ilike', "%{$this->groupSearch}%") $q->where('name_ar', 'ilike', "%{$this->groupSearch}%")
->orWhere('name', 'ilike', "%{$this->groupSearch}%") ->orWhere('name', 'ilike', "%{$this->groupSearch}%")
......
...@@ -3,11 +3,14 @@ ...@@ -3,11 +3,14 @@
namespace App\Livewire\Financial; namespace App\Livewire\Financial;
use App\Domain\Pricing\Models\Promotion; use App\Domain\Pricing\Models\Promotion;
use App\Domain\Shared\Traits\UsesBranchScope;
use Carbon\Carbon; use Carbon\Carbon;
use Livewire\Component; use Livewire\Component;
class CouponValidator extends Component class CouponValidator extends Component
{ {
use UsesBranchScope;
public string $code = ''; public string $code = '';
public ?array $result = null; public ?array $result = null;
public bool $checking = false; public bool $checking = false;
...@@ -20,7 +23,10 @@ public function check(): void ...@@ -20,7 +23,10 @@ public function check(): void
return; return;
} }
$branchId = $this->getActiveBranchId();
$promotion = Promotion::where('code', strtoupper(trim($this->code))) $promotion = Promotion::where('code', strtoupper(trim($this->code)))
->when($branchId, fn ($q) => $q->where(fn ($b) => $b->whereNull('branch_id')->orWhere('branch_id', $branchId)))
->where('is_active', true) ->where('is_active', true)
->first(); ->first();
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
use App\Domain\Financial\Models\Invoice; use App\Domain\Financial\Models\Invoice;
use App\Domain\Financial\Models\PaymentPlan; use App\Domain\Financial\Models\PaymentPlan;
use App\Domain\Shared\Models\SystemSetting; use App\Domain\Shared\Models\SystemSetting;
use App\Domain\Shared\Traits\UsesBranchScope;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
...@@ -13,6 +14,8 @@ ...@@ -13,6 +14,8 @@
#[Layout('layouts.app')] #[Layout('layouts.app')]
class PaymentPlanCreate extends Component class PaymentPlanCreate extends Component
{ {
use UsesBranchScope;
public ?int $invoiceId = null; public ?int $invoiceId = null;
public int $installmentCount = 3; public int $installmentCount = 3;
public string $frequency = 'monthly'; public string $frequency = 'monthly';
...@@ -161,8 +164,10 @@ public function save(): void ...@@ -161,8 +164,10 @@ public function save(): void
public function render() public function render()
{ {
$branchId = $this->getActiveBranchId();
$invoice = $this->invoiceId ? Invoice::find($this->invoiceId) : null; $invoice = $this->invoiceId ? Invoice::find($this->invoiceId) : null;
$invoices = Invoice::whereIn('status', ['sent', 'partially_paid', 'overdue']) $invoices = Invoice::whereIn('status', ['sent', 'partially_paid', 'overdue'])
->when($branchId, fn ($q) => $q->whereHasMorph('billable', [\App\Domain\Participant\Models\Participant::class], fn ($pp) => $pp->where('branch_id', $branchId)))
->where('due_amount', '>', 0) ->where('due_amount', '>', 0)
->orderByDesc('created_at') ->orderByDesc('created_at')
->limit(50) ->limit(50)
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
use App\Domain\HR\Enums\EmploymentType; use App\Domain\HR\Enums\EmploymentType;
use App\Domain\HR\Models\Employee; use App\Domain\HR\Models\Employee;
use App\Domain\Identity\Services\PermissionService; use App\Domain\Identity\Services\PermissionService;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Livewire\Concerns\WithSorting; use App\Livewire\Concerns\WithSorting;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
...@@ -17,7 +18,7 @@ ...@@ -17,7 +18,7 @@
#[Title('الموظفين')] #[Title('الموظفين')]
class EmployeeList extends Component class EmployeeList extends Component
{ {
use WithPagination, WithSorting; use WithPagination, WithSorting, UsesBranchScope;
#[Url] #[Url]
public string $search = ''; public string $search = '';
...@@ -31,6 +32,12 @@ class EmployeeList extends Component ...@@ -31,6 +32,12 @@ class EmployeeList extends Component
#[Url] #[Url]
public string $branchId = ''; public string $branchId = '';
public function mount(): void
{
// Default the branch filter to whatever branch the user is working in.
$this->branchId = $this->branchId ?: (string) ($this->getActiveBranchId() ?? '');
}
public function updatedSearch(): void public function updatedSearch(): void
{ {
$this->resetPage(); $this->resetPage();
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
use App\Domain\HR\Models\Payslip; use App\Domain\HR\Models\Payslip;
use App\Domain\HR\Models\Trainer; use App\Domain\HR\Models\Trainer;
use App\Domain\HR\Services\PayrollService; use App\Domain\HR\Services\PayrollService;
use App\Domain\Shared\Traits\UsesBranchScope;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
use Livewire\Attributes\Url; use Livewire\Attributes\Url;
...@@ -18,7 +19,7 @@ ...@@ -18,7 +19,7 @@
#[Title('إدارة الرواتب')] #[Title('إدارة الرواتب')]
class PayrollDashboard extends Component class PayrollDashboard extends Component
{ {
use WithPagination; use WithPagination, UsesBranchScope;
#[Url] #[Url]
public string $periodFilter = ''; public string $periodFilter = '';
...@@ -77,8 +78,10 @@ public function loadStats(): void ...@@ -77,8 +78,10 @@ public function loadStats(): void
->first(); ->first();
$this->totalPayrollThisMonth = $currentPeriod?->total_net ?? 0; $this->totalPayrollThisMonth = $currentPeriod?->total_net ?? 0;
$branchId = $this->getActiveBranchId();
$this->pendingApprovals = Payslip::where('academy_id', $academyId) $this->pendingApprovals = Payslip::where('academy_id', $academyId)
->when($branchId, fn ($q) => $q->whereHas('trainer.employee', fn ($e) => $e->forBranch($branchId)))
->whereIn('status', [ ->whereIn('status', [
PayslipStatus::Draft->value, PayslipStatus::Draft->value,
PayslipStatus::PendingApproval->value, PayslipStatus::PendingApproval->value,
...@@ -86,12 +89,14 @@ public function loadStats(): void ...@@ -86,12 +89,14 @@ public function loadStats(): void
->count(); ->count();
$this->paidThisMonth = Payslip::where('academy_id', $academyId) $this->paidThisMonth = Payslip::where('academy_id', $academyId)
->when($branchId, fn ($q) => $q->whereHas('trainer.employee', fn ($e) => $e->forBranch($branchId)))
->where('status', PayslipStatus::Paid->value) ->where('status', PayslipStatus::Paid->value)
->whereMonth('paid_at', now()->month) ->whereMonth('paid_at', now()->month)
->whereYear('paid_at', now()->year) ->whereYear('paid_at', now()->year)
->sum('net_amount'); ->sum('net_amount');
$this->activeTrainers = Trainer::where('academy_id', $academyId) $this->activeTrainers = Trainer::where('academy_id', $academyId)
->when($branchId, fn ($q) => $q->whereHas('employee', fn ($e) => $e->forBranch($branchId)))
->where('status', 'active') ->where('status', 'active')
->count(); ->count();
} }
...@@ -221,6 +226,7 @@ public function render() ...@@ -221,6 +226,7 @@ public function render()
$payslipsQuery = Payslip::with(['trainer.employee.person', 'trainer.person', 'period']) $payslipsQuery = Payslip::with(['trainer.employee.person', 'trainer.person', 'period'])
->where('academy_id', app('current_academy')->id) ->where('academy_id', app('current_academy')->id)
->when($this->getActiveBranchId(), fn ($q) => $q->whereHas('trainer.employee', fn ($e) => $e->forBranch($this->getActiveBranchId())))
->latest(); ->latest();
if ($this->payslipStatus) { if ($this->payslipStatus) {
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
use App\Domain\HR\Models\Trainer; use App\Domain\HR\Models\Trainer;
use App\Domain\HR\Models\TrainerAdvance; use App\Domain\HR\Models\TrainerAdvance;
use App\Domain\HR\Services\TrainerAdvanceService; use App\Domain\HR\Services\TrainerAdvanceService;
use App\Domain\Shared\Traits\UsesBranchScope;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
use Livewire\Attributes\Url; use Livewire\Attributes\Url;
...@@ -16,7 +17,7 @@ ...@@ -16,7 +17,7 @@
#[Title('السلف')] #[Title('السلف')]
class TrainerAdvances extends Component class TrainerAdvances extends Component
{ {
use WithPagination; use WithPagination, UsesBranchScope;
#[Url] #[Url]
public string $statusFilter = ''; public string $statusFilter = '';
...@@ -125,7 +126,9 @@ public function resumeAdvance(int $id): void ...@@ -125,7 +126,9 @@ public function resumeAdvance(int $id): void
public function render() public function render()
{ {
$branchId = $this->getActiveBranchId();
$query = TrainerAdvance::with(['trainer.employee.person', 'trainer.person']) $query = TrainerAdvance::with(['trainer.employee.person', 'trainer.person'])
->when($branchId, fn ($q) => $q->whereHas('trainer.employee', fn ($e) => $e->forBranch($branchId)))
->latest('issued_date'); ->latest('issued_date');
if ($this->statusFilter) { if ($this->statusFilter) {
...@@ -142,6 +145,7 @@ public function render() ...@@ -142,6 +145,7 @@ public function render()
} }
$trainers = Trainer::where('status', 'active') $trainers = Trainer::where('status', 'active')
->when($branchId, fn ($q) => $q->whereHas('employee', fn ($e) => $e->forBranch($branchId)))
->with('employee.person', 'person') ->with('employee.person', 'person')
->get(); ->get();
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
use App\Domain\HR\Models\Trainer; use App\Domain\HR\Models\Trainer;
use App\Domain\HR\Services\TrainerService; use App\Domain\HR\Services\TrainerService;
use App\Domain\Shared\Exceptions\DomainException; use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
use Livewire\Component; use Livewire\Component;
...@@ -15,6 +16,8 @@ ...@@ -15,6 +16,8 @@
#[Title('بيانات المدرب')] #[Title('بيانات المدرب')]
class TrainerForm extends Component class TrainerForm extends Component
{ {
use UsesBranchScope;
public ?Trainer $trainer = null; public ?Trainer $trainer = null;
public bool $editing = false; public bool $editing = false;
...@@ -250,7 +253,9 @@ public function render() ...@@ -250,7 +253,9 @@ public function render()
{ {
$searchResults = []; $searchResults = [];
if (strlen($this->employeeSearch) >= 2) { if (strlen($this->employeeSearch) >= 2) {
$branchId = $this->getActiveBranchId();
$searchResults = Employee::with('person') $searchResults = Employee::with('person')
->when($branchId, fn ($q) => $q->forBranch($branchId))
->active() ->active()
->doesntHave('trainer') ->doesntHave('trainer')
->whereHas('person', function ($q) { ->whereHas('person', function ($q) {
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
use App\Domain\HR\Enums\TrainerStatus; use App\Domain\HR\Enums\TrainerStatus;
use App\Domain\HR\Models\Trainer; use App\Domain\HR\Models\Trainer;
use App\Domain\Identity\Services\PermissionService; use App\Domain\Identity\Services\PermissionService;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Models\Activity; use App\Domain\Training\Models\Activity;
use App\Livewire\Concerns\WithSorting; use App\Livewire\Concerns\WithSorting;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
...@@ -18,7 +19,7 @@ ...@@ -18,7 +19,7 @@
#[Title('المدربين')] #[Title('المدربين')]
class TrainerList extends Component class TrainerList extends Component
{ {
use WithPagination, WithSorting; use WithPagination, WithSorting, UsesBranchScope;
#[Url] #[Url]
public string $search = ''; public string $search = '';
...@@ -46,7 +47,9 @@ public function updatedCompensationModel(): void ...@@ -46,7 +47,9 @@ public function updatedCompensationModel(): void
public function render() public function render()
{ {
$query = Trainer::with(['employee.person', 'employee.branch', 'person']); $branchId = $this->getActiveBranchId();
$query = Trainer::with(['employee.person', 'employee.branch', 'person'])
->when($branchId, fn ($q) => $q->whereHas('employee', fn ($e) => $e->forBranch($branchId)));
if ($this->sortBy === 'name_ar') { if ($this->sortBy === 'name_ar') {
$query->join('employees', 'trainers.employee_id', '=', 'employees.id') $query->join('employees', 'trainers.employee_id', '=', 'employees.id')
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
use App\Domain\Financial\Models\InvoiceItem; use App\Domain\Financial\Models\InvoiceItem;
use App\Domain\Inventory\Models\Product; use App\Domain\Inventory\Models\Product;
use App\Domain\Shared\Traits\UsesBranchScope;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
...@@ -15,7 +16,7 @@ ...@@ -15,7 +16,7 @@
#[Title('تسليم المنتجات الأساسية')] #[Title('تسليم المنتجات الأساسية')]
class EssentialDeliveries extends Component class EssentialDeliveries extends Component
{ {
use WithPagination; use WithPagination, UsesBranchScope;
#[Url] #[Url]
public ?int $product_id = null; public ?int $product_id = null;
...@@ -71,14 +72,17 @@ public function markUndelivered(int $itemId): void ...@@ -71,14 +72,17 @@ public function markUndelivered(int $itemId): void
public function render() public function render()
{ {
$branchId = $this->getActiveBranchId();
$essentialProductIds = Product::where('is_essential', true) $essentialProductIds = Product::where('is_essential', true)
->where('is_active', true) ->where('is_active', true)
->when($branchId, fn ($q) => $q->where(fn ($b) => $b->whereNull('branch_id')->orWhere('branch_id', $branchId)))
->pluck('id'); ->pluck('id');
$query = InvoiceItem::with(['invoice.billable.person', 'deliveredBy']) $query = InvoiceItem::with(['invoice.billable.person', 'deliveredBy'])
->whereIn('itemable_id', $essentialProductIds) ->whereIn('itemable_id', $essentialProductIds)
->where('itemable_type', Product::class) ->where('itemable_type', Product::class)
->whereHas('invoice', fn ($q) => $q->whereIn('status', ['sent', 'paid', 'partially_paid'])) ->whereHas('invoice', fn ($q) => $q->whereIn('status', ['sent', 'paid', 'partially_paid']))
->when($branchId, fn ($q) => $q->whereHas('invoice', fn ($i) => $i->whereHasMorph('billable', [\App\Domain\Participant\Models\Participant::class], fn ($pp) => $pp->where('branch_id', $branchId))))
->when($this->product_id, fn ($q) => $q->where('itemable_id', $this->product_id)) ->when($this->product_id, fn ($q) => $q->where('itemable_id', $this->product_id))
->when($this->status === 'pending', fn ($q) => $q->where('is_delivered', false)) ->when($this->status === 'pending', fn ($q) => $q->where('is_delivered', false))
->when($this->status === 'delivered', fn ($q) => $q->where('is_delivered', true)) ->when($this->status === 'delivered', fn ($q) => $q->where('is_delivered', true))
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
use App\Domain\Inventory\Models\Product; use App\Domain\Inventory\Models\Product;
use App\Domain\Inventory\Services\KitService; use App\Domain\Inventory\Services\KitService;
use App\Domain\Shared\Exceptions\DomainException; use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
use Livewire\Component; use Livewire\Component;
...@@ -14,6 +15,8 @@ ...@@ -14,6 +15,8 @@
#[Title('الأطقم')] #[Title('الأطقم')]
class KitForm extends Component class KitForm extends Component
{ {
use UsesBranchScope;
public ?Kit $kit = null; public ?Kit $kit = null;
public bool $editing = false; public bool $editing = false;
...@@ -158,8 +161,10 @@ public function save(KitService $kitService): void ...@@ -158,8 +161,10 @@ public function save(KitService $kitService): void
public function render() public function render()
{ {
$branchId = $this->getActiveBranchId();
return view('livewire.inventory.kit-form', [ return view('livewire.inventory.kit-form', [
'products' => Product::where('is_active', true) 'products' => Product::where('is_active', true)
->when($branchId, fn ($q) => $q->where(fn ($b) => $b->whereNull('branch_id')->orWhere('branch_id', $branchId)))
->orderBy('name_ar') ->orderBy('name_ar')
->get(['id', 'name_ar', 'name', 'sku']), ->get(['id', 'name_ar', 'name', 'sku']),
]); ]);
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
use App\Domain\Inventory\Models\Warehouse; use App\Domain\Inventory\Models\Warehouse;
use App\Domain\Inventory\Services\KitService; use App\Domain\Inventory\Services\KitService;
use App\Domain\Shared\Exceptions\DomainException; use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Livewire\Concerns\AppliesRoleScope; use App\Livewire\Concerns\AppliesRoleScope;
use App\Livewire\Concerns\WithSorting; use App\Livewire\Concerns\WithSorting;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
...@@ -18,7 +19,7 @@ ...@@ -18,7 +19,7 @@
#[Title('الأطقم')] #[Title('الأطقم')]
class KitList extends Component class KitList extends Component
{ {
use WithPagination, AppliesRoleScope, WithSorting; use WithPagination, AppliesRoleScope, WithSorting, UsesBranchScope;
protected string $scopePermission = 'inventory.manage'; protected string $scopePermission = 'inventory.manage';
...@@ -123,7 +124,9 @@ public function disassemble(KitService $kitService): void ...@@ -123,7 +124,9 @@ public function disassemble(KitService $kitService): void
public function render() public function render()
{ {
$branchId = $this->getActiveBranchId();
$query = Kit::query() $query = Kit::query()
->when($branchId, fn ($q) => $q->whereHas('components.product', fn ($p) => $p->where(fn ($b) => $b->whereNull('branch_id')->orWhere('branch_id', $branchId))))
->withCount('components') ->withCount('components')
->when($this->search, function ($q) { ->when($this->search, function ($q) {
$search = $this->search; $search = $this->search;
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
use App\Domain\Inventory\Models\Warehouse; use App\Domain\Inventory\Models\Warehouse;
use App\Domain\Inventory\Services\InventoryService; use App\Domain\Inventory\Services\InventoryService;
use App\Domain\Shared\Exceptions\DomainException; use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
use Livewire\Component; use Livewire\Component;
...@@ -16,6 +17,8 @@ ...@@ -16,6 +17,8 @@
#[Title('تسوية المخزون')] #[Title('تسوية المخزون')]
class StockAdjustmentWizard extends Component class StockAdjustmentWizard extends Component
{ {
use UsesBranchScope;
public int $currentStep = 1; public int $currentStep = 1;
public int $totalSteps = 5; public int $totalSteps = 5;
public bool $completed = false; public bool $completed = false;
...@@ -191,7 +194,10 @@ private function rulesForStep(int $step): array ...@@ -191,7 +194,10 @@ private function rulesForStep(int $step): array
public function render() public function render()
{ {
$warehouses = Warehouse::active()->orderBy('name_ar')->get(['id', 'name_ar', 'name', 'code']); $branchId = $this->getActiveBranchId();
$warehouses = Warehouse::active()
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->orderBy('name_ar')->get(['id', 'name_ar', 'name', 'code']);
$products = collect(); $products = collect();
if ($this->warehouseId) { if ($this->warehouseId) {
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
use App\Domain\Inventory\Models\Warehouse; use App\Domain\Inventory\Models\Warehouse;
use App\Domain\Inventory\Services\StockCountService; use App\Domain\Inventory\Services\StockCountService;
use App\Domain\Shared\Exceptions\DomainException; use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
use Livewire\Component; use Livewire\Component;
...@@ -16,6 +17,8 @@ ...@@ -16,6 +17,8 @@
#[Title('جرد مخزني جديد')] #[Title('جرد مخزني جديد')]
class StockCountForm extends Component class StockCountForm extends Component
{ {
use UsesBranchScope;
public ?int $warehouse_id = null; public ?int $warehouse_id = null;
public ?int $stockCountId = null; public ?int $stockCountId = null;
public string $notes = ''; public string $notes = '';
...@@ -174,7 +177,9 @@ public function save(): void ...@@ -174,7 +177,9 @@ public function save(): void
public function render() public function render()
{ {
return view('livewire.inventory.stock-count-form', [ return view('livewire.inventory.stock-count-form', [
'warehouses' => Warehouse::active()->orderBy('name_ar')->get(['id', 'name_ar', 'code']), 'warehouses' => Warehouse::active()
->when($this->getActiveBranchId(), fn ($q) => $q->where('branch_id', $this->getActiveBranchId()))
->orderBy('name_ar')->get(['id', 'name_ar', 'code']),
]); ]);
} }
} }
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
use App\Domain\Inventory\Services\StockCountService; use App\Domain\Inventory\Services\StockCountService;
use App\Domain\Shared\Exceptions\DomainException; use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Exceptions\InvalidStatusTransitionException; use App\Domain\Shared\Exceptions\InvalidStatusTransitionException;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Livewire\Concerns\AppliesRoleScope; use App\Livewire\Concerns\AppliesRoleScope;
use App\Livewire\Concerns\WithSorting; use App\Livewire\Concerns\WithSorting;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
...@@ -19,7 +20,7 @@ ...@@ -19,7 +20,7 @@
#[Title('الجرد المخزني')] #[Title('الجرد المخزني')]
class StockCountList extends Component class StockCountList extends Component
{ {
use WithPagination, AppliesRoleScope, WithSorting; use WithPagination, AppliesRoleScope, WithSorting, UsesBranchScope;
protected string $scopePermission = 'inventory.manage'; protected string $scopePermission = 'inventory.manage';
...@@ -76,7 +77,9 @@ public function cancel(int $stockCountId): void ...@@ -76,7 +77,9 @@ public function cancel(int $stockCountId): void
public function render() public function render()
{ {
$branchId = $this->getActiveBranchId();
$query = StockCount::query() $query = StockCount::query()
->when($branchId, fn ($q) => $q->whereHas('warehouse', fn ($w) => $w->where('branch_id', $branchId)))
->with(['warehouse', 'creator']) ->with(['warehouse', 'creator'])
->withCount('items') ->withCount('items')
->when($this->search, function ($q) { ->when($this->search, function ($q) {
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
use App\Domain\Financial\Services\InvoiceService; use App\Domain\Financial\Services\InvoiceService;
use App\Domain\Participant\Models\Participant; use App\Domain\Participant\Models\Participant;
use App\Domain\Shared\Exceptions\DomainException; use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
use Livewire\Component; use Livewire\Component;
...@@ -13,6 +14,8 @@ ...@@ -13,6 +14,8 @@
#[Title('إنشاء فاتورة جديدة')] #[Title('إنشاء فاتورة جديدة')]
class CreateInvoiceWizard extends Component class CreateInvoiceWizard extends Component
{ {
use UsesBranchScope;
public int $currentStep = 1; public int $currentStep = 1;
public int $totalSteps = 5; public int $totalSteps = 5;
public bool $completed = false; public bool $completed = false;
...@@ -213,7 +216,9 @@ public function render() ...@@ -213,7 +216,9 @@ public function render()
{ {
$searchResults = collect(); $searchResults = collect();
if (strlen($this->participantSearch) >= 2 && !$this->participantId) { if (strlen($this->participantSearch) >= 2 && !$this->participantId) {
$branchId = $this->getActiveBranchId();
$searchResults = Participant::query() $searchResults = Participant::query()
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->with('person') ->with('person')
->where(function ($q) { ->where(function ($q) {
$search = $this->participantSearch; $search = $this->participantSearch;
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
use App\Domain\Financial\Services\InvoiceService; use App\Domain\Financial\Services\InvoiceService;
use App\Domain\Participant\Models\Participant; use App\Domain\Participant\Models\Participant;
use App\Domain\Shared\Exceptions\DomainException; use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use Livewire\Attributes\Computed; use Livewire\Attributes\Computed;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
...@@ -14,6 +15,8 @@ ...@@ -14,6 +15,8 @@
#[Title('إنشاء فاتورة')] #[Title('إنشاء فاتورة')]
class InvoiceCreate extends Component class InvoiceCreate extends Component
{ {
use UsesBranchScope;
public ?int $participant_id = null; public ?int $participant_id = null;
public string $contact_name = ''; public string $contact_name = '';
public string $contact_phone = ''; public string $contact_phone = '';
...@@ -142,8 +145,10 @@ public function total(): float ...@@ -142,8 +145,10 @@ public function total(): float
public function render() public function render()
{ {
$branchId = $this->getActiveBranchId();
return view('livewire.invoices.invoice-create', [ return view('livewire.invoices.invoice-create', [
'participants' => Participant::with('person') 'participants' => Participant::with('person')
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereIn('status', ['registered', 'active']) ->whereIn('status', ['registered', 'active'])
->orderByDesc('created_at') ->orderByDesc('created_at')
->limit(200) ->limit(200)
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
use App\Domain\Financial\Services\PaymentService; use App\Domain\Financial\Services\PaymentService;
use App\Domain\Financial\Services\RefundService; use App\Domain\Financial\Services\RefundService;
use App\Domain\Shared\Exceptions\DomainException; use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Computed; use Livewire\Attributes\Computed;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
...@@ -19,6 +20,8 @@ ...@@ -19,6 +20,8 @@
#[Title('تفاصيل الفاتورة')] #[Title('تفاصيل الفاتورة')]
class InvoiceShow extends Component class InvoiceShow extends Component
{ {
use UsesBranchScope;
public Invoice $invoice; public Invoice $invoice;
// Payment form // Payment form
...@@ -62,7 +65,7 @@ public function recordPayment(PaymentService $service): void ...@@ -62,7 +65,7 @@ public function recordPayment(PaymentService $service): void
try { try {
$service->recordPayment([ $service->recordPayment([
'academy_id' => $this->invoice->academy_id, 'academy_id' => $this->invoice->academy_id,
'branch_id' => auth()->user()->branch_id ?? $this->invoice->branch_id ?? null, 'branch_id' => $this->getActiveBranchId() ?? auth()->user()->branch_id ?? $this->invoice->branch_id ?? null,
'invoice_id' => $this->invoice->id, 'invoice_id' => $this->invoice->id,
'reference' => 'PAY-' . now()->format('YmdHis') . '-' . rand(100, 999), 'reference' => 'PAY-' . now()->format('YmdHis') . '-' . rand(100, 999),
'direction' => 'inbound', 'direction' => 'inbound',
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
use App\Domain\POS\Enums\POSPaymentMethod; use App\Domain\POS\Enums\POSPaymentMethod;
use App\Domain\POS\Services\POSService; use App\Domain\POS\Services\POSService;
use App\Domain\Shared\Exceptions\DomainException; use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
use Livewire\Component; use Livewire\Component;
...@@ -18,6 +19,8 @@ ...@@ -18,6 +19,8 @@
#[Title('نقطة البيع')] #[Title('نقطة البيع')]
class POSTerminal extends Component class POSTerminal extends Component
{ {
use UsesBranchScope;
// Cart state // Cart state
public array $cart = []; public array $cart = [];
public ?int $participantId = null; public ?int $participantId = null;
...@@ -74,7 +77,8 @@ public function updatedParticipantSearch(): void ...@@ -74,7 +77,8 @@ public function updatedParticipantSearch(): void
return; return;
} }
$this->searchResults = Participant::whereHas('person', fn ($q) => $q->where('name_ar', 'ilike', "%{$this->participantSearch}%") $this->searchResults = Participant::when($this->getActiveBranchId(), fn ($q) => $q->where('branch_id', $this->getActiveBranchId()))
->whereHas('person', fn ($q) => $q->where('name_ar', 'ilike', "%{$this->participantSearch}%")
->orWhere('phone', 'like', "%{$this->participantSearch}%") ->orWhere('phone', 'like', "%{$this->participantSearch}%")
)->with('person')->limit(10)->get() )->with('person')->limit(10)->get()
->map(fn ($p) => ['id' => $p->id, 'name' => $p->person->name_ar, 'phone' => $p->person->phone ?? '']) ->map(fn ($p) => ['id' => $p->id, 'name' => $p->person->name_ar, 'phone' => $p->person->phone ?? ''])
...@@ -362,7 +366,7 @@ public function checkout(POSService $posService): void ...@@ -362,7 +366,7 @@ public function checkout(POSService $posService): void
$transaction = $posService->processTransaction( $transaction = $posService->processTransaction(
cartItems: $this->cart, cartItems: $this->cart,
cashier: auth()->user(), cashier: auth()->user(),
branchId: auth()->user()->branch_id ?? 0, branchId: $this->getActiveBranchId() ?? auth()->user()->branch_id ?? 0,
paymentMethod: $this->paymentMethod, paymentMethod: $this->paymentMethod,
participant: $participant, participant: $participant,
couponCode: $this->couponCode ?: null, couponCode: $this->couponCode ?: null,
...@@ -422,7 +426,7 @@ public function newTransaction(): void ...@@ -422,7 +426,7 @@ public function newTransaction(): void
public function render() public function render()
{ {
$branchId = auth()->user()->branch_id ?? null; $branchId = $this->getActiveBranchId() ?? auth()->user()->branch_id ?? null;
$products = Product::where('is_active', true) $products = Product::where('is_active', true)
->when($branchId, fn ($q) => $q->where(function ($sub) use ($branchId) { ->when($branchId, fn ($q) => $q->where(function ($sub) use ($branchId) {
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
namespace App\Livewire\Pricing; namespace App\Livewire\Pricing;
use App\Domain\Pricing\Models\BasePrice; use App\Domain\Pricing\Models\BasePrice;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Livewire\Concerns\WithSorting; use App\Livewire\Concerns\WithSorting;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
...@@ -14,7 +15,7 @@ ...@@ -14,7 +15,7 @@
#[Title('الأسعار الأساسية')] #[Title('الأسعار الأساسية')]
class BasePriceList extends Component class BasePriceList extends Component
{ {
use WithPagination, WithSorting; use WithPagination, WithSorting, UsesBranchScope;
#[Url] #[Url]
public string $search = ''; public string $search = '';
...@@ -46,7 +47,9 @@ public function toggleActive(string $uuid): void ...@@ -46,7 +47,9 @@ public function toggleActive(string $uuid): void
public function render() public function render()
{ {
$branchId = $this->getActiveBranchId();
$query = BasePrice::query() $query = BasePrice::query()
->when($branchId, fn ($q) => $q->forBranch($branchId))
->with(['branch', 'priceable']) ->with(['branch', 'priceable'])
->when($this->search, fn ($q) => $q->where('name_ar', 'ilike', "%{$this->search}%")) ->when($this->search, fn ($q) => $q->where('name_ar', 'ilike', "%{$this->search}%"))
->when($this->activeFilter !== '', fn ($q) => $q->where('is_active', $this->activeFilter === '1')) ->when($this->activeFilter !== '', fn ($q) => $q->where('is_active', $this->activeFilter === '1'))
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
use App\Domain\Pricing\Enums\PricingRuleType; use App\Domain\Pricing\Enums\PricingRuleType;
use App\Domain\Pricing\Models\PricingRule; use App\Domain\Pricing\Models\PricingRule;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Livewire\Concerns\WithSorting; use App\Livewire\Concerns\WithSorting;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
...@@ -15,7 +16,7 @@ ...@@ -15,7 +16,7 @@
#[Title('قواعد التسعير')] #[Title('قواعد التسعير')]
class PricingRuleList extends Component class PricingRuleList extends Component
{ {
use WithPagination, WithSorting; use WithPagination, WithSorting, UsesBranchScope;
#[Url] #[Url]
public string $search = ''; public string $search = '';
...@@ -61,7 +62,9 @@ public function toggleActive(string $uuid): void ...@@ -61,7 +62,9 @@ public function toggleActive(string $uuid): void
public function render() public function render()
{ {
$branchId = $this->getActiveBranchId();
$query = PricingRule::query() $query = PricingRule::query()
->when($branchId, fn ($q) => $q->forBranch($branchId))
->with(['branch']) ->with(['branch'])
->when($this->search, fn ($q) => $q->where('name_ar', 'ilike', "%{$this->search}%")) ->when($this->search, fn ($q) => $q->where('name_ar', 'ilike', "%{$this->search}%"))
->when($this->ruleType, fn ($q) => $q->where('rule_type', $this->ruleType)) ->when($this->ruleType, fn ($q) => $q->where('rule_type', $this->ruleType))
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
use App\Domain\Pricing\Enums\PromotionType; use App\Domain\Pricing\Enums\PromotionType;
use App\Domain\Pricing\Models\Promotion; use App\Domain\Pricing\Models\Promotion;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Livewire\Concerns\WithSorting; use App\Livewire\Concerns\WithSorting;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
...@@ -15,7 +16,7 @@ ...@@ -15,7 +16,7 @@
#[Title('العروض والكوبونات')] #[Title('العروض والكوبونات')]
class PromotionList extends Component class PromotionList extends Component
{ {
use WithPagination, WithSorting; use WithPagination, WithSorting, UsesBranchScope;
#[Url] #[Url]
public string $search = ''; public string $search = '';
...@@ -55,7 +56,9 @@ public function toggleActive(string $uuid): void ...@@ -55,7 +56,9 @@ public function toggleActive(string $uuid): void
public function render() public function render()
{ {
$branchId = $this->getActiveBranchId();
$query = Promotion::query() $query = Promotion::query()
->when($branchId, fn ($q) => $q->where(fn ($b) => $b->whereNull('branch_id')->orWhere('branch_id', $branchId)))
->with(['branch']) ->with(['branch'])
->when($this->search, fn ($q) => $q->where(function ($q2) { ->when($this->search, fn ($q) => $q->where(function ($q2) {
$q2->where('name_ar', 'ilike', "%{$this->search}%") $q2->where('name_ar', 'ilike', "%{$this->search}%")
......
<?php
namespace App\Livewire\Public;
use App\Domain\Website\Models\ContactSubmission;
use Illuminate\Support\Facades\RateLimiter;
use Livewire\Component;
/**
* Public contact form rendered by the contact_form block.
*
* Unauthenticated and internet-facing, so it is rate limited by IP and carries
* a honeypot; submissions land in the existing admin inbox unchanged.
*/
class WebsiteContactForm extends Component
{
public ?int $blockId = null;
public bool $showSubject = true;
public bool $showPhone = true;
public bool $requirePhone = false;
public ?string $submitLabel = null;
public ?string $successMessage = null;
public string $name = '';
public string $email = '';
public string $phone = '';
public string $subject = '';
public string $message = '';
/** Bots fill hidden fields; humans do not. */
public string $website = '';
public bool $sent = false;
protected function rules(): array
{
return [
'name' => 'required|string|min:3|max:120',
'email' => 'required|email|max:180',
'phone' => ($this->requirePhone ? 'required' : 'nullable').'|string|max:30',
'subject' => ($this->showSubject ? 'nullable' : 'nullable').'|string|max:200',
'message' => 'required|string|min:10|max:5000',
];
}
protected function messages(): array
{
return [
'name.required' => __('الاسم مطلوب'),
'name.min' => __('الاسم قصير جدًا'),
'email.required' => __('البريد الإلكتروني مطلوب'),
'email.email' => __('البريد الإلكتروني غير صالح'),
'phone.required' => __('رقم الهاتف مطلوب'),
'message.required' => __('الرسالة مطلوبة'),
'message.min' => __('الرسالة قصيرة جدًا'),
];
}
public function submit(): void
{
if (filled($this->website)) {
// Honeypot tripped — report success without persisting anything.
$this->sent = true;
return;
}
$key = 'website-contact:'.request()->ip();
if (RateLimiter::tooManyAttempts($key, 5)) {
$this->addError('message', __('لقد أرسلت رسائل كثيرة. برجاء المحاولة لاحقًا.'));
return;
}
$data = $this->validate();
RateLimiter::hit($key, 3600);
ContactSubmission::create([
'name' => $data['name'],
'email' => $data['email'],
'phone' => $data['phone'] ?: null,
'subject' => $this->showSubject ? ($data['subject'] ?: null) : null,
'message' => $data['message'],
'status' => 'new',
]);
$this->reset(['name', 'email', 'phone', 'subject', 'message']);
$this->sent = true;
}
public function render()
{
return view('livewire.public.website-contact-form');
}
}
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
use App\Domain\Financial\Models\Invoice; use App\Domain\Financial\Models\Invoice;
use App\Domain\Financial\Models\Payment; use App\Domain\Financial\Models\Payment;
use App\Domain\Shared\Traits\UsesBranchScope;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Url; use Livewire\Attributes\Url;
...@@ -12,6 +13,8 @@ ...@@ -12,6 +13,8 @@
#[Layout('layouts.app')] #[Layout('layouts.app')]
class FinancialReport extends Component class FinancialReport extends Component
{ {
use UsesBranchScope;
#[Url] #[Url]
public string $period = 'month'; public string $period = 'month';
...@@ -59,21 +62,26 @@ public function updatedPeriod(): void ...@@ -59,21 +62,26 @@ public function updatedPeriod(): void
public function render() public function render()
{ {
$branchId = $this->getActiveBranchId();
$payments = Payment::where('status', 'confirmed') $payments = Payment::where('status', 'confirmed')
->whereBetween('created_at', [$this->dateFrom, $this->dateTo . ' 23:59:59']) ->whereBetween('created_at', [$this->dateFrom, $this->dateTo . ' 23:59:59'])
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->get(); ->get();
$totalRevenue = $payments->sum('amount'); $totalRevenue = $payments->sum('amount');
$paymentsByMethod = $payments->groupBy('method') $paymentsByMethod = $payments->groupBy('method')
->map(fn ($group) => $group->sum('amount')); ->map(fn ($group) => $group->sum('amount'));
$invoices = Invoice::whereBetween('created_at', [$this->dateFrom, $this->dateTo . ' 23:59:59'])->get(); $invoices = Invoice::whereBetween('created_at', [$this->dateFrom, $this->dateTo . ' 23:59:59'])
->when($branchId, fn ($q) => $q->whereHasMorph('billable', [\App\Domain\Participant\Models\Participant::class], fn ($pp) => $pp->where('branch_id', $branchId)))
->get();
$totalInvoiced = $invoices->sum('total_amount'); $totalInvoiced = $invoices->sum('total_amount');
$totalOutstanding = $invoices->whereIn('status', ['sent', 'partially_paid', 'overdue'])->sum('due_amount'); $totalOutstanding = $invoices->whereIn('status', ['sent', 'partially_paid', 'overdue'])->sum('due_amount');
$overdueCount = $invoices->where('status', 'overdue')->count(); $overdueCount = $invoices->where('status', 'overdue')->count();
$dailyRevenue = Payment::where('status', 'confirmed') $dailyRevenue = Payment::where('status', 'confirmed')
->whereBetween('created_at', [$this->dateFrom, $this->dateTo . ' 23:59:59']) ->whereBetween('created_at', [$this->dateFrom, $this->dateTo . ' 23:59:59'])
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->select(DB::raw('DATE(created_at) as date'), DB::raw('SUM(amount) as total')) ->select(DB::raw('DATE(created_at) as date'), DB::raw('SUM(amount) as total'))
->groupBy('date') ->groupBy('date')
->orderBy('date') ->orderBy('date')
...@@ -85,6 +93,12 @@ public function render() ...@@ -85,6 +93,12 @@ public function render()
->where('invoices.status', '!=', 'cancelled') ->where('invoices.status', '!=', 'cancelled')
->whereBetween('invoices.created_at', [$this->dateFrom, $this->dateTo . ' 23:59:59']) ->whereBetween('invoices.created_at', [$this->dateFrom, $this->dateTo . ' 23:59:59'])
->whereNotNull('invoice_items.itemable_type') ->whereNotNull('invoice_items.itemable_type')
->when($branchId, fn ($q) => $q->whereExists(function ($s) use ($branchId) {
$s->selectRaw('1')->from('participants')
->whereColumn('participants.id', 'invoices.billable_id')
->where('invoices.billable_type', \App\Domain\Participant\Models\Participant::class)
->where('participants.branch_id', $branchId);
}))
->select('invoice_items.description', DB::raw('SUM(invoice_items.total_amount) as revenue'), DB::raw('COUNT(*) as count')) ->select('invoice_items.description', DB::raw('SUM(invoice_items.total_amount) as revenue'), DB::raw('COUNT(*) as count'))
->groupBy('invoice_items.description') ->groupBy('invoice_items.description')
->orderByDesc('revenue') ->orderByDesc('revenue')
......
<?php
namespace App\Livewire\Website;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Website\Models\WebsiteMenu;
use App\Domain\Website\Models\WebsiteMenuItem;
use App\Domain\Website\Models\WebsitePage;
use App\Domain\Website\Services\WebsiteMenuService;
use Livewire\Attributes\Url;
use Livewire\Component;
class MenuManager extends Component
{
#[Url]
public string $menuKey = 'primary';
public bool $showForm = false;
public ?int $editingId = null;
public ?int $parentId = null;
public string $label = '';
public string $label_en = '';
public string $link_type = 'page';
public ?int $website_page_id = null;
public string $url = '';
public string $anchor = '';
public string $icon = '';
public bool $open_in_new_tab = false;
public bool $is_visible = true;
public bool $highlight = false;
public function mount(): void
{
$this->authorize('settings.manage');
}
protected function rules(): array
{
return [
'label' => 'required|string|max:255',
'label_en' => 'nullable|string|max:255',
'link_type' => 'required|in:page,url,anchor,route,none',
'website_page_id' => 'nullable|integer|exists:website_pages,id',
'url' => 'nullable|string|max:500',
'anchor' => 'nullable|string|max:120',
'icon' => 'nullable|string|max:60',
];
}
protected function messages(): array
{
return [
'label.required' => __('نص العنصر مطلوب'),
'link_type.required' => __('نوع الرابط مطلوب'),
'link_type.in' => __('نوع الرابط غير صالح'),
'website_page_id.exists' => __('الصفحة المختارة غير موجودة'),
'url.max' => __('الرابط طويل جدًا'),
];
}
public function addItem(?int $parentId = null): void
{
$this->resetForm();
$this->parentId = $parentId;
$this->showForm = true;
}
public function edit(int $id): void
{
$item = WebsiteMenuItem::findOrFail($id);
$this->editingId = $item->id;
$this->parentId = $item->parent_id;
$this->label = $item->label ?? '';
$this->label_en = $item->label_en ?? '';
$this->link_type = $item->link_type;
$this->website_page_id = $item->website_page_id;
$this->url = $item->url ?? '';
$this->anchor = $item->anchor ?? '';
$this->icon = $item->icon ?? '';
$this->open_in_new_tab = $item->open_in_new_tab;
$this->is_visible = $item->is_visible;
$this->highlight = $item->highlight;
$this->showForm = true;
}
public function save(WebsiteMenuService $service): void
{
$this->authorize('settings.manage');
$data = $this->validate();
$data += [
'open_in_new_tab' => $this->open_in_new_tab,
'is_visible' => $this->is_visible,
'highlight' => $this->highlight,
];
try {
if ($this->editingId) {
$service->updateItem(WebsiteMenuItem::findOrFail($this->editingId), $data);
session()->flash('success', __('تم تحديث العنصر'));
} else {
$menu = $service->getOrCreate($this->menuKey);
$parent = $this->parentId ? WebsiteMenuItem::find($this->parentId) : null;
$service->addItem($menu, $data, $parent);
session()->flash('success', __('تمت إضافة العنصر'));
}
$this->showForm = false;
$this->resetForm();
} catch (DomainException $e) {
$this->addError('label', $e->getMessage());
}
}
public function delete(int $id, WebsiteMenuService $service): void
{
$this->authorize('settings.manage');
$service->deleteItem(WebsiteMenuItem::findOrFail($id));
session()->flash('success', __('تم حذف العنصر'));
}
public function move(int $id, int $direction, WebsiteMenuService $service): void
{
$this->authorize('settings.manage');
$service->move(WebsiteMenuItem::findOrFail($id), $direction);
}
public function toggleVisible(int $id, WebsiteMenuService $service): void
{
$this->authorize('settings.manage');
$item = WebsiteMenuItem::findOrFail($id);
$service->updateItem($item, ['is_visible' => ! $item->is_visible]);
}
private function resetForm(): void
{
$this->reset([
'editingId', 'parentId', 'label', 'label_en', 'website_page_id',
'url', 'anchor', 'icon', 'open_in_new_tab', 'highlight',
]);
$this->link_type = 'page';
$this->is_visible = true;
$this->resetErrorBag();
}
public function render(WebsiteMenuService $service)
{
$menu = $service->getOrCreate($this->menuKey);
return view('livewire.website.menu-manager', [
'menu' => $menu,
'items' => $menu->items()->with('childrenRecursive')->get(),
'pages' => WebsitePage::orderBy('sort_order')->get(),
'menuKeys' => WebsiteMenuService::KEYS,
]);
}
}
This diff is collapsed.
<?php
namespace App\Livewire\Website;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Website\Models\WebsitePage;
use App\Domain\Website\Services\WebsitePageService;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
class PageManager extends Component
{
use WithPagination;
#[Url]
public string $search = '';
#[Url]
public string $status = '';
#[Url]
public string $sortBy = 'sort_order';
#[Url]
public string $sortDir = 'asc';
public bool $showForm = false;
public ?int $editingId = null;
public string $title = '';
public string $title_en = '';
public string $slug = '';
public string $layout = 'default';
public string $meta_description = '';
public bool $is_published = false;
public bool $is_homepage = false;
public function mount(): void
{
$this->authorize('settings.manage');
}
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedStatus(): void
{
$this->resetPage();
}
protected function rules(): array
{
return [
'title' => 'required|string|max:255',
'title_en' => 'nullable|string|max:255',
'slug' => 'nullable|string|max:160|regex:/^[a-z0-9\-\/]*$/',
'layout' => 'required|in:default,full_width,narrow,blank,landing',
'meta_description' => 'nullable|string|max:320',
];
}
protected function messages(): array
{
return [
'title.required' => __('اسم الصفحة مطلوب'),
'title.max' => __('اسم الصفحة طويل جدًا'),
'slug.regex' => __('الرابط يجب أن يحتوي على حروف إنجليزية صغيرة وأرقام وشرطات فقط'),
'layout.required' => __('التخطيط مطلوب'),
'layout.in' => __('التخطيط المختار غير صالح'),
'meta_description.max' => __('وصف الصفحة طويل جدًا'),
];
}
public function create(): void
{
$this->resetForm();
$this->showForm = true;
}
public function edit(int $id): void
{
$page = WebsitePage::findOrFail($id);
$this->editingId = $page->id;
$this->title = $page->title ?? '';
$this->title_en = $page->title_en ?? '';
$this->slug = $page->slug;
$this->layout = $page->layout;
$this->meta_description = $page->meta_description ?? '';
$this->is_published = $page->is_published;
$this->is_homepage = $page->is_homepage;
$this->showForm = true;
}
public function save(WebsitePageService $service): void
{
$this->authorize('settings.manage');
$data = $this->validate();
$data['is_published'] = $this->is_published;
$data['is_homepage'] = $this->is_homepage;
try {
if ($this->editingId) {
$service->update(WebsitePage::findOrFail($this->editingId), $data);
session()->flash('success', __('تم تحديث الصفحة'));
} else {
$page = $service->create($data, auth()->user());
session()->flash('success', __('تم إنشاء الصفحة'));
$this->redirectRoute('website.manage.builder', ['page' => $page->id], navigate: true);
return;
}
$this->showForm = false;
$this->resetForm();
} catch (DomainException $e) {
$this->addError('title', $e->getMessage());
}
}
public function duplicate(int $id, WebsitePageService $service): void
{
$this->authorize('settings.manage');
try {
$service->duplicate(WebsitePage::findOrFail($id), auth()->user());
session()->flash('success', __('تم نسخ الصفحة'));
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
}
}
public function makeHomepage(int $id, WebsitePageService $service): void
{
$this->authorize('settings.manage');
$service->makeHomepage(WebsitePage::findOrFail($id));
session()->flash('success', __('تم تعيين الصفحة كصفحة رئيسية'));
}
public function togglePublish(int $id, WebsitePageService $service): void
{
$this->authorize('settings.manage');
$page = WebsitePage::findOrFail($id);
try {
$service->update($page, ['is_published' => ! $page->is_published]);
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
}
}
public function delete(int $id, WebsitePageService $service): void
{
$this->authorize('settings.manage');
try {
$service->delete(WebsitePage::findOrFail($id));
session()->flash('success', __('تم حذف الصفحة'));
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
}
}
public function sort(string $column): void
{
$this->sortDir = ($this->sortBy === $column && $this->sortDir === 'asc') ? 'desc' : 'asc';
$this->sortBy = $column;
}
private function resetForm(): void
{
$this->reset(['editingId', 'title', 'title_en', 'slug', 'meta_description', 'is_published', 'is_homepage']);
$this->layout = 'default';
$this->resetErrorBag();
}
public function render()
{
$pages = WebsitePage::query()
->when($this->search, fn ($q) => $q->where(fn ($w) => $w
->where('title', 'ilike', "%{$this->search}%")
->orWhere('title_en', 'ilike', "%{$this->search}%")
->orWhere('slug', 'ilike', "%{$this->search}%")))
->when($this->status === 'published', fn ($q) => $q->where('is_published', true))
->when($this->status === 'draft', fn ($q) => $q->where('is_published', false))
->withCount('allBlocks')
->orderBy($this->sortBy, $this->sortDir)
->paginate(15);
return view('livewire.website.page-manager', compact('pages'));
}
}
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