Commit 7af211fc authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix(attendance): stop the roster moving under the coach's finger

Taking attendance re-sorted the list by status on every render, so the
moment a coach marked someone the row jumped somewhere else and everyone
below it shifted. Coaches lost their place, could not tell who was already
handled, and recorded the same player several times.

The roster is now ordered by name with the record id as a tie-break —
never by anything the coach can change from this screen — so the list
holds still. Marking a player takes them out of the working list entirely
and into a collapsed "تم تسجيلهم" section, grouped by status with counts,
where the decision can be reviewed or changed. A confirmation toast names
the player and the status that was saved, and a progress card shows how
many are left.

Also here:
- markAs/markPresent/saveRecordNote now resolve the record within this
  session instead of by bare id, and reject statuses outside the four the
  screen offers
- service calls are wrapped in try/catch, so a blocked medical certificate
  shows an Arabic message instead of an error page
- the polymorphic subject relation is eager-loaded with morphWith (was an
  N+1 on every player row)
- one responsive card list replaces the duplicated mobile/desktop markup;
  targets are ≥36px, the progress bar carries progressbar semantics and
  the toast is an aria-live region
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
parent 48a79a76
......@@ -9,9 +9,12 @@
use App\Domain\Financial\Enums\InvoiceStatus;
use App\Domain\Financial\Models\Invoice;
use App\Domain\Participant\Models\Participant;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Training\Models\Enrollment;
use App\Domain\Training\Models\TrainingSession;
use App\Models\User;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Support\Collection;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
......@@ -20,6 +23,15 @@
#[Title('تسجيل الحضور')]
class TakeAttendance extends Component
{
/**
* The four statuses a trainer can assign from this screen, in display order.
* Everything else (no_show, partial, ...) is read-only here.
*/
private const ACTION_STATUSES = ['present', 'absent', 'late', 'excused'];
/** Order the recorded groups appear in inside the collapsed section. */
private const RECORDED_GROUP_ORDER = ['present', 'late', 'absent', 'excused'];
public TrainingSession $session;
public string $sessionNotes = '';
......@@ -27,6 +39,15 @@ class TakeAttendance extends Component
/** @var array<int, string> Per-record notes keyed by record id */
public array $recordNotes = [];
/** Whether the "already recorded" section is expanded. */
public bool $showRecorded = false;
/** Details of the last mark, shown as a confirmation toast. */
public ?array $lastMarked = null;
/** Increments on every mark so the toast re-mounts (and re-animates) each time. */
public int $markSeq = 0;
public function mount(TrainingSession $session, AttendanceGenerationService $generationService): void
{
$this->authorize('attendance.mark');
......@@ -47,33 +68,57 @@ public function saveNotes(): void
public function saveRecordNote(int $recordId): void
{
$record = AttendanceRecord::findOrFail($recordId);
$note = $this->recordNotes[$recordId] ?? '';
$record->update(['notes' => $note]);
$record = $this->findSessionRecord($recordId);
if (!$record) {
return;
}
$record->update(['notes' => $this->recordNotes[$recordId] ?? '']);
session()->flash("record_note_saved_{$recordId}", __('تم حفظ الملاحظة'));
}
public function markAs(int $recordId, string $status, AttendanceMarkingService $service): void
{
$record = AttendanceRecord::findOrFail($recordId);
$record = $this->findSessionRecord($recordId);
if (!$this->canMarkRecord($record)) {
if (!$record || !$this->canMarkRecord($record)) {
return;
}
if (!in_array($status, self::ACTION_STATUSES, true)) {
return;
}
$statusEnum = AttendanceStatus::from($status);
try {
$service->markStatus($record, $statusEnum, auth()->user());
} catch (DomainException $e) {
$this->addError('mark', $e->getMessage());
return;
}
$this->confirmMark($record, $statusEnum);
}
public function markPresent(int $recordId, AttendanceMarkingService $service): void
{
$record = AttendanceRecord::findOrFail($recordId);
$record = $this->findSessionRecord($recordId);
if (!$this->canMarkRecord($record)) {
if (!$record || !$this->canMarkRecord($record)) {
return;
}
$service->markPresent($record, auth()->user());
try {
$updated = $service->markPresent($record, auth()->user());
} catch (DomainException $e) {
$this->addError('mark', $e->getMessage());
return;
}
$this->confirmMark($record, $updated->status);
}
public function markAllPresent(AttendanceMarkingService $service): void
......@@ -82,11 +127,70 @@ public function markAllPresent(AttendanceMarkingService $service): void
->where('status', AttendanceStatus::Expected)
->get();
$marked = 0;
$blocked = 0;
foreach ($records as $record) {
if ($this->canMarkRecord($record)) {
if (!$this->canMarkRecord($record)) {
continue;
}
try {
$service->markPresent($record, auth()->user());
$marked++;
} catch (DomainException) {
$blocked++;
}
}
if ($blocked > 0) {
$this->addError('mark', __('تعذر تسجيل :count لاعب — الشهادة الطبية مطلوبة أو منتهية', ['count' => $blocked]));
}
if ($marked > 0) {
$this->markSeq++;
$this->lastMarked = [
'name' => __(':count لاعب', ['count' => $marked]),
'label' => AttendanceStatus::Present->label(),
'color' => AttendanceStatus::Present->color(),
];
}
}
public function toggleRecorded(): void
{
$this->showRecorded = !$this->showRecorded;
}
public function dismissConfirmation(): void
{
$this->lastMarked = null;
}
private function confirmMark(AttendanceRecord $record, AttendanceStatus $status): void
{
$this->markSeq++;
$this->lastMarked = [
'name' => $this->subjectName($record),
'label' => $status->label(),
'color' => $status->color(),
];
}
private function findSessionRecord(int $recordId): ?AttendanceRecord
{
return AttendanceRecord::where('training_session_id', $this->session->id)
->where('id', $recordId)
->first();
}
private function subjectName(AttendanceRecord $record): string
{
if ($record->subject_type === Participant::class) {
return $record->subject?->person?->name_ar ?? '-';
}
return $record->subject?->name ?? '-';
}
private function canMarkRecord(AttendanceRecord $record): bool
......@@ -120,17 +224,47 @@ private function canManageTrainers(): bool
return $roleLevel >= 60;
}
/**
* Stable, status-independent ordering: by name, then by id as a tie-break.
* Marking a player must never move anybody in the list.
*/
private function sortByName(Collection $records): Collection
{
return $records
->sortBy(fn (AttendanceRecord $record) => $this->subjectName($record)
. '#' . str_pad((string) $record->id, 12, '0', STR_PAD_LEFT))
->values();
}
public function render()
{
$records = AttendanceRecord::where('training_session_id', $this->session->id)
->with('subject')
->orderBy('subject_type')
->orderBy('status')
->with(['subject' => fn (MorphTo $morphTo) => $morphTo->morphWith([
Participant::class => ['person'],
])])
->orderBy('id')
->get();
// Split into trainers and participants
$trainerRecords = $records->where('subject_type', User::class)->values();
$participantRecords = $records->where('subject_type', Participant::class)->values();
$trainerRecords = $this->sortByName($records->where('subject_type', User::class));
$participantRecords = $this->sortByName($records->where('subject_type', Participant::class));
// The working list holds only players nobody has decided on yet.
// Once marked, a player leaves it and lands in the recorded section.
$pendingParticipants = $participantRecords
->where('status', AttendanceStatus::Expected)
->values();
$recordedParticipants = $participantRecords
->filter(fn (AttendanceRecord $record) => $record->status !== AttendanceStatus::Expected)
->values();
$recordedGroups = $recordedParticipants
->groupBy(fn (AttendanceRecord $record) => $record->status->value)
->sortBy(function ($group, $statusValue) {
$position = array_search($statusValue, self::RECORDED_GROUP_ORDER, true);
return $position === false ? count(self::RECORDED_GROUP_ORDER) : $position;
});
// Populate per-record notes from DB (only on first load or if not yet set)
foreach ($records as $record) {
......@@ -177,16 +311,28 @@ public function render()
'excused' => $records->where('status', AttendanceStatus::Excused)->count(),
];
$canManageTrainers = $this->canManageTrainers();
$participantsTotal = $participantRecords->count();
$participantsDone = $participantsTotal - $pendingParticipants->count();
$progress = [
'total' => $participantsTotal,
'done' => $participantsDone,
'pending' => $pendingParticipants->count(),
'percent' => $participantsTotal > 0
? (int) round(($participantsDone / $participantsTotal) * 100)
: 0,
];
return view('livewire.attendance.take-attendance', [
'trainerRecords' => $trainerRecords,
'participantRecords' => $participantRecords,
'records' => $records,
'pendingParticipants' => $pendingParticipants,
'recordedParticipants' => $recordedParticipants,
'recordedGroups' => $recordedGroups,
'summary' => $summary,
'statuses' => AttendanceStatus::cases(),
'progress' => $progress,
'notPaidParticipantIds' => $notPaidParticipantIds,
'canManageTrainers' => $canManageTrainers,
'canManageTrainers' => $this->canManageTrainers(),
'currentUserId' => auth()->id(),
]);
}
......
{{--
Per-record note field (desktop only keeps the mobile marking flow uncluttered).
@param \App\Domain\Attendance\Models\AttendanceRecord $record
--}}
<div class="hidden md:flex items-center gap-2 mt-2">
<input
type="text"
wire:model.blur="recordNotes.{{ $record->id }}"
wire:change="saveRecordNote({{ $record->id }})"
class="w-full rounded-lg border-gray-300 text-xs placeholder-gray-400 focus:border-indigo-500 focus:ring-indigo-500"
placeholder="{{ __('ملاحظة على اللاعب...') }}"
aria-label="{{ __('ملاحظة') }}"
>
@if(session("record_note_saved_{$record->id}"))
<span class="shrink-0 text-xs text-green-600">{{ __('تم') }}</span>
@endif
</div>
{{--
Attendance action buttons for a single record.
@param \App\Domain\Attendance\Models\AttendanceRecord $record
@param string $layout 'primary' (working list: حاضر/غائب large) | 'compact' (recorded list)
@param bool $canMark
--}}
@php
$layout = $layout ?? 'primary';
$canMark = $canMark ?? true;
$buttons = [
'present' => ['label' => 'حاضر', 'on' => 'bg-green-600 text-white border-green-600 shadow-sm', 'off' => 'bg-white text-green-700 border-green-300 hover:bg-green-50 active:bg-green-100'],
'absent' => ['label' => 'غائب', 'on' => 'bg-red-600 text-white border-red-600 shadow-sm', 'off' => 'bg-white text-red-700 border-red-300 hover:bg-red-50 active:bg-red-100'],
'late' => ['label' => 'متأخر', 'on' => 'bg-amber-600 text-white border-amber-600 shadow-sm', 'off' => 'bg-white text-amber-700 border-amber-300 hover:bg-amber-50 active:bg-amber-100'],
'excused' => ['label' => 'معذور', 'on' => 'bg-blue-600 text-white border-blue-600 shadow-sm', 'off' => 'bg-white text-blue-700 border-blue-300 hover:bg-blue-50 active:bg-blue-100'],
];
$groups = $layout === 'primary'
? [['present', 'absent'], ['late', 'excused']]
: [['present', 'absent', 'late', 'excused']];
$sizeFor = fn (string $status) => match (true) {
$layout === 'compact' => 'h-9 text-[11px] font-medium',
in_array($status, ['present', 'absent'], true) => 'h-12 text-sm font-bold',
default => 'h-10 text-xs font-medium',
};
@endphp
@foreach($groups as $groupIndex => $group)
<div class="grid gap-1.5 {{ $layout === 'compact' ? 'grid-cols-4' : 'grid-cols-2 gap-2' }} {{ $groupIndex > 0 ? 'mt-2' : '' }}">
@foreach($group as $status)
@php $isCurrent = $record->status->value === $status; @endphp
<button
type="button"
wire:click="markAs({{ $record->id }}, '{{ $status }}')"
wire:loading.attr="disabled"
wire:target="markAs({{ $record->id }}, '{{ $status }}')"
@disabled(!$canMark)
aria-pressed="{{ $isCurrent ? 'true' : 'false' }}"
class="inline-flex items-center justify-center gap-1 rounded-lg border transition-colors disabled:cursor-not-allowed disabled:opacity-50 {{ $sizeFor($status) }} {{ $isCurrent ? $buttons[$status]['on'] : $buttons[$status]['off'] }}">
<span wire:loading.remove wire:target="markAs({{ $record->id }}, '{{ $status }}')">
{{ __($buttons[$status]['label']) }}
</span>
<span wire:loading wire:target="markAs({{ $record->id }}, '{{ $status }}')">
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
</span>
</button>
@endforeach
</div>
@endforeach
<div>
@php
use App\Domain\Attendance\Enums\AttendanceStatus;
$chipClasses = [
'gray' => 'bg-gray-100 text-gray-700 ring-1 ring-gray-200',
'green' => 'bg-green-100 text-green-800 ring-1 ring-green-200',
'amber' => 'bg-amber-100 text-amber-800 ring-1 ring-amber-200',
'red' => 'bg-red-100 text-red-800 ring-1 ring-red-200',
'blue' => 'bg-blue-100 text-blue-800 ring-1 ring-blue-200',
'orange' => 'bg-orange-100 text-orange-800 ring-1 ring-orange-200',
'yellow' => 'bg-yellow-100 text-yellow-800 ring-1 ring-yellow-200',
'purple' => 'bg-purple-100 text-purple-800 ring-1 ring-purple-200',
];
$dotClasses = [
'gray' => 'bg-gray-400',
'green' => 'bg-green-500',
'amber' => 'bg-amber-500',
'red' => 'bg-red-500',
'blue' => 'bg-blue-500',
'orange' => 'bg-orange-500',
'yellow' => 'bg-yellow-500',
'purple' => 'bg-purple-500',
];
@endphp
<div class="pb-28">
<!-- Header -->
<div class="mb-4 sm:mb-6">
<div class="mb-4">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<h1 class="text-xl sm:text-2xl font-bold text-gray-900">{{ __('تسجيل الحضور') }}</h1>
......@@ -18,438 +44,297 @@ class="w-full sm:w-auto inline-flex items-center justify-center gap-2 px-4 py-2.
</div>
</div>
<!-- Summary Cards -->
<div class="grid grid-cols-3 lg:grid-cols-6 gap-2 sm:gap-4 mb-4 sm:mb-6">
<div class="p-3 sm:p-4 bg-white border border-gray-200 rounded-lg">
<p class="text-sm text-gray-500">{{ __('الإجمالي') }}</p>
<p class="text-xl sm:text-2xl font-bold text-gray-900" dir="ltr">{{ $summary['total'] }}</p>
</div>
<div class="p-3 sm:p-4 bg-green-50 border border-green-200 rounded-lg">
<p class="text-sm text-green-700">{{ __('حاضر') }}</p>
<p class="text-xl sm:text-2xl font-bold text-green-700" dir="ltr">{{ $summary['present'] }}</p>
</div>
<div class="p-3 sm:p-4 bg-amber-50 border border-amber-200 rounded-lg">
<p class="text-sm text-amber-700">{{ __('متأخر') }}</p>
<p class="text-xl sm:text-2xl font-bold text-amber-700" dir="ltr">{{ $summary['late'] }}</p>
</div>
<div class="p-3 sm:p-4 bg-red-50 border border-red-200 rounded-lg">
<p class="text-sm text-red-700">{{ __('غائب') }}</p>
<p class="text-xl sm:text-2xl font-bold text-red-700" dir="ltr">{{ $summary['absent'] }}</p>
</div>
<div class="p-3 sm:p-4 bg-gray-50 border border-gray-200 rounded-lg">
<p class="text-sm text-gray-500">{{ __('متبقي') }}</p>
<p class="text-xl sm:text-2xl font-bold text-gray-700" dir="ltr">{{ $summary['expected'] }}</p>
</div>
<div class="p-3 sm:p-4 bg-blue-50 border border-blue-200 rounded-lg">
<p class="text-sm text-blue-700">{{ __('معذور') }}</p>
<p class="text-xl sm:text-2xl font-bold text-blue-700" dir="ltr">{{ $summary['excused'] }}</p>
</div>
</div>
<!-- Session Notes -->
<div class="mb-4 sm:mb-6 bg-white border border-gray-200 rounded-lg p-4 sm:p-6">
<label for="sessionNotes" class="block text-sm font-medium text-gray-700 mb-2">{{ __('ملاحظات الجلسة') }}</label>
<textarea
id="sessionNotes"
wire:model="sessionNotes"
rows="3"
class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm"
placeholder="{{ __('أضف ملاحظات أو تعليقات حول هذه الجلسة...') }}"
></textarea>
<div class="mt-2 flex items-center gap-3">
<button
wire:click="saveNotes"
wire:loading.attr="disabled"
wire:target="saveNotes"
class="inline-flex items-center gap-2 px-3 py-2.5 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed">
<span wire:loading.remove wire:target="saveNotes">{{ __('حفظ الملاحظات') }}</span>
<span wire:loading wire:target="saveNotes">{{ __('جارٍ الحفظ...') }}</span>
</button>
@if(session('notes_saved'))
<span class="text-sm text-green-600">{{ session('notes_saved') }}</span>
@endif
<!-- Progress -->
<div class="mb-4 rounded-2xl border border-gray-200 bg-white p-4 sm:p-5">
<div class="flex items-end justify-between gap-4">
<div>
<p class="text-xs font-medium text-gray-500">{{ __('باقي عليك') }}</p>
<p class="text-3xl font-bold {{ $progress['pending'] > 0 ? 'text-gray-900' : 'text-green-600' }}" dir="ltr">{{ $progress['pending'] }}</p>
<p class="mt-0.5 text-xs text-gray-500">{{ __('من إجمالي') }} <span dir="ltr">{{ $progress['total'] }}</span> {{ __('لاعب') }}</p>
</div>
<div class="text-end">
<p class="text-xs font-medium text-gray-500">{{ __('تم تسجيلهم') }}</p>
<p class="text-3xl font-bold text-green-600" dir="ltr">{{ $progress['done'] }}</p>
</div>
</div>
<div class="mt-3 h-2 w-full overflow-hidden rounded-full bg-gray-100"
role="progressbar"
aria-valuemin="0"
aria-valuemax="{{ $progress['total'] }}"
aria-valuenow="{{ $progress['done'] }}"
aria-label="{{ __('نسبة التسجيل') }}">
<div class="h-full rounded-full bg-green-500 transition-all duration-300" style="width: {{ $progress['percent'] }}%"></div>
</div>
<div class="mt-3 flex flex-wrap items-center gap-1.5">
@foreach([
AttendanceStatus::Present->value => $summary['present'],
AttendanceStatus::Late->value => $summary['late'],
AttendanceStatus::Absent->value => $summary['absent'],
AttendanceStatus::Excused->value => $summary['excused'],
] as $statusValue => $count)
@php $statusCase = AttendanceStatus::from($statusValue); @endphp
<span class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium {{ $chipClasses[$statusCase->color()] }}">
{{ $statusCase->label() }}
<span dir="ltr" class="font-bold">{{ $count }}</span>
</span>
@endforeach
</div>
</div>
<!-- Bulk Action -->
@if($summary['expected'] > 0)
<div class="mb-4">
<button
wire:click="markAllPresent"
wire:loading.attr="disabled"
wire:target="markAllPresent"
class="inline-flex items-center gap-2 px-4 py-2.5 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed">
<span wire:loading.remove wire:target="markAllPresent">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</span>
<span wire:loading wire:target="markAllPresent">
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
</span>
<span wire:loading.remove wire:target="markAllPresent">{{ __('تحضير الجميع') }}</span>
<span wire:loading wire:target="markAllPresent">{{ __('جارٍ التحضير...') }}</span>
</button>
@error('mark')
<div class="mb-4 flex items-start gap-2 rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700">
<svg class="mt-0.5 h-4 w-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M5 19h14a2 2 0 001.84-2.75L13.74 4a2 2 0 00-3.48 0l-7.1 12.25A2 2 0 004.99 19z"/></svg>
<span>{{ $message }}</span>
</div>
@endif
@enderror
<div wire:loading.class="opacity-50 pointer-events-none" wire:target="markAs, markPresent, markAllPresent, saveRecordNote">
<div wire:loading.class="opacity-60 pointer-events-none" wire:target="markAs, markPresent, markAllPresent">
{{-- ========== TRAINERS SECTION ========== --}}
{{-- ========== TRAINERS ========== --}}
@if($trainerRecords->isNotEmpty())
<div class="mb-6">
<div class="flex items-center gap-2 mb-3">
<div class="w-8 h-8 bg-purple-100 rounded-lg flex items-center justify-center">
<svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
<div class="flex items-center gap-2 mb-2">
<div class="flex h-7 w-7 items-center justify-center rounded-lg bg-purple-100">
<svg class="h-4 w-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
</div>
<h2 class="text-base font-bold text-gray-800">{{ __('المدربون') }}</h2>
<span class="text-xs text-gray-500">({{ $trainerRecords->count() }})</span>
<h2 class="text-sm font-bold text-gray-800">{{ __('المدربون') }}</h2>
<span class="text-xs text-gray-500" dir="ltr">({{ $trainerRecords->count() }})</span>
</div>
<div class="bg-white border border-purple-200 rounded-lg overflow-hidden">
{{-- Mobile cards --}}
<div class="md:hidden space-y-2 p-3">
<div class="space-y-2">
@foreach($trainerRecords as $record)
@php
$isOwnRecord = $record->subject_id === $currentUserId;
$canMark = $canManageTrainers || $isOwnRecord;
@endphp
<div class="border {{ $isOwnRecord ? 'border-purple-300 bg-purple-50' : 'border-gray-200 bg-white' }} rounded-lg p-3 {{ !$canMark ? 'opacity-50' : '' }}">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-gray-900">
<div wire:key="trainer-{{ $record->id }}"
class="rounded-xl border p-3 {{ $isOwnRecord ? 'border-purple-300 bg-purple-50' : 'border-gray-200 bg-white' }} {{ !$canMark ? 'opacity-60' : '' }}">
<div class="flex items-center justify-between gap-2">
<span class="min-w-0 truncate text-sm font-semibold text-gray-900">
{{ $record->subject?->name ?? '-' }}
@if($isOwnRecord)
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-bold bg-purple-600 text-white ms-1">{{ __('أنت') }}</span>
<span class="ms-1 inline-flex items-center rounded bg-purple-600 px-1.5 py-0.5 text-[10px] font-bold text-white">{{ __('أنت') }}</span>
@endif
</span>
@php $color = $record->status->color(); @endphp
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium
@switch($color)
@case('green') bg-green-100 text-green-800 @break
@case('amber') bg-amber-100 text-amber-800 @break
@case('red') bg-red-100 text-red-800 @break
@case('blue') bg-blue-100 text-blue-800 @break
@default bg-gray-100 text-gray-800
@endswitch
">
<span class="shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium {{ $chipClasses[$record->status->color()] }}">
{{ $record->status->label() }}
</span>
</div>
<div class="grid grid-cols-4 gap-1.5">
@foreach(['present' => ['حاضر', 'green'], 'late' => ['متأخر', 'amber'], 'absent' => ['غائب', 'red'], 'excused' => ['معذور', 'blue']] as $st => [$label, $clr])
<button wire:click="markAs({{ $record->id }}, '{{ $st }}')" wire:loading.attr="disabled"
@if(!$canMark) disabled @endif
class="py-2.5 text-xs font-medium rounded border text-center
{{ $record->status->value === $st ? "bg-{$clr}-600 text-white border-{$clr}-600" : "text-{$clr}-700 border-{$clr}-300 hover:bg-{$clr}-50" }}
disabled:opacity-50 disabled:cursor-not-allowed">
{{ __($label) }}
</button>
@endforeach
</div>
<div class="mt-2">
@include('livewire.attendance.partials.status-buttons', ['record' => $record, 'layout' => 'compact', 'canMark' => $canMark])
</div>
@endforeach
</div>
{{-- Desktop table --}}
<div class="hidden md:block overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-purple-50">
<tr>
<th class="px-4 py-3 text-start text-xs font-medium text-purple-700 uppercase">{{ __('المدرب') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-purple-700 uppercase">{{ __('الحالة') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-purple-700 uppercase">{{ __('وقت الحضور') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-purple-700 uppercase">{{ __('الإجراءات') }}</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@foreach($trainerRecords as $record)
@php
$isOwnRecord = $record->subject_id === $currentUserId;
$canMark = $canManageTrainers || $isOwnRecord;
@endphp
<tr class="{{ $isOwnRecord ? 'bg-purple-50' : 'hover:bg-gray-50' }} {{ !$canMark ? 'opacity-50' : '' }}">
<td class="px-4 py-3 whitespace-nowrap">
<span class="text-sm font-medium text-gray-900">
{{ $record->subject?->name ?? '-' }}
</span>
@if($isOwnRecord)
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-bold bg-purple-600 text-white ms-1">{{ __('أنت') }}</span>
@endif
</td>
<td class="px-4 py-3 whitespace-nowrap">
@php $color = $record->status->color(); @endphp
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
@switch($color)
@case('green') bg-green-100 text-green-800 @break
@case('amber') bg-amber-100 text-amber-800 @break
@case('red') bg-red-100 text-red-800 @break
@case('blue') bg-blue-100 text-blue-800 @break
@default bg-gray-100 text-gray-800
@endswitch
">
{{ $record->status->label() }}
</span>
</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500" dir="ltr">
{{ $record->check_in_at?->format('H:i') ?? '-' }}
</td>
<td class="px-4 py-3 whitespace-nowrap">
<div class="flex items-center gap-1">
@foreach(['present' => ['حاضر', 'green', \App\Domain\Attendance\Enums\AttendanceStatus::Present], 'late' => ['متأخر', 'amber', \App\Domain\Attendance\Enums\AttendanceStatus::Late], 'absent' => ['غائب', 'red', \App\Domain\Attendance\Enums\AttendanceStatus::Absent], 'excused' => ['معذور', 'blue', \App\Domain\Attendance\Enums\AttendanceStatus::Excused]] as $st => [$label, $clr, $enum])
<button
wire:click="markAs({{ $record->id }}, '{{ $st }}')"
wire:loading.attr="disabled"
@if(!$canMark) disabled @endif
class="inline-flex items-center px-2 py-1 text-xs font-medium rounded border
{{ $record->status === $enum ? "bg-{$clr}-600 text-white border-{$clr}-600" : "text-{$clr}-700 border-{$clr}-300 hover:bg-{$clr}-50" }}
disabled:opacity-50 disabled:cursor-not-allowed"
title="{{ __($label) }}">
{{ __($label) }}
</button>
@endforeach
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
@endif
{{-- ========== PARTICIPANTS SECTION ========== --}}
{{-- ========== PLAYERS — WORKING LIST ========== --}}
@if($participantRecords->isNotEmpty())
<div>
<div class="flex items-center gap-2 mb-3">
<div class="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
<div class="mb-2 flex flex-wrap items-center justify-between gap-2">
<div class="flex items-center gap-2">
<div class="flex h-7 w-7 items-center justify-center rounded-lg bg-blue-100">
<svg class="h-4 w-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
</div>
<h2 class="text-sm font-bold text-gray-800">{{ __('لم يتم تسجيلهم بعد') }}</h2>
<span class="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-bold text-gray-700" dir="ltr">{{ $pendingParticipants->count() }}</span>
</div>
<h2 class="text-base font-bold text-gray-800">{{ __('اللاعبون') }}</h2>
<span class="text-xs text-gray-500">({{ $participantRecords->count() }})</span>
@if($pendingParticipants->isNotEmpty())
<button
type="button"
wire:click="markAllPresent"
wire:loading.attr="disabled"
wire:target="markAllPresent"
class="inline-flex items-center gap-1.5 rounded-lg bg-green-600 px-3 py-2 text-xs font-medium text-white hover:bg-green-700 disabled:cursor-not-allowed disabled:opacity-50">
<svg wire:loading.remove wire:target="markAllPresent" class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
<svg wire:loading wire:target="markAllPresent" class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
<span wire:loading.remove wire:target="markAllPresent">{{ __('تحضير الجميع') }}</span>
<span wire:loading wire:target="markAllPresent">{{ __('جارٍ التحضير...') }}</span>
</button>
@endif
</div>
<div class="bg-white border border-gray-200 rounded-lg overflow-hidden">
@if($participantRecords->isEmpty())
<div class="p-12 text-center">
<p class="text-sm text-gray-500">{{ __('لا يوجد لاعبون في هذه الجلسة') }}</p>
@if($pendingParticipants->isEmpty())
<div class="rounded-2xl border border-green-200 bg-green-50 p-6 text-center">
<svg class="mx-auto h-10 w-10 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
<p class="mt-2 text-sm font-bold text-green-800">{{ __('تم تسجيل كل اللاعبين') }}</p>
<p class="mt-1 text-xs text-green-700">{{ __('افتح قائمة «تم تسجيلهم» بالأسفل لمراجعة أو تعديل أي لاعب.') }}</p>
</div>
@else
{{-- Mobile cards --}}
<div class="md:hidden space-y-2 p-3">
@foreach($participantRecords as $record)
<div class="space-y-2">
@foreach($pendingParticipants as $record)
@php $isUnpaid = in_array($record->subject_id, $notPaidParticipantIds); @endphp
<div class="{{ $isUnpaid ? 'bg-red-50 border-red-300' : 'bg-white border-gray-200' }} border rounded-lg p-3">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium {{ $isUnpaid ? 'text-red-700' : 'text-gray-900' }}">
<div wire:key="pending-{{ $record->id }}"
class="rounded-xl border p-3 {{ $isUnpaid ? 'border-red-300 bg-red-50' : 'border-gray-200 bg-white' }}">
<div class="flex items-center justify-between gap-2">
<span class="min-w-0 truncate text-base font-semibold {{ $isUnpaid ? 'text-red-700' : 'text-gray-900' }}">
{{ $record->subject?->person?->name_ar ?? '-' }}
</span>
@if($record->subject?->is_free)
<x-ui.free-player-badge :small="true" />
@elseif($isUnpaid)
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-bold bg-red-600 text-white ms-1">{{ __('غير مدفوع') }}</span>
<span class="shrink-0 rounded bg-red-600 px-1.5 py-0.5 text-[10px] font-bold text-white">{{ __('غير مدفوع') }}</span>
@endif
</span>
@php $color = $record->status->color(); @endphp
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium
@switch($color)
@case('green') bg-green-100 text-green-800 @break
@case('amber') bg-amber-100 text-amber-800 @break
@case('red') bg-red-100 text-red-800 @break
@case('blue') bg-blue-100 text-blue-800 @break
@case('orange') bg-orange-100 text-orange-800 @break
@case('yellow') bg-yellow-100 text-yellow-800 @break
@case('purple') bg-purple-100 text-purple-800 @break
@default bg-gray-100 text-gray-800
@endswitch
">
{{ $record->status->label() }}
</span>
</div>
<div class="grid grid-cols-4 gap-1.5">
<button wire:click="markAs({{ $record->id }}, 'present')" wire:loading.attr="disabled" wire:target="markAs({{ $record->id }}, 'present')"
class="py-2.5 text-xs font-medium rounded border text-center
{{ $record->status === \App\Domain\Attendance\Enums\AttendanceStatus::Present ? 'bg-green-600 text-white border-green-600' : 'text-green-700 border-green-300 hover:bg-green-50' }} disabled:opacity-50">
<span wire:loading.remove wire:target="markAs({{ $record->id }}, 'present')">{{ __('حاضر') }}</span>
<span wire:loading wire:target="markAs({{ $record->id }}, 'present')">...</span>
</button>
<button wire:click="markAs({{ $record->id }}, 'late')" wire:loading.attr="disabled" wire:target="markAs({{ $record->id }}, 'late')"
class="py-2.5 text-xs font-medium rounded border text-center
{{ $record->status === \App\Domain\Attendance\Enums\AttendanceStatus::Late ? 'bg-amber-600 text-white border-amber-600' : 'text-amber-700 border-amber-300 hover:bg-amber-50' }} disabled:opacity-50">
<span wire:loading.remove wire:target="markAs({{ $record->id }}, 'late')">{{ __('متأخر') }}</span>
<span wire:loading wire:target="markAs({{ $record->id }}, 'late')">...</span>
</button>
<button wire:click="markAs({{ $record->id }}, 'absent')" wire:loading.attr="disabled" wire:target="markAs({{ $record->id }}, 'absent')"
class="py-2.5 text-xs font-medium rounded border text-center
{{ $record->status === \App\Domain\Attendance\Enums\AttendanceStatus::Absent ? 'bg-red-600 text-white border-red-600' : 'text-red-700 border-red-300 hover:bg-red-50' }} disabled:opacity-50">
<span wire:loading.remove wire:target="markAs({{ $record->id }}, 'absent')">{{ __('غائب') }}</span>
<span wire:loading wire:target="markAs({{ $record->id }}, 'absent')">...</span>
</button>
<button wire:click="markAs({{ $record->id }}, 'excused')" wire:loading.attr="disabled" wire:target="markAs({{ $record->id }}, 'excused')"
class="py-2.5 text-xs font-medium rounded border text-center
{{ $record->status === \App\Domain\Attendance\Enums\AttendanceStatus::Excused ? 'bg-blue-600 text-white border-blue-600' : 'text-blue-700 border-blue-300 hover:bg-blue-50' }} disabled:opacity-50">
<span wire:loading.remove wire:target="markAs({{ $record->id }}, 'excused')">{{ __('معذور') }}</span>
<span wire:loading wire:target="markAs({{ $record->id }}, 'excused')">...</span>
</button>
<div class="mt-2.5">
@include('livewire.attendance.partials.status-buttons', ['record' => $record, 'layout' => 'primary', 'canMark' => true])
</div>
@include('livewire.attendance.partials.record-note', ['record' => $record])
</div>
@endforeach
</div>
@endif
{{-- ========== PLAYERS — ALREADY RECORDED (collapsed) ========== --}}
@if($recordedParticipants->isNotEmpty())
<div class="mt-4">
<button
type="button"
wire:click="toggleRecorded"
aria-expanded="{{ $showRecorded ? 'true' : 'false' }}"
class="w-full rounded-xl border border-gray-200 bg-white px-4 py-3 text-start hover:bg-gray-50">
<div class="flex items-center justify-between gap-3">
<div class="flex min-w-0 items-center gap-2">
<svg class="h-5 w-5 shrink-0 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
<span class="text-sm font-bold text-gray-900">{{ __('تم تسجيلهم') }}</span>
<span class="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-bold text-gray-700" dir="ltr">{{ $recordedParticipants->count() }}</span>
</div>
<div class="flex shrink-0 items-center gap-2 text-xs font-medium text-gray-500">
<span>{{ $showRecorded ? __('إخفاء') : __('عرض / تعديل') }}</span>
<svg class="h-4 w-4 transition-transform {{ $showRecorded ? 'rotate-180' : '' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</div>
</div>
<div class="mt-2 flex flex-wrap items-center gap-1.5">
@foreach($recordedGroups as $statusValue => $group)
@php $groupStatus = AttendanceStatus::from($statusValue); @endphp
<span class="inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-medium {{ $chipClasses[$groupStatus->color()] }}">
{{ $groupStatus->label() }}
<span dir="ltr" class="font-bold">{{ $group->count() }}</span>
</span>
@endforeach
</div>
</button>
{{-- Desktop table --}}
<div class="hidden md:block overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500 uppercase">{{ __('الاسم') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500 uppercase">{{ __('الحالة') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500 uppercase">{{ __('وقت الحضور') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500 uppercase">{{ __('ملاحظة') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500 uppercase">{{ __('الإجراءات') }}</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@foreach($participantRecords as $record)
@php $isUnpaidRow = in_array($record->subject_id, $notPaidParticipantIds); @endphp
<tr class="{{ $isUnpaidRow ? 'bg-red-50 hover:bg-red-100' : 'hover:bg-gray-50' }}">
<td class="px-4 py-3 whitespace-nowrap">
<span class="text-sm font-medium {{ $isUnpaidRow ? 'text-red-700' : 'text-gray-900' }}">
@if($showRecorded)
<div class="mt-2 rounded-xl border border-gray-200 bg-gray-50 p-3">
<p class="mb-3 text-xs text-gray-500">{{ __('اضغط على الحالة الجديدة لتعديل تسجيل أي لاعب.') }}</p>
@foreach($recordedGroups as $statusValue => $group)
@php $groupStatus = AttendanceStatus::from($statusValue); @endphp
<div class="{{ $loop->first ? '' : 'mt-4' }}">
<div class="mb-1.5 flex items-center gap-2 px-1">
<span class="h-2 w-2 rounded-full {{ $dotClasses[$groupStatus->color()] }}"></span>
<span class="text-xs font-bold text-gray-700">{{ $groupStatus->label() }}</span>
<span class="text-xs text-gray-400" dir="ltr">({{ $group->count() }})</span>
</div>
<div class="space-y-1.5">
@foreach($group as $record)
@php $isUnpaidRecorded = in_array($record->subject_id, $notPaidParticipantIds); @endphp
<div wire:key="recorded-{{ $record->id }}"
class="rounded-lg border bg-white p-2.5 {{ $isUnpaidRecorded ? 'border-red-200' : 'border-gray-200' }}">
<div class="flex items-center justify-between gap-2">
<div class="min-w-0">
<p class="truncate text-sm font-medium {{ $isUnpaidRecorded ? 'text-red-700' : 'text-gray-900' }}">
{{ $record->subject?->person?->name_ar ?? '-' }}
@if($record->subject?->is_free)
<x-ui.free-player-badge :small="true" />
@elseif($isUnpaidRow)
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-bold bg-red-600 text-white ms-1">{{ __('غير مدفوع') }}</span>
@elseif($isUnpaidRecorded)
<span class="ms-1 rounded bg-red-600 px-1.5 py-0.5 text-[10px] font-bold text-white">{{ __('غير مدفوع') }}</span>
@endif
</p>
@if($record->check_in_at || $record->late_minutes)
<p class="mt-0.5 text-[11px] text-gray-400">
@if($record->check_in_at)
<span dir="ltr">{{ $record->check_in_at->format('H:i') }}</span>
@endif
</span>
</td>
<td class="px-4 py-3 whitespace-nowrap">
@php $color = $record->status->color(); @endphp
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
@switch($color)
@case('green') bg-green-100 text-green-800 @break
@case('amber') bg-amber-100 text-amber-800 @break
@case('red') bg-red-100 text-red-800 @break
@case('blue') bg-blue-100 text-blue-800 @break
@case('orange') bg-orange-100 text-orange-800 @break
@case('yellow') bg-yellow-100 text-yellow-800 @break
@case('purple') bg-purple-100 text-purple-800 @break
@default bg-gray-100 text-gray-800
@endswitch
">
{{ $record->status->label() }}
</span>
@if($record->late_minutes)
<span class="ms-1 text-xs text-gray-500" dir="ltr">({{ $record->late_minutes }} {{ __('د') }})</span>
<span dir="ltr">({{ $record->late_minutes }} {{ __('د') }})</span>
@endif
</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500" dir="ltr">
{{ $record->check_in_at?->format('H:i') ?? '-' }}
</td>
<td class="px-4 py-3" x-data="{ expanded: false }">
<div class="flex items-center gap-1">
<input
type="text"
wire:model.blur="recordNotes.{{ $record->id }}"
wire:change="saveRecordNote({{ $record->id }})"
class="w-32 text-xs rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 placeholder-gray-400"
placeholder="{{ __('ملاحظة...') }}"
x-show="!expanded"
>
<textarea
wire:model.blur="recordNotes.{{ $record->id }}"
wire:change="saveRecordNote({{ $record->id }})"
rows="2"
class="w-48 text-xs rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 placeholder-gray-400"
placeholder="{{ __('ملاحظة...') }}"
x-show="expanded"
x-cloak
></textarea>
<button
type="button"
@click="expanded = !expanded"
class="text-gray-400 hover:text-gray-600 shrink-0"
:title="expanded ? '{{ __('تصغير') }}' : '{{ __('توسيع') }}'"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path x-show="!expanded" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"/>
<path x-show="expanded" x-cloak stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 4H5v4M15 4h4v4M9 20H5v-4M15 20h4v-4"/>
</svg>
</button>
</div>
@if(session("record_note_saved_{$record->id}"))
<span class="text-xs text-green-600 mt-0.5 block">{{ __('تم') }}</span>
</p>
@endif
</td>
<td class="px-4 py-3 whitespace-nowrap">
<div class="flex items-center gap-1">
<button
wire:click="markAs({{ $record->id }}, 'present')"
wire:loading.attr="disabled"
wire:target="markAs({{ $record->id }}, 'present')"
class="inline-flex items-center px-2 py-1 text-xs font-medium rounded border
{{ $record->status === \App\Domain\Attendance\Enums\AttendanceStatus::Present ? 'bg-green-600 text-white border-green-600' : 'text-green-700 border-green-300 hover:bg-green-50' }}
disabled:opacity-50"
title="{{ __('حاضر') }}">
<span wire:loading.remove wire:target="markAs({{ $record->id }}, 'present')">{{ __('حاضر') }}</span>
<span wire:loading wire:target="markAs({{ $record->id }}, 'present')" class="w-3 h-3">
<svg class="animate-spin w-3 h-3" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
</span>
</button>
<button
wire:click="markAs({{ $record->id }}, 'late')"
wire:loading.attr="disabled"
wire:target="markAs({{ $record->id }}, 'late')"
class="inline-flex items-center px-2 py-1 text-xs font-medium rounded border
{{ $record->status === \App\Domain\Attendance\Enums\AttendanceStatus::Late ? 'bg-amber-600 text-white border-amber-600' : 'text-amber-700 border-amber-300 hover:bg-amber-50' }}
disabled:opacity-50"
title="{{ __('متأخر') }}">
<span wire:loading.remove wire:target="markAs({{ $record->id }}, 'late')">{{ __('متأخر') }}</span>
<span wire:loading wire:target="markAs({{ $record->id }}, 'late')" class="w-3 h-3">
<svg class="animate-spin w-3 h-3" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
</span>
</button>
<button
wire:click="markAs({{ $record->id }}, 'absent')"
wire:loading.attr="disabled"
wire:target="markAs({{ $record->id }}, 'absent')"
class="inline-flex items-center px-2 py-1 text-xs font-medium rounded border
{{ $record->status === \App\Domain\Attendance\Enums\AttendanceStatus::Absent ? 'bg-red-600 text-white border-red-600' : 'text-red-700 border-red-300 hover:bg-red-50' }}
disabled:opacity-50"
title="{{ __('غائب') }}">
<span wire:loading.remove wire:target="markAs({{ $record->id }}, 'absent')">{{ __('غائب') }}</span>
<span wire:loading wire:target="markAs({{ $record->id }}, 'absent')" class="w-3 h-3">
<svg class="animate-spin w-3 h-3" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
</span>
</button>
<button
wire:click="markAs({{ $record->id }}, 'excused')"
wire:loading.attr="disabled"
wire:target="markAs({{ $record->id }}, 'excused')"
class="inline-flex items-center px-2 py-1 text-xs font-medium rounded border
{{ $record->status === \App\Domain\Attendance\Enums\AttendanceStatus::Excused ? 'bg-blue-600 text-white border-blue-600' : 'text-blue-700 border-blue-300 hover:bg-blue-50' }}
disabled:opacity-50"
title="{{ __('معذور') }}">
<span wire:loading.remove wire:target="markAs({{ $record->id }}, 'excused')">{{ __('معذور') }}</span>
<span wire:loading wire:target="markAs({{ $record->id }}, 'excused')" class="w-3 h-3">
<svg class="animate-spin w-3 h-3" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
</div>
<span class="shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium {{ $chipClasses[$record->status->color()] }}">
{{ $record->status->label() }}
</span>
</button>
</div>
</td>
</tr>
<div class="mt-2">
@include('livewire.attendance.partials.status-buttons', ['record' => $record, 'layout' => 'compact', 'canMark' => true])
</div>
@include('livewire.attendance.partials.record-note', ['record' => $record])
</div>
@endforeach
</div>
</div>
@endforeach
</tbody>
</table>
</div>
@endif
</div>
@endif
</div>
@endif
</div>
{{-- Empty state --}}
@if($trainerRecords->isEmpty() && $participantRecords->isEmpty())
<div class="bg-white border border-gray-200 rounded-lg p-12 text-center">
<div class="rounded-lg border border-gray-200 bg-white p-12 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>
<p class="mt-4 text-sm text-gray-500">{{ __('لا توجد سجلات حضور لهذه الجلسة') }}</p>
</div>
@endif
<!-- Session Notes -->
<div class="mt-6 rounded-xl border border-gray-200 bg-white p-4">
<label for="sessionNotes" class="mb-2 block text-sm font-medium text-gray-700">{{ __('ملاحظات الجلسة') }}</label>
<textarea
id="sessionNotes"
wire:model="sessionNotes"
rows="3"
class="block w-full rounded-lg border-gray-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
placeholder="{{ __('أضف ملاحظات أو تعليقات حول هذه الجلسة...') }}"
></textarea>
<div class="mt-2 flex items-center gap-3">
<button
type="button"
wire:click="saveNotes"
wire:loading.attr="disabled"
wire:target="saveNotes"
class="inline-flex items-center gap-2 rounded-lg bg-indigo-600 px-3 py-2.5 text-sm font-medium text-white hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-50">
<span wire:loading.remove wire:target="saveNotes">{{ __('حفظ الملاحظات') }}</span>
<span wire:loading wire:target="saveNotes">{{ __('جارٍ الحفظ...') }}</span>
</button>
@if(session('notes_saved'))
<span class="text-sm text-green-600">{{ session('notes_saved') }}</span>
@endif
</div>
</div>
{{-- Confirmation toast: proves the tap was recorded, and offers the way back to it --}}
@if($lastMarked)
<div wire:key="mark-toast-{{ $markSeq }}"
x-data="{ shown: true }"
x-init="setTimeout(() => shown = false, 5000)"
x-show="shown"
x-transition.opacity.duration.200ms
role="status"
aria-live="polite"
class="fixed bottom-4 start-4 end-4 z-40 mx-auto max-w-md rounded-xl border {{ $chipClasses[$lastMarked['color']] }} px-4 py-3 shadow-lg">
<div class="flex items-center justify-between gap-3">
<div class="flex min-w-0 items-center gap-2">
<svg class="h-5 w-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
<p class="min-w-0 truncate text-sm font-bold">
{{ $lastMarked['name'] }} — {{ $lastMarked['label'] }}
</p>
</div>
<button
type="button"
wire:click="$set('showRecorded', true)"
class="shrink-0 rounded-lg bg-white/70 px-2.5 py-1 text-xs font-medium hover:bg-white">
{{ __('تعديل') }}
</button>
</div>
</div>
@endif
</div>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment