Commit 4cb2acc4 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Remove 23 hardcoded business decisions: all financial/operational behaviors...

Remove 23 hardcoded business decisions: all financial/operational behaviors now configurable per-academy

Every penalty, compensation, auto-invoice, and automation is now guarded by a
SettingsService toggle (default OFF for financial, ON for operational). Added
88 system settings across 8 groups with Arabic labels, conditional UI visibility
for dependent fields, and a new "رواتب وتعويضات" admin tab.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 101a0f35
...@@ -17,9 +17,6 @@ ...@@ -17,9 +17,6 @@
class AttendanceMarkingService class AttendanceMarkingService
{ {
private const DEFAULT_PARTICIPANT_GRACE_MINUTES = 15;
private const DEFAULT_TRAINER_GRACE_MINUTES = 10;
public function __construct( public function __construct(
private SettingsService $settings, private SettingsService $settings,
) {} ) {}
...@@ -111,9 +108,10 @@ public function markCheckOut(AttendanceRecord $record, User $marker, ?Carbon $ch ...@@ -111,9 +108,10 @@ public function markCheckOut(AttendanceRecord $record, User $marker, ?Carbon $ch
// If left early (before session end), mark accordingly // If left early (before session end), mark accordingly
$session = $record->session; $session = $record->session;
$sessionEnd = Carbon::parse($session->session_date->format('Y-m-d') . ' ' . $session->end_time); $sessionEnd = Carbon::parse($session->session_date->format('Y-m-d') . ' ' . $session->end_time);
$earlyLeaveMinutes = (int) $this->settings->get('early_leave_threshold_minutes', 10);
$status = $record->status; $status = $record->status;
if ($checkOutTime->lt($sessionEnd->subMinutes(10))) { if ($checkOutTime->lt($sessionEnd->subMinutes($earlyLeaveMinutes))) {
$status = AttendanceStatus::LeftEarly; $status = AttendanceStatus::LeftEarly;
} }
...@@ -184,10 +182,10 @@ private function getGraceMinutes(AttendanceRecord $record): int ...@@ -184,10 +182,10 @@ private function getGraceMinutes(AttendanceRecord $record): int
{ {
// Trainers (users) get less grace // Trainers (users) get less grace
if ($record->subject_type === \App\Models\User::class) { if ($record->subject_type === \App\Models\User::class) {
return self::DEFAULT_TRAINER_GRACE_MINUTES; return (int) $this->settings->get('trainer_grace_minutes', 10);
} }
return self::DEFAULT_PARTICIPANT_GRACE_MINUTES; return (int) $this->settings->get('participant_grace_minutes', 15);
} }
private function enforceMedicalCertificate(AttendanceRecord $record): void private function enforceMedicalCertificate(AttendanceRecord $record): void
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
use App\Domain\Attendance\Events\AttendanceMarked; use App\Domain\Attendance\Events\AttendanceMarked;
use App\Domain\HR\Models\Trainer; use App\Domain\HR\Models\Trainer;
use App\Domain\HR\Services\CompensationCalculatorService; use App\Domain\HR\Services\CompensationCalculatorService;
use App\Domain\Shared\Services\SettingsService;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
...@@ -12,10 +13,15 @@ class GenerateTrainerCompensation implements ShouldQueue ...@@ -12,10 +13,15 @@ class GenerateTrainerCompensation implements ShouldQueue
{ {
public function __construct( public function __construct(
private CompensationCalculatorService $calculator, private CompensationCalculatorService $calculator,
private SettingsService $settings,
) {} ) {}
public function handle(AttendanceMarked $event): void public function handle(AttendanceMarked $event): void
{ {
if (!(bool) $this->settings->get('auto_trainer_compensation_enabled', false)) {
return;
}
try { try {
$record = $event->record; $record = $event->record;
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
use App\Domain\Training\Events\SessionCancelled; use App\Domain\Training\Events\SessionCancelled;
use App\Domain\HR\Models\Trainer; use App\Domain\HR\Models\Trainer;
use App\Domain\HR\Services\CompensationCalculatorService; use App\Domain\HR\Services\CompensationCalculatorService;
use App\Domain\Shared\Services\SettingsService;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
...@@ -12,10 +13,15 @@ class HandleSessionCancelled implements ShouldQueue ...@@ -12,10 +13,15 @@ class HandleSessionCancelled implements ShouldQueue
{ {
public function __construct( public function __construct(
private CompensationCalculatorService $calculator, private CompensationCalculatorService $calculator,
private SettingsService $settings,
) {} ) {}
public function handle(SessionCancelled $event): void public function handle(SessionCancelled $event): void
{ {
if (!(bool) $this->settings->get('cancelled_session_pay_enabled', false)) {
return;
}
try { try {
$session = $event->session; $session = $event->session;
......
...@@ -28,6 +28,10 @@ public function calculateForSession( ...@@ -28,6 +28,10 @@ public function calculateForSession(
string $attendanceStatus, // 'present', 'late', 'substitute' string $attendanceStatus, // 'present', 'late', 'substitute'
User $actor, User $actor,
): ?TrainerCompensation { ): ?TrainerCompensation {
if (!(bool) $this->settings->get('auto_trainer_compensation_enabled', false)) {
return null;
}
// Don't double-create // Don't double-create
$existing = TrainerCompensation::where('trainer_id', $trainer->id) $existing = TrainerCompensation::where('trainer_id', $trainer->id)
->where('training_session_id', $trainingSessionId) ->where('training_session_id', $trainingSessionId)
...@@ -75,7 +79,11 @@ public function calculatePenalty( ...@@ -75,7 +79,11 @@ public function calculatePenalty(
int $attendanceRecordId, int $attendanceRecordId,
User $actor, User $actor,
): ?TrainerCompensation { ): ?TrainerCompensation {
$penaltyAmount = (int) $this->settings->get('absence_penalty_amount', 10000); if (!(bool) $this->settings->get('trainer_absence_penalty_enabled', false)) {
return null;
}
$penaltyAmount = (int) $this->settings->get('absence_penalty_amount', 0);
if ($penaltyAmount <= 0) { if ($penaltyAmount <= 0) {
return null; return null;
} }
...@@ -117,7 +125,11 @@ public function calculateLatePenalty( ...@@ -117,7 +125,11 @@ public function calculateLatePenalty(
int $attendanceRecordId, int $attendanceRecordId,
User $actor, User $actor,
): ?TrainerCompensation { ): ?TrainerCompensation {
$latePenalty = (int) $this->settings->get('late_penalty_amount', 5000); if (!(bool) $this->settings->get('trainer_late_penalty_enabled', false)) {
return null;
}
$latePenalty = (int) $this->settings->get('late_penalty_amount', 0);
if ($latePenalty <= 0) { if ($latePenalty <= 0) {
return null; return null;
} }
...@@ -159,7 +171,11 @@ public function calculateCancelledSessionPay( ...@@ -159,7 +171,11 @@ public function calculateCancelledSessionPay(
int $trainingSessionId, int $trainingSessionId,
User $actor, User $actor,
): ?TrainerCompensation { ): ?TrainerCompensation {
$payPercent = (int) $this->settings->get('cancelled_session_pay_percent', 50); if (!(bool) $this->settings->get('cancelled_session_pay_enabled', false)) {
return null;
}
$payPercent = (int) $this->settings->get('cancelled_session_pay_percent', 0);
if ($payPercent <= 0) { if ($payPercent <= 0) {
return null; return null;
} }
...@@ -208,6 +224,10 @@ public function calculatePlayerPay( ...@@ -208,6 +224,10 @@ public function calculatePlayerPay(
return null; return null;
} }
if (!(bool) $this->settings->get('per_player_compensation_enabled', false)) {
return null;
}
// Resolve trainer's user_id (head_trainer_id on groups references users.id) // Resolve trainer's user_id (head_trainer_id on groups references users.id)
$userId = $trainer->employee?->user_id; $userId = $trainer->employee?->user_id;
if (!$userId) { if (!$userId) {
...@@ -270,6 +290,10 @@ public function calculateRevenueShare( ...@@ -270,6 +290,10 @@ public function calculateRevenueShare(
return null; return null;
} }
if (!(bool) $this->settings->get('revenue_share_enabled', false)) {
return null;
}
// Sum payments received for groups where trainer is head_trainer // Sum payments received for groups where trainer is head_trainer
$userId = $trainer->employee?->user_id; $userId = $trainer->employee?->user_id;
if (!$userId) { if (!$userId) {
......
...@@ -70,6 +70,10 @@ public function getOrCreateCurrentPeriod(int $academyId, User $actor): PayrollPe ...@@ -70,6 +70,10 @@ public function getOrCreateCurrentPeriod(int $academyId, User $actor): PayrollPe
public function calculatePeriod(PayrollPeriod $period, User $actor): PayrollPeriod public function calculatePeriod(PayrollPeriod $period, User $actor): PayrollPeriod
{ {
return DB::transaction(function () use ($period, $actor) { return DB::transaction(function () use ($period, $actor) {
if (!(bool) $this->settings->get('payroll_enabled', false)) {
throw new DomainException('نظام الرواتب غير مفعل — يرجى تفعيله من إعدادات النظام');
}
$period->update(['status' => PayrollPeriodStatus::Calculating]); $period->update(['status' => PayrollPeriodStatus::Calculating]);
$academyId = $period->academy_id; $academyId = $period->academy_id;
...@@ -158,8 +162,9 @@ public function generatePayslip( ...@@ -158,8 +162,9 @@ public function generatePayslip(
// Advance deduction // Advance deduction
$advanceDeduction = $this->calculateAdvanceDeduction($trainer); $advanceDeduction = $this->calculateAdvanceDeduction($trainer);
// Insurance — Egypt: 11% employee share applied to base salary only // Insurance — Egypt: employee share applied to base salary only
$insurancePercent = (float) $this->settings->get('social_insurance_percent', 11); $insuranceEnabled = (bool) $this->settings->get('social_insurance_enabled', false);
$insurancePercent = $insuranceEnabled ? (float) $this->settings->get('social_insurance_percent', 0) : 0;
$insuranceAmount = 0; $insuranceAmount = 0;
if ($baseSalary > 0 && $insurancePercent > 0) { if ($baseSalary > 0 && $insurancePercent > 0) {
$insuranceAmount = (int) round($baseSalary * $insurancePercent / 100); $insuranceAmount = (int) round($baseSalary * $insurancePercent / 100);
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
use App\Domain\Inventory\Models\Product; use App\Domain\Inventory\Models\Product;
use App\Domain\Inventory\Models\Warehouse; use App\Domain\Inventory\Models\Warehouse;
use App\Domain\Inventory\Services\InventoryService; use App\Domain\Inventory\Services\InventoryService;
use App\Domain\Shared\Services\SettingsService;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
...@@ -14,10 +15,15 @@ class CreateReceivingMovements implements ShouldQueue ...@@ -14,10 +15,15 @@ class CreateReceivingMovements implements ShouldQueue
{ {
public function __construct( public function __construct(
private InventoryService $inventoryService, private InventoryService $inventoryService,
private SettingsService $settings,
) {} ) {}
public function handle(PurchaseOrderReceived $event): void public function handle(PurchaseOrderReceived $event): void
{ {
if (!(bool) $this->settings->get('auto_receive_inventory_enabled', true)) {
return;
}
try { try {
$purchaseOrder = $event->purchaseOrder; $purchaseOrder = $event->purchaseOrder;
$warehouse = $purchaseOrder->warehouse; $warehouse = $purchaseOrder->warehouse;
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
use App\Domain\Inventory\Models\Warehouse; use App\Domain\Inventory\Models\Warehouse;
use App\Domain\Inventory\Services\InventoryService; use App\Domain\Inventory\Services\InventoryService;
use App\Domain\Shared\Services\PlatformFeeService; use App\Domain\Shared\Services\PlatformFeeService;
use App\Domain\Shared\Services\SettingsService;
use App\Domain\Participant\Models\Participant; use App\Domain\Participant\Models\Participant;
use App\Domain\POS\Enums\POSItemType; use App\Domain\POS\Enums\POSItemType;
use App\Domain\POS\Enums\POSPaymentMethod; use App\Domain\POS\Enums\POSPaymentMethod;
...@@ -33,6 +34,7 @@ public function __construct( ...@@ -33,6 +34,7 @@ public function __construct(
private CashSessionService $cashSessionService, private CashSessionService $cashSessionService,
private PlatformFeeService $platformFeeService, private PlatformFeeService $platformFeeService,
private InventoryService $inventoryService, private InventoryService $inventoryService,
private SettingsService $settings,
) {} ) {}
/** /**
...@@ -315,6 +317,10 @@ private function createEnrollmentIfNeeded(Participant $participant, int $program ...@@ -315,6 +317,10 @@ private function createEnrollmentIfNeeded(Participant $participant, int $program
*/ */
private function deductInventoryIfTracked(int $productId, int $quantity, int $branchId, POSTransaction $posTransaction, User $cashier): void private function deductInventoryIfTracked(int $productId, int $quantity, int $branchId, POSTransaction $posTransaction, User $cashier): void
{ {
if (!(bool) $this->settings->get('auto_inventory_deduction_on_sale', true)) {
return;
}
$product = Product::find($productId); $product = Product::find($productId);
if (!$product || !$product->track_inventory) { if (!$product || !$product->track_inventory) {
return; return;
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
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\Scheduling\Events\AssignmentCreated; use App\Domain\Scheduling\Events\AssignmentCreated;
use App\Domain\Shared\Services\SettingsService;
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 App\Models\User; use App\Models\User;
...@@ -13,8 +14,16 @@ ...@@ -13,8 +14,16 @@
class GenerateTrainerAttendance implements ShouldQueue class GenerateTrainerAttendance implements ShouldQueue
{ {
public function __construct(
private SettingsService $settings,
) {}
public function handle(AssignmentCreated $event): void public function handle(AssignmentCreated $event): void
{ {
if (!(bool) $this->settings->get('trainer_attendance_tracking_enabled', true)) {
return;
}
try { try {
$assignment = $event->assignment; $assignment = $event->assignment;
......
...@@ -16,6 +16,7 @@ class SystemSetting extends Model ...@@ -16,6 +16,7 @@ class SystemSetting extends Model
'key', 'key',
'value', 'value',
'type', 'type',
'label_ar',
'description_ar', 'description_ar',
'is_public', 'is_public',
]; ];
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
use App\Domain\Participant\Models\Participant; use App\Domain\Participant\Models\Participant;
use App\Domain\Pricing\Services\PricingService; use App\Domain\Pricing\Services\PricingService;
use App\Domain\Shared\Exceptions\DomainException; use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Services\SettingsService;
use App\Domain\Training\Events\EnrollmentCancelled; use App\Domain\Training\Events\EnrollmentCancelled;
use App\Domain\Training\Events\EnrollmentCompleted; use App\Domain\Training\Events\EnrollmentCompleted;
use App\Domain\Training\Events\EnrollmentCreated; use App\Domain\Training\Events\EnrollmentCreated;
...@@ -23,6 +24,7 @@ public function __construct( ...@@ -23,6 +24,7 @@ public function __construct(
private readonly TrainingGroupService $groupService, private readonly TrainingGroupService $groupService,
private readonly PricingService $pricingService, private readonly PricingService $pricingService,
private readonly InvoiceService $invoiceService, private readonly InvoiceService $invoiceService,
private readonly SettingsService $settings,
) {} ) {}
public function enroll(Participant $participant, TrainingGroup $group, User $actor, ?array $options = []): Enrollment public function enroll(Participant $participant, TrainingGroup $group, User $actor, ?array $options = []): Enrollment
...@@ -88,7 +90,7 @@ public function enroll(Participant $participant, TrainingGroup $group, User $act ...@@ -88,7 +90,7 @@ public function enroll(Participant $participant, TrainingGroup $group, User $act
$this->groupService->incrementCount($group); $this->groupService->incrementCount($group);
// Auto-create invoice if program has a price (skip if invoice already provided via options) // Auto-create invoice if program has a price (skip if invoice already provided via options)
if (empty($options['invoice_id'])) { if (empty($options['invoice_id']) && (bool) $this->settings->get('auto_invoice_on_enrollment', false)) {
$this->createEnrollmentInvoice($enrollment, $participant, $group, $actor); $this->createEnrollmentInvoice($enrollment, $participant, $group, $actor);
} }
...@@ -161,7 +163,11 @@ private function findOrCreateGroup(TrainingProgram $program, User $actor, bool $ ...@@ -161,7 +163,11 @@ private function findOrCreateGroup(TrainingProgram $program, User $actor, bool $
return $leastFullGroup; return $leastFullGroup;
} }
// Auto-create a new group // Auto-create a new group (only if enabled)
if (!(bool) $this->settings->get('auto_create_groups_enabled', false)) {
throw new DomainException('لا توجد مجموعة متاحة — يرجى إنشاء مجموعة يدوياً');
}
return $this->autoCreateGroup($program, $actor); return $this->autoCreateGroup($program, $actor);
} }
......
...@@ -38,6 +38,7 @@ public function loadSettings(): void ...@@ -38,6 +38,7 @@ public function loadSettings(): void
$this->settingsMeta = $records->mapWithKeys(fn ($s) => [ $this->settingsMeta = $records->mapWithKeys(fn ($s) => [
$s->key => [ $s->key => [
'type' => $s->type, 'type' => $s->type,
'label_ar' => $s->label_ar,
'description_ar' => $s->description_ar, 'description_ar' => $s->description_ar,
], ],
])->toArray(); ])->toArray();
...@@ -57,6 +58,17 @@ public function save(SettingsService $service): void ...@@ -57,6 +58,17 @@ public function save(SettingsService $service): void
session()->flash('success', __('تم حفظ الإعدادات بنجاح')); session()->flash('success', __('تم حفظ الإعدادات بنجاح'));
} }
/**
* Settings that should only be visible when their parent boolean is enabled.
* Format: 'child_key' => 'parent_boolean_key'
*/
public static array $dependsOn = [
'absence_penalty_amount' => 'trainer_absence_penalty_enabled',
'late_penalty_amount' => 'trainer_late_penalty_enabled',
'cancelled_session_pay_percent' => 'cancelled_session_pay_enabled',
'social_insurance_percent' => 'social_insurance_enabled',
];
public function render() public function render()
{ {
$groups = [ $groups = [
...@@ -66,10 +78,13 @@ public function render() ...@@ -66,10 +78,13 @@ public function render()
'pricing' => 'تسعير', 'pricing' => 'تسعير',
'notifications' => 'إشعارات', 'notifications' => 'إشعارات',
'enrollment' => 'تسجيلات', 'enrollment' => 'تسجيلات',
'inventory' => 'مخزون',
'payroll' => 'رواتب وتعويضات',
]; ];
return view('livewire.settings.system-settings-form', [ return view('livewire.settings.system-settings-form', [
'groups' => $groups, 'groups' => $groups,
'dependsOn' => static::$dependsOn,
]); ]);
} }
} }
<?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('system_settings', function (Blueprint $table) {
$table->string('label_ar')->nullable()->after('type');
});
}
public function down(): void
{
Schema::table('system_settings', function (Blueprint $table) {
$table->dropColumn('label_ar');
});
}
};
...@@ -19,82 +19,108 @@ public function run(): void ...@@ -19,82 +19,108 @@ public function run(): void
$settings = [ $settings = [
// General // General
['group' => 'general', 'key' => 'academy_name_ar', 'value' => 'أكاديمية الكابتن', 'type' => 'string', 'description_ar' => 'اسم الأكاديمية بالعربية (يظهر في الفواتير والتقارير)'], ['group' => 'general', 'key' => 'academy_name_ar', 'value' => 'أكاديمية الكابتن', 'type' => 'string', 'label_ar' => 'اسم الأكاديمية (عربي)', 'description_ar' => 'اسم الأكاديمية بالعربية (يظهر في الفواتير والتقارير)'],
['group' => 'general', 'key' => 'academy_name_en', 'value' => 'El Captain Academy', 'type' => 'string', 'description_ar' => 'اسم الأكاديمية بالإنجليزية'], ['group' => 'general', 'key' => 'academy_name_en', 'value' => 'El Captain Academy', 'type' => 'string', 'label_ar' => 'اسم الأكاديمية (إنجليزي)', 'description_ar' => 'اسم الأكاديمية بالإنجليزية'],
['group' => 'general', 'key' => 'default_language', 'value' => 'ar', 'type' => 'string', 'description_ar' => 'اللغة الافتراضية للنظام (ar أو en)'], ['group' => 'general', 'key' => 'default_language', 'value' => 'ar', 'type' => 'string', 'label_ar' => 'اللغة الافتراضية', 'description_ar' => 'اللغة الافتراضية للنظام (ar أو en)'],
['group' => 'general', 'key' => 'date_format', 'value' => 'Y-m-d', 'type' => 'string', 'description_ar' => 'صيغة التاريخ (Y-m-d أو d/m/Y)'], ['group' => 'general', 'key' => 'date_format', 'value' => 'Y-m-d', 'type' => 'string', 'label_ar' => 'صيغة التاريخ', 'description_ar' => 'صيغة التاريخ (Y-m-d أو d/m/Y)'],
['group' => 'general', 'key' => 'time_format', 'value' => 'H:i', 'type' => 'string', 'description_ar' => 'صيغة الوقت (H:i أو h:i A)'], ['group' => 'general', 'key' => 'time_format', 'value' => 'H:i', 'type' => 'string', 'label_ar' => 'صيغة الوقت', 'description_ar' => 'صيغة الوقت (H:i أو h:i A)'],
['group' => 'general', 'key' => 'week_starts_on', 'value' => '6', 'type' => 'integer', 'description_ar' => 'أول يوم في الأسبوع (6=السبت، 0=الأحد، 1=الإثنين)'], ['group' => 'general', 'key' => 'week_starts_on', 'value' => '6', 'type' => 'integer', 'label_ar' => 'أول يوم في الأسبوع', 'description_ar' => 'أول يوم في الأسبوع (6=السبت، 0=الأحد، 1=الإثنين)'],
['group' => 'general', 'key' => 'pagination_size', 'value' => '25', 'type' => 'integer', 'description_ar' => 'عدد العناصر في كل صفحة'], ['group' => 'general', 'key' => 'pagination_size', 'value' => '25', 'type' => 'integer', 'label_ar' => 'عدد العناصر في الصفحة', 'description_ar' => 'عدد العناصر في كل صفحة'],
['group' => 'general', 'key' => 'session_timeout_minutes', 'value' => '120', 'type' => 'integer', 'description_ar' => 'مهلة انتهاء الجلسة بالدقائق'], ['group' => 'general', 'key' => 'session_timeout_minutes', 'value' => '120', 'type' => 'integer', 'label_ar' => 'مهلة انتهاء الجلسة (دقائق)', 'description_ar' => 'مهلة انتهاء الجلسة بالدقائق'],
// Financial // Financial
['group' => 'financial', 'key' => 'currency_code', 'value' => 'EGP', 'type' => 'string', 'description_ar' => 'رمز العملة (EGP, SAR, AED)'], ['group' => 'financial', 'key' => 'currency_code', 'value' => 'EGP', 'type' => 'string', 'label_ar' => 'رمز العملة', 'description_ar' => 'رمز العملة (EGP, SAR, AED)'],
['group' => 'financial', 'key' => 'currency_symbol', 'value' => 'ج.م', 'type' => 'string', 'description_ar' => 'رمز العملة للعرض'], ['group' => 'financial', 'key' => 'currency_symbol', 'value' => 'ج.م', 'type' => 'string', 'label_ar' => 'رمز العملة للعرض', 'description_ar' => 'رمز العملة للعرض'],
['group' => 'financial', 'key' => 'tax_enabled', 'value' => '0', 'type' => 'boolean', 'description_ar' => 'تفعيل الضريبة على الفواتير'], ['group' => 'financial', 'key' => 'tax_enabled', 'value' => '0', 'type' => 'boolean', 'label_ar' => 'تفعيل الضريبة', 'description_ar' => 'تفعيل الضريبة على الفواتير'],
['group' => 'financial', 'key' => 'tax_percentage', 'value' => '14', 'type' => 'float', 'description_ar' => 'نسبة الضريبة المضافة (%)'], ['group' => 'financial', 'key' => 'tax_percentage', 'value' => '14', 'type' => 'float', 'label_ar' => 'نسبة الضريبة (%)', 'description_ar' => 'نسبة الضريبة المضافة (%)'],
['group' => 'financial', 'key' => 'tax_registration_number', 'value' => '', 'type' => 'string', 'description_ar' => 'رقم التسجيل الضريبي'], ['group' => 'financial', 'key' => 'tax_registration_number', 'value' => '', 'type' => 'string', 'label_ar' => 'رقم التسجيل الضريبي', 'description_ar' => 'رقم التسجيل الضريبي'],
['group' => 'financial', 'key' => 'invoice_prefix', 'value' => 'INV', 'type' => 'string', 'description_ar' => 'بادئة رقم الفاتورة'], ['group' => 'financial', 'key' => 'invoice_prefix', 'value' => 'INV', 'type' => 'string', 'label_ar' => 'بادئة رقم الفاتورة', 'description_ar' => 'بادئة رقم الفاتورة'],
['group' => 'financial', 'key' => 'receipt_prefix', 'value' => 'RCP', 'type' => 'string', 'description_ar' => 'بادئة رقم الإيصال'], ['group' => 'financial', 'key' => 'receipt_prefix', 'value' => 'RCP', 'type' => 'string', 'label_ar' => 'بادئة رقم الإيصال', 'description_ar' => 'بادئة رقم الإيصال'],
['group' => 'financial', 'key' => 'payment_due_days', 'value' => '7', 'type' => 'integer', 'description_ar' => 'عدد أيام استحقاق الدفع بعد إصدار الفاتورة'], ['group' => 'financial', 'key' => 'payment_due_days', 'value' => '7', 'type' => 'integer', 'label_ar' => 'أيام استحقاق الدفع', 'description_ar' => 'عدد أيام استحقاق الدفع بعد إصدار الفاتورة'],
['group' => 'financial', 'key' => 'max_discount_percent', 'value' => '50', 'type' => 'integer', 'description_ar' => 'الحد الأقصى لنسبة الخصم (%)'], ['group' => 'financial', 'key' => 'max_discount_percent', 'value' => '50', 'type' => 'integer', 'label_ar' => 'الحد الأقصى للخصم (%)', 'description_ar' => 'الحد الأقصى لنسبة الخصم (%)'],
['group' => 'financial', 'key' => 'refund_requires_approval', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'هل يتطلب الاسترداد موافقة مسبقة؟'], ['group' => 'financial', 'key' => 'refund_requires_approval', 'value' => '1', 'type' => 'boolean', 'label_ar' => 'الاسترداد يتطلب موافقة', 'description_ar' => 'هل يتطلب الاسترداد موافقة مسبقة؟'],
['group' => 'financial', 'key' => 'refund_approval_threshold', 'value' => '50000', 'type' => 'integer', 'description_ar' => 'حد مبلغ الاسترداد الذي يتطلب موافقة (بالقروش)'], ['group' => 'financial', 'key' => 'refund_approval_threshold', 'value' => '50000', 'type' => 'integer', 'label_ar' => 'حد مبلغ الاسترداد (بالقرش)', 'description_ar' => 'حد مبلغ الاسترداد الذي يتطلب موافقة (بالقروش)'],
['group' => 'financial', 'key' => 'wallet_enabled', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'تفعيل نظام المحفظة الإلكترونية'], ['group' => 'financial', 'key' => 'wallet_enabled', 'value' => '1', 'type' => 'boolean', 'label_ar' => 'تفعيل المحفظة', 'description_ar' => 'تفعيل نظام المحفظة الإلكترونية'],
['group' => 'financial', 'key' => 'installment_late_fee', 'value' => '500', 'type' => 'integer', 'description_ar' => 'غرامة التأخير على الأقساط (بالقروش)'], ['group' => 'financial', 'key' => 'installment_late_fee', 'value' => '500', 'type' => 'integer', 'label_ar' => 'غرامة تأخير الأقساط (بالقرش)', 'description_ar' => 'غرامة التأخير على الأقساط (بالقروش)'],
['group' => 'financial', 'key' => 'auto_overdue_after_days', 'value' => '3', 'type' => 'integer', 'description_ar' => 'عدد الأيام بعد تاريخ الاستحقاق لتحويل الفاتورة لمتأخرة'], ['group' => 'financial', 'key' => 'auto_overdue_after_days', 'value' => '3', 'type' => 'integer', 'label_ar' => 'أيام التحويل لمتأخرة', 'description_ar' => 'عدد الأيام بعد تاريخ الاستحقاق لتحويل الفاتورة لمتأخرة'],
['group' => 'financial', 'key' => 'platform_fee_customer_pays', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'رسوم المنصة يدفعها العميل (مفعّل: تُضاف للإجمالي — معطّل: تُخصم من إيراد الأكاديمية)'], ['group' => 'financial', 'key' => 'platform_fee_customer_pays', 'value' => '1', 'type' => 'boolean', 'label_ar' => 'العميل يدفع رسوم المنصة', 'description_ar' => 'رسوم المنصة يدفعها العميل (مفعّل: تُضاف للإجمالي — معطّل: تُخصم من إيراد الأكاديمية)'],
// Attendance // Attendance
['group' => 'attendance', 'key' => 'participant_grace_period_minutes', 'value' => '15', 'type' => 'integer', 'description_ar' => 'فترة السماح بالتأخير للمشاركين (بالدقائق)'], ['group' => 'attendance', 'key' => 'participant_grace_period_minutes', 'value' => '15', 'type' => 'integer', 'label_ar' => 'فترة السماح للمشاركين (دقائق)', 'description_ar' => 'فترة السماح بالتأخير للمشاركين (بالدقائق)'],
['group' => 'attendance', 'key' => 'trainer_grace_period_minutes', 'value' => '10', 'type' => 'integer', 'description_ar' => 'فترة السماح بالتأخير للمدربين (بالدقائق)'], ['group' => 'attendance', 'key' => 'trainer_grace_period_minutes', 'value' => '10', 'type' => 'integer', 'label_ar' => 'فترة السماح للمدربين (دقائق)', 'description_ar' => 'فترة السماح بالتأخير للمدربين (بالدقائق)'],
['group' => 'attendance', 'key' => 'auto_absent_after_hours', 'value' => '2', 'type' => 'integer', 'description_ar' => 'ساعات بعد نهاية الحصة لتسجيل غياب تلقائي'], ['group' => 'attendance', 'key' => 'auto_absent_after_hours', 'value' => '2', 'type' => 'integer', 'label_ar' => 'ساعات الغياب التلقائي', 'description_ar' => 'ساعات بعد نهاية الحصة لتسجيل غياب تلقائي'],
['group' => 'attendance', 'key' => 'consecutive_absences_for_suspend', 'value' => '5', 'type' => 'integer', 'description_ar' => 'عدد الغيابات المتتالية لإيقاف المشترك'], ['group' => 'attendance', 'key' => 'consecutive_absences_for_suspend', 'value' => '5', 'type' => 'integer', 'label_ar' => 'غيابات متتالية للإيقاف', 'description_ar' => 'عدد الغيابات المتتالية لإيقاف المشترك'],
['group' => 'attendance', 'key' => 'min_attendance_percent', 'value' => '75', 'type' => 'integer', 'description_ar' => 'الحد الأدنى لنسبة الحضور (%)'], ['group' => 'attendance', 'key' => 'min_attendance_percent', 'value' => '75', 'type' => 'integer', 'label_ar' => 'الحد الأدنى لنسبة الحضور (%)', 'description_ar' => 'الحد الأدنى لنسبة الحضور (%)'],
['group' => 'attendance', 'key' => 'notify_guardian_on_absence', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'إشعار ولي الأمر عند الغياب'], ['group' => 'attendance', 'key' => 'notify_guardian_on_absence', 'value' => '1', 'type' => 'boolean', 'label_ar' => 'إشعار ولي الأمر عند الغياب', 'description_ar' => 'إشعار ولي الأمر عند الغياب'],
['group' => 'attendance', 'key' => 'allow_excuse_after_hours', 'value' => '48', 'type' => 'integer', 'description_ar' => 'مدة صلاحية تقديم عذر بعد الحصة (بالساعات)'], ['group' => 'attendance', 'key' => 'allow_excuse_after_hours', 'value' => '48', 'type' => 'integer', 'label_ar' => 'صلاحية تقديم عذر (ساعات)', 'description_ar' => 'مدة صلاحية تقديم عذر بعد الحصة (بالساعات)'],
['group' => 'attendance', 'key' => 'qr_check_in_enabled', 'value' => '0', 'type' => 'boolean', 'description_ar' => 'تفعيل تسجيل الحضور بـ QR Code'], ['group' => 'attendance', 'key' => 'qr_check_in_enabled', 'value' => '0', 'type' => 'boolean', 'label_ar' => 'حضور QR Code', 'description_ar' => 'تفعيل تسجيل الحضور بـ QR Code'],
// New attendance settings
['group' => 'attendance', 'key' => 'trainer_attendance_tracking_enabled', 'value' => '1', 'type' => 'boolean', 'label_ar' => 'تتبع حضور المدربين', 'description_ar' => 'إنشاء سجلات حضور متوقعة للمدربين عند تعيينهم'],
['group' => 'attendance', 'key' => 'trainer_grace_minutes', 'value' => '10', 'type' => 'integer', 'label_ar' => 'فترة السماح للمدرب (دقائق)', 'description_ar' => 'عدد الدقائق المسموح بها قبل اعتبار المدرب متأخراً'],
['group' => 'attendance', 'key' => 'participant_grace_minutes', 'value' => '15', 'type' => 'integer', 'label_ar' => 'فترة السماح للمشترك (دقائق)', 'description_ar' => 'عدد الدقائق المسموح بها قبل اعتبار المشترك متأخراً'],
['group' => 'attendance', 'key' => 'early_leave_threshold_minutes', 'value' => '10', 'type' => 'integer', 'label_ar' => 'حد الانصراف المبكر (دقائق)', 'description_ar' => 'إذا غادر قبل نهاية الحصة بهذه المدة يُسجل كمنصرف مبكراً'],
// Pricing // Pricing
['group' => 'pricing', 'key' => 'global_max_discount', 'value' => '50', 'type' => 'integer', 'description_ar' => 'الحد الأقصى للخصم الكلي (%)'], ['group' => 'pricing', 'key' => 'global_max_discount', 'value' => '50', 'type' => 'integer', 'label_ar' => 'الحد الأقصى للخصم الكلي (%)', 'description_ar' => 'الحد الأقصى للخصم الكلي (%)'],
['group' => 'pricing', 'key' => 'allow_price_override', 'value' => '0', 'type' => 'boolean', 'description_ar' => 'السماح بتجاوز السعر يدوياً في نقطة البيع'], ['group' => 'pricing', 'key' => 'allow_price_override', 'value' => '0', 'type' => 'boolean', 'label_ar' => 'السماح بتجاوز السعر', 'description_ar' => 'السماح بتجاوز السعر يدوياً في نقطة البيع'],
['group' => 'pricing', 'key' => 'auto_apply_family_discount', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'تطبيق خصم العائلة تلقائياً'], ['group' => 'pricing', 'key' => 'auto_apply_family_discount', 'value' => '1', 'type' => 'boolean', 'label_ar' => 'خصم العائلة التلقائي', 'description_ar' => 'تطبيق خصم العائلة تلقائياً'],
['group' => 'pricing', 'key' => 'family_discount_start_child', 'value' => '2', 'type' => 'integer', 'description_ar' => 'رقم الطفل الذي يبدأ عنده خصم العائلة'], ['group' => 'pricing', 'key' => 'family_discount_start_child', 'value' => '2', 'type' => 'integer', 'label_ar' => 'بداية خصم العائلة (الطفل رقم)', 'description_ar' => 'رقم الطفل الذي يبدأ عنده خصم العائلة'],
['group' => 'pricing', 'key' => 'trial_period_days', 'value' => '7', 'type' => 'integer', 'description_ar' => 'مدة الفترة التجريبية (بالأيام)'], ['group' => 'pricing', 'key' => 'trial_period_days', 'value' => '7', 'type' => 'integer', 'label_ar' => 'الفترة التجريبية (أيام)', 'description_ar' => 'مدة الفترة التجريبية (بالأيام)'],
['group' => 'pricing', 'key' => 'early_enrollment_days', 'value' => '14', 'type' => 'integer', 'description_ar' => 'أيام التسجيل المبكر (للخصم)'], ['group' => 'pricing', 'key' => 'early_enrollment_days', 'value' => '14', 'type' => 'integer', 'label_ar' => 'أيام التسجيل المبكر', 'description_ar' => 'أيام التسجيل المبكر (للخصم)'],
['group' => 'pricing', 'key' => 'coupon_max_uses_default', 'value' => '100', 'type' => 'integer', 'description_ar' => 'الحد الافتراضي لاستخدام الكوبون'], ['group' => 'pricing', 'key' => 'coupon_max_uses_default', 'value' => '100', 'type' => 'integer', 'label_ar' => 'الحد الافتراضي لاستخدام الكوبون', 'description_ar' => 'الحد الافتراضي لاستخدام الكوبون'],
// Notifications // Notifications
['group' => 'notifications', 'key' => 'email_enabled', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'تفعيل إرسال البريد الإلكتروني'], ['group' => 'notifications', 'key' => 'email_enabled', 'value' => '1', 'type' => 'boolean', 'label_ar' => 'تفعيل البريد الإلكتروني', 'description_ar' => 'تفعيل إرسال البريد الإلكتروني'],
['group' => 'notifications', 'key' => 'sms_enabled', 'value' => '0', 'type' => 'boolean', 'description_ar' => 'تفعيل إرسال الرسائل القصيرة'], ['group' => 'notifications', 'key' => 'sms_enabled', 'value' => '0', 'type' => 'boolean', 'label_ar' => 'تفعيل الرسائل القصيرة', 'description_ar' => 'تفعيل إرسال الرسائل القصيرة'],
['group' => 'notifications', 'key' => 'sms_provider', 'value' => 'log', 'type' => 'string', 'description_ar' => 'مزود خدمة SMS (log, twilio, vonage)'], ['group' => 'notifications', 'key' => 'sms_provider', 'value' => 'log', 'type' => 'string', 'label_ar' => 'مزود خدمة SMS', 'description_ar' => 'مزود خدمة SMS (log, twilio, vonage)'],
['group' => 'notifications', 'key' => 'sms_rate_limit_per_hour', 'value' => '100', 'type' => 'integer', 'description_ar' => 'الحد الأقصى للرسائل في الساعة'], ['group' => 'notifications', 'key' => 'sms_rate_limit_per_hour', 'value' => '100', 'type' => 'integer', 'label_ar' => 'حد الرسائل في الساعة', 'description_ar' => 'الحد الأقصى للرسائل في الساعة'],
['group' => 'notifications', 'key' => 'digest_mode_enabled', 'value' => '0', 'type' => 'boolean', 'description_ar' => 'تفعيل وضع الملخص اليومي'], ['group' => 'notifications', 'key' => 'digest_mode_enabled', 'value' => '0', 'type' => 'boolean', 'label_ar' => 'وضع الملخص اليومي', 'description_ar' => 'تفعيل وضع الملخص اليومي'],
['group' => 'notifications', 'key' => 'digest_send_time', 'value' => '08:00', 'type' => 'string', 'description_ar' => 'وقت إرسال الملخص اليومي'], ['group' => 'notifications', 'key' => 'digest_send_time', 'value' => '08:00', 'type' => 'string', 'label_ar' => 'وقت إرسال الملخص', 'description_ar' => 'وقت إرسال الملخص اليومي'],
['group' => 'notifications', 'key' => 'notify_admin_on_payment', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'إشعار المدير عند تسجيل دفعة'], ['group' => 'notifications', 'key' => 'notify_admin_on_payment', 'value' => '1', 'type' => 'boolean', 'label_ar' => 'إشعار المدير عند الدفع', 'description_ar' => 'إشعار المدير عند تسجيل دفعة'],
['group' => 'notifications', 'key' => 'notify_guardian_on_enrollment', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'إشعار ولي الأمر عند التسجيل'], ['group' => 'notifications', 'key' => 'notify_guardian_on_enrollment', 'value' => '1', 'type' => 'boolean', 'label_ar' => 'إشعار ولي الأمر عند التسجيل', 'description_ar' => 'إشعار ولي الأمر عند التسجيل'],
['group' => 'notifications', 'key' => 'payment_reminder_days_before', 'value' => '3', 'type' => 'integer', 'description_ar' => 'عدد أيام قبل الاستحقاق لإرسال تذكير الدفع'], ['group' => 'notifications', 'key' => 'payment_reminder_days_before', 'value' => '3', 'type' => 'integer', 'label_ar' => 'أيام تذكير الدفع', 'description_ar' => 'عدد أيام قبل الاستحقاق لإرسال تذكير الدفع'],
// Enrollment // Enrollment
['group' => 'enrollment', 'key' => 'allow_waitlist', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'تفعيل قائمة الانتظار عند امتلاء المجموعة'], ['group' => 'enrollment', 'key' => 'allow_waitlist', 'value' => '1', 'type' => 'boolean', 'label_ar' => 'تفعيل قائمة الانتظار', 'description_ar' => 'تفعيل قائمة الانتظار عند امتلاء المجموعة'],
['group' => 'enrollment', 'key' => 'auto_promote_waitlist', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'ترقية المسجلين في قائمة الانتظار تلقائياً'], ['group' => 'enrollment', 'key' => 'auto_promote_waitlist', 'value' => '1', 'type' => 'boolean', 'label_ar' => 'ترقية الانتظار تلقائياً', 'description_ar' => 'ترقية المسجلين في قائمة الانتظار تلقائياً'],
['group' => 'enrollment', 'key' => 'require_medical_report', 'value' => '0', 'type' => 'boolean', 'description_ar' => 'إلزام رفع تقرير طبي عند التسجيل'], ['group' => 'enrollment', 'key' => 'require_medical_report', 'value' => '0', 'type' => 'boolean', 'label_ar' => 'إلزام تقرير طبي', 'description_ar' => 'إلزام رفع تقرير طبي عند التسجيل'],
['group' => 'enrollment', 'key' => 'require_guardian_for_minors', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'إلزام ربط ولي أمر للقاصرين'], ['group' => 'enrollment', 'key' => 'require_guardian_for_minors', 'value' => '1', 'type' => 'boolean', 'label_ar' => 'إلزام ولي أمر للقاصرين', 'description_ar' => 'إلزام ربط ولي أمر للقاصرين'],
['group' => 'enrollment', 'key' => 'minor_age_threshold', 'value' => '18', 'type' => 'integer', 'description_ar' => 'السن الأدنى للتسجيل بدون ولي أمر'], ['group' => 'enrollment', 'key' => 'minor_age_threshold', 'value' => '18', 'type' => 'integer', 'label_ar' => 'سن التسجيل بدون ولي أمر', 'description_ar' => 'السن الأدنى للتسجيل بدون ولي أمر'],
['group' => 'enrollment', 'key' => 'max_groups_per_participant', 'value' => '5', 'type' => 'integer', 'description_ar' => 'الحد الأقصى لعدد المجموعات لكل مشترك'], ['group' => 'enrollment', 'key' => 'max_groups_per_participant', 'value' => '5', 'type' => 'integer', 'label_ar' => 'الحد الأقصى للمجموعات', 'description_ar' => 'الحد الأقصى لعدد المجموعات لكل مشترك'],
['group' => 'enrollment', 'key' => 'allow_transfer_between_groups', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'السماح بنقل المشاركين بين المجموعات'], ['group' => 'enrollment', 'key' => 'allow_transfer_between_groups', 'value' => '1', 'type' => 'boolean', 'label_ar' => 'السماح بالنقل بين المجموعات', 'description_ar' => 'السماح بنقل المشاركين بين المجموعات'],
['group' => 'enrollment', 'key' => 'freeze_max_days', 'value' => '30', 'type' => 'integer', 'description_ar' => 'أقصى مدة تجميد الاشتراك (بالأيام)'], ['group' => 'enrollment', 'key' => 'freeze_max_days', 'value' => '30', 'type' => 'integer', 'label_ar' => 'أقصى مدة تجميد (أيام)', 'description_ar' => 'أقصى مدة تجميد الاشتراك (بالأيام)'],
['group' => 'enrollment', 'key' => 'freeze_max_times', 'value' => '2', 'type' => 'integer', 'description_ar' => 'أقصى عدد مرات التجميد في الاشتراك الواحد'], ['group' => 'enrollment', 'key' => 'freeze_max_times', 'value' => '2', 'type' => 'integer', 'label_ar' => 'أقصى مرات التجميد', 'description_ar' => 'أقصى عدد مرات التجميد في الاشتراك الواحد'],
['group' => 'enrollment', 'key' => 'enrollment_expiry_days', 'value' => '30', 'type' => 'integer', 'description_ar' => 'مدة صلاحية الاشتراك الافتراضية (بالأيام)'], ['group' => 'enrollment', 'key' => 'enrollment_expiry_days', 'value' => '30', 'type' => 'integer', 'label_ar' => 'صلاحية الاشتراك (أيام)', 'description_ar' => 'مدة صلاحية الاشتراك الافتراضية (بالأيام)'],
['group' => 'enrollment', 'key' => 'require_documents_upload', 'value' => '0', 'type' => 'boolean', 'label_ar' => 'تفعيل رفع المستندات', 'description_ar' => 'تفعيل نظام رفع المستندات (يسمح للمشتركين والموظفين برفع مستندات)'],
['group' => 'enrollment', 'key' => 'require_medical_certificate', 'value' => '0', 'type' => 'boolean', 'label_ar' => 'إلزام شهادة طبية', 'description_ar' => 'إلزام رفع شهادة طبية سارية'],
['group' => 'enrollment', 'key' => 'medical_certificate_expiry_required', 'value' => '1', 'type' => 'boolean', 'label_ar' => 'إلزام تاريخ انتهاء الشهادة', 'description_ar' => 'إلزام إدخال تاريخ انتهاء الشهادة الطبية عند الموافقة'],
['group' => 'enrollment', 'key' => 'medical_certificate_max_age_months', 'value' => '12', 'type' => 'integer', 'label_ar' => 'صلاحية الشهادة الطبية (أشهر)', 'description_ar' => 'أقصى مدة صلاحية الشهادة الطبية بالأشهر'],
['group' => 'enrollment', 'key' => 'block_attendance_without_medical', 'value' => '0', 'type' => 'boolean', 'label_ar' => 'منع الحضور بدون شهادة', 'description_ar' => 'منع تسجيل الحضور بدون شهادة طبية سارية'],
['group' => 'enrollment', 'key' => 'allowed_document_types', 'value' => '["birth_certificate","national_id","medical_certificate","passport","photo","contract","qualification","insurance","other"]', 'type' => 'json', 'label_ar' => 'أنواع المستندات المسموحة', 'description_ar' => 'أنواع المستندات المسموح رفعها'],
['group' => 'enrollment', 'key' => 'max_document_size_mb', 'value' => '10', 'type' => 'integer', 'label_ar' => 'حد حجم الملف (ميجابايت)', 'description_ar' => 'الحد الأقصى لحجم الملف المرفوع (ميجابايت)'],
// New enrollment settings
['group' => 'enrollment', 'key' => 'auto_invoice_on_enrollment', 'value' => '0', 'type' => 'boolean', 'label_ar' => 'إنشاء فاتورة تلقائياً عند التسجيل', 'description_ar' => 'إنشاء فاتورة للمشترك تلقائياً عند تسجيله في برنامج'],
['group' => 'enrollment', 'key' => 'auto_create_groups_enabled', 'value' => '0', 'type' => 'boolean', 'label_ar' => 'إنشاء مجموعات تلقائياً', 'description_ar' => 'إنشاء مجموعة جديدة تلقائياً إذا كانت المجموعات الحالية ممتلئة'],
['group' => 'enrollment', 'key' => 'auto_enrollment_on_pos_purchase', 'value' => '1', 'type' => 'boolean', 'label_ar' => 'تسجيل تلقائي عند الشراء', 'description_ar' => 'تسجيل المشترك في البرنامج تلقائياً عند شرائه من نقطة البيع'],
// Documents // Inventory
['group' => 'enrollment', 'key' => 'require_documents_upload', 'value' => '0', 'type' => 'boolean', 'description_ar' => 'تفعيل نظام رفع المستندات (يسمح للمشتركين والموظفين برفع مستندات)'], ['group' => 'inventory', 'key' => 'auto_inventory_deduction_on_sale', 'value' => '1', 'type' => 'boolean', 'label_ar' => 'خصم المخزون تلقائياً عند البيع', 'description_ar' => 'خصم الكمية من المخزون تلقائياً عند إتمام عملية بيع'],
['group' => 'enrollment', 'key' => 'require_medical_certificate', 'value' => '0', 'type' => 'boolean', 'description_ar' => 'إلزام رفع شهادة طبية سارية'], ['group' => 'inventory', 'key' => 'auto_receive_inventory_enabled', 'value' => '1', 'type' => 'boolean', 'label_ar' => 'استلام المخزون تلقائياً', 'description_ar' => 'إنشاء حركات مخزنية تلقائياً عند تأكيد أمر الشراء'],
['group' => 'enrollment', 'key' => 'medical_certificate_expiry_required', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'إلزام إدخال تاريخ انتهاء الشهادة الطبية عند الموافقة'],
['group' => 'enrollment', 'key' => 'medical_certificate_max_age_months', 'value' => '12', 'type' => 'integer', 'description_ar' => 'أقصى مدة صلاحية الشهادة الطبية بالأشهر'], // Payroll & Compensation
['group' => 'enrollment', 'key' => 'block_attendance_without_medical', 'value' => '0', 'type' => 'boolean', 'description_ar' => 'منع تسجيل الحضور بدون شهادة طبية سارية'], ['group' => 'payroll', 'key' => 'payroll_enabled', 'value' => '0', 'type' => 'boolean', 'label_ar' => 'تفعيل نظام الرواتب', 'description_ar' => 'تفعيل حساب الرواتب وكشوف المرتبات'],
['group' => 'enrollment', 'key' => 'allowed_document_types', 'value' => '["birth_certificate","national_id","medical_certificate","passport","photo","contract","qualification","insurance","other"]', 'type' => 'json', 'description_ar' => 'أنواع المستندات المسموح رفعها'], ['group' => 'payroll', 'key' => 'auto_trainer_compensation_enabled', 'value' => '0', 'type' => 'boolean', 'label_ar' => 'الاحتساب التلقائي لأجور المدربين', 'description_ar' => 'احتساب أجر المدرب تلقائياً عند تسجيل حضوره'],
['group' => 'enrollment', 'key' => 'max_document_size_mb', 'value' => '10', 'type' => 'integer', 'description_ar' => 'الحد الأقصى لحجم الملف المرفوع (ميجابايت)'], ['group' => 'payroll', 'key' => 'trainer_absence_penalty_enabled', 'value' => '0', 'type' => 'boolean', 'label_ar' => 'تفعيل خصم الغياب', 'description_ar' => 'خصم مبلغ من المدرب عند الغياب بدون عذر'],
['group' => 'payroll', 'key' => 'absence_penalty_amount', 'value' => '0', 'type' => 'integer', 'label_ar' => 'مبلغ خصم الغياب (بالقرش)', 'description_ar' => 'المبلغ المخصوم عند كل غياب (بالقروش)'],
['group' => 'payroll', 'key' => 'trainer_late_penalty_enabled', 'value' => '0', 'type' => 'boolean', 'label_ar' => 'تفعيل خصم التأخير', 'description_ar' => 'خصم مبلغ من المدرب عند التأخير'],
['group' => 'payroll', 'key' => 'late_penalty_amount', 'value' => '0', 'type' => 'integer', 'label_ar' => 'مبلغ خصم التأخير (بالقرش)', 'description_ar' => 'المبلغ المخصوم عند كل تأخير (بالقروش)'],
['group' => 'payroll', 'key' => 'cancelled_session_pay_enabled', 'value' => '0', 'type' => 'boolean', 'label_ar' => 'تعويض الحصص الملغاة', 'description_ar' => 'دفع نسبة من أجر المدرب عند إلغاء حصة من الأكاديمية'],
['group' => 'payroll', 'key' => 'cancelled_session_pay_percent', 'value' => '0', 'type' => 'integer', 'label_ar' => 'نسبة تعويض الإلغاء (%)', 'description_ar' => 'النسبة المئوية من أجر الحصة التي يحصل عليها المدرب'],
['group' => 'payroll', 'key' => 'per_player_compensation_enabled', 'value' => '0', 'type' => 'boolean', 'label_ar' => 'تفعيل الأجر حسب اللاعب', 'description_ar' => 'احتساب أجر إضافي للمدرب عن كل لاعب نشط'],
['group' => 'payroll', 'key' => 'revenue_share_enabled', 'value' => '0', 'type' => 'boolean', 'label_ar' => 'تفعيل حصة الإيرادات', 'description_ar' => 'احتساب نسبة من إيرادات المجموعة كأجر للمدرب'],
['group' => 'payroll', 'key' => 'social_insurance_enabled', 'value' => '0', 'type' => 'boolean', 'label_ar' => 'تفعيل خصم التأمينات', 'description_ar' => 'خصم نسبة التأمينات الاجتماعية من الراتب'],
['group' => 'payroll', 'key' => 'social_insurance_percent', 'value' => '0', 'type' => 'integer', 'label_ar' => 'نسبة التأمينات (%)', 'description_ar' => 'النسبة المئوية للتأمينات المخصومة من الراتب الأساسي'],
['group' => 'payroll', 'key' => 'payroll_day', 'value' => '28', 'type' => 'integer', 'label_ar' => 'يوم صرف الرواتب', 'description_ar' => 'اليوم من الشهر الذي تبدأ فيه فترة الرواتب الجديدة'],
]; ];
foreach ($settings as $setting) { foreach ($settings as $setting) {
...@@ -106,6 +132,7 @@ public function run(): void ...@@ -106,6 +132,7 @@ public function run(): void
'key' => $setting['key'], 'key' => $setting['key'],
'value' => $setting['value'], 'value' => $setting['value'],
'type' => $setting['type'], 'type' => $setting['type'],
'label_ar' => $setting['label_ar'],
'description_ar' => $setting['description_ar'], 'description_ar' => $setting['description_ar'],
'is_public' => false, 'is_public' => false,
'created_at' => $now, 'created_at' => $now,
...@@ -114,6 +141,6 @@ public function run(): void ...@@ -114,6 +141,6 @@ public function run(): void
); );
} }
$this->command->info('Seeded ' . count($settings) . ' system settings across 6 groups.'); $this->command->info('Seeded ' . count($settings) . ' system settings across 8 groups.');
} }
} }
...@@ -54,16 +54,26 @@ class="px-4 py-2.5 text-sm font-medium whitespace-nowrap border-b-2 transition-c ...@@ -54,16 +54,26 @@ class="px-4 py-2.5 text-sm font-medium whitespace-nowrap border-b-2 transition-c
<p class="mt-4 text-sm text-gray-500">{{ __('لا توجد إعدادات في هذه المجموعة بعد') }}</p> <p class="mt-4 text-sm text-gray-500">{{ __('لا توجد إعدادات في هذه المجموعة بعد') }}</p>
</div> </div>
@else @else
<div class="space-y-6"> <div class="space-y-6" x-data="{ settings: @js($settings) }">
@foreach($settings as $key => $value) @foreach($settings as $key => $value)
@php @php
$meta = $settingsMeta[$key] ?? ['type' => 'string', 'description_ar' => '']; $meta = $settingsMeta[$key] ?? ['type' => 'string', 'description_ar' => ''];
$type = $meta['type'] ?? 'string'; $type = $meta['type'] ?? 'string';
$description = $meta['description_ar'] ?? ''; $description = $meta['description_ar'] ?? '';
$parentKey = $dependsOn[$key] ?? null;
$label = !empty($meta['label_ar']) ? $meta['label_ar'] : str_replace('_', ' ', $key);
@endphp @endphp
<div> <div
@if($parentKey)
x-show="settings['{{ $parentKey }}'] == '1' || settings['{{ $parentKey }}'] === true"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 -translate-y-1"
x-transition:enter-end="opacity-100 translate-y-0"
class="ps-4 border-s-2 border-blue-200"
@endif
>
<label for="setting-{{ $key }}" class="block text-sm font-medium text-gray-700 mb-1"> <label for="setting-{{ $key }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ str_replace('_', ' ', $key) }} {{ $label }}
</label> </label>
@if($type === 'boolean') @if($type === 'boolean')
...@@ -72,6 +82,7 @@ class="px-4 py-2.5 text-sm font-medium whitespace-nowrap border-b-2 transition-c ...@@ -72,6 +82,7 @@ class="px-4 py-2.5 text-sm font-medium whitespace-nowrap border-b-2 transition-c
type="checkbox" type="checkbox"
id="setting-{{ $key }}" id="setting-{{ $key }}"
wire:model="settings.{{ $key }}" wire:model="settings.{{ $key }}"
@change="settings['{{ $key }}'] = $el.checked ? '1' : '0'"
value="1" value="1"
@checked(filter_var($value, FILTER_VALIDATE_BOOLEAN)) @checked(filter_var($value, FILTER_VALIDATE_BOOLEAN))
class="sr-only peer"> class="sr-only peer">
......
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