Commit 45a76a04 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Fix receptionist registration wizard — full rewrite of broken business logic

Problems fixed:
- MissingRulesException crash on step 4 (empty rules passed to validate())
- Guardian was never actually created as Person + Guardian record
- Guardian was never linked to participant via guardian_participant pivot
- Enrollment in program never happened (service didn't handle program_id)
- medical_notes was passed at wrong level (it's on people table, not participants)
- relation 'guardian' missing from validation (exists in DB CHECK constraint)

Now the wizard properly:
1. Creates guardian Person + Guardian record (or reuses existing)
2. Creates participant Person record with all fields (phone, national_id, medical_notes)
3. Creates Participant via ParticipantService with guardian linked
4. Attaches guardian via guardian_participant pivot
5. Enrolls in program via EnrollmentService (auto-finds/creates group)
6. Added participant phone + national_id fields to step 2
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 2fadfa10
......@@ -2,12 +2,18 @@
namespace App\Livewire\Receptionist;
use App\Domain\Identity\Models\Guardian;
use App\Domain\Identity\Models\Person;
use App\Domain\Identity\Services\PersonService;
use App\Domain\Participant\Models\Participant;
use App\Domain\Participant\Services\DuplicateDetectionService;
use App\Domain\Participant\Services\ParticipantService;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Models\Activity;
use App\Domain\Training\Models\TrainingProgram;
use App\Domain\Training\Services\EnrollmentService;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
......@@ -30,11 +36,13 @@ class NewRegistrationWizard extends Component
public string $guardian_national_id = '';
public string $guardian_relation = 'father';
// Step 2: Participant info
// Step 2: Participant (the actual player/child)
public string $participant_name_ar = '';
public string $participant_name = '';
public ?string $participant_date_of_birth = null;
public string $participant_gender = 'male';
public string $participant_phone = '';
public string $participant_national_id = '';
public string $participant_medical_notes = '';
// Step 3: Program selection
......@@ -62,19 +70,39 @@ public function mount(): void
$this->branchId = $this->getActiveBranchIdOrFail();
}
public function rules(): array
public function nextStep(): void
{
return match ($this->currentStep) {
$rules = $this->rulesForStep($this->currentStep);
if (!empty($rules)) {
$this->validate($rules, $this->messages());
}
if ($this->currentStep === 1 && !$this->duplicateCheckDone) {
$this->checkDuplicates();
if (!empty($this->potentialDuplicates)) {
return;
}
}
$this->currentStep = min($this->currentStep + 1, $this->totalSteps);
}
private function rulesForStep(int $step): array
{
return match ($step) {
1 => [
'guardian_name_ar' => 'required|string|max:255',
'guardian_phone' => 'required|string|max:20',
'guardian_national_id' => 'nullable|string|max:14',
'guardian_relation' => 'required|in:father,mother,brother,sister,uncle,aunt,grandfather,grandmother,other',
'guardian_relation' => 'required|in:father,mother,brother,sister,uncle,aunt,grandfather,grandmother,guardian,other',
],
2 => [
'participant_name_ar' => 'required|string|max:255',
'participant_date_of_birth' => 'required|date|before:today',
'participant_gender' => 'required|in:male,female',
'participant_phone' => 'nullable|string|max:20',
'participant_national_id' => 'nullable|string|max:14',
'participant_medical_notes' => 'nullable|string|max:1000',
],
3 => [
......@@ -109,24 +137,6 @@ public function messages(): array
];
}
public function nextStep(): void
{
$this->validate();
// Check for duplicates when moving from step 1 to step 2
if ($this->currentStep === 1 && !$this->duplicateCheckDone) {
$this->checkDuplicates();
if (!empty($this->potentialDuplicates)) {
return; // Stay on step 1, show warning
}
}
$this->currentStep = min($this->currentStep + 1, $this->totalSteps);
}
/**
* Check for potential duplicate registrations based on guardian info.
*/
public function checkDuplicates(): void
{
$service = app(DuplicateDetectionService::class);
......@@ -136,7 +146,6 @@ public function checkDuplicates(): void
phone: $this->guardian_phone ?: null,
);
// Only show matches with confidence >= 80
$highConfidence = $matches->filter(fn ($m) => $m['confidence'] >= 80);
$this->potentialDuplicates = $highConfidence->map(fn ($m) => [
......@@ -151,24 +160,17 @@ public function checkDuplicates(): void
$this->duplicateCheckDone = true;
// If no high-confidence matches, proceed automatically
if (empty($this->potentialDuplicates)) {
$this->currentStep = min($this->currentStep + 1, $this->totalSteps);
}
}
/**
* User chose to proceed with registration despite duplicates.
*/
public function proceedDespiteDuplicates(): void
{
$this->potentialDuplicates = [];
$this->currentStep = min($this->currentStep + 1, $this->totalSteps);
}
/**
* User chose to use an existing person record.
*/
public function useExistingPerson(int $personId): void
{
$this->useExistingPersonId = $personId;
......@@ -210,54 +212,92 @@ public function updatedSelectedActivityId(): void
$this->selected_program_id = null;
}
public function confirm(ParticipantService $service): void
public function confirm(): void
{
try {
// Step 4 is review — when confirmed, we process registration
// ParticipantService::register() handles:
// - Creating guardian person + guardian record
// - Creating participant person + participant record
// - Linking guardian to participant
// - EnrollmentService::enrollInProgram() for the selected program
// - InvoiceService::create() for the fee
// - PaymentService::recordPayment() if pay_now is true
$registrationData = [
'person' => [
DB::transaction(function () {
$actor = auth()->user();
$personService = app(PersonService::class);
$participantService = app(ParticipantService::class);
$enrollmentService = app(EnrollmentService::class);
// 1. Create or reuse guardian's Person record
if ($this->useExistingPersonId) {
$guardianPerson = Person::findOrFail($this->useExistingPersonId);
} else {
$guardianPerson = Person::where('phone', $this->guardian_phone)->first();
if (!$guardianPerson) {
$guardianPerson = $personService->create([
'name_ar' => $this->guardian_name_ar,
'name' => $this->guardian_name ?: $this->guardian_name_ar,
'phone' => $this->guardian_phone,
'national_id' => $this->guardian_national_id ?: null,
], $actor);
}
}
// 2. Create or find the Guardian record
$guardian = Guardian::firstOrCreate(
['person_id' => $guardianPerson->id],
[
'academy_id' => app('current_academy')->id,
'relationship_type' => $this->guardian_relation,
'is_emergency_contact' => true,
'is_financial_responsible' => true,
'can_pickup' => true,
]
);
// 3. Create participant's Person record
$participantPerson = $personService->create([
'name_ar' => $this->participant_name_ar,
'name' => $this->participant_name,
'name' => $this->participant_name ?: $this->participant_name_ar,
'date_of_birth' => $this->participant_date_of_birth,
'gender' => $this->participant_gender,
],
'branch_id' => $this->branchId,
'registration_source' => 'walk_in',
'medical_notes' => $this->participant_medical_notes ?: null,
'guardian' => [
'name_ar' => $this->guardian_name_ar,
'name' => $this->guardian_name,
'phone' => $this->guardian_phone,
'national_id' => $this->guardian_national_id ?: null,
'relation' => $this->guardian_relation,
],
'program_id' => $this->selected_program_id,
'pay_now' => $this->pay_now,
'payment_method' => $this->pay_now ? $this->payment_method : null,
];
// If user chose to use an existing person, pass that info
if ($this->useExistingPersonId) {
$registrationData['existing_guardian_person_id'] = $this->useExistingPersonId;
}
$result = $service->register($registrationData, auth()->user());
$this->completed = true;
$this->participant_number = $result->participant_number ?? null;
$this->currentStep = 6;
'phone' => $this->participant_phone ?: null,
'national_id' => $this->participant_national_id ?: null,
'medical_notes' => $this->participant_medical_notes ?: null,
], $actor);
// 4. Create Participant record via service
$participant = $participantService->register([
'person_id' => $participantPerson->id,
'branch_id' => $this->branchId,
'registration_source' => 'walk_in',
'primary_guardian_id' => $guardian->id,
'primary_activity_id' => $this->selected_activity_id,
], $actor);
// 5. Link guardian to participant via pivot
$participant->guardians()->attach($guardian->id, [
'relationship_type' => $this->guardian_relation,
'is_primary' => true,
'is_emergency_contact' => true,
'can_pickup' => true,
'receives_notifications' => true,
'can_authorize_payment' => true,
]);
// 6. Enroll in program
$program = TrainingProgram::findOrFail($this->selected_program_id);
$enrollment = $enrollmentService->enrollInProgram(
$participant,
$program,
$actor,
['payment_status' => $this->pay_now ? 'paid' : 'pending']
);
$this->completed = true;
$this->participant_number = $participant->participant_number;
$this->enrollment_summary = $program->name_ar . ' — ' . ($enrollment->group?->name_ar ?? '');
$this->currentStep = 6;
});
session()->flash('success', __('تم التسجيل بنجاح'));
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
} catch (\Throwable $e) {
session()->flash('error', 'حدث خطأ: ' . $e->getMessage());
}
}
......@@ -295,6 +335,7 @@ public function render()
'aunt' => 'عمة/خالة',
'grandfather' => 'جد',
'grandmother' => 'جدة',
'guardian' => 'وصي',
'other' => 'أخرى',
],
]);
......
......@@ -188,13 +188,13 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-blue-600 text-white
{{-- Step 2: Participant Info --}}
@if($currentStep === 2)
<div>
<h2 class="text-lg font-semibold text-gray-800 mb-6">{{ __('بيانات المشترك') }}</h2>
<h2 class="text-lg font-semibold text-gray-800 mb-6">{{ __('بيانات المشترك (اللاعب)') }}</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
<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="participant_name_ar"
class="w-full px-4 py-3 min-h-16 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-lg"
placeholder="{{ __('اسم المشترك') }}">
placeholder="{{ __('اسم اللاعب / المشترك') }}">
@error('participant_name_ar') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
......@@ -202,7 +202,7 @@ class="w-full px-4 py-3 min-h-16 border border-gray-300 rounded-lg focus:ring-2
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('الاسم بالإنجليزية') }}</label>
<input type="text" wire:model="participant_name" dir="ltr"
class="w-full px-4 py-3 min-h-16 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-lg"
placeholder="Participant name">
placeholder="Player / participant name">
</div>
<div>
......@@ -235,6 +235,22 @@ class="w-full px-4 py-3 min-h-16 border border-gray-300 rounded-lg focus:ring-2
@error('participant_gender') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('رقم هاتف المشترك') }}</label>
<input type="tel" wire:model="participant_phone" dir="ltr"
class="w-full px-4 py-3 min-h-16 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-lg"
placeholder="01xxxxxxxxx">
@error('participant_phone') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('الرقم القومي للمشترك') }}</label>
<input type="text" wire:model="participant_national_id" dir="ltr"
class="w-full px-4 py-3 min-h-16 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-lg"
placeholder="00000000000000" maxlength="14">
@error('participant_national_id') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('ملاحظات طبية') }}</label>
<textarea wire:model="participant_medical_notes" rows="3"
......@@ -386,7 +402,7 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-blue-600 text-white
{{-- Participant Summary --}}
<div class="p-4 bg-gray-50 rounded-xl border border-gray-200">
<div class="flex items-center justify-between mb-3">
<h3 class="font-semibold text-gray-700">{{ __('المشترك') }}</h3>
<h3 class="font-semibold text-gray-700">{{ __('المشترك (اللاعب)') }}</h3>
<button wire:click="goToStep(2)" class="text-sm text-blue-600 hover:text-blue-800">{{ __('تعديل') }}</button>
</div>
<div class="grid grid-cols-2 gap-3 text-sm">
......@@ -402,6 +418,18 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-blue-600 text-white
<span class="text-gray-500">{{ __('الجنس') }}:</span>
<span class="font-medium text-gray-800 ms-1">{{ $participant_gender === 'male' ? __('ذكر') : __('أنثى') }}</span>
</div>
@if($participant_phone)
<div>
<span class="text-gray-500">{{ __('الهاتف') }}:</span>
<span class="font-medium text-gray-800 ms-1" dir="ltr">{{ $participant_phone }}</span>
</div>
@endif
@if($participant_national_id)
<div>
<span class="text-gray-500">{{ __('الرقم القومي') }}:</span>
<span class="font-medium text-gray-800 ms-1" dir="ltr">{{ $participant_national_id }}</span>
</div>
@endif
@if($participant_medical_notes)
<div class="col-span-2">
<span class="text-gray-500">{{ __('ملاحظات طبية') }}:</span>
......
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