Commit bbd5895f authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add free player feature + trainer delete/edit with full cascade

- Free player: is_free column, toggle button on participant show, badge
  component across all views, skip invoice on enrollment/renewal for free players
- Trainer delete: full cascade (cancel compensations/advances, remove future
  attendance, cancel assignments, nullify trainer refs, deactivate user,
  archive employee/person, free national_id for reimport)
- Trainer edit: person data fields (name, national_id, phone) editable from
  trainer form
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 737d470e
......@@ -32,6 +32,9 @@ public function handle(InvoiceService $invoiceService, PricingService $pricingSe
])
->whereNotNull('billing_cycle');
})
->whereHas('participant', function ($q) {
$q->where('is_free', false);
})
->with(['program', 'participant.person', 'group'])
->get();
......
......@@ -2,13 +2,20 @@
namespace App\Domain\HR\Services;
use App\Domain\Attendance\Models\AttendanceRecord;
use App\Domain\HR\Models\Employee;
use App\Domain\HR\Models\Trainer;
use App\Domain\HR\Models\TrainerAvailability;
use App\Domain\HR\Models\TrainerQualification;
use App\Domain\Scheduling\Models\Assignment;
use App\Domain\Training\Models\TrainingGroup;
use App\Domain\Training\Models\TrainingProgram;
use App\Domain\Training\Models\TrainingSession;
use App\Models\User;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Exceptions\InvalidStatusTransitionException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class TrainerService
{
......@@ -102,6 +109,89 @@ public function setAvailability(Trainer $trainer, array $slots): void
});
}
public function delete(Trainer $trainer, User $actor): void
{
DB::transaction(function () use ($trainer, $actor) {
$trainerUserId = $trainer->employee?->user_id;
// 1. Cancel pending compensations (keep approved/paid as history)
$trainer->compensations()
->whereIn('status', ['pending', 'disputed'])
->update(['status' => 'cancelled']);
// 2. Cancel active advances (write off remaining balance)
$trainer->advances()
->where('status', 'active')
->update([
'status' => 'cancelled',
'remaining_balance' => 0,
]);
// 3. Remove future attendance expectations
if ($trainerUserId) {
AttendanceRecord::where('subject_type', User::class)
->where('subject_id', $trainerUserId)
->where('status', 'expected')
->whereHas('session', fn ($q) => $q->where('session_date', '>', now()->toDateString()))
->delete();
}
// 4. Cancel active group assignments + clear trainer references
if ($trainerUserId) {
Assignment::where('user_id', $trainerUserId)
->where('status', 'active')
->update(['status' => 'cancelled']);
TrainingGroup::where('head_trainer_id', $trainerUserId)
->update(['head_trainer_id' => null]);
TrainingProgram::where('default_trainer_id', $trainerUserId)
->update(['default_trainer_id' => null]);
TrainingSession::where('trainer_id', $trainerUserId)
->where('session_date', '>', now()->toDateString())
->update(['trainer_id' => null]);
TrainingSession::where('assistant_trainer_id', $trainerUserId)
->where('session_date', '>', now()->toDateString())
->update(['assistant_trainer_id' => null]);
}
// 5. Deactivate user account
if ($trainerUserId) {
User::where('id', $trainerUserId)->update(['status' => 'inactive']);
}
// 6. Soft-delete employee (mangles unique fields like employee_number)
if ($trainer->employee) {
$trainer->employee->delete();
}
// 7. Soft-delete the person record (frees national_id for reimport)
$person = $trainer->employee?->person ?? $trainer->person;
if ($person) {
$originalNid = $person->national_id;
$person->delete();
// Mangle national_id to free the unique constraint
if ($originalNid) {
$person->national_id = $originalNid . '_deleted_' . $trainer->id;
$person->saveQuietly();
}
}
// 8. Delete trainer record (hard delete since no soft deletes on this table)
Log::channel('audit')->info('trainer_deleted', [
'trainer_id' => $trainer->id,
'trainer_number' => $trainer->trainer_number,
'person_name' => $person?->name_ar,
'deleted_by' => $actor->id,
'deleted_by_name' => $actor->name,
]);
$trainer->delete();
});
}
private function generateTrainerNumber(int $academyId): string
{
$lastNumber = Trainer::withoutGlobalScopes()
......
......@@ -49,6 +49,7 @@ class Participant extends Model
'status',
'status_changed_at',
'status_reason',
'is_free',
'membership_type',
'membership_id',
'membership_expires_at',
......@@ -74,6 +75,7 @@ class Participant extends Model
'outstanding_balance' => 'integer',
'height_cm' => 'decimal:1',
'weight_kg' => 'decimal:1',
'is_free' => 'boolean',
'metadata' => 'array',
'jersey_number' => 'integer',
];
......
......@@ -75,6 +75,7 @@ public function register(array $data, User $actor): Participant
'status_changed_at' => now(),
'membership_type' => $data['membership_type'] ?? null,
'membership_id' => $data['membership_id'] ?? null,
'is_free' => $data['is_free'] ?? false,
'notes' => $data['notes'] ?? null,
'metadata' => $data['metadata'] ?? [],
'created_by' => $actor->id,
......
......@@ -84,11 +84,11 @@ public function enroll(Participant $participant, TrainingGroup $group, User $act
'enrollment_date' => now()->toDateString(),
'start_date' => $options['start_date'] ?? $group->start_date ?? now()->toDateString(),
'end_date' => $options['end_date'] ?? $group->end_date,
'next_billing_date' => $this->calculateFirstBillingDate($group->program),
'next_billing_date' => $participant->is_free ? null : $this->calculateFirstBillingDate($group->program),
'status' => 'active',
'enrolled_by' => $actor->id,
'invoice_id' => $options['invoice_id'] ?? null,
'payment_status' => $options['payment_status'] ?? 'pending',
'payment_status' => $participant->is_free ? 'waived' : ($options['payment_status'] ?? 'pending'),
'sessions_total' => $group->program?->total_sessions,
]);
......@@ -100,8 +100,8 @@ public function enroll(Participant $participant, TrainingGroup $group, User $act
$participant->update(['status' => 'active']);
}
// Auto-create invoice if program has a price (skip if invoice already provided or explicitly skipped)
if (empty($options['invoice_id']) && empty($options['skip_auto_invoice']) && (bool) $this->settings->get('auto_invoice_on_enrollment', false)) {
// Auto-create invoice if program has a price (skip if invoice already provided, explicitly skipped, or free player)
if (empty($options['invoice_id']) && empty($options['skip_auto_invoice']) && !$participant->is_free && (bool) $this->settings->get('auto_invoice_on_enrollment', false)) {
$this->createEnrollmentInvoice($enrollment, $participant, $group, $actor);
}
......
......@@ -23,6 +23,12 @@ class TrainerForm extends Component
public ?int $employeeId = null;
public string $selectedEmployeeName = '';
// Person fields (editable when editing)
public string $personNameAr = '';
public string $personName = '';
public string $personPhone = '';
public string $personNationalId = '';
// Form fields
public string $bio = '';
public string $bioAr = '';
......@@ -53,6 +59,14 @@ public function mount(?Trainer $trainer = null): void
$this->employeeId = $trainer->employee_id;
$this->selectedEmployeeName = $trainer->employee->person->name_ar ?? '';
// Person data
$person = $trainer->employee?->person ?? $trainer->person;
$this->personNameAr = $person?->name_ar ?? '';
$this->personName = $person?->name ?? '';
$this->personPhone = $person?->phone ?? '';
$this->personNationalId = $person?->national_id ?? '';
$this->bio = $trainer->bio ?? '';
$this->bioAr = $trainer->bio_ar ?? '';
$this->specializations = $trainer->specializations ?? [];
......@@ -80,7 +94,14 @@ public function mount(?Trainer $trainer = null): void
public function rules(): array
{
return [
$personRules = $this->editing ? [
'personNameAr' => 'required|string|max:100',
'personName' => 'nullable|string|max:100',
'personPhone' => 'nullable|string|max:20',
'personNationalId' => 'nullable|string|max:30',
] : [];
return array_merge($personRules, [
'employeeId' => 'required|exists:employees,id',
'sports' => 'required|array|min:1',
'sports.*' => 'integer|exists:activities,id',
......@@ -94,12 +115,17 @@ public function rules(): array
'groupRate' => 'nullable|numeric|min:0',
'playerRate' => 'nullable|numeric|min:0',
'revenueSharePercent' => 'nullable|numeric|min:0|max:100',
];
]);
}
public function messages(): array
{
return [
'personNameAr.required' => 'الاسم بالعربية مطلوب',
'personNameAr.max' => 'الاسم لا يمكن أن يتجاوز 100 حرف',
'personName.max' => 'الاسم بالإنجليزية لا يمكن أن يتجاوز 100 حرف',
'personPhone.max' => 'رقم الهاتف لا يمكن أن يتجاوز 20 حرف',
'personNationalId.max' => 'الرقم القومي لا يمكن أن يتجاوز 30 حرف',
'employeeId.required' => 'يجب اختيار موظف',
'employeeId.exists' => 'الموظف المختار غير موجود',
'sports.required' => 'يجب اختيار نشاط واحد على الأقل',
......@@ -134,6 +160,18 @@ public function save(TrainerService $service): void
try {
if ($this->editing) {
$service->update($this->trainer, $data, auth()->user());
// Update person data
$person = $this->trainer->employee?->person ?? $this->trainer->person;
if ($person) {
$person->update([
'name_ar' => $this->personNameAr,
'name' => $this->personName ?: null,
'phone' => $this->personPhone ?: null,
'national_id' => $this->personNationalId ?: null,
]);
}
session()->flash('success', __('تم تحديث بيانات المدرب بنجاح'));
} else {
$employee = Employee::findOrFail($this->employeeId);
......
......@@ -5,7 +5,9 @@
use App\Domain\Attendance\Enums\AttendanceStatus;
use App\Domain\Attendance\Models\AttendanceRecord;
use App\Domain\HR\Models\Trainer;
use App\Domain\HR\Services\TrainerService;
use App\Domain\Scheduling\Models\Assignment;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Training\Models\TrainingGroup;
use App\Domain\Training\Models\TrainingSession;
use Livewire\Attributes\Layout;
......@@ -20,6 +22,10 @@ class TrainerShow extends Component
public string $activeTab = 'overview';
public ?int $assignGroupId = null;
// Delete confirmation
public bool $showDeleteModal = false;
public string $deleteConfirmation = '';
public function mount(Trainer $trainer): void
{
$this->authorize('trainers.list');
......@@ -86,6 +92,44 @@ public function removeGroupAssignment(int $assignmentId): void
session()->flash('success', __('تم إلغاء تعيين المجموعة'));
}
public function openDeleteModal(): void
{
$this->authorize('trainers.delete');
$this->deleteConfirmation = '';
$this->showDeleteModal = true;
}
public function closeDeleteModal(): void
{
$this->showDeleteModal = false;
$this->deleteConfirmation = '';
}
public function confirmDelete(TrainerService $service): void
{
$this->authorize('trainers.delete');
$person = $this->trainer->employee?->person ?? $this->trainer->person;
$expectedName = $person?->name_ar ?? '';
if ($this->deleteConfirmation !== $expectedName) {
$this->addError('deleteConfirmation', __('الاسم المدخل لا يتطابق مع اسم المدرب'));
return;
}
try {
$service->delete($this->trainer, auth()->user());
session()->flash('success', __('تم حذف المدرب وأرشفة بياناته بنجاح'));
$this->redirect(route('trainers.list'), navigate: true);
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
$this->showDeleteModal = false;
} catch (\Throwable $e) {
session()->flash('error', __('حدث خطأ أثناء الحذف'));
$this->showDeleteModal = false;
}
}
public function render()
{
$person = $this->trainer->employee?->person ?? $this->trainer->person;
......
......@@ -22,6 +22,7 @@ class POSTerminal extends Component
public array $cart = [];
public ?int $participantId = null;
public string $participantName = '';
public bool $participantIsFree = false;
public string $participantSearch = '';
public string $couponCode = '';
public string $paymentMethod = 'cash';
......@@ -85,6 +86,7 @@ public function selectParticipant(int $id): void
$participant = Participant::with('person')->findOrFail($id);
$this->participantId = $participant->id;
$this->participantName = $participant->person->name_ar;
$this->participantIsFree = (bool) $participant->is_free;
$this->participantSearch = '';
$this->searchResults = [];
}
......@@ -93,6 +95,7 @@ public function clearParticipant(): void
{
$this->participantId = null;
$this->participantName = '';
$this->participantIsFree = false;
$this->searchResults = [];
$this->essentialWarnings = [];
}
......
......@@ -49,6 +49,7 @@ class ParticipantForm extends Component
public ?string $height_cm = null;
public ?string $weight_kg = null;
public string $notes = '';
public bool $is_free = false;
// Guardian (editable in edit mode)
public string $guardian_phone = '';
......@@ -90,6 +91,7 @@ public function mount(?Participant $participant = null): void
$this->height_cm = $participant->height_cm;
$this->weight_kg = $participant->weight_kg;
$this->notes = $participant->notes ?? '';
$this->is_free = (bool) $participant->is_free;
// Load guardian phone for editing
if ($participant->primaryGuardian?->person) {
......@@ -270,6 +272,7 @@ public function save(ParticipantService $service): void
'height_cm' => $this->height_cm ?: null,
'weight_kg' => $this->weight_kg ?: null,
'notes' => $this->notes ?: null,
'is_free' => $this->is_free,
], auth()->user());
session()->flash('success', __('تم تحديث بيانات المشترك بنجاح'));
......@@ -292,6 +295,7 @@ public function save(ParticipantService $service): void
'height_cm' => $this->height_cm ?: null,
'weight_kg' => $this->weight_kg ?: null,
'notes' => $this->notes ?: null,
'is_free' => $this->is_free,
];
if ($this->person_id) {
......
......@@ -157,6 +157,17 @@ public function confirmRefund(RefundService $refundService, EnrollmentService $e
}
}
public function toggleFreeStatus(): void
{
$this->authorize('participants.update');
$this->participant->update(['is_free' => !$this->participant->is_free]);
$this->participant->refresh();
$label = $this->participant->is_free ? __('تم تحويل اللاعب إلى مجاني') : __('تم إلغاء صفة اللاعب المجاني');
session()->flash('success', $label);
}
public function render()
{
$currentStatus = $this->participant->status->value ?? $this->participant->status;
......
......@@ -35,6 +35,7 @@ class CollectPaymentWizard extends Component
public string $searchMode = 'participant'; // 'participant' or 'invoice'
public ?int $selected_participant_id = null;
public ?string $selected_participant_name = null;
public bool $selected_participant_is_free = false;
// Step 2: Invoice selection
public ?int $selected_invoice_id = null;
......@@ -69,6 +70,7 @@ public function mount(?string $participant = null): void
if ($p) {
$this->selected_participant_id = $p->id;
$this->selected_participant_name = $p->person?->name_ar ?? $p->person?->name ?? '';
$this->selected_participant_is_free = (bool) $p->is_free;
$this->generatePendingRenewals();
$this->currentStep = 2;
}
......@@ -128,9 +130,11 @@ public function selectInvoiceDirectly(int $id): void
$participant = Participant::with('person')->find($invoice->billable_id);
$this->selected_participant_id = $participant?->id;
$this->selected_participant_name = $participant?->person?->name_ar ?? $invoice->contact_name ?? '';
$this->selected_participant_is_free = (bool) ($participant?->is_free ?? false);
} else {
$this->selected_participant_id = null;
$this->selected_participant_name = $invoice->contact_name ?? __('عميل عابر');
$this->selected_participant_is_free = false;
}
$this->selected_invoice_id = $invoice->id;
......@@ -142,6 +146,7 @@ public function selectParticipant(int $id, string $name): void
{
$this->selected_participant_id = $id;
$this->selected_participant_name = $name;
$this->selected_participant_is_free = (bool) Participant::where('id', $id)->value('is_free');
$this->generatePendingRenewals();
}
......
......@@ -36,6 +36,9 @@ class EnrollExistingWizard extends Component
public ?int $selected_activity_id = null;
public ?int $selected_program_id = null;
// Free player toggle
public bool $is_free = false;
// Step 3: Payment
public bool $pay_now = false;
public string $payment_method = 'cash';
......@@ -86,6 +89,7 @@ public function selectParticipant(int $id, string $name): void
{
$this->selected_participant_id = $id;
$this->selected_participant_name = $name;
$this->is_free = Participant::where('id', $id)->value('is_free') ?? false;
}
public function nextStep(): void
......@@ -119,13 +123,17 @@ public function confirm(): void
$participant = Participant::findOrFail($this->selected_participant_id);
$program = TrainingProgram::findOrFail($this->selected_program_id);
if ($this->is_free && !$participant->is_free) {
$participant->update(['is_free' => true]);
}
$enrollment = $enrollmentService->enrollInProgram(
$participant,
$program,
auth()->user(),
[
'pay_now' => $this->pay_now,
'payment_method' => $this->pay_now ? $this->payment_method : null,
'pay_now' => $this->is_free ? false : $this->pay_now,
'payment_method' => (!$this->is_free && $this->pay_now) ? $this->payment_method : null,
]
);
......
......@@ -63,6 +63,7 @@ class NewRegistrationWizard extends Component
public string $participant_governorate = '';
public bool $participant_is_foreign = false;
public bool $participant_nid_decoded = false;
public bool $is_free = false;
// Step 2: Guardian info
public string $guardian_name_ar = '';
......@@ -305,8 +306,8 @@ public function nextStep(): void
}
}
// Price guard on step 4 — block if no base price for selected program
if ($this->currentStep === 4 && $this->selected_program_id) {
// Price guard on step 4 — block if no base price for selected program (skip for free players)
if ($this->currentStep === 4 && $this->selected_program_id && !$this->is_free) {
$program = TrainingProgram::find($this->selected_program_id);
if ($program && $this->resolveProgramFee($program) === 0) {
$this->addError('selected_program_id', 'لا يوجد سعر محدد لهذا البرنامج — تواصل مع المسؤول');
......@@ -981,6 +982,7 @@ public function confirm(): void
'primary_activity_id' => $this->selected_activity_id,
'membership_type' => $this->membership_type,
'membership_id' => $this->membership_type === 'member' ? $this->membership_id : null,
'is_free' => $this->is_free,
], $actor);
// 5. Link guardian to participant via pivot
......@@ -1030,9 +1032,9 @@ public function confirm(): void
]);
}
// 8. Create invoice if there's a fee or hot-buy items
// 8. Create invoice if there's a fee or hot-buy items (skip for free players)
$invoice = null;
if ($subtotal > 0 || $finalTotal !== $computedTotal) {
if (!$this->is_free && ($subtotal > 0 || $finalTotal !== $computedTotal)) {
$invoiceItems = [];
// When override is active, compute how much the program fee is adjusted.
......
......@@ -45,6 +45,7 @@ class RetroactiveEnrollmentWizard extends Component
public string $participant_national_id = '';
public string $participant_medical_notes = '';
public bool $participant_nid_decoded = false;
public bool $is_free = false;
// Step 2: Guardian info
public string $guardian_name_ar = '';
......@@ -271,7 +272,8 @@ public function confirm(): void
}
}
// Price validation — don't allow 0-amount invoices unless explicitly overridden
// Price validation — don't allow 0-amount invoices unless explicitly overridden (skip for free players)
if (!$this->is_free) {
$effectivePrice = $this->monthlyPrice;
if ($this->priceOverrideEnabled && $this->priceOverrideInput !== '' && $actor->is_super_admin) {
$effectivePrice = max(0, (int) round((float) $this->priceOverrideInput * 100));
......@@ -280,6 +282,7 @@ public function confirm(): void
session()->flash('error', __('لا يوجد سعر محدد لهذا البرنامج — يرجى إضافة سعر أو استخدام تعديل السعر'));
return;
}
}
try {
DB::transaction(function () use ($actor) {
......@@ -329,6 +332,7 @@ public function confirm(): void
'registration_source' => 'walk_in',
'primary_guardian_id' => $guardian->id,
'primary_activity_id' => $this->selected_activity_id,
'is_free' => $this->is_free,
], $actor);
// 5. Link guardian to participant
......@@ -355,7 +359,17 @@ public function confirm(): void
]
);
// 7. Calculate effective per-month price
// 7. Calculate effective per-month price (skip all invoicing for free players)
$invoicesCreated = 0;
$paymentsRecorded = 0;
if ($this->is_free) {
$enrollment->update([
'next_billing_date' => null,
'payment_status' => 'waived',
]);
} else {
$perMonth = $this->monthlyPrice;
$unpaidCount = $this->unpaidMonthsCount;
......@@ -378,8 +392,6 @@ public function confirm(): void
}
// 8. Generate invoices per month
$invoicesCreated = 0;
$paymentsRecorded = 0;
$totalRetroactiveRevenue = 0;
$unpaidIndex = 0;
......@@ -532,6 +544,8 @@ public function confirm(): void
'last_billed_at' => now()->toDateString(),
]);
} // end else (!is_free)
$this->invoices_created = $invoicesCreated;
$this->payments_recorded = $paymentsRecorded;
$this->participant_uuid = $participant->uuid;
......
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('participants', function (Blueprint $table) {
$table->boolean('is_free')->default(false)->after('status_reason');
});
Schema::table('participants', function (Blueprint $table) {
$table->index(['academy_id', 'is_free']);
});
}
public function down(): void
{
Schema::table('participants', function (Blueprint $table) {
$table->dropIndex(['academy_id', 'is_free']);
$table->dropColumn('is_free');
});
}
};
@props(['small' => false])
<span {{ $attributes->merge(['class' => 'inline-flex items-center gap-1 rounded-full font-bold text-white bg-emerald-500 ' . ($small ? 'px-2 py-0.5 text-xs' : 'px-3 py-1 text-sm')]) }}>
<svg class="{{ $small ? 'w-3 h-3' : 'w-4 h-4' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7"/>
</svg>
لاعب مجاني
</span>
......@@ -136,7 +136,9 @@ class="flex items-center justify-between px-4 py-3.5 min-h-[48px] cursor-pointer
{{-- Name --}}
<span class="text-sm font-medium {{ $isPresent ? 'text-gray-900 dark:text-white' : 'text-red-700 dark:text-red-300 line-through' }}">
{{ $participantName }}
@if ($isUnpaid)
@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>
@endif
</span>
......
......@@ -241,7 +241,9 @@ class="inline-flex items-center px-2 py-1 text-xs font-medium rounded border
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium {{ $isUnpaid ? 'text-red-700' : 'text-gray-900' }}">
{{ $record->subject?->person?->name_ar ?? '-' }}
@if($isUnpaid)
@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>
@endif
</span>
......@@ -310,7 +312,9 @@ class="py-2.5 text-xs font-medium rounded border text-center
<td class="px-4 py-3 whitespace-nowrap">
<span class="text-sm font-medium {{ $isUnpaidRow ? 'text-red-700' : 'text-gray-900' }}">
{{ $record->subject?->person?->name_ar ?? '-' }}
@if($isUnpaidRow)
@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>
@endif
</span>
......
......@@ -78,9 +78,12 @@ class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 f
<tr class="hover:bg-gray-50">
<td class="px-4 py-3">
@if($enrollment->participant)
<div class="flex items-center gap-2">
<a href="{{ route('participants.show', $enrollment->participant) }}" wire:navigate class="text-blue-600 hover:text-blue-800 hover:underline font-medium">
{{ $enrollment->participant->person?->name_ar ?? '—' }}
</a>
@if($enrollment->participant->is_free) <x-ui.free-player-badge :small="true" /> @endif
</div>
@if($enrollment->participant->participant_number)
<p class="text-xs text-gray-500" dir="ltr">
{{ $enrollment->participant->participant_number }}
......@@ -241,11 +244,12 @@ class="mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium py-2">
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
<div class="flex items-start justify-between gap-3 mb-3">
<div class="min-w-0 flex-1">
<h3 class="truncate">
<h3 class="truncate flex items-center gap-2">
@if($enrollment->participant)
<a href="{{ route('participants.show', $enrollment->participant) }}" wire:navigate class="text-blue-600 hover:text-blue-800 hover:underline font-medium">
{{ $enrollment->participant->person?->name_ar ?? '—' }}
</a>
@if($enrollment->participant->is_free) <x-ui.free-player-badge :small="true" /> @endif
@else
<span class="text-gray-400">{{ __('محذوف') }}</span>
@endif
......
......@@ -411,6 +411,7 @@ class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 text-sm
</span>
</div>
<div>
<div class="flex items-center gap-2">
@can('participants.list')
<a href="{{ route('participants.show', $enrollment->participant) }}" wire:navigate
class="text-sm font-medium text-blue-600 hover:text-blue-800 hover:underline">
......@@ -421,6 +422,8 @@ class="text-sm font-medium text-blue-600 hover:text-blue-800 hover:underline">
{{ $enrollment->participant?->person?->name_ar ?? '—' }}
</span>
@endcan
@if($enrollment->participant?->is_free) <x-ui.free-player-badge :small="true" /> @endif
</div>
@if($enrollment->participant?->person?->phone)
<p class="text-xs text-gray-500" dir="ltr">{{ $enrollment->participant->person->phone }}</p>
@endif
......
......@@ -48,6 +48,39 @@ class="w-full px-4 py-3 text-start hover:bg-gray-50 border-b border-gray-100 las
@error('employeeId') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
</div>
{{-- Person Data (only when editing) --}}
@if($editing)
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
<h2 class="text-lg font-semibold text-gray-900 mb-4">{{ __('البيانات الشخصية') }}</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('الاسم بالعربية') }} <span class="text-red-500">*</span></label>
<input type="text" wire:model="personNameAr"
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
@error('personNameAr') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('الاسم بالإنجليزية') }}</label>
<input type="text" wire:model="personName" dir="ltr"
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
@error('personName') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('الرقم القومي') }}</label>
<input type="text" wire:model="personNationalId" dir="ltr"
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500" placeholder="00000000000000">
@error('personNationalId') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('رقم الهاتف') }}</label>
<input type="text" wire:model="personPhone" dir="ltr"
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500" placeholder="01xxxxxxxxx">
@error('personPhone') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
</div>
</div>
</div>
@endif
{{-- Activities (Required) --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
<h2 class="text-lg font-semibold text-gray-900 mb-4">{{ __('الأنشطة') }} <span class="text-red-500">*</span></h2>
......
......@@ -3,6 +3,9 @@
@if(session('success'))
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm">{{ session('success') }}</div>
@endif
@if(session('error'))
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">{{ session('error') }}</div>
@endif
{{-- Header Card --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6 mb-6">
......@@ -54,6 +57,12 @@ class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 text-sm
{{ __('المستحقات') }}
</a>
@endcan
@can('trainers.delete')
<button wire:click="openDeleteModal"
class="px-3 py-2 bg-red-50 text-red-600 rounded-lg hover:bg-red-100 text-sm font-medium transition border border-red-200">
{{ __('حذف') }}
</button>
@endcan
<a href="{{ route('trainers.list') }}" wire:navigate
class="px-4 py-2 text-gray-600 hover:text-gray-800 text-sm">
{{ __('رجوع') }}
......@@ -594,4 +603,56 @@ class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 text-sm
</div>
</div>
</div>
{{-- Delete Confirmation Modal --}}
@if($showDeleteModal)
<div class="fixed inset-0 z-50 flex items-center justify-center p-4" x-data x-trap.noscroll="true">
<div class="fixed inset-0 bg-black/50" wire:click="closeDeleteModal"></div>
<div class="relative bg-white rounded-xl shadow-xl w-full max-w-md p-6">
<div class="text-center mb-4">
<div class="w-14 h-14 rounded-full bg-red-100 mx-auto flex items-center justify-center mb-3">
<svg class="w-7 h-7 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
</div>
<h3 class="text-lg font-bold text-gray-800">{{ __('حذف المدرب نهائياً') }}</h3>
<p class="text-sm text-gray-600 mt-2">{{ __('سيتم حذف المدرب وإلغاء جميع التعيينات والمستحقات المعلقة. هذا الإجراء لا يمكن التراجع عنه.') }}</p>
</div>
<div class="bg-red-50 border border-red-200 rounded-lg p-3 mb-4">
<p class="text-xs text-red-700 font-medium mb-1">{{ __('سيتم تنفيذ الإجراءات التالية:') }}</p>
<ul class="text-xs text-red-600 space-y-1 list-disc list-inside">
<li>{{ __('إلغاء المستحقات والسلف المعلقة') }}</li>
<li>{{ __('إلغاء تعيينات المجموعات') }}</li>
<li>{{ __('حذف سجلات الحضور المستقبلية') }}</li>
<li>{{ __('تعطيل حساب المستخدم') }}</li>
<li>{{ __('أرشفة بيانات الموظف وتحرير الرقم القومي') }}</li>
</ul>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">
{{ __('للتأكيد، اكتب اسم المدرب:') }}
<span class="font-bold text-red-600">{{ $person?->name_ar }}</span>
</label>
<input type="text" wire:model="deleteConfirmation"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-red-500 focus:border-red-500"
placeholder="{{ $person?->name_ar }}">
@error('deleteConfirmation')
<p class="text-xs text-red-500 mt-1">{{ $message }}</p>
@enderror
</div>
<div class="flex gap-3">
<button wire:click="confirmDelete" wire:loading.attr="disabled"
class="flex-1 px-4 py-2.5 bg-red-600 text-white rounded-lg hover:bg-red-700 text-sm font-medium transition disabled:opacity-50">
<span wire:loading.remove wire:target="confirmDelete">{{ __('تأكيد الحذف') }}</span>
<span wire:loading wire:target="confirmDelete">{{ __('جارٍ الحذف...') }}</span>
</button>
<button wire:click="closeDeleteModal"
class="flex-1 px-4 py-2.5 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 text-sm font-medium transition">
{{ __('إلغاء') }}
</button>
</div>
</div>
</div>
@endif
</div>
......@@ -397,6 +397,22 @@ class="w-full px-4 py-2.5 text-sm border border-gray-300 rounded-lg focus:ring-2
@endif
</div>
{{-- Free Player Toggle --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6">
<label class="flex items-center gap-3 cursor-pointer p-3 rounded-lg border border-gray-200 hover:border-emerald-300 transition-colors"
:class="$wire.is_free && 'border-emerald-500 bg-emerald-50'">
<input type="checkbox" wire:model.live="is_free"
class="w-5 h-5 text-emerald-600 border-gray-300 rounded focus:ring-emerald-500">
<div class="flex items-center gap-2">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7"/>
</svg>
<span class="text-sm font-medium text-gray-700">{{ __('لاعب مجاني') }}</span>
<span class="text-xs text-gray-500">{{ __('(معفى من جميع الرسوم والتجديدات)') }}</span>
</div>
</label>
</div>
{{-- Section 6: Notes --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6">
<h2 class="text-base sm:text-lg font-semibold text-gray-800 mb-4 border-b border-gray-100 pb-2">{{ __('ملاحظات') }}</h2>
......
......@@ -77,7 +77,10 @@ class="w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 f
{{ $participant->participant_number }}
</td>
<td class="px-4 py-3">
<div class="flex items-center gap-2">
<a href="{{ route('participants.show', $participant) }}" wire:navigate class="font-medium text-gray-800 hover:text-blue-600">{{ $participant->person?->name_ar }}</a>
@if($participant->is_free) <x-ui.free-player-badge :small="true" /> @endif
</div>
@if($participant->person?->name)
<p class="text-xs text-gray-500" dir="ltr">{{ $participant->person->name }}</p>
@endif
......@@ -166,7 +169,10 @@ class="w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 f
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 hover:border-blue-300 transition">
<div class="flex items-start justify-between gap-3">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<p class="font-semibold text-gray-800 truncate">{{ $participant->person?->name_ar }}</p>
@if($participant->is_free) <x-ui.free-player-badge :small="true" /> @endif
</div>
<p class="text-xs text-gray-500 font-mono mt-0.5" dir="ltr">{{ $participant->participant_number }}</p>
</div>
<span class="px-2 py-0.5 text-xs bg-{{ $color }}-100 text-{{ $color }}-700 rounded-full whitespace-nowrap">
......
......@@ -25,9 +25,12 @@
</span>
</div>
<div>
<div class="flex items-center gap-3">
<h1 class="text-xl font-bold text-gray-800">
{{ $participant->person?->name_ar }}
</h1>
@if($participant->is_free) <x-ui.free-player-badge /> @endif
</div>
@if($participant->person?->name)
<p class="text-sm text-gray-500" dir="ltr">
{{ $participant->person->name }}
......@@ -56,7 +59,19 @@
</div>
</div>
</div>
<div class="flex items-center gap-2 self-start sm:self-auto">
<div class="flex items-center gap-2 self-start sm:self-auto flex-wrap">
@can('participants.update')
<button
x-data
x-on:click="if(confirm('{{ $participant->is_free ? __('هل تريد إلغاء صفة اللاعب المجاني؟ سيتم محاسبته من التجديد القادم.') : __('هل تريد تحويل هذا اللاعب إلى لاعب مجاني؟ لن يتم محاسبته بعد الآن.') }}')) $wire.toggleFreeStatus()"
class="px-3 py-2 rounded-lg text-xs font-medium transition-colors {{ $participant->is_free ? 'bg-amber-100 text-amber-700 hover:bg-amber-200' : 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200' }}">
@if($participant->is_free)
{{ __('إلغاء المجانية') }}
@else
{{ __('تحويل لمجاني') }}
@endif
</button>
@endcan
@can('participants.delete')
<button wire:click="$dispatch('open-delete-participant', { id: {{ $participant->id }} })"
class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 text-sm font-medium transition-colors">
......
......@@ -42,6 +42,7 @@ class="inline-flex items-center gap-2 px-4 py-2.5 min-h-[44px] bg-amber-600 text
<span class="px-3 py-1.5 min-h-[44px] flex items-center bg-blue-50 text-blue-700 rounded-lg text-xs sm:text-sm font-medium truncate">
{{ $participantName }}
</span>
@if($participantIsFree) <x-ui.free-player-badge :small="true" /> @endif
<button wire:click="clearParticipant" class="min-w-[44px] min-h-[44px] flex items-center justify-center text-gray-400 hover:text-red-500 transition-colors">
<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="M6 18L18 6M6 6l12 12"/>
......
......@@ -110,6 +110,7 @@ class="w-full px-4 py-3 min-h-16 border border-gray-300 rounded-lg focus:ring-2
</svg>
</div>
<span class="font-medium text-amber-800">{{ $selected_participant_name }}</span>
@if($selected_participant_is_free) <x-ui.free-player-badge :small="true" /> @endif
</div>
<button wire:click="$set('selected_participant_id', null)" class="text-amber-600 hover:text-amber-800 text-sm">
{{ __('تغيير') }}
......
......@@ -103,7 +103,10 @@ class="w-full px-4 py-3 min-h-16 border border-gray-300 rounded-lg focus:ring-2
<button wire:click="selectParticipant({{ $result->id }}, '{{ $result->person?->name_ar }}')"
class="w-full p-4 min-h-16 border border-gray-200 rounded-xl text-start hover:border-emerald-300 hover:bg-emerald-50 transition-all flex items-center justify-between">
<div>
<div class="flex items-center gap-2">
<p class="font-medium text-gray-800">{{ $result->person?->name_ar }}</p>
@if($result->is_free) <x-ui.free-player-badge :small="true" /> @endif
</div>
<div class="flex items-center gap-3 mt-1 text-xs text-gray-500">
@if($result->participant_number)
<span dir="ltr">{{ $result->participant_number }}</span>
......@@ -138,6 +141,24 @@ class="w-full p-4 min-h-16 border border-gray-200 rounded-xl text-start hover:bo
@error('selected_participant_id') <p class="mt-2 text-sm text-red-600">{{ $message }}</p> @enderror
{{-- Free Player Toggle (shown after selecting a participant) --}}
@if($selected_participant_id)
<div class="mt-4">
<label class="flex items-center gap-3 cursor-pointer p-3 rounded-lg border border-gray-200 hover:border-emerald-300 transition-colors"
:class="$wire.is_free && 'border-emerald-500 bg-emerald-50'">
<input type="checkbox" wire:model.live="is_free"
class="w-5 h-5 text-emerald-600 border-gray-300 rounded focus:ring-emerald-500">
<div class="flex items-center gap-2">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7"/>
</svg>
<span class="text-sm font-medium text-gray-700">{{ __('لاعب مجاني') }}</span>
<span class="text-xs text-gray-500">{{ __('(معفى من الرسوم)') }}</span>
</div>
</label>
</div>
@endif
<div class="flex justify-end mt-8">
<button wire:click="nextStep" wire:loading.attr="disabled"
@if(!$selected_participant_id) disabled @endif
......
......@@ -315,6 +315,22 @@ class="px-4 py-2 bg-gray-200 text-gray-700 text-sm font-medium rounded-lg hover:
@endif
@endif
{{-- Free Player Toggle --}}
<div class="sm:col-span-2">
<label class="flex items-center gap-3 cursor-pointer p-3 rounded-lg border border-gray-200 hover:border-emerald-300 transition-colors"
:class="$wire.is_free && 'border-emerald-500 bg-emerald-50'">
<input type="checkbox" wire:model.live="is_free"
class="w-5 h-5 text-emerald-600 border-gray-300 rounded focus:ring-emerald-500">
<div class="flex items-center gap-2">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7"/>
</svg>
<span class="text-sm font-medium text-gray-700">{{ __('لاعب مجاني') }}</span>
<span class="text-xs text-gray-500">{{ __('(معفى من الرسوم)') }}</span>
</div>
</label>
</div>
{{-- Medical notes --}}
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('ملاحظات طبية (اختياري)') }}</label>
......
......@@ -135,6 +135,22 @@ class="w-full px-4 py-3 min-h-14 border border-gray-300 rounded-lg focus:ring-2
placeholder="01012345678">
</div>
{{-- Free Player Toggle --}}
<div class="sm:col-span-2">
<label class="flex items-center gap-3 cursor-pointer p-3 rounded-lg border border-gray-200 hover:border-emerald-300 transition-colors"
:class="$wire.is_free && 'border-emerald-500 bg-emerald-50'">
<input type="checkbox" wire:model.live="is_free"
class="w-5 h-5 text-emerald-600 border-gray-300 rounded focus:ring-emerald-500">
<div class="flex items-center gap-2">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7"/>
</svg>
<span class="text-sm font-medium text-gray-700">{{ __('لاعب مجاني') }}</span>
<span class="text-xs text-gray-500">{{ __('(معفى من الرسوم)') }}</span>
</div>
</label>
</div>
{{-- Medical Notes --}}
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('ملاحظات طبية') }}</label>
......
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