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 ...@@ -32,6 +32,9 @@ public function handle(InvoiceService $invoiceService, PricingService $pricingSe
]) ])
->whereNotNull('billing_cycle'); ->whereNotNull('billing_cycle');
}) })
->whereHas('participant', function ($q) {
$q->where('is_free', false);
})
->with(['program', 'participant.person', 'group']) ->with(['program', 'participant.person', 'group'])
->get(); ->get();
......
...@@ -2,13 +2,20 @@ ...@@ -2,13 +2,20 @@
namespace App\Domain\HR\Services; namespace App\Domain\HR\Services;
use App\Domain\Attendance\Models\AttendanceRecord;
use App\Domain\HR\Models\Employee; use App\Domain\HR\Models\Employee;
use App\Domain\HR\Models\Trainer; use App\Domain\HR\Models\Trainer;
use App\Domain\HR\Models\TrainerAvailability; use App\Domain\HR\Models\TrainerAvailability;
use App\Domain\HR\Models\TrainerQualification; 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\Models\User;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Exceptions\InvalidStatusTransitionException; use App\Domain\Shared\Exceptions\InvalidStatusTransitionException;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class TrainerService class TrainerService
{ {
...@@ -102,6 +109,89 @@ public function setAvailability(Trainer $trainer, array $slots): void ...@@ -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 private function generateTrainerNumber(int $academyId): string
{ {
$lastNumber = Trainer::withoutGlobalScopes() $lastNumber = Trainer::withoutGlobalScopes()
......
...@@ -49,6 +49,7 @@ class Participant extends Model ...@@ -49,6 +49,7 @@ class Participant extends Model
'status', 'status',
'status_changed_at', 'status_changed_at',
'status_reason', 'status_reason',
'is_free',
'membership_type', 'membership_type',
'membership_id', 'membership_id',
'membership_expires_at', 'membership_expires_at',
...@@ -74,6 +75,7 @@ class Participant extends Model ...@@ -74,6 +75,7 @@ class Participant extends Model
'outstanding_balance' => 'integer', 'outstanding_balance' => 'integer',
'height_cm' => 'decimal:1', 'height_cm' => 'decimal:1',
'weight_kg' => 'decimal:1', 'weight_kg' => 'decimal:1',
'is_free' => 'boolean',
'metadata' => 'array', 'metadata' => 'array',
'jersey_number' => 'integer', 'jersey_number' => 'integer',
]; ];
......
...@@ -75,6 +75,7 @@ public function register(array $data, User $actor): Participant ...@@ -75,6 +75,7 @@ public function register(array $data, User $actor): Participant
'status_changed_at' => now(), 'status_changed_at' => now(),
'membership_type' => $data['membership_type'] ?? null, 'membership_type' => $data['membership_type'] ?? null,
'membership_id' => $data['membership_id'] ?? null, 'membership_id' => $data['membership_id'] ?? null,
'is_free' => $data['is_free'] ?? false,
'notes' => $data['notes'] ?? null, 'notes' => $data['notes'] ?? null,
'metadata' => $data['metadata'] ?? [], 'metadata' => $data['metadata'] ?? [],
'created_by' => $actor->id, 'created_by' => $actor->id,
......
...@@ -84,11 +84,11 @@ public function enroll(Participant $participant, TrainingGroup $group, User $act ...@@ -84,11 +84,11 @@ public function enroll(Participant $participant, TrainingGroup $group, User $act
'enrollment_date' => now()->toDateString(), 'enrollment_date' => now()->toDateString(),
'start_date' => $options['start_date'] ?? $group->start_date ?? now()->toDateString(), 'start_date' => $options['start_date'] ?? $group->start_date ?? now()->toDateString(),
'end_date' => $options['end_date'] ?? $group->end_date, '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', 'status' => 'active',
'enrolled_by' => $actor->id, 'enrolled_by' => $actor->id,
'invoice_id' => $options['invoice_id'] ?? null, '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, 'sessions_total' => $group->program?->total_sessions,
]); ]);
...@@ -100,8 +100,8 @@ public function enroll(Participant $participant, TrainingGroup $group, User $act ...@@ -100,8 +100,8 @@ public function enroll(Participant $participant, TrainingGroup $group, User $act
$participant->update(['status' => 'active']); $participant->update(['status' => 'active']);
} }
// Auto-create invoice if program has a price (skip if invoice already provided or explicitly skipped) // 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']) && (bool) $this->settings->get('auto_invoice_on_enrollment', false)) { 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); $this->createEnrollmentInvoice($enrollment, $participant, $group, $actor);
} }
......
...@@ -23,6 +23,12 @@ class TrainerForm extends Component ...@@ -23,6 +23,12 @@ class TrainerForm extends Component
public ?int $employeeId = null; public ?int $employeeId = null;
public string $selectedEmployeeName = ''; public string $selectedEmployeeName = '';
// Person fields (editable when editing)
public string $personNameAr = '';
public string $personName = '';
public string $personPhone = '';
public string $personNationalId = '';
// Form fields // Form fields
public string $bio = ''; public string $bio = '';
public string $bioAr = ''; public string $bioAr = '';
...@@ -53,6 +59,14 @@ public function mount(?Trainer $trainer = null): void ...@@ -53,6 +59,14 @@ public function mount(?Trainer $trainer = null): void
$this->employeeId = $trainer->employee_id; $this->employeeId = $trainer->employee_id;
$this->selectedEmployeeName = $trainer->employee->person->name_ar ?? ''; $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->bio = $trainer->bio ?? '';
$this->bioAr = $trainer->bio_ar ?? ''; $this->bioAr = $trainer->bio_ar ?? '';
$this->specializations = $trainer->specializations ?? []; $this->specializations = $trainer->specializations ?? [];
...@@ -80,7 +94,14 @@ public function mount(?Trainer $trainer = null): void ...@@ -80,7 +94,14 @@ public function mount(?Trainer $trainer = null): void
public function rules(): array 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', 'employeeId' => 'required|exists:employees,id',
'sports' => 'required|array|min:1', 'sports' => 'required|array|min:1',
'sports.*' => 'integer|exists:activities,id', 'sports.*' => 'integer|exists:activities,id',
...@@ -94,12 +115,17 @@ public function rules(): array ...@@ -94,12 +115,17 @@ public function rules(): array
'groupRate' => 'nullable|numeric|min:0', 'groupRate' => 'nullable|numeric|min:0',
'playerRate' => 'nullable|numeric|min:0', 'playerRate' => 'nullable|numeric|min:0',
'revenueSharePercent' => 'nullable|numeric|min:0|max:100', 'revenueSharePercent' => 'nullable|numeric|min:0|max:100',
]; ]);
} }
public function messages(): array public function messages(): array
{ {
return [ return [
'personNameAr.required' => 'الاسم بالعربية مطلوب',
'personNameAr.max' => 'الاسم لا يمكن أن يتجاوز 100 حرف',
'personName.max' => 'الاسم بالإنجليزية لا يمكن أن يتجاوز 100 حرف',
'personPhone.max' => 'رقم الهاتف لا يمكن أن يتجاوز 20 حرف',
'personNationalId.max' => 'الرقم القومي لا يمكن أن يتجاوز 30 حرف',
'employeeId.required' => 'يجب اختيار موظف', 'employeeId.required' => 'يجب اختيار موظف',
'employeeId.exists' => 'الموظف المختار غير موجود', 'employeeId.exists' => 'الموظف المختار غير موجود',
'sports.required' => 'يجب اختيار نشاط واحد على الأقل', 'sports.required' => 'يجب اختيار نشاط واحد على الأقل',
...@@ -134,6 +160,18 @@ public function save(TrainerService $service): void ...@@ -134,6 +160,18 @@ public function save(TrainerService $service): void
try { try {
if ($this->editing) { if ($this->editing) {
$service->update($this->trainer, $data, auth()->user()); $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', __('تم تحديث بيانات المدرب بنجاح')); session()->flash('success', __('تم تحديث بيانات المدرب بنجاح'));
} else { } else {
$employee = Employee::findOrFail($this->employeeId); $employee = Employee::findOrFail($this->employeeId);
......
...@@ -5,7 +5,9 @@ ...@@ -5,7 +5,9 @@
use App\Domain\Attendance\Enums\AttendanceStatus; use App\Domain\Attendance\Enums\AttendanceStatus;
use App\Domain\Attendance\Models\AttendanceRecord; use App\Domain\Attendance\Models\AttendanceRecord;
use App\Domain\HR\Models\Trainer; use App\Domain\HR\Models\Trainer;
use App\Domain\HR\Services\TrainerService;
use App\Domain\Scheduling\Models\Assignment; use App\Domain\Scheduling\Models\Assignment;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Training\Models\TrainingGroup; use App\Domain\Training\Models\TrainingGroup;
use App\Domain\Training\Models\TrainingSession; use App\Domain\Training\Models\TrainingSession;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
...@@ -20,6 +22,10 @@ class TrainerShow extends Component ...@@ -20,6 +22,10 @@ class TrainerShow extends Component
public string $activeTab = 'overview'; public string $activeTab = 'overview';
public ?int $assignGroupId = null; public ?int $assignGroupId = null;
// Delete confirmation
public bool $showDeleteModal = false;
public string $deleteConfirmation = '';
public function mount(Trainer $trainer): void public function mount(Trainer $trainer): void
{ {
$this->authorize('trainers.list'); $this->authorize('trainers.list');
...@@ -86,6 +92,44 @@ public function removeGroupAssignment(int $assignmentId): void ...@@ -86,6 +92,44 @@ public function removeGroupAssignment(int $assignmentId): void
session()->flash('success', __('تم إلغاء تعيين المجموعة')); 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() public function render()
{ {
$person = $this->trainer->employee?->person ?? $this->trainer->person; $person = $this->trainer->employee?->person ?? $this->trainer->person;
......
...@@ -22,6 +22,7 @@ class POSTerminal extends Component ...@@ -22,6 +22,7 @@ class POSTerminal extends Component
public array $cart = []; public array $cart = [];
public ?int $participantId = null; public ?int $participantId = null;
public string $participantName = ''; public string $participantName = '';
public bool $participantIsFree = false;
public string $participantSearch = ''; public string $participantSearch = '';
public string $couponCode = ''; public string $couponCode = '';
public string $paymentMethod = 'cash'; public string $paymentMethod = 'cash';
...@@ -85,6 +86,7 @@ public function selectParticipant(int $id): void ...@@ -85,6 +86,7 @@ public function selectParticipant(int $id): void
$participant = Participant::with('person')->findOrFail($id); $participant = Participant::with('person')->findOrFail($id);
$this->participantId = $participant->id; $this->participantId = $participant->id;
$this->participantName = $participant->person->name_ar; $this->participantName = $participant->person->name_ar;
$this->participantIsFree = (bool) $participant->is_free;
$this->participantSearch = ''; $this->participantSearch = '';
$this->searchResults = []; $this->searchResults = [];
} }
...@@ -93,6 +95,7 @@ public function clearParticipant(): void ...@@ -93,6 +95,7 @@ public function clearParticipant(): void
{ {
$this->participantId = null; $this->participantId = null;
$this->participantName = ''; $this->participantName = '';
$this->participantIsFree = false;
$this->searchResults = []; $this->searchResults = [];
$this->essentialWarnings = []; $this->essentialWarnings = [];
} }
......
...@@ -49,6 +49,7 @@ class ParticipantForm extends Component ...@@ -49,6 +49,7 @@ class ParticipantForm extends Component
public ?string $height_cm = null; public ?string $height_cm = null;
public ?string $weight_kg = null; public ?string $weight_kg = null;
public string $notes = ''; public string $notes = '';
public bool $is_free = false;
// Guardian (editable in edit mode) // Guardian (editable in edit mode)
public string $guardian_phone = ''; public string $guardian_phone = '';
...@@ -90,6 +91,7 @@ public function mount(?Participant $participant = null): void ...@@ -90,6 +91,7 @@ public function mount(?Participant $participant = null): void
$this->height_cm = $participant->height_cm; $this->height_cm = $participant->height_cm;
$this->weight_kg = $participant->weight_kg; $this->weight_kg = $participant->weight_kg;
$this->notes = $participant->notes ?? ''; $this->notes = $participant->notes ?? '';
$this->is_free = (bool) $participant->is_free;
// Load guardian phone for editing // Load guardian phone for editing
if ($participant->primaryGuardian?->person) { if ($participant->primaryGuardian?->person) {
...@@ -270,6 +272,7 @@ public function save(ParticipantService $service): void ...@@ -270,6 +272,7 @@ public function save(ParticipantService $service): void
'height_cm' => $this->height_cm ?: null, 'height_cm' => $this->height_cm ?: null,
'weight_kg' => $this->weight_kg ?: null, 'weight_kg' => $this->weight_kg ?: null,
'notes' => $this->notes ?: null, 'notes' => $this->notes ?: null,
'is_free' => $this->is_free,
], auth()->user()); ], auth()->user());
session()->flash('success', __('تم تحديث بيانات المشترك بنجاح')); session()->flash('success', __('تم تحديث بيانات المشترك بنجاح'));
...@@ -292,6 +295,7 @@ public function save(ParticipantService $service): void ...@@ -292,6 +295,7 @@ public function save(ParticipantService $service): void
'height_cm' => $this->height_cm ?: null, 'height_cm' => $this->height_cm ?: null,
'weight_kg' => $this->weight_kg ?: null, 'weight_kg' => $this->weight_kg ?: null,
'notes' => $this->notes ?: null, 'notes' => $this->notes ?: null,
'is_free' => $this->is_free,
]; ];
if ($this->person_id) { if ($this->person_id) {
......
...@@ -157,6 +157,17 @@ public function confirmRefund(RefundService $refundService, EnrollmentService $e ...@@ -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() public function render()
{ {
$currentStatus = $this->participant->status->value ?? $this->participant->status; $currentStatus = $this->participant->status->value ?? $this->participant->status;
......
...@@ -35,6 +35,7 @@ class CollectPaymentWizard extends Component ...@@ -35,6 +35,7 @@ class CollectPaymentWizard extends Component
public string $searchMode = 'participant'; // 'participant' or 'invoice' public string $searchMode = 'participant'; // 'participant' or 'invoice'
public ?int $selected_participant_id = null; public ?int $selected_participant_id = null;
public ?string $selected_participant_name = null; public ?string $selected_participant_name = null;
public bool $selected_participant_is_free = false;
// Step 2: Invoice selection // Step 2: Invoice selection
public ?int $selected_invoice_id = null; public ?int $selected_invoice_id = null;
...@@ -69,6 +70,7 @@ public function mount(?string $participant = null): void ...@@ -69,6 +70,7 @@ public function mount(?string $participant = null): void
if ($p) { if ($p) {
$this->selected_participant_id = $p->id; $this->selected_participant_id = $p->id;
$this->selected_participant_name = $p->person?->name_ar ?? $p->person?->name ?? ''; $this->selected_participant_name = $p->person?->name_ar ?? $p->person?->name ?? '';
$this->selected_participant_is_free = (bool) $p->is_free;
$this->generatePendingRenewals(); $this->generatePendingRenewals();
$this->currentStep = 2; $this->currentStep = 2;
} }
...@@ -128,9 +130,11 @@ public function selectInvoiceDirectly(int $id): void ...@@ -128,9 +130,11 @@ public function selectInvoiceDirectly(int $id): void
$participant = Participant::with('person')->find($invoice->billable_id); $participant = Participant::with('person')->find($invoice->billable_id);
$this->selected_participant_id = $participant?->id; $this->selected_participant_id = $participant?->id;
$this->selected_participant_name = $participant?->person?->name_ar ?? $invoice->contact_name ?? ''; $this->selected_participant_name = $participant?->person?->name_ar ?? $invoice->contact_name ?? '';
$this->selected_participant_is_free = (bool) ($participant?->is_free ?? false);
} else { } else {
$this->selected_participant_id = null; $this->selected_participant_id = null;
$this->selected_participant_name = $invoice->contact_name ?? __('عميل عابر'); $this->selected_participant_name = $invoice->contact_name ?? __('عميل عابر');
$this->selected_participant_is_free = false;
} }
$this->selected_invoice_id = $invoice->id; $this->selected_invoice_id = $invoice->id;
...@@ -142,6 +146,7 @@ public function selectParticipant(int $id, string $name): void ...@@ -142,6 +146,7 @@ public function selectParticipant(int $id, string $name): void
{ {
$this->selected_participant_id = $id; $this->selected_participant_id = $id;
$this->selected_participant_name = $name; $this->selected_participant_name = $name;
$this->selected_participant_is_free = (bool) Participant::where('id', $id)->value('is_free');
$this->generatePendingRenewals(); $this->generatePendingRenewals();
} }
......
...@@ -36,6 +36,9 @@ class EnrollExistingWizard extends Component ...@@ -36,6 +36,9 @@ class EnrollExistingWizard extends Component
public ?int $selected_activity_id = null; public ?int $selected_activity_id = null;
public ?int $selected_program_id = null; public ?int $selected_program_id = null;
// Free player toggle
public bool $is_free = false;
// Step 3: Payment // Step 3: Payment
public bool $pay_now = false; public bool $pay_now = false;
public string $payment_method = 'cash'; public string $payment_method = 'cash';
...@@ -86,6 +89,7 @@ public function selectParticipant(int $id, string $name): void ...@@ -86,6 +89,7 @@ public function selectParticipant(int $id, string $name): void
{ {
$this->selected_participant_id = $id; $this->selected_participant_id = $id;
$this->selected_participant_name = $name; $this->selected_participant_name = $name;
$this->is_free = Participant::where('id', $id)->value('is_free') ?? false;
} }
public function nextStep(): void public function nextStep(): void
...@@ -119,13 +123,17 @@ public function confirm(): void ...@@ -119,13 +123,17 @@ public function confirm(): void
$participant = Participant::findOrFail($this->selected_participant_id); $participant = Participant::findOrFail($this->selected_participant_id);
$program = TrainingProgram::findOrFail($this->selected_program_id); $program = TrainingProgram::findOrFail($this->selected_program_id);
if ($this->is_free && !$participant->is_free) {
$participant->update(['is_free' => true]);
}
$enrollment = $enrollmentService->enrollInProgram( $enrollment = $enrollmentService->enrollInProgram(
$participant, $participant,
$program, $program,
auth()->user(), auth()->user(),
[ [
'pay_now' => $this->pay_now, 'pay_now' => $this->is_free ? false : $this->pay_now,
'payment_method' => $this->pay_now ? $this->payment_method : null, 'payment_method' => (!$this->is_free && $this->pay_now) ? $this->payment_method : null,
] ]
); );
......
...@@ -63,6 +63,7 @@ class NewRegistrationWizard extends Component ...@@ -63,6 +63,7 @@ class NewRegistrationWizard extends Component
public string $participant_governorate = ''; public string $participant_governorate = '';
public bool $participant_is_foreign = false; public bool $participant_is_foreign = false;
public bool $participant_nid_decoded = false; public bool $participant_nid_decoded = false;
public bool $is_free = false;
// Step 2: Guardian info // Step 2: Guardian info
public string $guardian_name_ar = ''; public string $guardian_name_ar = '';
...@@ -305,8 +306,8 @@ public function nextStep(): void ...@@ -305,8 +306,8 @@ public function nextStep(): void
} }
} }
// Price guard on step 4 — block if no base price for selected program // 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) { if ($this->currentStep === 4 && $this->selected_program_id && !$this->is_free) {
$program = TrainingProgram::find($this->selected_program_id); $program = TrainingProgram::find($this->selected_program_id);
if ($program && $this->resolveProgramFee($program) === 0) { if ($program && $this->resolveProgramFee($program) === 0) {
$this->addError('selected_program_id', 'لا يوجد سعر محدد لهذا البرنامج — تواصل مع المسؤول'); $this->addError('selected_program_id', 'لا يوجد سعر محدد لهذا البرنامج — تواصل مع المسؤول');
...@@ -981,6 +982,7 @@ public function confirm(): void ...@@ -981,6 +982,7 @@ public function confirm(): void
'primary_activity_id' => $this->selected_activity_id, 'primary_activity_id' => $this->selected_activity_id,
'membership_type' => $this->membership_type, 'membership_type' => $this->membership_type,
'membership_id' => $this->membership_type === 'member' ? $this->membership_id : null, 'membership_id' => $this->membership_type === 'member' ? $this->membership_id : null,
'is_free' => $this->is_free,
], $actor); ], $actor);
// 5. Link guardian to participant via pivot // 5. Link guardian to participant via pivot
...@@ -1030,9 +1032,9 @@ public function confirm(): void ...@@ -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; $invoice = null;
if ($subtotal > 0 || $finalTotal !== $computedTotal) { if (!$this->is_free && ($subtotal > 0 || $finalTotal !== $computedTotal)) {
$invoiceItems = []; $invoiceItems = [];
// When override is active, compute how much the program fee is adjusted. // When override is active, compute how much the program fee is adjusted.
......
...@@ -45,6 +45,7 @@ class RetroactiveEnrollmentWizard extends Component ...@@ -45,6 +45,7 @@ class RetroactiveEnrollmentWizard extends Component
public string $participant_national_id = ''; public string $participant_national_id = '';
public string $participant_medical_notes = ''; public string $participant_medical_notes = '';
public bool $participant_nid_decoded = false; public bool $participant_nid_decoded = false;
public bool $is_free = false;
// Step 2: Guardian info // Step 2: Guardian info
public string $guardian_name_ar = ''; public string $guardian_name_ar = '';
...@@ -271,7 +272,8 @@ public function confirm(): void ...@@ -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; $effectivePrice = $this->monthlyPrice;
if ($this->priceOverrideEnabled && $this->priceOverrideInput !== '' && $actor->is_super_admin) { if ($this->priceOverrideEnabled && $this->priceOverrideInput !== '' && $actor->is_super_admin) {
$effectivePrice = max(0, (int) round((float) $this->priceOverrideInput * 100)); $effectivePrice = max(0, (int) round((float) $this->priceOverrideInput * 100));
...@@ -280,6 +282,7 @@ public function confirm(): void ...@@ -280,6 +282,7 @@ public function confirm(): void
session()->flash('error', __('لا يوجد سعر محدد لهذا البرنامج — يرجى إضافة سعر أو استخدام تعديل السعر')); session()->flash('error', __('لا يوجد سعر محدد لهذا البرنامج — يرجى إضافة سعر أو استخدام تعديل السعر'));
return; return;
} }
}
try { try {
DB::transaction(function () use ($actor) { DB::transaction(function () use ($actor) {
...@@ -329,6 +332,7 @@ public function confirm(): void ...@@ -329,6 +332,7 @@ public function confirm(): void
'registration_source' => 'walk_in', 'registration_source' => 'walk_in',
'primary_guardian_id' => $guardian->id, 'primary_guardian_id' => $guardian->id,
'primary_activity_id' => $this->selected_activity_id, 'primary_activity_id' => $this->selected_activity_id,
'is_free' => $this->is_free,
], $actor); ], $actor);
// 5. Link guardian to participant // 5. Link guardian to participant
...@@ -355,7 +359,17 @@ public function confirm(): void ...@@ -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; $perMonth = $this->monthlyPrice;
$unpaidCount = $this->unpaidMonthsCount; $unpaidCount = $this->unpaidMonthsCount;
...@@ -378,8 +392,6 @@ public function confirm(): void ...@@ -378,8 +392,6 @@ public function confirm(): void
} }
// 8. Generate invoices per month // 8. Generate invoices per month
$invoicesCreated = 0;
$paymentsRecorded = 0;
$totalRetroactiveRevenue = 0; $totalRetroactiveRevenue = 0;
$unpaidIndex = 0; $unpaidIndex = 0;
...@@ -532,6 +544,8 @@ public function confirm(): void ...@@ -532,6 +544,8 @@ public function confirm(): void
'last_billed_at' => now()->toDateString(), 'last_billed_at' => now()->toDateString(),
]); ]);
} // end else (!is_free)
$this->invoices_created = $invoicesCreated; $this->invoices_created = $invoicesCreated;
$this->payments_recorded = $paymentsRecorded; $this->payments_recorded = $paymentsRecorded;
$this->participant_uuid = $participant->uuid; $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 ...@@ -136,7 +136,9 @@ class="flex items-center justify-between px-4 py-3.5 min-h-[48px] cursor-pointer
{{-- Name --}} {{-- Name --}}
<span class="text-sm font-medium {{ $isPresent ? 'text-gray-900 dark:text-white' : 'text-red-700 dark:text-red-300 line-through' }}"> <span class="text-sm font-medium {{ $isPresent ? 'text-gray-900 dark:text-white' : 'text-red-700 dark:text-red-300 line-through' }}">
{{ $participantName }} {{ $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> <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 @endif
</span> </span>
......
...@@ -241,7 +241,9 @@ class="inline-flex items-center px-2 py-1 text-xs font-medium rounded border ...@@ -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"> <div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium {{ $isUnpaid ? 'text-red-700' : 'text-gray-900' }}"> <span class="text-sm font-medium {{ $isUnpaid ? 'text-red-700' : 'text-gray-900' }}">
{{ $record->subject?->person?->name_ar ?? '-' }} {{ $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> <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 @endif
</span> </span>
...@@ -310,7 +312,9 @@ class="py-2.5 text-xs font-medium rounded border text-center ...@@ -310,7 +312,9 @@ class="py-2.5 text-xs font-medium rounded border text-center
<td class="px-4 py-3 whitespace-nowrap"> <td class="px-4 py-3 whitespace-nowrap">
<span class="text-sm font-medium {{ $isUnpaidRow ? 'text-red-700' : 'text-gray-900' }}"> <span class="text-sm font-medium {{ $isUnpaidRow ? 'text-red-700' : 'text-gray-900' }}">
{{ $record->subject?->person?->name_ar ?? '-' }} {{ $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> <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 @endif
</span> </span>
......
...@@ -78,9 +78,12 @@ class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 f ...@@ -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"> <tr class="hover:bg-gray-50">
<td class="px-4 py-3"> <td class="px-4 py-3">
@if($enrollment->participant) @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"> <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 ?? '—' }} {{ $enrollment->participant->person?->name_ar ?? '—' }}
</a> </a>
@if($enrollment->participant->is_free) <x-ui.free-player-badge :small="true" /> @endif
</div>
@if($enrollment->participant->participant_number) @if($enrollment->participant->participant_number)
<p class="text-xs text-gray-500" dir="ltr"> <p class="text-xs text-gray-500" dir="ltr">
{{ $enrollment->participant->participant_number }} {{ $enrollment->participant->participant_number }}
...@@ -241,11 +244,12 @@ class="mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium py-2"> ...@@ -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="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="flex items-start justify-between gap-3 mb-3">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<h3 class="truncate"> <h3 class="truncate flex items-center gap-2">
@if($enrollment->participant) @if($enrollment->participant)
<a href="{{ route('participants.show', $enrollment->participant) }}" wire:navigate class="text-blue-600 hover:text-blue-800 hover:underline font-medium"> <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 ?? '—' }} {{ $enrollment->participant->person?->name_ar ?? '—' }}
</a> </a>
@if($enrollment->participant->is_free) <x-ui.free-player-badge :small="true" /> @endif
@else @else
<span class="text-gray-400">{{ __('محذوف') }}</span> <span class="text-gray-400">{{ __('محذوف') }}</span>
@endif @endif
......
...@@ -411,6 +411,7 @@ class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 text-sm ...@@ -411,6 +411,7 @@ class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 text-sm
</span> </span>
</div> </div>
<div> <div>
<div class="flex items-center gap-2">
@can('participants.list') @can('participants.list')
<a href="{{ route('participants.show', $enrollment->participant) }}" wire:navigate <a href="{{ route('participants.show', $enrollment->participant) }}" wire:navigate
class="text-sm font-medium text-blue-600 hover:text-blue-800 hover:underline"> 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"> ...@@ -421,6 +422,8 @@ class="text-sm font-medium text-blue-600 hover:text-blue-800 hover:underline">
{{ $enrollment->participant?->person?->name_ar ?? '—' }} {{ $enrollment->participant?->person?->name_ar ?? '—' }}
</span> </span>
@endcan @endcan
@if($enrollment->participant?->is_free) <x-ui.free-player-badge :small="true" /> @endif
</div>
@if($enrollment->participant?->person?->phone) @if($enrollment->participant?->person?->phone)
<p class="text-xs text-gray-500" dir="ltr">{{ $enrollment->participant->person->phone }}</p> <p class="text-xs text-gray-500" dir="ltr">{{ $enrollment->participant->person->phone }}</p>
@endif @endif
......
...@@ -48,6 +48,39 @@ class="w-full px-4 py-3 text-start hover:bg-gray-50 border-b border-gray-100 las ...@@ -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 @error('employeeId') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
</div> </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) --}} {{-- Activities (Required) --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5"> <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> <h2 class="text-lg font-semibold text-gray-900 mb-4">{{ __('الأنشطة') }} <span class="text-red-500">*</span></h2>
......
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
@if(session('success')) @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> <div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm">{{ session('success') }}</div>
@endif @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 --}} {{-- Header Card --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6 mb-6"> <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 ...@@ -54,6 +57,12 @@ class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 text-sm
{{ __('المستحقات') }} {{ __('المستحقات') }}
</a> </a>
@endcan @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 <a href="{{ route('trainers.list') }}" wire:navigate
class="px-4 py-2 text-gray-600 hover:text-gray-800 text-sm"> 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 ...@@ -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> </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> </div>
...@@ -397,6 +397,22 @@ class="w-full px-4 py-2.5 text-sm border border-gray-300 rounded-lg focus:ring-2 ...@@ -397,6 +397,22 @@ class="w-full px-4 py-2.5 text-sm border border-gray-300 rounded-lg focus:ring-2
@endif @endif
</div> </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 --}} {{-- Section 6: Notes --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6"> <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> <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 ...@@ -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 }} {{ $participant->participant_number }}
</td> </td>
<td class="px-4 py-3"> <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> <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) @if($participant->person?->name)
<p class="text-xs text-gray-500" dir="ltr">{{ $participant->person->name }}</p> <p class="text-xs text-gray-500" dir="ltr">{{ $participant->person->name }}</p>
@endif @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 ...@@ -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="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 items-start justify-between gap-3">
<div class="flex-1 min-w-0"> <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> <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> <p class="text-xs text-gray-500 font-mono mt-0.5" dir="ltr">{{ $participant->participant_number }}</p>
</div> </div>
<span class="px-2 py-0.5 text-xs bg-{{ $color }}-100 text-{{ $color }}-700 rounded-full whitespace-nowrap"> <span class="px-2 py-0.5 text-xs bg-{{ $color }}-100 text-{{ $color }}-700 rounded-full whitespace-nowrap">
......
...@@ -25,9 +25,12 @@ ...@@ -25,9 +25,12 @@
</span> </span>
</div> </div>
<div> <div>
<div class="flex items-center gap-3">
<h1 class="text-xl font-bold text-gray-800"> <h1 class="text-xl font-bold text-gray-800">
{{ $participant->person?->name_ar }} {{ $participant->person?->name_ar }}
</h1> </h1>
@if($participant->is_free) <x-ui.free-player-badge /> @endif
</div>
@if($participant->person?->name) @if($participant->person?->name)
<p class="text-sm text-gray-500" dir="ltr"> <p class="text-sm text-gray-500" dir="ltr">
{{ $participant->person->name }} {{ $participant->person->name }}
...@@ -56,7 +59,19 @@ ...@@ -56,7 +59,19 @@
</div> </div>
</div> </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') @can('participants.delete')
<button wire:click="$dispatch('open-delete-participant', { id: {{ $participant->id }} })" <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"> 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 ...@@ -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"> <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 }} {{ $participantName }}
</span> </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"> <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"> <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"/> <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 ...@@ -110,6 +110,7 @@ class="w-full px-4 py-3 min-h-16 border border-gray-300 rounded-lg focus:ring-2
</svg> </svg>
</div> </div>
<span class="font-medium text-amber-800">{{ $selected_participant_name }}</span> <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> </div>
<button wire:click="$set('selected_participant_id', null)" class="text-amber-600 hover:text-amber-800 text-sm"> <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 ...@@ -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 }}')" <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"> 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>
<div class="flex items-center gap-2">
<p class="font-medium text-gray-800">{{ $result->person?->name_ar }}</p> <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"> <div class="flex items-center gap-3 mt-1 text-xs text-gray-500">
@if($result->participant_number) @if($result->participant_number)
<span dir="ltr">{{ $result->participant_number }}</span> <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 ...@@ -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 @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"> <div class="flex justify-end mt-8">
<button wire:click="nextStep" wire:loading.attr="disabled" <button wire:click="nextStep" wire:loading.attr="disabled"
@if(!$selected_participant_id) disabled @endif @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: ...@@ -315,6 +315,22 @@ class="px-4 py-2 bg-gray-200 text-gray-700 text-sm font-medium rounded-lg hover:
@endif @endif
@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 --}} {{-- Medical notes --}}
<div class="sm:col-span-2"> <div class="sm:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('ملاحظات طبية (اختياري)') }}</label> <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 ...@@ -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"> placeholder="01012345678">
</div> </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 --}} {{-- Medical Notes --}}
<div class="sm:col-span-2"> <div class="sm:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('ملاحظات طبية') }}</label> <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