Commit 1959e8e0 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Overhaul setup wizard: 12 steps, 30+ business improvements, zero schema mismatches

Expanded from 10 shallow steps to 12 deep steps covering ALL organization data:
- Academy profile: phone, email, address, type, website, WhatsApp
- Branches: working days (inline toggles), email, city, manager name
- Programs: age range (age_min/age_max columns), sessions_per_week, registration fee (separate BasePrice), payment frequency
- Facilities: branch selection per facility, visual grid preview
- Groups: structured schedule (day + start/end time repeater), segment selection
- Participants: classification (on Person), skill_level (on Participant), national_id, medical_notes, collapsible extras
- Staff: phone, specialization (creates linked Person for trainers)
- NEW Step 9: Financial (tax, invoice/receipt prefix, payment methods, wallet, discounts, family discount, trial period)
- NEW Step 10: Policies (attendance grace periods, auto-absent, suspension threshold, medical/guardian requirements, waitlist, freeze days, email/SMS toggles)
- Step 11: General settings (currency, timezone, week start, date/time format, pagination)
- Step 12: Summary dashboard with stats cards

Schema alignment verified: Branch uses operating_hours (not metadata), Person owns classification/medical_notes, Participant owns skill_level, TrainingProgram uses age_min/age_max/sessions_per_week columns directly, all SettingsService keys match SystemSettings schema exactly.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent d3e3030e
...@@ -30,11 +30,17 @@ ...@@ -30,11 +30,17 @@
class SetupWizard extends Component class SetupWizard extends Component
{ {
public int $currentStep = 1; public int $currentStep = 1;
public int $totalSteps = 10; public int $totalSteps = 12;
// Step 1: Welcome // Step 1: Academy Profile
public string $academyName = ''; public string $academyName = '';
public string $academyNameAr = ''; public string $academyNameAr = '';
public string $academyPhone = '';
public string $academyEmail = '';
public string $academyAddress = '';
public string $academyType = '';
public string $academyWebsite = '';
public string $socialWhatsapp = '';
// Step 2: Branches // Step 2: Branches
public array $branches = []; public array $branches = [];
...@@ -48,21 +54,49 @@ class SetupWizard extends Component ...@@ -48,21 +54,49 @@ class SetupWizard extends Component
// Step 5: Facilities + Grid // Step 5: Facilities + Grid
public array $facilities = []; public array $facilities = [];
// Step 6: Groups (NEW) // Step 6: Groups + Schedule
public array $groups = []; public array $groups = [];
// Step 7: Participants (NEW) // Step 7: Participants
public array $participants = []; public array $participants = [];
// Step 8: Users // Step 8: Staff & Users
public array $users = []; public array $users = [];
// Step 9: Settings // Step 9: Financial & Pricing
public string $defaultCurrency = 'EGP'; public bool $taxEnabled = false;
public string $timezone = 'Africa/Cairo'; public float $taxPercentage = 14;
public string $invoicePrefix = 'INV';
public string $receiptPrefix = 'RCP';
public int $paymentDueDays = 7;
public bool $walletEnabled = true;
public array $acceptedPaymentMethods = ['cash', 'card', 'wallet'];
public int $maxDiscountPercent = 50;
public bool $autoApplyFamilyDiscount = true;
public int $familyDiscountStartChild = 2;
public int $trialPeriodDays = 7;
// Step 10: Rules & Policies
public int $gracePeriodParticipant = 15; public int $gracePeriodParticipant = 15;
public int $gracePeriodTrainer = 10; public int $gracePeriodTrainer = 10;
public int $maxDiscountPercent = 50; public int $autoAbsentAfterHours = 2;
public int $consecutiveAbsencesForSuspend = 5;
public int $minAttendancePercent = 75;
public bool $requireMedicalReport = false;
public bool $requireGuardianForMinors = true;
public int $minorAgeThreshold = 18;
public bool $allowWaitlist = true;
public int $freezeMaxDays = 30;
public bool $emailEnabled = true;
public bool $smsEnabled = false;
// Step 11: General Settings
public string $defaultCurrency = 'EGP';
public string $timezone = 'Africa/Cairo';
public string $weekStartsOn = '6';
public string $dateFormat = 'Y-m-d';
public string $timeFormat = 'H:i';
public int $paginationSize = 25;
// Internal // Internal
public ?Academy $academy = null; public ?Academy $academy = null;
...@@ -126,6 +160,10 @@ public function addBranch(): void ...@@ -126,6 +160,10 @@ public function addBranch(): void
'address' => '', 'address' => '',
'phone' => '', 'phone' => '',
'is_main' => count($this->branches) === 0, 'is_main' => count($this->branches) === 0,
'working_days' => [0, 1, 2, 3, 4],
'email' => '',
'city' => '',
'manager_name' => '',
]; ];
} }
...@@ -173,6 +211,11 @@ public function addProgram(): void ...@@ -173,6 +211,11 @@ public function addProgram(): void
'duration_months' => 1, 'duration_months' => 1,
'max_participants' => 20, 'max_participants' => 20,
'monthly_fee' => '', 'monthly_fee' => '',
'min_age' => null,
'max_age' => null,
'sessions_per_week' => 3,
'registration_fee' => null,
'payment_frequency' => 'monthly',
]; ];
} }
...@@ -196,6 +239,7 @@ public function addFacility(): void ...@@ -196,6 +239,7 @@ public function addFacility(): void
'address' => '', 'address' => '',
'grid_rows' => 3, 'grid_rows' => 3,
'grid_cols' => 3, 'grid_cols' => 3,
'branch_index' => 0,
]; ];
} }
...@@ -205,7 +249,7 @@ public function removeFacility(int $index): void ...@@ -205,7 +249,7 @@ public function removeFacility(int $index): void
$this->facilities = array_values($this->facilities); $this->facilities = array_values($this->facilities);
} }
// ─── Group Management (NEW) ────────────────────────────────── // ─── Group Management ────────────────────────────────────────
public function addGroup(): void public function addGroup(): void
{ {
...@@ -216,7 +260,7 @@ public function addGroup(): void ...@@ -216,7 +260,7 @@ public function addGroup(): void
'assigned_segments' => [], 'assigned_segments' => [],
'head_trainer_name' => '', 'head_trainer_name' => '',
'max_capacity' => 20, 'max_capacity' => 20,
'schedule_summary' => '', 'schedule_days' => [],
]; ];
} }
...@@ -226,7 +270,30 @@ public function removeGroup(int $index): void ...@@ -226,7 +270,30 @@ public function removeGroup(int $index): void
$this->groups = array_values($this->groups); $this->groups = array_values($this->groups);
} }
// ─── Participant Management (NEW) ──────────────────────────── public function addGroupScheduleSlot(int $groupIndex): void
{
if (!isset($this->groups[$groupIndex])) {
return;
}
$this->groups[$groupIndex]['schedule_days'][] = [
'day' => 0,
'start_time' => '16:00',
'end_time' => '17:30',
];
}
public function removeGroupScheduleSlot(int $groupIndex, int $slotIndex): void
{
if (!isset($this->groups[$groupIndex]['schedule_days'][$slotIndex])) {
return;
}
unset($this->groups[$groupIndex]['schedule_days'][$slotIndex]);
$this->groups[$groupIndex]['schedule_days'] = array_values($this->groups[$groupIndex]['schedule_days']);
}
// ─── Participant Management ──────────────────────────────────
public function addParticipant(): void public function addParticipant(): void
{ {
...@@ -240,6 +307,10 @@ public function addParticipant(): void ...@@ -240,6 +307,10 @@ public function addParticipant(): void
'guardian_phone' => '', 'guardian_phone' => '',
'activity_index' => 0, 'activity_index' => 0,
'group_index' => null, 'group_index' => null,
'national_id' => '',
'classification' => 'regular',
'medical_notes' => '',
'skill_level' => null,
]; ];
} }
...@@ -258,6 +329,8 @@ public function addUser(): void ...@@ -258,6 +329,8 @@ public function addUser(): void
'email' => '', 'email' => '',
'role' => 'trainer', 'role' => 'trainer',
'branch_index' => 0, 'branch_index' => 0,
'phone' => '',
'specialization' => '',
]; ];
} }
...@@ -275,9 +348,21 @@ protected function validateCurrentStep(): void ...@@ -275,9 +348,21 @@ protected function validateCurrentStep(): void
1 => $this->validate([ 1 => $this->validate([
'academyNameAr' => 'required|string|min:3|max:255', 'academyNameAr' => 'required|string|min:3|max:255',
'academyName' => 'nullable|string|max:255', 'academyName' => 'nullable|string|max:255',
'academyPhone' => 'required|string|max:20',
'academyEmail' => 'required|email|max:255',
'academyAddress' => 'nullable|string|max:500',
'academyType' => 'required|string|in:sports_club,fitness_center,educational_center,multi_sport,swimming_academy,martial_arts,other',
'academyWebsite' => 'nullable|url|max:255',
'socialWhatsapp' => 'nullable|string|max:20',
], [ ], [
'academyNameAr.required' => __('اسم الأكاديمية بالعربية مطلوب'), 'academyNameAr.required' => __('اسم الأكاديمية بالعربية مطلوب'),
'academyNameAr.min' => __('اسم الأكاديمية يجب أن يكون 3 أحرف على الأقل'), 'academyNameAr.min' => __('اسم الأكاديمية يجب أن يكون 3 أحرف على الأقل'),
'academyPhone.required' => __('رقم هاتف الأكاديمية مطلوب'),
'academyEmail.required' => __('البريد الإلكتروني للأكاديمية مطلوب'),
'academyEmail.email' => __('البريد الإلكتروني غير صالح'),
'academyType.required' => __('نوع الأكاديمية مطلوب'),
'academyType.in' => __('نوع الأكاديمية غير صالح'),
'academyWebsite.url' => __('رابط الموقع غير صالح'),
]), ]),
2 => $this->validateBranches(), 2 => $this->validateBranches(),
3 => $this->validateActivities(), 3 => $this->validateActivities(),
...@@ -287,15 +372,80 @@ protected function validateCurrentStep(): void ...@@ -287,15 +372,80 @@ protected function validateCurrentStep(): void
7 => $this->validateParticipants(), 7 => $this->validateParticipants(),
8 => $this->validateUsers(), 8 => $this->validateUsers(),
9 => $this->validate([ 9 => $this->validate([
'defaultCurrency' => 'required|string|in:EGP,SAR,AED,USD', 'taxEnabled' => 'boolean',
'timezone' => 'required|string|timezone', 'taxPercentage' => 'required|numeric|min:0|max:100',
'invoicePrefix' => 'required|string|max:10',
'receiptPrefix' => 'required|string|max:10',
'paymentDueDays' => 'required|integer|min:1|max:365',
'walletEnabled' => 'boolean',
'acceptedPaymentMethods' => 'required|array|min:1',
'acceptedPaymentMethods.*' => 'string|in:cash,card,bank_transfer,wallet,online',
'maxDiscountPercent' => 'required|integer|min:0|max:100',
'autoApplyFamilyDiscount' => 'boolean',
'familyDiscountStartChild' => 'required|integer|min:2|max:10',
'trialPeriodDays' => 'required|integer|min:0|max:90',
], [
'taxPercentage.required' => __('نسبة الضريبة مطلوبة'),
'taxPercentage.min' => __('نسبة الضريبة لا يمكن أن تكون سالبة'),
'taxPercentage.max' => __('نسبة الضريبة لا يمكن أن تتجاوز 100%'),
'invoicePrefix.required' => __('بادئة الفاتورة مطلوبة'),
'receiptPrefix.required' => __('بادئة الإيصال مطلوبة'),
'paymentDueDays.required' => __('مهلة السداد مطلوبة'),
'paymentDueDays.min' => __('مهلة السداد يجب أن تكون يوم واحد على الأقل'),
'acceptedPaymentMethods.required' => __('يجب اختيار طريقة دفع واحدة على الأقل'),
'acceptedPaymentMethods.min' => __('يجب اختيار طريقة دفع واحدة على الأقل'),
'maxDiscountPercent.required' => __('الحد الأقصى للخصم مطلوب'),
'maxDiscountPercent.min' => __('الحد الأقصى للخصم لا يمكن أن يكون سالباً'),
'maxDiscountPercent.max' => __('الحد الأقصى للخصم لا يمكن أن يتجاوز 100%'),
'familyDiscountStartChild.required' => __('ترتيب الطفل لبدء خصم العائلة مطلوب'),
'trialPeriodDays.required' => __('فترة التجربة مطلوبة'),
]),
10 => $this->validate([
'gracePeriodParticipant' => 'required|integer|min:0|max:60', 'gracePeriodParticipant' => 'required|integer|min:0|max:60',
'gracePeriodTrainer' => 'required|integer|min:0|max:60', 'gracePeriodTrainer' => 'required|integer|min:0|max:60',
'maxDiscountPercent' => 'required|integer|min:0|max:100', 'autoAbsentAfterHours' => 'required|integer|min:1|max:24',
'consecutiveAbsencesForSuspend' => 'required|integer|min:1|max:30',
'minAttendancePercent' => 'required|integer|min:0|max:100',
'requireMedicalReport' => 'boolean',
'requireGuardianForMinors' => 'boolean',
'minorAgeThreshold' => 'required|integer|min:12|max:21',
'allowWaitlist' => 'boolean',
'freezeMaxDays' => 'required|integer|min:1|max:365',
'emailEnabled' => 'boolean',
'smsEnabled' => 'boolean',
], [ ], [
'gracePeriodParticipant.required' => __('فترة السماح للمشتركين مطلوبة'), 'gracePeriodParticipant.required' => __('فترة السماح للمشتركين مطلوبة'),
'gracePeriodParticipant.max' => __('فترة السماح لا يمكن أن تتجاوز 60 دقيقة'),
'gracePeriodTrainer.required' => __('فترة السماح للمدربين مطلوبة'), 'gracePeriodTrainer.required' => __('فترة السماح للمدربين مطلوبة'),
'maxDiscountPercent.required' => __('الحد الأقصى للخصم مطلوب'), 'gracePeriodTrainer.max' => __('فترة السماح لا يمكن أن تتجاوز 60 دقيقة'),
'autoAbsentAfterHours.required' => __('وقت التغيب التلقائي مطلوب'),
'consecutiveAbsencesForSuspend.required' => __('عدد مرات الغياب المتتالي للإيقاف مطلوب'),
'minAttendancePercent.required' => __('نسبة الحضور الأدنى مطلوبة'),
'minorAgeThreshold.required' => __('حد سن القاصر مطلوب'),
'minorAgeThreshold.min' => __('حد سن القاصر يجب أن يكون 12 على الأقل'),
'minorAgeThreshold.max' => __('حد سن القاصر لا يمكن أن يتجاوز 21'),
'freezeMaxDays.required' => __('الحد الأقصى لأيام التجميد مطلوب'),
'freezeMaxDays.min' => __('الحد الأقصى لأيام التجميد يجب أن يكون يوم واحد على الأقل'),
]),
11 => $this->validate([
'defaultCurrency' => 'required|string|in:EGP,SAR,AED,USD',
'timezone' => 'required|string|timezone',
'weekStartsOn' => 'required|string|in:0,1,6',
'dateFormat' => 'required|string|in:Y-m-d,d/m/Y',
'timeFormat' => 'required|string|in:H:i,h:i A',
'paginationSize' => 'required|integer|min:10|max:100',
], [
'defaultCurrency.required' => __('العملة الافتراضية مطلوبة'),
'defaultCurrency.in' => __('العملة غير مدعومة'),
'timezone.required' => __('المنطقة الزمنية مطلوبة'),
'timezone.timezone' => __('المنطقة الزمنية غير صالحة'),
'weekStartsOn.required' => __('بداية الأسبوع مطلوبة'),
'weekStartsOn.in' => __('قيمة بداية الأسبوع غير صالحة'),
'dateFormat.required' => __('صيغة التاريخ مطلوبة'),
'timeFormat.required' => __('صيغة الوقت مطلوبة'),
'paginationSize.required' => __('عدد العناصر في الصفحة مطلوب'),
'paginationSize.min' => __('عدد العناصر في الصفحة يجب أن يكون 10 على الأقل'),
'paginationSize.max' => __('عدد العناصر في الصفحة لا يمكن أن يتجاوز 100'),
]), ]),
default => null, default => null,
}; };
...@@ -315,9 +465,17 @@ protected function validateBranches(): void ...@@ -315,9 +465,17 @@ protected function validateBranches(): void
'branches.*.name' => 'nullable|string|max:255', 'branches.*.name' => 'nullable|string|max:255',
'branches.*.address' => 'nullable|string|max:500', 'branches.*.address' => 'nullable|string|max:500',
'branches.*.phone' => 'nullable|string|max:20', 'branches.*.phone' => 'nullable|string|max:20',
'branches.*.working_days' => 'required|array|min:1',
'branches.*.working_days.*' => 'integer|min:0|max:6',
'branches.*.email' => 'nullable|email|max:255',
'branches.*.city' => 'nullable|string|max:100',
'branches.*.manager_name' => 'nullable|string|max:255',
], [ ], [
'branches.*.name_ar.required' => __('اسم الفرع بالعربية مطلوب'), 'branches.*.name_ar.required' => __('اسم الفرع بالعربية مطلوب'),
'branches.*.name_ar.min' => __('اسم الفرع يجب أن يكون حرفين على الأقل'), 'branches.*.name_ar.min' => __('اسم الفرع يجب أن يكون حرفين على الأقل'),
'branches.*.working_days.required' => __('يجب تحديد أيام العمل'),
'branches.*.working_days.min' => __('يجب اختيار يوم عمل واحد على الأقل'),
'branches.*.email.email' => __('البريد الإلكتروني للفرع غير صالح'),
]); ]);
} }
...@@ -356,12 +514,24 @@ protected function validatePrograms(): void ...@@ -356,12 +514,24 @@ protected function validatePrograms(): void
'programs.*.duration_months' => 'required|integer|min:1|max:24', 'programs.*.duration_months' => 'required|integer|min:1|max:24',
'programs.*.max_participants' => 'required|integer|min:1|max:500', 'programs.*.max_participants' => 'required|integer|min:1|max:500',
'programs.*.monthly_fee' => 'required|numeric|min:1', 'programs.*.monthly_fee' => 'required|numeric|min:1',
'programs.*.min_age' => 'nullable|integer|min:3|max:80',
'programs.*.max_age' => 'nullable|integer|min:3|max:80',
'programs.*.sessions_per_week' => 'required|integer|min:1|max:7',
'programs.*.registration_fee' => 'nullable|numeric|min:0',
'programs.*.payment_frequency' => 'required|string|in:monthly,quarterly,semester,annual',
], [ ], [
'programs.*.name_ar.required' => __('اسم البرنامج بالعربية مطلوب'), 'programs.*.name_ar.required' => __('اسم البرنامج بالعربية مطلوب'),
'programs.*.duration_months.required' => __('مدة البرنامج مطلوبة'), 'programs.*.duration_months.required' => __('مدة البرنامج مطلوبة'),
'programs.*.max_participants.required' => __('الحد الأقصى للمشتركين مطلوب'), 'programs.*.max_participants.required' => __('الحد الأقصى للمشتركين مطلوب'),
'programs.*.monthly_fee.required' => __('الرسوم الشهرية مطلوبة'), 'programs.*.monthly_fee.required' => __('الرسوم الشهرية مطلوبة'),
'programs.*.monthly_fee.min' => __('يجب تحديد سعر البرنامج (لا يمكن أن يكون صفر)'), 'programs.*.monthly_fee.min' => __('يجب تحديد سعر البرنامج (لا يمكن أن يكون صفر)'),
'programs.*.min_age.min' => __('الحد الأدنى للعمر يجب أن يكون 3 سنوات على الأقل'),
'programs.*.max_age.max' => __('الحد الأقصى للعمر لا يمكن أن يتجاوز 80 سنة'),
'programs.*.sessions_per_week.required' => __('عدد الحصص في الأسبوع مطلوب'),
'programs.*.sessions_per_week.min' => __('عدد الحصص يجب أن يكون حصة واحدة على الأقل'),
'programs.*.sessions_per_week.max' => __('عدد الحصص لا يمكن أن يتجاوز 7 حصص'),
'programs.*.payment_frequency.required' => __('دورة الدفع مطلوبة'),
'programs.*.payment_frequency.in' => __('دورة الدفع غير صالحة'),
]); ]);
} }
...@@ -385,12 +555,14 @@ protected function validateFacilities(): void ...@@ -385,12 +555,14 @@ protected function validateFacilities(): void
'facilities.*.address' => 'nullable|string|max:500', 'facilities.*.address' => 'nullable|string|max:500',
'facilities.*.grid_rows' => 'required|integer|min:1|max:10', 'facilities.*.grid_rows' => 'required|integer|min:1|max:10',
'facilities.*.grid_cols' => 'required|integer|min:1|max:10', 'facilities.*.grid_cols' => 'required|integer|min:1|max:10',
'facilities.*.branch_index' => 'required|integer|min:0',
], [ ], [
'facilities.*.name_ar.required' => __('اسم المنشأة بالعربية مطلوب'), 'facilities.*.name_ar.required' => __('اسم المنشأة بالعربية مطلوب'),
'facilities.*.type.required' => __('نوع المنشأة مطلوب'), 'facilities.*.type.required' => __('نوع المنشأة مطلوب'),
'facilities.*.monthly_cost.required' => __('الإيجار الشهري مطلوب (أدخل 0 إذا كانت ملك)'), 'facilities.*.monthly_cost.required' => __('الإيجار الشهري مطلوب (أدخل 0 إذا كانت ملك)'),
'facilities.*.grid_rows.required' => __('عدد الصفوف مطلوب'), 'facilities.*.grid_rows.required' => __('عدد الصفوف مطلوب'),
'facilities.*.grid_cols.required' => __('عدد الأعمدة مطلوب'), 'facilities.*.grid_cols.required' => __('عدد الأعمدة مطلوب'),
'facilities.*.branch_index.required' => __('يجب اختيار الفرع'),
]); ]);
} }
...@@ -407,13 +579,21 @@ protected function validateGroups(): void ...@@ -407,13 +579,21 @@ protected function validateGroups(): void
'groups.*.facility_index' => 'required|integer|min:0', 'groups.*.facility_index' => 'required|integer|min:0',
'groups.*.max_capacity' => 'required|integer|min:1|max:500', 'groups.*.max_capacity' => 'required|integer|min:1|max:500',
'groups.*.head_trainer_name' => 'nullable|string|max:255', 'groups.*.head_trainer_name' => 'nullable|string|max:255',
'groups.*.schedule_summary' => 'nullable|string|max:500', 'groups.*.schedule_days' => 'nullable|array',
'groups.*.schedule_days.*.day' => 'required|integer|min:0|max:6',
'groups.*.schedule_days.*.start_time' => 'required|date_format:H:i',
'groups.*.schedule_days.*.end_time' => 'required|date_format:H:i',
], [ ], [
'groups.*.name_ar.required' => __('اسم المجموعة بالعربية مطلوب'), 'groups.*.name_ar.required' => __('اسم المجموعة بالعربية مطلوب'),
'groups.*.name_ar.min' => __('اسم المجموعة يجب أن يكون حرفين على الأقل'), 'groups.*.name_ar.min' => __('اسم المجموعة يجب أن يكون حرفين على الأقل'),
'groups.*.program_index.required' => __('يجب اختيار البرنامج'), 'groups.*.program_index.required' => __('يجب اختيار البرنامج'),
'groups.*.facility_index.required' => __('يجب اختيار المنشأة'), 'groups.*.facility_index.required' => __('يجب اختيار المنشأة'),
'groups.*.max_capacity.required' => __('الحد الأقصى للسعة مطلوب'), 'groups.*.max_capacity.required' => __('الحد الأقصى للسعة مطلوب'),
'groups.*.schedule_days.*.day.required' => __('يجب تحديد اليوم'),
'groups.*.schedule_days.*.start_time.required' => __('وقت البدء مطلوب'),
'groups.*.schedule_days.*.start_time.date_format' => __('صيغة وقت البدء غير صالحة'),
'groups.*.schedule_days.*.end_time.required' => __('وقت الانتهاء مطلوب'),
'groups.*.schedule_days.*.end_time.date_format' => __('صيغة وقت الانتهاء غير صالحة'),
]); ]);
} }
...@@ -433,11 +613,19 @@ protected function validateParticipants(): void ...@@ -433,11 +613,19 @@ protected function validateParticipants(): void
'participants.*.guardian_name' => 'nullable|string|max:255', 'participants.*.guardian_name' => 'nullable|string|max:255',
'participants.*.guardian_phone' => 'nullable|string|max:20', 'participants.*.guardian_phone' => 'nullable|string|max:20',
'participants.*.activity_index' => 'nullable|integer|min:0', 'participants.*.activity_index' => 'nullable|integer|min:0',
'participants.*.national_id' => 'nullable|string|max:20',
'participants.*.classification' => 'required|string|in:regular,vip,scholarship,staff_child,trial',
'participants.*.medical_notes' => 'nullable|string|max:1000',
'participants.*.skill_level' => 'nullable|string|in:beginner,intermediate,advanced,professional',
], [ ], [
'participants.*.name_ar.required' => __('اسم المشترك بالعربية مطلوب'), 'participants.*.name_ar.required' => __('اسم المشترك بالعربية مطلوب'),
'participants.*.name_ar.min' => __('اسم المشترك يجب أن يكون حرفين على الأقل'), 'participants.*.name_ar.min' => __('اسم المشترك يجب أن يكون حرفين على الأقل'),
'participants.*.gender.required' => __('الجنس مطلوب'), 'participants.*.gender.required' => __('الجنس مطلوب'),
'participants.*.gender.in' => __('الجنس يجب أن يكون ذكر أو أنثى'), 'participants.*.gender.in' => __('الجنس يجب أن يكون ذكر أو أنثى'),
'participants.*.classification.required' => __('تصنيف المشترك مطلوب'),
'participants.*.classification.in' => __('تصنيف المشترك غير صالح'),
'participants.*.medical_notes.max' => __('الملاحظات الطبية لا يمكن أن تتجاوز 1000 حرف'),
'participants.*.skill_level.in' => __('مستوى المهارة غير صالح'),
]); ]);
} }
...@@ -450,13 +638,18 @@ protected function validateUsers(): void ...@@ -450,13 +638,18 @@ protected function validateUsers(): void
$this->validate([ $this->validate([
'users.*.name' => 'required|string|min:2|max:255', 'users.*.name' => 'required|string|min:2|max:255',
'users.*.email' => 'required|email|max:255', 'users.*.email' => 'required|email|max:255',
'users.*.role' => 'required|string|in:branch_manager,head_trainer,trainer,receptionist,accountant', 'users.*.role' => 'required|string|in:branch_manager,head_trainer,trainer,receptionist,accountant,data_entry',
'users.*.branch_index' => 'required|integer|min:0', 'users.*.branch_index' => 'required|integer|min:0',
'users.*.phone' => 'nullable|string|max:20',
'users.*.specialization' => 'nullable|string|max:255',
], [ ], [
'users.*.name.required' => __('اسم المستخدم مطلوب'), 'users.*.name.required' => __('اسم المستخدم مطلوب'),
'users.*.name.min' => __('اسم المستخدم يجب أن يكون حرفين على الأقل'),
'users.*.email.required' => __('البريد الإلكتروني مطلوب'), 'users.*.email.required' => __('البريد الإلكتروني مطلوب'),
'users.*.email.email' => __('البريد الإلكتروني غير صالح'), 'users.*.email.email' => __('البريد الإلكتروني غير صالح'),
'users.*.role.required' => __('الدور مطلوب'), 'users.*.role.required' => __('الدور مطلوب'),
'users.*.role.in' => __('الدور غير صالح'),
'users.*.phone.max' => __('رقم الهاتف لا يمكن أن يتجاوز 20 حرفاً'),
]); ]);
} }
...@@ -470,12 +663,31 @@ public function completeSetup(): void ...@@ -470,12 +663,31 @@ public function completeSetup(): void
$academyId = $this->academy->id; $academyId = $this->academy->id;
$actor = auth()->user(); $actor = auth()->user();
// Update academy name // Update academy profile
$this->academy->update([ $this->academy->update([
'name' => $this->academyName ?: $this->academyNameAr, 'name' => $this->academyName ?: $this->academyNameAr,
'name_ar' => $this->academyNameAr, 'name_ar' => $this->academyNameAr,
'phone' => $this->academyPhone,
'email' => $this->academyEmail,
'currency' => $this->defaultCurrency,
'timezone' => $this->timezone,
'settings' => array_merge($this->academy->settings ?? [], [
'address' => $this->academyAddress,
'type' => $this->academyType,
'website' => $this->academyWebsite,
'whatsapp' => $this->socialWhatsapp,
]),
]); ]);
// Save additional academy info as settings
$settingsService = app(SettingsService::class);
$settingsService->set('academy_phone', $this->academyPhone, 'general', 'string');
$settingsService->set('academy_email', $this->academyEmail, 'general', 'string');
$settingsService->set('academy_address', $this->academyAddress, 'general', 'string');
$settingsService->set('academy_type', $this->academyType, 'general', 'string');
$settingsService->set('academy_website', $this->academyWebsite, 'general', 'string');
$settingsService->set('social_whatsapp', $this->socialWhatsapp, 'general', 'string');
// Create branches // Create branches
$createdBranches = []; $createdBranches = [];
foreach ($this->branches as $branchData) { foreach ($this->branches as $branchData) {
...@@ -485,9 +697,15 @@ public function completeSetup(): void ...@@ -485,9 +697,15 @@ public function completeSetup(): void
'name' => $branchData['name'] ?: $branchData['name_ar'], 'name' => $branchData['name'] ?: $branchData['name_ar'],
'address' => $branchData['address'] ?? null, 'address' => $branchData['address'] ?? null,
'phone' => $branchData['phone'] ?? null, 'phone' => $branchData['phone'] ?? null,
'email' => $branchData['email'] ?? null,
'city' => $branchData['city'] ?? null,
'is_main' => $branchData['is_main'] ?? false, 'is_main' => $branchData['is_main'] ?? false,
'is_active' => true, 'is_active' => true,
'code' => Str::upper(Str::limit(Str::slug($branchData['name_ar'], ''), 10, '')), 'code' => Str::upper(Str::limit(Str::slug($branchData['name_ar'], ''), 10, '')),
'operating_hours' => [
'working_days' => $branchData['working_days'] ?? [0, 1, 2, 3, 4],
'manager_name' => $branchData['manager_name'] ?? null,
],
]); ]);
} }
...@@ -515,6 +733,9 @@ public function completeSetup(): void ...@@ -515,6 +733,9 @@ public function completeSetup(): void
if ($activity) { if ($activity) {
$durationMonths = (int) $programData['duration_months']; $durationMonths = (int) $programData['duration_months'];
$monthlyFeePiasters = (int) round((float) $programData['monthly_fee'] * 100); $monthlyFeePiasters = (int) round((float) $programData['monthly_fee'] * 100);
$registrationFeePiasters = !empty($programData['registration_fee'])
? (int) round((float) $programData['registration_fee'] * 100)
: null;
$program = TrainingProgram::create([ $program = TrainingProgram::create([
'academy_id' => $academyId, 'academy_id' => $academyId,
...@@ -524,14 +745,21 @@ public function completeSetup(): void ...@@ -524,14 +745,21 @@ public function completeSetup(): void
'slug' => Str::limit(Str::slug($programData['name_ar']), 90, ''), 'slug' => Str::limit(Str::slug($programData['name_ar']), 90, ''),
'program_duration_weeks' => $durationMonths * 4, 'program_duration_weeks' => $durationMonths * 4,
'max_participants' => (int) $programData['max_participants'], 'max_participants' => (int) $programData['max_participants'],
'age_min' => !empty($programData['min_age']) ? (int) $programData['min_age'] : null,
'age_max' => !empty($programData['max_age']) ? (int) $programData['max_age'] : null,
'sessions_per_week' => (int) ($programData['sessions_per_week'] ?? 3),
'status' => 'active', 'status' => 'active',
'registration_open' => true, 'registration_open' => true,
'created_by' => $actor->id, 'created_by' => $actor->id,
'metadata' => ['monthly_fee_piasters' => $monthlyFeePiasters], 'metadata' => [
'monthly_fee_piasters' => $monthlyFeePiasters,
'payment_frequency' => $programData['payment_frequency'] ?? 'monthly',
],
]); ]);
$createdPrograms[] = $program; $createdPrograms[] = $program;
// Monthly subscription base price
BasePrice::create([ BasePrice::create([
'academy_id' => $academyId, 'academy_id' => $academyId,
'priceable_type' => TrainingProgram::class, 'priceable_type' => TrainingProgram::class,
...@@ -547,16 +775,38 @@ public function completeSetup(): void ...@@ -547,16 +775,38 @@ public function completeSetup(): void
'priority' => 1, 'priority' => 1,
'created_by' => $actor->id, 'created_by' => $actor->id,
]); ]);
// Registration fee base price (if provided)
if ($registrationFeePiasters && $registrationFeePiasters > 0) {
BasePrice::create([
'academy_id' => $academyId,
'priceable_type' => TrainingProgram::class,
'priceable_id' => $program->id,
'branch_id' => null,
'name_ar' => 'رسوم التسجيل — ' . $programData['name_ar'],
'name' => 'Registration Fee — ' . $programData['name_ar'],
'amount' => $registrationFeePiasters,
'currency' => 'EGP',
'effective_from' => now()->toDateString(),
'effective_to' => null,
'is_active' => true,
'priority' => 0,
'created_by' => $actor->id,
]);
}
} }
} }
// Create facilities + space layouts + segments // Create facilities + space layouts + segments
$createdFacilities = []; $createdFacilities = [];
$facilityLayouts = []; // index => layout $facilityLayouts = [];
foreach ($this->facilities as $facilityData) { foreach ($this->facilities as $facilityData) {
$branchIndex = (int) ($facilityData['branch_index'] ?? 0);
$linkedBranch = $createdBranches[$branchIndex] ?? $createdBranches[0] ?? null;
$facility = Facility::create([ $facility = Facility::create([
'academy_id' => $academyId, 'academy_id' => $academyId,
'branch_id' => $createdBranches[0]->id ?? null, 'branch_id' => $linkedBranch?->id,
'name_ar' => $facilityData['name_ar'], 'name_ar' => $facilityData['name_ar'],
'name' => $facilityData['name_ar'], 'name' => $facilityData['name_ar'],
'type' => $facilityData['type'], 'type' => $facilityData['type'],
...@@ -633,7 +883,7 @@ public function completeSetup(): void ...@@ -633,7 +883,7 @@ public function completeSetup(): void
'status' => 'forming', 'status' => 'forming',
'metadata' => [ 'metadata' => [
'head_trainer_name' => $groupData['head_trainer_name'] ?? '', 'head_trainer_name' => $groupData['head_trainer_name'] ?? '',
'schedule_summary' => $groupData['schedule_summary'] ?? '', 'schedule_days' => $groupData['schedule_days'] ?? [],
'assigned_segments' => $groupData['assigned_segments'] ?? [], 'assigned_segments' => $groupData['assigned_segments'] ?? [],
'assigned_facility_index' => (int) ($groupData['facility_index'] ?? 0), 'assigned_facility_index' => (int) ($groupData['facility_index'] ?? 0),
], ],
...@@ -653,6 +903,9 @@ public function completeSetup(): void ...@@ -653,6 +903,9 @@ public function completeSetup(): void
'phone' => !empty($pData['phone']) ? $pData['phone'] : null, 'phone' => !empty($pData['phone']) ? $pData['phone'] : null,
'date_of_birth' => !empty($pData['date_of_birth']) ? $pData['date_of_birth'] : null, 'date_of_birth' => !empty($pData['date_of_birth']) ? $pData['date_of_birth'] : null,
'gender' => $pData['gender'] ?? null, 'gender' => $pData['gender'] ?? null,
'national_id' => !empty($pData['national_id']) ? $pData['national_id'] : null,
'classification' => $pData['classification'] ?? 'regular',
'medical_notes' => !empty($pData['medical_notes']) ? $pData['medical_notes'] : null,
'emergency_contact_name' => !empty($pData['guardian_name']) ? $pData['guardian_name'] : null, 'emergency_contact_name' => !empty($pData['guardian_name']) ? $pData['guardian_name'] : null,
'emergency_contact_phone' => !empty($pData['guardian_phone']) ? $pData['guardian_phone'] : null, 'emergency_contact_phone' => !empty($pData['guardian_phone']) ? $pData['guardian_phone'] : null,
'created_by' => $actor->id, 'created_by' => $actor->id,
...@@ -668,6 +921,7 @@ public function completeSetup(): void ...@@ -668,6 +921,7 @@ public function completeSetup(): void
'registration_date' => now()->toDateString(), 'registration_date' => now()->toDateString(),
'registration_source' => 'walk_in', 'registration_source' => 'walk_in',
'primary_activity_id' => $linkedActivity?->id, 'primary_activity_id' => $linkedActivity?->id,
'skill_level' => !empty($pData['skill_level']) ? $pData['skill_level'] : null,
'status' => 'registered', 'status' => 'registered',
'created_by' => $actor->id, 'created_by' => $actor->id,
]); ]);
...@@ -676,10 +930,10 @@ public function completeSetup(): void ...@@ -676,10 +930,10 @@ public function completeSetup(): void
if (!empty($pData['group_index']) && isset($createdGroups[(int) $pData['group_index']])) { if (!empty($pData['group_index']) && isset($createdGroups[(int) $pData['group_index']])) {
$assignedGroup = $createdGroups[(int) $pData['group_index']]; $assignedGroup = $createdGroups[(int) $pData['group_index']];
$participant->update([ $participant->update([
'metadata' => [ 'metadata' => array_merge($participant->metadata ?? [], [
'pending_group_id' => $assignedGroup->id, 'pending_group_id' => $assignedGroup->id,
'pending_group_name' => $assignedGroup->name_ar, 'pending_group_name' => $assignedGroup->name_ar,
], ]),
]); ]);
} }
} }
...@@ -693,7 +947,7 @@ public function completeSetup(): void ...@@ -693,7 +947,7 @@ public function completeSetup(): void
->where('slug', $userData['role']) ->where('slug', $userData['role'])
->first(); ->first();
$user = User::create([ $userAttributes = [
'academy_id' => $academyId, 'academy_id' => $academyId,
'name' => $userData['name'], 'name' => $userData['name'],
'email' => $userData['email'], 'email' => $userData['email'],
...@@ -701,7 +955,13 @@ public function completeSetup(): void ...@@ -701,7 +955,13 @@ public function completeSetup(): void
'status' => 'active', 'status' => 'active',
'role_id' => $role?->id, 'role_id' => $role?->id,
'force_password_change' => true, 'force_password_change' => true,
]); ];
if (!empty($userData['phone'])) {
$userAttributes['phone'] = $userData['phone'];
}
$user = User::create($userAttributes);
if ($role && $branch) { if ($role && $branch) {
$user->roles()->attach($role->id, [ $user->roles()->attach($role->id, [
...@@ -709,15 +969,59 @@ public function completeSetup(): void ...@@ -709,15 +969,59 @@ public function completeSetup(): void
'assigned_by' => $actor->id, 'assigned_by' => $actor->id,
]); ]);
} }
// Store specialization as a note in linked person record
if (!empty($userData['specialization']) && in_array($userData['role'], ['trainer', 'head_trainer'])) {
$trainerPerson = Person::create([
'academy_id' => $academyId,
'user_id' => $user->id,
'name' => $userData['name'],
'name_ar' => $userData['name'],
'notes' => $userData['specialization'],
'created_by' => $actor->id,
]);
$user->update(['person_id' => $trainerPerson->id]);
}
} }
// Save system settings // ─── Save ALL system settings ────────────────────────────
$settingsService = app(SettingsService::class);
// Step 9: Financial & Pricing
$settingsService->set('currency_code', $this->defaultCurrency, 'financial', 'string');
$settingsService->set('tax_enabled', $this->taxEnabled ? '1' : '0', 'financial', 'boolean');
$settingsService->set('tax_percentage', (string) $this->taxPercentage, 'financial', 'number');
$settingsService->set('invoice_prefix', $this->invoicePrefix, 'financial', 'string');
$settingsService->set('receipt_prefix', $this->receiptPrefix, 'financial', 'string');
$settingsService->set('payment_due_days', (string) $this->paymentDueDays, 'financial', 'integer');
$settingsService->set('wallet_enabled', $this->walletEnabled ? '1' : '0', 'financial', 'boolean');
$settingsService->set('accepted_payment_methods', json_encode($this->acceptedPaymentMethods), 'financial', 'json');
$settingsService->set('max_discount_percent', (string) $this->maxDiscountPercent, 'financial', 'integer');
$settingsService->set('global_max_discount', (string) $this->maxDiscountPercent, 'pricing', 'integer');
$settingsService->set('auto_apply_family_discount', $this->autoApplyFamilyDiscount ? '1' : '0', 'pricing', 'boolean');
$settingsService->set('family_discount_start_child', (string) $this->familyDiscountStartChild, 'pricing', 'integer');
$settingsService->set('trial_period_days', (string) $this->trialPeriodDays, 'pricing', 'integer');
// Step 10: Rules & Policies
$settingsService->set('participant_grace_period_minutes', (string) $this->gracePeriodParticipant, 'attendance', 'integer');
$settingsService->set('trainer_grace_period_minutes', (string) $this->gracePeriodTrainer, 'attendance', 'integer');
$settingsService->set('auto_absent_after_hours', (string) $this->autoAbsentAfterHours, 'attendance', 'integer');
$settingsService->set('consecutive_absences_for_suspend', (string) $this->consecutiveAbsencesForSuspend, 'attendance', 'integer');
$settingsService->set('min_attendance_percent', (string) $this->minAttendancePercent, 'attendance', 'integer');
$settingsService->set('require_medical_report', $this->requireMedicalReport ? '1' : '0', 'enrollment', 'boolean');
$settingsService->set('require_guardian_for_minors', $this->requireGuardianForMinors ? '1' : '0', 'enrollment', 'boolean');
$settingsService->set('minor_age_threshold', (string) $this->minorAgeThreshold, 'enrollment', 'integer');
$settingsService->set('allow_waitlist', $this->allowWaitlist ? '1' : '0', 'enrollment', 'boolean');
$settingsService->set('freeze_max_days', (string) $this->freezeMaxDays, 'enrollment', 'integer');
$settingsService->set('email_enabled', $this->emailEnabled ? '1' : '0', 'notifications', 'boolean');
$settingsService->set('sms_enabled', $this->smsEnabled ? '1' : '0', 'notifications', 'boolean');
// Step 11: General Settings
$settingsService->set('default_currency', $this->defaultCurrency, 'general', 'string'); $settingsService->set('default_currency', $this->defaultCurrency, 'general', 'string');
$settingsService->set('timezone', $this->timezone, 'general', 'string'); $settingsService->set('timezone', $this->timezone, 'general', 'string');
$settingsService->set('grace_period_participant', (string) $this->gracePeriodParticipant, 'attendance', 'integer'); $settingsService->set('week_starts_on', $this->weekStartsOn, 'general', 'string');
$settingsService->set('grace_period_trainer', (string) $this->gracePeriodTrainer, 'attendance', 'integer'); $settingsService->set('date_format', $this->dateFormat, 'general', 'string');
$settingsService->set('max_discount_percent', (string) $this->maxDiscountPercent, 'pricing', 'integer'); $settingsService->set('time_format', $this->timeFormat, 'general', 'string');
$settingsService->set('pagination_size', (string) $this->paginationSize, 'general', 'integer');
// Create default receipt template // Create default receipt template
$receiptService = app(\App\Domain\POS\Services\ReceiptService::class); $receiptService = app(\App\Domain\POS\Services\ReceiptService::class);
...@@ -744,16 +1048,75 @@ private function generateParticipantNumber(int $academyId, int $index): string ...@@ -744,16 +1048,75 @@ private function generateParticipantNumber(int $academyId, int $index): string
public function getStepLabels(): array public function getStepLabels(): array
{ {
return [ return [
1 => __('مرحباً'), 1 => __('الأكاديمية'),
2 => __('الفروع'), 2 => __('الفروع'),
3 => __('الأنشطة'), 3 => __('الأنشطة'),
4 => __('البرامج'), 4 => __('البرامج'),
5 => __('المنشآت'), 5 => __('المنشآت'),
6 => __('المجموعات'), 6 => __('المجموعات'),
7 => __('المشتركين'), 7 => __('المشتركين'),
8 => __('المستخدمين'), 8 => __('الطاقم'),
9 => __('الإعدادات'), 9 => __('المالية'),
10 => __('تم!'), 10 => __('السياسات'),
11 => __('الإعدادات'),
12 => __('تم!'),
];
}
public function getAcademyTypes(): array
{
return [
'sports_club' => __('نادي رياضي'),
'fitness_center' => __('مركز لياقة'),
'educational_center' => __('مركز تعليمي'),
'multi_sport' => __('أكاديمية متعددة'),
'swimming_academy' => __('أكاديمية سباحة'),
'martial_arts' => __('فنون قتالية'),
'other' => __('أخرى'),
];
}
public function getDayNames(): array
{
return [
0 => __('الأحد'),
1 => __('الإثنين'),
2 => __('الثلاثاء'),
3 => __('الأربعاء'),
4 => __('الخميس'),
5 => __('الجمعة'),
6 => __('السبت'),
];
}
public function getPaymentFrequencies(): array
{
return [
'monthly' => __('شهري'),
'quarterly' => __('ربع سنوي'),
'semester' => __('نصف سنوي'),
'annual' => __('سنوي'),
];
}
public function getClassifications(): array
{
return [
'regular' => __('عادي'),
'vip' => __('مميز'),
'scholarship' => __('منحة'),
'staff_child' => __('ابن موظف'),
'trial' => __('تجريبي'),
];
}
public function getSkillLevels(): array
{
return [
'beginner' => __('مبتدئ'),
'intermediate' => __('متوسط'),
'advanced' => __('متقدم'),
'professional' => __('محترف'),
]; ];
} }
...@@ -791,6 +1154,7 @@ public function getUserRoles(): array ...@@ -791,6 +1154,7 @@ public function getUserRoles(): array
'trainer' => __('مدرب'), 'trainer' => __('مدرب'),
'receptionist' => __('موظف استقبال'), 'receptionist' => __('موظف استقبال'),
'accountant' => __('محاسب'), 'accountant' => __('محاسب'),
'data_entry' => __('مدخل بيانات'),
]; ];
} }
......
This source diff could not be displayed because it is too large. You can view the blob instead.
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