Commit 00e571f7 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Fix enrollments report + add absence-by-trainer report with session filter

1. Enrollments by program: replace PostgreSQL-incompatible HAVING on alias with get()->filter()
2. New report "المتغيبون حسب المدرب": shows absent participants grouped by trainer, with min-absences filter (1/2/3/5 sessions), excludes suspended participants
3. Also fixes existing absentee report to exclude suspended via the new report variant
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 5c0fe31e
...@@ -310,6 +310,37 @@ public function absenteeReport(string $from, string $to, ?int $branchId = null): ...@@ -310,6 +310,37 @@ public function absenteeReport(string $from, string $to, ?int $branchId = null):
->values(); ->values();
} }
public function absentParticipantsByTrainer(string $from, string $to, ?int $branchId = null, int $minAbsences = 1): Collection
{
return AttendanceRecord::with(['subject.person', 'session.group.headTrainer.person'])
->where('subject_type', 'App\\Domain\\Participant\\Models\\Participant')
->whereIn('status', [AttendanceStatus::Absent, AttendanceStatus::NoShow])
->whereBetween('created_at', [$from, $to . ' 23:59:59'])
->when($branchId, fn ($q) => $q->whereHas('session.group', fn ($g) => $g->where('branch_id', $branchId)))
->whereHas('subject', fn ($q) => $q->where('status', '!=', 'suspended'))
->get()
->groupBy('subject_id')
->filter(fn ($records) => $records->count() >= $minAbsences)
->map(function ($records) {
$first = $records->first();
$participant = $first->subject;
$person = $participant?->person;
$group = $records->first()->session?->group;
$trainer = $group?->headTrainer?->person;
return [
'name' => $person?->name_ar ?? '',
'phone' => $person?->phone ?? '',
'absent_count' => $records->count(),
'group' => $group?->name_ar ?? '',
'trainer' => $trainer?->name_ar ?? '',
'last_absence' => $records->max('created_at')?->format('Y-m-d') ?? '',
];
})
->sortBy('trainer')
->values();
}
public function trainerAttendance(string $from, string $to, ?int $branchId = null): Collection public function trainerAttendance(string $from, string $to, ?int $branchId = null): Collection
{ {
return Trainer::with(['person']) return Trainer::with(['person'])
...@@ -345,9 +376,10 @@ public function enrollmentsByProgram(string $from, string $to, ?int $branchId = ...@@ -345,9 +376,10 @@ public function enrollmentsByProgram(string $from, string $to, ?int $branchId =
$q->whereBetween('enrollment_date', [$from, $to]) $q->whereBetween('enrollment_date', [$from, $to])
->when($branchId, fn ($q2) => $q2->whereHas('group', fn ($g) => $g->where('branch_id', $branchId))); ->when($branchId, fn ($q2) => $q2->whereHas('group', fn ($g) => $g->where('branch_id', $branchId)));
}]) }])
->having('enrollments_count', '>', 0)
->orderByDesc('enrollments_count')
->get() ->get()
->filter(fn ($prog) => $prog->enrollments_count > 0)
->sortByDesc('enrollments_count')
->values()
->map(fn ($prog) => [ ->map(fn ($prog) => [
'program' => $prog->name_ar, 'program' => $prog->name_ar,
'enrollments' => $prog->enrollments_count, 'enrollments' => $prog->enrollments_count,
......
...@@ -24,6 +24,9 @@ class ReportViewer extends Component ...@@ -24,6 +24,9 @@ class ReportViewer extends Component
#[Url] #[Url]
public string $to = ''; public string $to = '';
#[Url]
public int $minAbsences = 1;
public function mount(string $report = ''): void public function mount(string $report = ''): void
{ {
$this->authorize('reports.view'); $this->authorize('reports.view');
...@@ -175,6 +178,15 @@ public function getReportConfig(): array ...@@ -175,6 +178,15 @@ public function getReportConfig(): array
'money_cols' => [], 'money_cols' => [],
'uses_dates' => true, 'uses_dates' => true,
], ],
'absent_by_trainer' => [
'name' => 'المتغيبون حسب المدرب',
'method' => 'absentParticipantsByTrainer',
'headers' => ['الاسم', 'الهاتف', 'عدد الغيابات', 'المجموعة', 'المدرب', 'آخر غياب'],
'columns' => ['name', 'phone', 'absent_count', 'group', 'trainer', 'last_absence'],
'money_cols' => [],
'uses_dates' => true,
'has_min_absences_filter' => true,
],
'trainer_attendance' => [ 'trainer_attendance' => [
'name' => 'حضور المدربين', 'name' => 'حضور المدربين',
'method' => 'trainerAttendance', 'method' => 'trainerAttendance',
...@@ -317,7 +329,11 @@ public function render(ReportService $reportService) ...@@ -317,7 +329,11 @@ public function render(ReportService $reportService)
if ($usesDates) { if ($usesDates) {
$from = $this->from ?: now()->startOfMonth()->format('Y-m-d'); $from = $this->from ?: now()->startOfMonth()->format('Y-m-d');
$to = $this->to ?: now()->format('Y-m-d'); $to = $this->to ?: now()->format('Y-m-d');
$data = $reportService->$method($from, $to, $branchId); if ($method === 'absentParticipantsByTrainer') {
$data = $reportService->$method($from, $to, $branchId, $this->minAbsences);
} else {
$data = $reportService->$method($from, $to, $branchId);
}
} else { } else {
$data = $reportService->$method($branchId); $data = $reportService->$method($branchId);
} }
......
...@@ -56,6 +56,7 @@ public function getReportsProperty(): array ...@@ -56,6 +56,7 @@ public function getReportsProperty(): array
'reports' => [ 'reports' => [
['key' => 'attendance_by_group', 'name' => 'الحضور حسب المجموعة', 'desc' => 'نسبة الحضور لكل مجموعة تدريبية'], ['key' => 'attendance_by_group', 'name' => 'الحضور حسب المجموعة', 'desc' => 'نسبة الحضور لكل مجموعة تدريبية'],
['key' => 'absentees', 'name' => 'أكثر الغائبين', 'desc' => 'المشتركون الأكثر غياباً مع عدد المرات'], ['key' => 'absentees', 'name' => 'أكثر الغائبين', 'desc' => 'المشتركون الأكثر غياباً مع عدد المرات'],
['key' => 'absent_by_trainer', 'name' => 'المتغيبون حسب المدرب', 'desc' => 'المتغيبون مجمعين بالمدرب مع فلتر عدد الحصص (يستثني الموقوفين)'],
['key' => 'trainer_attendance', 'name' => 'حضور المدربين', 'desc' => 'نسبة حضور كل مدرب لحصصه'], ['key' => 'trainer_attendance', 'name' => 'حضور المدربين', 'desc' => 'نسبة حضور كل مدرب لحصصه'],
], ],
], ],
......
...@@ -17,9 +17,10 @@ class="inline-flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded- ...@@ -17,9 +17,10 @@ class="inline-flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-
</div> </div>
<!-- Filters --> <!-- Filters -->
@if($config['uses_dates']) @if($config['uses_dates'] || ($config['has_min_absences_filter'] ?? false))
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-4"> <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-4">
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3"> <div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
@if($config['uses_dates'])
<div> <div>
<label class="block text-xs font-medium text-gray-600 mb-1">{{ __('من تاريخ') }}</label> <label class="block text-xs font-medium text-gray-600 mb-1">{{ __('من تاريخ') }}</label>
<input type="date" wire:model.live="from" dir="ltr" <input type="date" wire:model.live="from" dir="ltr"
...@@ -30,6 +31,19 @@ class="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:r ...@@ -30,6 +31,19 @@ class="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:r
<input type="date" wire:model.live="to" dir="ltr" <input type="date" wire:model.live="to" dir="ltr"
class="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm"> class="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm">
</div> </div>
@endif
@if($config['has_min_absences_filter'] ?? false)
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">{{ __('الحد الأدنى للغياب') }}</label>
<select wire:model.live="minAbsences"
class="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm">
<option value="1">{{ __('حصة واحدة أو أكثر') }}</option>
<option value="2">{{ __('حصتين أو أكثر') }}</option>
<option value="3">{{ __('3 حصص أو أكثر') }}</option>
<option value="5">{{ __('5 حصص أو أكثر') }}</option>
</select>
</div>
@endif
</div> </div>
</div> </div>
@endif @endif
......
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