Commit 0cfe5e09 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Fix trainer attendance: correct subject_type across all queries + add self-view

All trainer attendance records are stored as subject_type=User::class with
subject_id=user.id (via GenerateTrainerAttendance listener). Multiple places
were querying the wrong type ('trainer', 'staff', or Trainer::class), returning
zero results. Fixed: ReportService, GenerateTrainerCompensation, Dashboard,
TrainerShow. Added trainer self-attendance section to TrainerDashboard with
rate, breakdown, and recent records.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 1fa2ae79
...@@ -25,13 +25,14 @@ public function handle(AttendanceMarked $event): void ...@@ -25,13 +25,14 @@ public function handle(AttendanceMarked $event): void
try { try {
$record = $event->record; $record = $event->record;
// Only process trainer/staff attendance, not participant // Only process trainer attendance (stored as User::class), not participant
if ($record->subject_type !== 'trainer' && $record->subject_type !== 'staff') { if ($record->subject_type !== \App\Models\User::class) {
return; return;
} }
// Find the trainer by subject_id // Find the trainer by user_id → employee → trainer
$trainer = Trainer::find($record->subject_id); $trainer = Trainer::whereHas('employee', fn ($q) => $q->where('user_id', $record->subject_id))
->first();
if (!$trainer || $trainer->status->value !== 'active') { if (!$trainer || $trainer->status->value !== 'active') {
return; return;
} }
......
...@@ -343,12 +343,17 @@ public function absentParticipantsByTrainer(string $from, string $to, ?int $bran ...@@ -343,12 +343,17 @@ public function absentParticipantsByTrainer(string $from, string $to, ?int $bran
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', 'employee'])
->where('status', 'active') ->where('status', 'active')
->get() ->get()
->map(function ($trainer) use ($from, $to, $branchId) { ->map(function ($trainer) use ($from, $to, $branchId) {
$records = AttendanceRecord::where('subject_type', 'App\\Domain\\HR\\Models\\Trainer') $userId = $trainer->employee?->user_id;
->where('subject_id', $trainer->id) if (! $userId) {
return null;
}
$records = AttendanceRecord::where('subject_type', \App\Models\User::class)
->where('subject_id', $userId)
->whereBetween('created_at', [$from, $to . ' 23:59:59']) ->whereBetween('created_at', [$from, $to . ' 23:59:59'])
->when($branchId, fn ($q) => $q->whereHas('session.group', fn ($g) => $g->where('branch_id', $branchId))); ->when($branchId, fn ($q) => $q->whereHas('session.group', fn ($g) => $g->where('branch_id', $branchId)));
$total = (clone $records)->count(); $total = (clone $records)->count();
...@@ -363,7 +368,7 @@ public function trainerAttendance(string $from, string $to, ?int $branchId = nul ...@@ -363,7 +368,7 @@ public function trainerAttendance(string $from, string $to, ?int $branchId = nul
'rate' => $total > 0 ? round(($present / $total) * 100, 1) : 0, 'rate' => $total > 0 ? round(($present / $total) * 100, 1) : 0,
]; ];
}) })
->filter(fn ($t) => $t['total_sessions'] > 0) ->filter(fn ($t) => $t && $t['total_sessions'] > 0)
->sortBy('rate') ->sortBy('rate')
->values(); ->values();
} }
......
...@@ -61,7 +61,7 @@ public function render() ...@@ -61,7 +61,7 @@ public function render()
->count(); ->count();
$trainersPresent = AttendanceRecord::whereHas('session', fn ($q) => $q->where('session_date', $today)) $trainersPresent = AttendanceRecord::whereHas('session', fn ($q) => $q->where('session_date', $today))
->where('subject_type', 'trainer') ->where('subject_type', \App\Models\User::class)
->whereIn('status', [AttendanceStatus::Present, AttendanceStatus::Late]) ->whereIn('status', [AttendanceStatus::Present, AttendanceStatus::Late])
->count(); ->count();
......
...@@ -57,10 +57,9 @@ public function render() ...@@ -57,10 +57,9 @@ public function render()
->limit(20) ->limit(20)
->get(); ->get();
// Attendance stats for this trainer (as subject) // Attendance stats for this trainer (records stored as User::class)
$trainerClass = Trainer::class; $attendanceQuery = AttendanceRecord::where('subject_type', \App\Models\User::class)
$attendanceQuery = AttendanceRecord::where('subject_type', $trainerClass) ->where('subject_id', $trainerUserId);
->where('subject_id', $this->trainer->id);
$totalSessionsAttended = (clone $attendanceQuery)->count(); $totalSessionsAttended = (clone $attendanceQuery)->count();
$presentCount = (clone $attendanceQuery)->where('status', AttendanceStatus::Present)->count(); $presentCount = (clone $attendanceQuery)->where('status', AttendanceStatus::Present)->count();
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
namespace App\Livewire\Trainer; namespace App\Livewire\Trainer;
use App\Domain\Attendance\Enums\AttendanceStatus;
use App\Domain\Attendance\Models\AttendanceRecord;
use App\Domain\Scheduling\Enums\AssignmentStatus; use App\Domain\Scheduling\Enums\AssignmentStatus;
use App\Domain\Scheduling\Models\Assignment; use App\Domain\Scheduling\Models\Assignment;
use App\Domain\Training\Enums\SessionStatus; use App\Domain\Training\Enums\SessionStatus;
...@@ -75,12 +77,41 @@ public function render() ...@@ -75,12 +77,41 @@ public function render()
->whereIn('status', [SessionStatus::Scheduled, SessionStatus::InProgress, SessionStatus::Completed]) ->whereIn('status', [SessionStatus::Scheduled, SessionStatus::InProgress, SessionStatus::Completed])
->count(); ->count();
// My attendance stats (this month)
$myAttendanceQuery = AttendanceRecord::where('subject_type', \App\Models\User::class)
->where('subject_id', $user->id)
->whereBetween('created_at', [$monthStart . ' 00:00:00', $monthEnd . ' 23:59:59']);
$myTotalRecords = (clone $myAttendanceQuery)->count();
$myPresentCount = (clone $myAttendanceQuery)->where('status', AttendanceStatus::Present)->count();
$myLateCount = (clone $myAttendanceQuery)->where('status', AttendanceStatus::Late)->count();
$myAbsentCount = (clone $myAttendanceQuery)->whereIn('status', [AttendanceStatus::Absent, AttendanceStatus::NoShow])->count();
$myCancelledExempt = (clone $myAttendanceQuery)->whereIn('status', [AttendanceStatus::Cancelled, AttendanceStatus::Exempt])->count();
$myAttendanceDenominator = $myTotalRecords - $myCancelledExempt;
$myAttendanceRate = $myAttendanceDenominator > 0
? round(($myPresentCount + $myLateCount) / $myAttendanceDenominator * 100, 1)
: 0;
// Recent attendance records (last 15)
$recentAttendance = AttendanceRecord::where('subject_type', \App\Models\User::class)
->where('subject_id', $user->id)
->with('session.group')
->orderByDesc('created_at')
->limit(15)
->get();
return view('livewire.trainer.trainer-dashboard', [ return view('livewire.trainer.trainer-dashboard', [
'todaySessions' => $todaySessions, 'todaySessions' => $todaySessions,
'upcomingSessions' => $upcomingSessions, 'upcomingSessions' => $upcomingSessions,
'assignedGroups' => $assignedGroups, 'assignedGroups' => $assignedGroups,
'sessionsThisMonth' => $sessionsThisMonth, 'sessionsThisMonth' => $sessionsThisMonth,
'totalSessionsThisMonth' => $totalSessionsThisMonth, 'totalSessionsThisMonth' => $totalSessionsThisMonth,
'myPresentCount' => $myPresentCount,
'myLateCount' => $myLateCount,
'myAbsentCount' => $myAbsentCount,
'myAttendanceRate' => $myAttendanceRate,
'recentAttendance' => $recentAttendance,
]); ]);
} }
} }
...@@ -70,6 +70,72 @@ ...@@ -70,6 +70,72 @@
</div> </div>
</div> </div>
{{-- My Attendance This Month --}}
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-700 mb-4">{{ __('حضوري هذا الشهر') }}</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
{{-- Attendance Rate Card --}}
<div class="bg-gradient-to-br from-indigo-50 to-blue-50 rounded-xl border border-indigo-100 p-5">
<div class="flex items-center justify-between mb-4">
<h3 class="text-sm font-medium text-indigo-700">{{ __('نسبة الحضور') }}</h3>
<span class="text-2xl font-bold text-indigo-700" dir="ltr">{{ $myAttendanceRate }}%</span>
</div>
<div class="w-full bg-indigo-200 rounded-full h-2.5 mb-4">
<div class="bg-indigo-600 h-2.5 rounded-full transition-all" style="width: {{ min($myAttendanceRate, 100) }}%"></div>
</div>
<div class="grid grid-cols-3 gap-3 text-center">
<div class="bg-white/70 rounded-lg p-2">
<p class="text-lg font-bold text-green-600" dir="ltr">{{ $myPresentCount }}</p>
<p class="text-xs text-gray-500">{{ __('حاضر') }}</p>
</div>
<div class="bg-white/70 rounded-lg p-2">
<p class="text-lg font-bold text-amber-600" dir="ltr">{{ $myLateCount }}</p>
<p class="text-xs text-gray-500">{{ __('متأخر') }}</p>
</div>
<div class="bg-white/70 rounded-lg p-2">
<p class="text-lg font-bold text-red-600" dir="ltr">{{ $myAbsentCount }}</p>
<p class="text-xs text-gray-500">{{ __('غائب') }}</p>
</div>
</div>
</div>
{{-- Recent Attendance Records --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
<h3 class="text-sm font-medium text-gray-700 mb-3">{{ __('آخر سجلات الحضور') }}</h3>
@if($recentAttendance->isEmpty())
<p class="text-sm text-gray-400 text-center py-4">{{ __('لا توجد سجلات حضور بعد') }}</p>
@else
<div class="space-y-2 max-h-64 overflow-y-auto">
@foreach($recentAttendance->take(8) as $record)
@php
$statusConfig = [
'present' => ['label' => 'حاضر', 'color' => 'bg-green-100 text-green-700'],
'late' => ['label' => 'متأخر', 'color' => 'bg-amber-100 text-amber-700'],
'absent' => ['label' => 'غائب', 'color' => 'bg-red-100 text-red-700'],
'no_show' => ['label' => 'لم يحضر', 'color' => 'bg-red-100 text-red-700'],
'excused' => ['label' => 'معذور', 'color' => 'bg-blue-100 text-blue-700'],
'cancelled' => ['label' => 'ملغاة', 'color' => 'bg-gray-100 text-gray-700'],
];
$st = $record->status->value;
$cfg = $statusConfig[$st] ?? ['label' => $st, 'color' => 'bg-gray-100 text-gray-700'];
@endphp
<div class="flex items-center justify-between py-2 border-b border-gray-50 last:border-0">
<div>
<p class="text-sm text-gray-800">{{ $record->session?->group?->name_ar ?? '-' }}</p>
<p class="text-xs text-gray-400" dir="ltr">{{ $record->created_at->format('Y-m-d') }}</p>
</div>
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium {{ $cfg['color'] }}">
{{ $cfg['label'] }}
</span>
</div>
@endforeach
</div>
@endif
</div>
</div>
</div>
{{-- Today's Sessions --}} {{-- Today's Sessions --}}
<div class="mb-8"> <div class="mb-8">
<h2 class="text-lg font-semibold text-gray-700 mb-4">{{ __('حصص اليوم') }}</h2> <h2 class="text-lg font-semibold text-gray-700 mb-4">{{ __('حصص اليوم') }}</h2>
......
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