Commit c000ea07 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Redesign all entity creation flows into integrated workflows

- CreateTrainerWizard: 9-step flow with activities, user account creation,
  and initial group assignment (was 6 disconnected steps)
- CreateProgramWizard: 7-step flow with head trainer, schedule template,
  initial group, BasePrice record, facility (was 4 steps, no group/trainer)
- NewRegistrationWizard: add parent account creation step so guardians
  can log in to the Parent Portal immediately
- UserForm: role-aware prompts — assigning trainer role creates
  Trainer+Employee records, parent role links Guardian, staff creates Employee
- EnrollmentForm: 3-panel guided experience with group cards, capacity bars,
  live price preview from PricingService, and schedule conflict detection
- TrainerCredentialsGenerated event for async email delivery

Closes the "isolated CRUD screens" problem — every entity creation now
handles the full business workflow in a single transaction.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 055ddfa1
<?php
namespace App\Domain\HR\Events;
use App\Models\User;
use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class TrainerCredentialsGenerated implements ShouldDispatchAfterCommit
{
use Dispatchable, SerializesModels;
public function __construct(
public readonly User $user,
public readonly string $plainPassword,
) {}
}
...@@ -11,9 +11,9 @@ ...@@ -11,9 +11,9 @@
class TrainingProgramService class TrainingProgramService
{ {
public function create(array $data, User $actor): TrainingProgram public function create(array $data, User $actor, bool $skipDefaultGroup = false): TrainingProgram
{ {
return DB::transaction(function () use ($data, $actor) { return DB::transaction(function () use ($data, $actor, $skipDefaultGroup) {
$slug = Str::limit($data['slug'] ?? Str::slug($data['name']), 90, ''); $slug = Str::limit($data['slug'] ?? Str::slug($data['name']), 90, '');
$exists = TrainingProgram::withTrashed()->where('slug', $slug)->exists(); $exists = TrainingProgram::withTrashed()->where('slug', $slug)->exists();
if ($exists) { if ($exists) {
...@@ -25,7 +25,9 @@ public function create(array $data, User $actor): TrainingProgram ...@@ -25,7 +25,9 @@ public function create(array $data, User $actor): TrainingProgram
'created_by' => $actor->id, 'created_by' => $actor->id,
])); ]));
$this->createDefaultGroup($program, $actor); if (!$skipDefaultGroup) {
$this->createDefaultGroup($program, $actor);
}
return $program; return $program;
}); });
......
This diff is collapsed.
This diff is collapsed.
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
use App\Domain\Identity\Models\Branch; use App\Domain\Identity\Models\Branch;
use App\Domain\Identity\Models\Guardian; use App\Domain\Identity\Models\Guardian;
use App\Domain\Identity\Models\Person; use App\Domain\Identity\Models\Person;
use App\Domain\Identity\Models\Role;
use App\Domain\Identity\Services\PersonService; use App\Domain\Identity\Services\PersonService;
use App\Domain\Inventory\Models\Kit; use App\Domain\Inventory\Models\Kit;
use App\Domain\Inventory\Models\Product; use App\Domain\Inventory\Models\Product;
...@@ -21,7 +22,10 @@ ...@@ -21,7 +22,10 @@
use App\Domain\Training\Models\Activity; use App\Domain\Training\Models\Activity;
use App\Domain\Training\Models\TrainingProgram; use App\Domain\Training\Models\TrainingProgram;
use App\Domain\Training\Services\EnrollmentService; use App\Domain\Training\Services\EnrollmentService;
use App\Models\User;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Livewire\Attributes\Computed; use Livewire\Attributes\Computed;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
...@@ -36,7 +40,7 @@ class NewRegistrationWizard extends Component ...@@ -36,7 +40,7 @@ class NewRegistrationWizard extends Component
public ?int $branchId = null; public ?int $branchId = null;
public int $currentStep = 1; public int $currentStep = 1;
public int $totalSteps = 6; public int $totalSteps = 7;
// Step 1: Guardian info // Step 1: Guardian info
public string $guardian_name_ar = ''; public string $guardian_name_ar = '';
...@@ -45,7 +49,13 @@ class NewRegistrationWizard extends Component ...@@ -45,7 +49,13 @@ class NewRegistrationWizard extends Component
public string $guardian_national_id = ''; public string $guardian_national_id = '';
public string $guardian_relation = 'father'; public string $guardian_relation = 'father';
// Step 2: Participant (the actual player/child) // Step 2: Parent Account
public bool $createParentAccount = true;
public string $parentEmail = '';
public string $parentPassword = '';
public bool $sendParentCredentials = true;
// Step 3: Participant (the actual player/child)
public string $participant_name_ar = ''; public string $participant_name_ar = '';
public string $participant_name = ''; public string $participant_name = '';
public ?string $participant_date_of_birth = null; public ?string $participant_date_of_birth = null;
...@@ -56,11 +66,11 @@ class NewRegistrationWizard extends Component ...@@ -56,11 +66,11 @@ class NewRegistrationWizard extends Component
public string $membership_type = 'non_member'; public string $membership_type = 'non_member';
public string $membership_id = ''; public string $membership_id = '';
// Step 3: Program selection // Step 4: Program selection
public ?int $selected_activity_id = null; public ?int $selected_activity_id = null;
public ?int $selected_program_id = null; public ?int $selected_program_id = null;
// Step 5: Payment // Step 6: Payment
public bool $pay_now = false; public bool $pay_now = false;
public string $payment_method = 'cash'; public string $payment_method = 'cash';
...@@ -146,8 +156,18 @@ public function nextStep(): void ...@@ -146,8 +156,18 @@ public function nextStep(): void
return; return;
} }
// Price guard on step 3 — block if no base price for selected program // Pre-fill parent email from guardian person data when moving to step 2
if ($this->currentStep === 3 && $this->selected_program_id) { if ($this->currentStep === 1 && $this->duplicateCheckDone && empty($this->parentEmail)) {
if ($this->useExistingPersonId) {
$existingPerson = Person::find($this->useExistingPersonId);
if ($existingPerson && $existingPerson->email) {
$this->parentEmail = $existingPerson->email;
}
}
}
// Price guard on step 4 — block if no base price for selected program
if ($this->currentStep === 4 && $this->selected_program_id) {
$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', 'لا يوجد سعر محدد لهذا البرنامج — تواصل مع المسؤول');
...@@ -167,7 +187,11 @@ private function rulesForStep(int $step): array ...@@ -167,7 +187,11 @@ private function rulesForStep(int $step): array
'guardian_national_id' => 'nullable|string|max:14', 'guardian_national_id' => 'nullable|string|max:14',
'guardian_relation' => 'required|in:father,mother,brother,sister,uncle,aunt,grandfather,grandmother,guardian,other', 'guardian_relation' => 'required|in:father,mother,brother,sister,uncle,aunt,grandfather,grandmother,guardian,other',
], ],
2 => [ 2 => $this->createParentAccount ? [
'parentEmail' => 'required|email|max:255|unique:users,email',
'parentPassword' => 'required|string|min:6|max:100',
] : [],
3 => [
'participant_name_ar' => 'required|string|max:255', 'participant_name_ar' => 'required|string|max:255',
'participant_date_of_birth' => 'required|date|before:today', 'participant_date_of_birth' => 'required|date|before:today',
'participant_gender' => 'required|in:male,female', 'participant_gender' => 'required|in:male,female',
...@@ -177,11 +201,11 @@ private function rulesForStep(int $step): array ...@@ -177,11 +201,11 @@ private function rulesForStep(int $step): array
'membership_type' => 'required|in:member,non_member', 'membership_type' => 'required|in:member,non_member',
'membership_id' => 'required_if:membership_type,member|nullable|string|max:50', 'membership_id' => 'required_if:membership_type,member|nullable|string|max:50',
], ],
3 => [ 4 => [
'selected_activity_id' => 'required|exists:activities,id', 'selected_activity_id' => 'required|exists:activities,id',
'selected_program_id' => 'required|exists:training_programs,id', 'selected_program_id' => 'required|exists:training_programs,id',
], ],
5 => [ 6 => [
'payment_method' => 'required_if:pay_now,true|in:cash,card,wallet', 'payment_method' => 'required_if:pay_now,true|in:cash,card,wallet',
], ],
default => [], default => [],
...@@ -195,6 +219,11 @@ public function messages(): array ...@@ -195,6 +219,11 @@ public function messages(): array
'guardian_phone.required' => 'رقم الهاتف مطلوب', 'guardian_phone.required' => 'رقم الهاتف مطلوب',
'guardian_relation.required' => 'صلة القرابة مطلوبة', 'guardian_relation.required' => 'صلة القرابة مطلوبة',
'guardian_relation.in' => 'صلة القرابة غير صالحة', 'guardian_relation.in' => 'صلة القرابة غير صالحة',
'parentEmail.required' => 'البريد الإلكتروني مطلوب لإنشاء الحساب',
'parentEmail.email' => 'صيغة البريد الإلكتروني غير صالحة',
'parentEmail.unique' => 'البريد الإلكتروني مستخدم بالفعل',
'parentPassword.required' => 'كلمة المرور مطلوبة',
'parentPassword.min' => 'كلمة المرور يجب أن تكون 6 أحرف على الأقل',
'participant_name_ar.required' => 'اسم المشترك مطلوب', 'participant_name_ar.required' => 'اسم المشترك مطلوب',
'participant_date_of_birth.required' => 'تاريخ الميلاد مطلوب', 'participant_date_of_birth.required' => 'تاريخ الميلاد مطلوب',
'participant_date_of_birth.before' => 'تاريخ الميلاد يجب أن يكون في الماضي', 'participant_date_of_birth.before' => 'تاريخ الميلاد يجب أن يكون في الماضي',
...@@ -282,6 +311,11 @@ private function resetDuplicateCheck(): void ...@@ -282,6 +311,11 @@ private function resetDuplicateCheck(): void
$this->useExistingPersonId = null; $this->useExistingPersonId = null;
} }
public function generateParentPassword(): void
{
$this->parentPassword = Str::random(8);
}
public function updatedSelectedActivityId(): void public function updatedSelectedActivityId(): void
{ {
$this->selected_program_id = null; $this->selected_program_id = null;
...@@ -419,6 +453,39 @@ public function confirm(): void ...@@ -419,6 +453,39 @@ public function confirm(): void
] ]
); );
// 2b. Create parent User account if requested
if ($this->createParentAccount && $this->parentEmail) {
$parentRole = Role::where('slug', 'parent')
->where('academy_id', app('current_academy')->id)
->first();
// Only create if no existing user is already linked
if (!$guardian->user_id) {
$parentUser = User::create([
'academy_id' => app('current_academy')->id,
'name' => $guardianPerson->name ?: $guardianPerson->name_ar,
'name_ar' => $guardianPerson->name_ar,
'email' => $this->parentEmail,
'phone' => $guardianPerson->phone,
'password' => Hash::make($this->parentPassword),
'person_id' => $guardianPerson->id,
'role_id' => $parentRole?->id,
'status' => 'active',
]);
$guardian->update(['user_id' => $parentUser->id]);
$guardianPerson->update(['user_id' => $parentUser->id]);
// Attach the parent role via pivot if role exists
if ($parentRole) {
$parentUser->roles()->attach($parentRole->id, [
'assigned_by' => $actor->id,
'created_at' => now(),
]);
}
}
}
// 3. Create participant's Person record // 3. Create participant's Person record
$participantPerson = $personService->create([ $participantPerson = $personService->create([
'name_ar' => $this->participant_name_ar, 'name_ar' => $this->participant_name_ar,
...@@ -551,7 +618,7 @@ public function confirm(): void ...@@ -551,7 +618,7 @@ public function confirm(): void
if ($invoice) { if ($invoice) {
$this->invoice_uuid = $invoice->uuid; $this->invoice_uuid = $invoice->uuid;
} }
$this->currentStep = 6; $this->currentStep = 7;
}); });
session()->flash('success', __('تم التسجيل بنجاح')); session()->flash('success', __('تم التسجيل بنجاح'));
......
This diff is collapsed.
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