Commit d74bc423 authored by Mahmoud Aglan's avatar Mahmoud Aglan

kokowawa

parent 8ec37422
[LocalizedFileNames]
Screenshot 2026-07-06 023737.png=@Screenshot 2026-07-06 023737,0
Screenshot 2026-07-06 023724.png=@Screenshot 2026-07-06 023724,0
Screenshot 2026-07-06 023709.png=@Screenshot 2026-07-06 023709,0
Screenshot 2026-07-06 023654.png=@Screenshot 2026-07-06 023654,0
Screenshot 2026-07-06 023646.png=@Screenshot 2026-07-06 023646,0
Screenshot 2026-07-06 023639.png=@Screenshot 2026-07-06 023639,0
Screenshot 2026-07-06 023625.png=@Screenshot 2026-07-06 023625,0
Screenshot 2026-07-06 023613.png=@Screenshot 2026-07-06 023613,0
Screenshot 2026-07-06 023607.png=@Screenshot 2026-07-06 023607,0
Screenshot 2026-07-06 023552.png=@Screenshot 2026-07-06 023552,0
Screenshot 2026-07-06 023546.png=@Screenshot 2026-07-06 023546,0
Screenshot 2026-07-06 023537.png=@Screenshot 2026-07-06 023537,0
Screenshot 2026-07-06 023525.png=@Screenshot 2026-07-06 023525,0
Screenshot 2026-07-06 023509.png=@Screenshot 2026-07-06 023509,0
Screenshot 2026-07-06 023452.png=@Screenshot 2026-07-06 023452,0
Screenshot 2026-07-06 023439.png=@Screenshot 2026-07-06 023439,0
Screenshot 2026-07-06 023432.png=@Screenshot 2026-07-06 023432,0
Screenshot 2026-07-06 023427.png=@Screenshot 2026-07-06 023427,0
Screenshot 2026-07-06 023412.png=@Screenshot 2026-07-06 023412,0
Screenshot 2026-07-06 023403.png=@Screenshot 2026-07-06 023403,0
Screenshot 2026-07-06 154014.png=@Screenshot 2026-07-06 154014,0
Screenshot 2026-07-06 154000.png=@Screenshot 2026-07-06 154000,0
Screenshot 2026-07-06 153949.png=@Screenshot 2026-07-06 153949,0
Screenshot 2026-07-06 153941.png=@Screenshot 2026-07-06 153941,0
Screenshot 2026-07-06 153932.png=@Screenshot 2026-07-06 153932,0
Screenshot 2026-07-06 153923.png=@Screenshot 2026-07-06 153923,0
Screenshot 2026-07-06 153916.png=@Screenshot 2026-07-06 153916,0
Screenshot 2026-07-06 153909.png=@Screenshot 2026-07-06 153909,0
Screenshot 2026-07-06 153821.png=@Screenshot 2026-07-06 153821,0
Screenshot 2026-07-06 153812.png=@Screenshot 2026-07-06 153812,0
Screenshot 2026-07-06 153802.png=@Screenshot 2026-07-06 153802,0
Screenshot 2026-07-06 153750.png=@Screenshot 2026-07-06 153750,0
Screenshot 2026-07-06 153731.png=@Screenshot 2026-07-06 153731,0
Screenshot 2026-07-06 153659.png=@Screenshot 2026-07-06 153659,0
Screenshot 2026-07-06 153645.png=@Screenshot 2026-07-06 153645,0
Screenshot 2026-07-06 153632.png=@Screenshot 2026-07-06 153632,0
Screenshot 2026-07-06 153625.png=@Screenshot 2026-07-06 153625,0
Screenshot 2026-07-06 153620.png=@Screenshot 2026-07-06 153620,0
Screenshot 2026-07-06 153554.png=@Screenshot 2026-07-06 153554,0
Screenshot 2026-07-06 153548.png=@Screenshot 2026-07-06 153548,0
Screenshot 2026-07-06 153541.png=@Screenshot 2026-07-06 153541,0
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
use App\Domain\Financial\Events\PaymentPlanDefaulted; use App\Domain\Financial\Events\PaymentPlanDefaulted;
use App\Domain\Financial\Models\Installment; use App\Domain\Financial\Models\Installment;
use App\Domain\Financial\Models\PaymentPlan; use App\Domain\Financial\Models\PaymentPlan;
use App\Domain\Shared\Models\SystemSetting;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
...@@ -13,12 +14,12 @@ class DetectPaymentPlanDefaults extends Command ...@@ -13,12 +14,12 @@ class DetectPaymentPlanDefaults extends Command
protected $signature = 'payments:detect-defaults'; protected $signature = 'payments:detect-defaults';
protected $description = 'كشف التخلف عن خطط الدفع'; protected $description = 'كشف التخلف عن خطط الدفع';
private const GRACE_DAYS = 7; private const DEFAULT_THRESHOLD = 2;
private const DEFAULT_THRESHOLD = 2; // missed installments to consider plan defaulted
public function handle(): int public function handle(): int
{ {
$graceCutoff = now()->subDays(self::GRACE_DAYS)->toDateString(); $graceDays = (int) SystemSetting::get('installment_grace_days', 7);
$graceCutoff = now()->subDays($graceDays)->toDateString();
// Find pending installments that are overdue past grace period // Find pending installments that are overdue past grace period
$overdueInstallments = Installment::where('status', 'pending') $overdueInstallments = Installment::where('status', 'pending')
......
<?php
namespace App\Console\Commands;
use App\Domain\Identity\Models\Role;
use App\Domain\Shared\Models\Academy;
use App\Models\User;
use Database\Seeders\FinancialAccountsSeeder;
use Database\Seeders\NotificationTemplateSeeder;
use Database\Seeders\PaymentNotificationTemplateSeeder;
use Database\Seeders\PermissionSeeder;
use Database\Seeders\RolesAndPermissionsSeeder;
use Database\Seeders\SystemSettingsSeeder;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class SetupFreshInstall extends Command
{
protected $signature = 'el-captain:setup {--no-interaction}';
protected $description = 'First-time setup: create academy, admin user, and seed reference data';
public function handle(): int
{
if (Academy::count() > 0) {
$this->warn('Academy already exists. Skipping setup.');
return self::SUCCESS;
}
$this->info('Setting up El Captain for first use...');
$academyNameAr = env('ACADEMY_NAME_AR', 'أكاديمية الكابتن');
$academyNameEn = env('ACADEMY_NAME_EN', 'El Captain Academy');
$adminEmail = env('ADMIN_EMAIL', 'admin@example.com');
$adminPassword = env('ADMIN_PASSWORD', 'password123');
$adminName = env('ADMIN_NAME', 'مدير النظام');
$slug = Str::slug($academyNameEn);
$academy = Academy::create([
'name' => $academyNameEn,
'name_ar' => $academyNameAr,
'slug' => $slug,
'email' => $adminEmail,
'currency' => 'EGP',
'timezone' => 'Africa/Cairo',
'locale' => 'ar',
'status' => 'active',
]);
$this->info("Academy created: {$academyNameAr}");
app()->instance('current_academy', $academy);
$admin = User::create([
'academy_id' => $academy->id,
'name' => $adminName,
'name_ar' => $adminName,
'email' => $adminEmail,
'password' => Hash::make($adminPassword),
'status' => 'active',
'is_super_admin' => true,
]);
$this->info("Admin user created: {$adminEmail}");
$this->call('db:seed', ['--class' => FinancialAccountsSeeder::class, '--force' => true]);
$this->call('db:seed', ['--class' => RolesAndPermissionsSeeder::class, '--force' => true]);
$this->call('db:seed', ['--class' => PermissionSeeder::class, '--force' => true]);
$this->call('db:seed', ['--class' => SystemSettingsSeeder::class, '--force' => true]);
$this->call('db:seed', ['--class' => PaymentNotificationTemplateSeeder::class, '--force' => true]);
if (class_exists(NotificationTemplateSeeder::class)) {
$this->call('db:seed', ['--class' => NotificationTemplateSeeder::class, '--force' => true]);
}
$ownerRole = Role::where('academy_id', $academy->id)
->where('slug', 'academy_owner')
->first();
if ($ownerRole) {
$admin->update(['role_id' => $ownerRole->id]);
$admin->roles()->syncWithoutDetaching([$ownerRole->id]);
}
$this->info('Roles, permissions, and settings seeded.');
$this->newLine();
$this->components->info("Setup complete! Log in at your app URL with: {$adminEmail}");
return self::SUCCESS;
}
}
...@@ -22,7 +22,7 @@ class TrainingProgram extends Model ...@@ -22,7 +22,7 @@ class TrainingProgram extends Model
protected array $uniqueFieldsToMangle = ['slug']; protected array $uniqueFieldsToMangle = ['slug'];
protected $fillable = [ protected $fillable = [
'academy_id', 'activity_id', 'branch_id', 'academy_id', 'activity_id', 'branch_id', 'default_trainer_id',
'name', 'name_ar', 'slug', 'description', 'description_ar', 'name', 'name_ar', 'slug', 'description', 'description_ar',
'objectives', 'skill_level', 'age_min', 'age_max', 'gender', 'objectives', 'skill_level', 'age_min', 'age_max', 'gender',
'sessions_per_week', 'session_duration_minutes', 'total_sessions', 'program_duration_weeks', 'sessions_per_week', 'session_duration_minutes', 'total_sessions', 'program_duration_weeks',
...@@ -71,6 +71,11 @@ public function branch(): BelongsTo ...@@ -71,6 +71,11 @@ public function branch(): BelongsTo
return $this->belongsTo(Branch::class); return $this->belongsTo(Branch::class);
} }
public function defaultTrainer(): BelongsTo
{
return $this->belongsTo(User::class, 'default_trainer_id');
}
public function creator(): BelongsTo public function creator(): BelongsTo
{ {
return $this->belongsTo(User::class, 'created_by'); return $this->belongsTo(User::class, 'created_by');
......
...@@ -25,6 +25,7 @@ public function create(array $data, User $actor): TrainingGroup ...@@ -25,6 +25,7 @@ public function create(array $data, User $actor): TrainingGroup
$program = TrainingProgram::findOrFail($data['training_program_id']); $program = TrainingProgram::findOrFail($data['training_program_id']);
return TrainingGroup::create(array_merge($data, [ return TrainingGroup::create(array_merge($data, [
'head_trainer_id' => $data['head_trainer_id'] ?? $program->default_trainer_id,
'max_capacity' => $data['max_capacity'] ?? $program->max_participants, 'max_capacity' => $data['max_capacity'] ?? $program->max_participants,
'status' => 'forming', 'status' => 'forming',
'status_changed_at' => now(), 'status_changed_at' => now(),
......
...@@ -49,6 +49,7 @@ private function createDefaultGroup(TrainingProgram $program, User $actor): Trai ...@@ -49,6 +49,7 @@ private function createDefaultGroup(TrainingProgram $program, User $actor): Trai
'academy_id' => $program->academy_id, 'academy_id' => $program->academy_id,
'training_program_id' => $program->id, 'training_program_id' => $program->id,
'branch_id' => $program->branch_id, 'branch_id' => $program->branch_id,
'head_trainer_id' => $program->default_trainer_id,
'name' => "Group {$program->name} 1", 'name' => "Group {$program->name} 1",
'name_ar' => "مجموعة {$programName} 1", 'name_ar' => "مجموعة {$programName} 1",
'code' => $code, 'code' => $code,
......
...@@ -43,6 +43,13 @@ class SystemSettings extends Component ...@@ -43,6 +43,13 @@ class SystemSettings extends Component
'refund_approval_threshold' => ['type' => 'number', 'label' => 'حد مبلغ الاسترداد للموافقة (قروش)', 'step' => '1', 'min' => 0], 'refund_approval_threshold' => ['type' => 'number', 'label' => 'حد مبلغ الاسترداد للموافقة (قروش)', 'step' => '1', 'min' => 0],
'installment_late_fee' => ['type' => 'number', 'label' => 'غرامة تأخير القسط (قروش)', 'step' => '1', 'min' => 0], 'installment_late_fee' => ['type' => 'number', 'label' => 'غرامة تأخير القسط (قروش)', 'step' => '1', 'min' => 0],
'auto_overdue_after_days' => ['type' => 'number', 'label' => 'أيام لتحويل الفاتورة لمتأخرة', 'step' => '1', 'min' => 1], 'auto_overdue_after_days' => ['type' => 'number', 'label' => 'أيام لتحويل الفاتورة لمتأخرة', 'step' => '1', 'min' => 1],
'installment_enabled' => ['type' => 'boolean', 'label' => 'تفعيل نظام التقسيط'],
'installment_min_down_payment_percent' => ['type' => 'number', 'label' => 'الحد الأدنى للمقدم (%)', 'step' => '1', 'min' => 0, 'max' => 100],
'installment_max_down_payment_percent' => ['type' => 'number', 'label' => 'الحد الأقصى للمقدم (%)', 'step' => '1', 'min' => 0, 'max' => 100],
'installment_max_months' => ['type' => 'number', 'label' => 'الحد الأقصى لعدد أشهر التقسيط', 'step' => '1', 'min' => 1, 'max' => 36],
'installment_min_amount' => ['type' => 'number', 'label' => 'الحد الأدنى لمبلغ الفاتورة للتقسيط (قروش)', 'step' => '1', 'min' => 0],
'installment_allowed_frequencies' => ['type' => 'select', 'label' => 'دورية الأقساط الافتراضية', 'options' => ['weekly' => 'أسبوعي', 'biweekly' => 'نصف شهري', 'monthly' => 'شهري', 'quarterly' => 'ربع سنوي']],
'installment_grace_days' => ['type' => 'number', 'label' => 'أيام السماح قبل احتساب التأخير', 'step' => '1', 'min' => 0],
], ],
'attendance' => [ 'attendance' => [
'participant_grace_period_minutes' => ['type' => 'number', 'label' => 'فترة السماح للمشتركين (دقيقة)', 'step' => '1', 'min' => 0], 'participant_grace_period_minutes' => ['type' => 'number', 'label' => 'فترة السماح للمشتركين (دقيقة)', 'step' => '1', 'min' => 0],
...@@ -120,6 +127,13 @@ class SystemSettings extends Component ...@@ -120,6 +127,13 @@ class SystemSettings extends Component
'refund_approval_threshold' => '50000', 'refund_approval_threshold' => '50000',
'installment_late_fee' => '500', 'installment_late_fee' => '500',
'auto_overdue_after_days' => '3', 'auto_overdue_after_days' => '3',
'installment_enabled' => '1',
'installment_min_down_payment_percent' => '20',
'installment_max_down_payment_percent' => '70',
'installment_max_months' => '12',
'installment_min_amount' => '10000',
'installment_allowed_frequencies' => 'monthly',
'installment_grace_days' => '7',
// attendance // attendance
'participant_grace_period_minutes' => '15', 'participant_grace_period_minutes' => '15',
'trainer_grace_period_minutes' => '10', 'trainer_grace_period_minutes' => '10',
......
...@@ -57,6 +57,11 @@ public function updatedDateTo(): void ...@@ -57,6 +57,11 @@ public function updatedDateTo(): void
$this->resetPage(); $this->resetPage();
} }
public function updatedFilter(): void
{
$this->resetPage();
}
public function render() public function render()
{ {
$branchId = $this->getActiveBranchId(); $branchId = $this->getActiveBranchId();
......
...@@ -46,6 +46,16 @@ public function updatedActionFilter(): void ...@@ -46,6 +46,16 @@ public function updatedActionFilter(): void
$this->resetPage(); $this->resetPage();
} }
public function updatedDateFrom(): void
{
$this->resetPage();
}
public function updatedDateTo(): void
{
$this->resetPage();
}
public function toggleExpand(int $id): void public function toggleExpand(int $id): void
{ {
$this->expandedRow = $this->expandedRow === $id ? 0 : $id; $this->expandedRow = $this->expandedRow === $id ? 0 : $id;
......
...@@ -22,6 +22,11 @@ public function updatedSearch(): void ...@@ -22,6 +22,11 @@ public function updatedSearch(): void
$this->resetPage(); $this->resetPage();
} }
public function updatedShowInactive(): void
{
$this->resetPage();
}
public function toggleActive(string $uuid): void public function toggleActive(string $uuid): void
{ {
$branch = Branch::where('uuid', $uuid)->firstOrFail(); $branch = Branch::where('uuid', $uuid)->firstOrFail();
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
use App\Domain\Financial\Models\Invoice; use App\Domain\Financial\Models\Invoice;
use App\Domain\Financial\Models\PaymentPlan; use App\Domain\Financial\Models\PaymentPlan;
use App\Domain\Shared\Models\SystemSetting;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
...@@ -19,11 +20,26 @@ class PaymentPlanCreate extends Component ...@@ -19,11 +20,26 @@ class PaymentPlanCreate extends Component
public int $downPayment = 0; public int $downPayment = 0;
public array $installments = []; public array $installments = [];
// Settings-driven constraints
public int $maxMonths = 12;
public int $minDownPaymentPercent = 20;
public int $maxDownPaymentPercent = 70;
public int $minInvoiceAmount = 10000;
public int $graceDays = 7;
public function mount(?int $invoice = null): void public function mount(?int $invoice = null): void
{ {
$this->authorize('invoices.create'); $this->authorize('invoices.create');
$this->invoiceId = $invoice; $this->invoiceId = $invoice;
$this->startDate = now()->addDays(7)->toDateString(); $this->startDate = now()->addDays(7)->toDateString();
$this->maxMonths = (int) SystemSetting::get('installment_max_months', 12);
$this->minDownPaymentPercent = (int) SystemSetting::get('installment_min_down_payment_percent', 20);
$this->maxDownPaymentPercent = (int) SystemSetting::get('installment_max_down_payment_percent', 70);
$this->minInvoiceAmount = (int) SystemSetting::get('installment_min_amount', 10000);
$this->graceDays = (int) SystemSetting::get('installment_grace_days', 7);
$this->frequency = SystemSetting::get('installment_allowed_frequencies', 'monthly');
$this->calculateInstallments(); $this->calculateInstallments();
} }
...@@ -91,6 +107,31 @@ public function save(): void ...@@ -91,6 +107,31 @@ public function save(): void
$invoice = Invoice::findOrFail($this->invoiceId); $invoice = Invoice::findOrFail($this->invoiceId);
if ($invoice->due_amount < $this->minInvoiceAmount) {
session()->flash('error', __('مبلغ الفاتورة أقل من الحد الأدنى للتقسيط'));
return;
}
if ($this->installmentCount > $this->maxMonths) {
session()->flash('error', __('عدد الأقساط يتجاوز الحد الأقصى المسموح') . " ({$this->maxMonths})");
return;
}
$downPaymentPiasters = $this->downPayment * 100;
$downPaymentPercent = $invoice->due_amount > 0
? round(($downPaymentPiasters / $invoice->due_amount) * 100)
: 0;
if ($downPaymentPercent < $this->minDownPaymentPercent) {
session()->flash('error', __('المقدم أقل من الحد الأدنى المطلوب') . " ({$this->minDownPaymentPercent}%)");
return;
}
if ($downPaymentPercent > $this->maxDownPaymentPercent) {
session()->flash('error', __('المقدم يتجاوز الحد الأقصى المسموح') . " ({$this->maxDownPaymentPercent}%)");
return;
}
DB::transaction(function () use ($invoice) { DB::transaction(function () use ($invoice) {
$plan = PaymentPlan::create([ $plan = PaymentPlan::create([
'academy_id' => $invoice->academy_id, 'academy_id' => $invoice->academy_id,
...@@ -130,6 +171,10 @@ public function render() ...@@ -130,6 +171,10 @@ public function render()
return view('livewire.financial.payment-plan-create', [ return view('livewire.financial.payment-plan-create', [
'invoice' => $invoice, 'invoice' => $invoice,
'invoices' => $invoices, 'invoices' => $invoices,
'maxMonths' => $this->maxMonths,
'minDownPaymentPercent' => $this->minDownPaymentPercent,
'maxDownPaymentPercent' => $this->maxDownPaymentPercent,
'minInvoiceAmount' => $this->minInvoiceAmount,
]); ]);
} }
} }
...@@ -41,6 +41,11 @@ public function updatedSearch(): void ...@@ -41,6 +41,11 @@ public function updatedSearch(): void
$this->resetPage(); $this->resetPage();
} }
public function updatedStatusFilter(): void
{
$this->resetPage();
}
public function openCreateModal(): void public function openCreateModal(): void
{ {
$this->reset(['selectedTrainerId', 'amount', 'installmentsCount', 'reason']); $this->reset(['selectedTrainerId', 'amount', 'installmentsCount', 'reason']);
......
...@@ -21,6 +21,11 @@ class NotificationCenter extends Component ...@@ -21,6 +21,11 @@ class NotificationCenter extends Component
#[Url] #[Url]
public string $filter = 'all'; public string $filter = 'all';
public function updatedFilter(): void
{
$this->resetPage();
}
public function markAsRead(int $id, NotificationService $service): void public function markAsRead(int $id, NotificationService $service): void
{ {
$service->markAsRead($id); $service->markAsRead($id);
......
...@@ -262,6 +262,7 @@ public function confirm(): void ...@@ -262,6 +262,7 @@ public function confirm(): void
'name' => $this->name ?: $this->nameAr, 'name' => $this->name ?: $this->nameAr,
'activity_id' => $this->activityId, 'activity_id' => $this->activityId,
'branch_id' => $this->branchId, 'branch_id' => $this->branchId,
'default_trainer_id' => $this->headTrainerId,
'skill_level' => $this->skillLevel ?: 'all', 'skill_level' => $this->skillLevel ?: 'all',
'age_min' => $this->ageMin ?: null, 'age_min' => $this->ageMin ?: null,
'age_max' => $this->ageMax ?: null, 'age_max' => $this->ageMax ?: null,
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
use App\Domain\Training\Models\TrainingProgram; use App\Domain\Training\Models\TrainingProgram;
use App\Domain\Training\Services\TrainingProgramService; use App\Domain\Training\Services\TrainingProgramService;
use App\Domain\Shared\Exceptions\DomainException; use App\Domain\Shared\Exceptions\DomainException;
use App\Models\User;
use Livewire\Attributes\Layout; use Livewire\Attributes\Layout;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
use Livewire\Component; use Livewire\Component;
...@@ -24,6 +25,7 @@ class ProgramForm extends Component ...@@ -24,6 +25,7 @@ class ProgramForm extends Component
public string $slug = ''; public string $slug = '';
public ?int $activity_id = null; public ?int $activity_id = null;
public ?int $branch_id = null; public ?int $branch_id = null;
public ?int $default_trainer_id = null;
public string $description = ''; public string $description = '';
public string $description_ar = ''; public string $description_ar = '';
public string $skill_level = 'all'; public string $skill_level = 'all';
...@@ -63,6 +65,7 @@ public function mount(?TrainingProgram $program = null): void ...@@ -63,6 +65,7 @@ public function mount(?TrainingProgram $program = null): void
$this->slug = $program->slug ?? ''; $this->slug = $program->slug ?? '';
$this->activity_id = $program->activity_id; $this->activity_id = $program->activity_id;
$this->branch_id = $program->branch_id; $this->branch_id = $program->branch_id;
$this->default_trainer_id = $program->default_trainer_id;
$this->description = $program->description ?? ''; $this->description = $program->description ?? '';
$this->description_ar = $program->description_ar ?? ''; $this->description_ar = $program->description_ar ?? '';
$this->skill_level = $program->skill_level ?? 'all'; $this->skill_level = $program->skill_level ?? 'all';
...@@ -113,6 +116,7 @@ public function rules(): array ...@@ -113,6 +116,7 @@ public function rules(): array
'slug' => 'nullable|string|max:100', 'slug' => 'nullable|string|max:100',
'activity_id' => 'required|exists:activities,id', 'activity_id' => 'required|exists:activities,id',
'branch_id' => 'nullable|exists:branches,id', 'branch_id' => 'nullable|exists:branches,id',
'default_trainer_id' => 'nullable|exists:users,id',
'description' => 'nullable|string', 'description' => 'nullable|string',
'description_ar' => 'nullable|string', 'description_ar' => 'nullable|string',
'skill_level' => 'required|in:all,beginner,intermediate,advanced,professional', 'skill_level' => 'required|in:all,beginner,intermediate,advanced,professional',
...@@ -188,6 +192,7 @@ public function save(TrainingProgramService $service): void ...@@ -188,6 +192,7 @@ public function save(TrainingProgramService $service): void
'slug' => $this->slug ?: null, 'slug' => $this->slug ?: null,
'activity_id' => $this->activity_id, 'activity_id' => $this->activity_id,
'branch_id' => $this->branch_id, 'branch_id' => $this->branch_id,
'default_trainer_id' => $this->default_trainer_id,
'status' => $this->status, 'status' => $this->status,
'description' => $this->description ?: null, 'description' => $this->description ?: null,
'description_ar' => $this->description_ar ?: null, 'description_ar' => $this->description_ar ?: null,
...@@ -276,6 +281,10 @@ public function render() ...@@ -276,6 +281,10 @@ public function render()
return view('livewire.programs.program-form', [ return view('livewire.programs.program-form', [
'activities' => Activity::where('is_active', true)->orderBy('name_ar')->get(['id', 'name_ar']), 'activities' => Activity::where('is_active', true)->orderBy('name_ar')->get(['id', 'name_ar']),
'branches' => Branch::orderBy('name_ar')->get(['id', 'name_ar']), 'branches' => Branch::orderBy('name_ar')->get(['id', 'name_ar']),
'trainers' => User::whereHas('roles', fn ($q) => $q->whereIn('slug', ['trainer', 'head_trainer']))
->where('status', 'active')
->orderBy('name')
->get(['id', 'name']),
]); ]);
} }
} }
...@@ -22,6 +22,7 @@ class UserList extends Component ...@@ -22,6 +22,7 @@ class UserList extends Component
public function updatedSearch(): void { $this->resetPage(); } public function updatedSearch(): void { $this->resetPage(); }
public function updatedRole(): void { $this->resetPage(); } public function updatedRole(): void { $this->resetPage(); }
public function updatedShowInactive(): void { $this->resetPage(); }
public function toggleActive(string $uuid): void public function toggleActive(string $uuid): void
{ {
......
<?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('training_programs', function (Blueprint $table) {
$table->foreignId('default_trainer_id')->nullable()->after('branch_id')->constrained('users')->nullOnDelete();
$table->index(['academy_id', 'default_trainer_id']);
});
}
public function down(): void
{
Schema::table('training_programs', function (Blueprint $table) {
$table->dropForeign(['default_trainer_id']);
$table->dropIndex(['academy_id', 'default_trainer_id']);
$table->dropColumn('default_trainer_id');
});
}
};
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
FROM php:8.3-apache
RUN a2enmod rewrite && \
sed -i 's/AllowOverride None/AllowOverride All/g' /etc/apache2/apache2.conf
WORKDIR /var/www/html
COPY index.php .htaccess ./
COPY docs/ docs/
RUN chown -R www-data:www-data /var/www/html
EXPOSE 80
{
"schemaVersion": 2,
"dockerfilePath": "./Dockerfile"
}
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
BUILD_DIR=$(mktemp -d)
APP_NAME="el-captain-docs"
SERVER="ubuntu@18.192.166.221"
SSH_KEY="/Users/mahmoudaglan/NewMigration/newServer.pem"
SSH_CMD="ssh -i $SSH_KEY -o StrictHostKeyChecking=no $SERVER"
echo "==> Preparing build context..."
cp "$SCRIPT_DIR/index.php" "$BUILD_DIR/"
cp "$SCRIPT_DIR/.htaccess" "$BUILD_DIR/"
cp "$SCRIPT_DIR/Dockerfile" "$BUILD_DIR/"
mkdir -p "$BUILD_DIR/docs/ar"
cp "$PROJECT_DIR/docs/ar/"*.md "$BUILD_DIR/docs/ar/"
# Remove macOS metadata
find "$BUILD_DIR" -name '._*' -delete
find "$BUILD_DIR" -name '.DS_Store' -delete
echo "==> Creating tarball..."
cd "$BUILD_DIR"
tar czf /tmp/el-captain-docs.tar.gz .
echo "==> Uploading to server..."
scp -i "$SSH_KEY" -o StrictHostKeyChecking=no /tmp/el-captain-docs.tar.gz "$SERVER:/tmp/"
echo "==> Building and deploying on server..."
$SSH_CMD bash -s <<'REMOTE'
set -e
rm -rf /tmp/el-captain-docs-build
mkdir -p /tmp/el-captain-docs-build
cd /tmp/el-captain-docs-build
tar xzf /tmp/el-captain-docs.tar.gz 2>/dev/null
find . -name '._*' -delete
echo "Building Docker image..."
sudo docker build -t img-captain--el-captain-docs:latest . 2>&1 | tail -3
echo "Updating service..."
sudo docker service update --image img-captain--el-captain-docs:latest --force srv-captain--el-captain-docs 2>&1 | tail -3
echo ""
echo "Verifying..."
sleep 3
CID=$(sudo docker ps -q --filter "name=srv-captain--el-captain-docs")
TITLE=$(sudo docker exec $CID curl -s http://localhost/ | grep -o '<title>[^<]*</title>')
echo "Live: $TITLE"
rm -rf /tmp/el-captain-docs-build /tmp/el-captain-docs.tar.gz
REMOTE
rm -rf "$BUILD_DIR" /tmp/el-captain-docs.tar.gz
echo ""
echo "==> Done! https://el-captain-docs.caprover.al-arcade.com"
<?php
$docsDir = __DIR__ . '/docs/ar';
if (!is_dir($docsDir)) {
$docsDir = __DIR__ . '/../docs/ar';
}
$files = glob($docsDir . '/*.md');
usort($files, fn($a, $b) => basename($a) <=> basename($b));
// Handle API requests FIRST before any other processing
if (isset($_GET['api'])) {
error_reporting(0);
ini_set('display_errors', '0');
while (ob_get_level()) ob_end_clean();
header('Content-Type: application/json; charset=utf-8');
if ($_GET['api'] === 'search') {
$q = $_GET['q'] ?? '';
$q = trim($q);
$results = [];
if (strlen($q) > 0) {
foreach ($files as $f) {
$raw = @file_get_contents($f);
if (!$raw) continue;
$bname = basename($f, '.md');
preg_match('/^#\s+(.+)$/mu', $raw, $m);
$fTitle = isset($m[1]) ? preg_replace('/^\d+\s*(?:—|–|-)\s*/u', '', $m[1]) : $bname;
$lines = explode("\n", $raw);
$hits = [];
foreach ($lines as $line) {
if (strpos($line, $q) !== false) {
$hits[] = trim($line);
if (count($hits) >= 4) break;
}
}
if (!empty($hits)) {
$results[] = ['slug' => $bname, 'title' => $fTitle, 'matches' => $hits, 'count' => count($hits)];
}
}
}
echo json_encode($results, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
exit;
}
if ($_GET['api'] === 'content') {
$slug = $_GET['slug'] ?? '';
foreach ($files as $f) {
$bname = basename($f, '.md');
if ($bname === $slug) {
$raw = file_get_contents($f);
preg_match('/^#\s+(.+)$/mu', $raw, $m);
$title = preg_replace('/^\d+\s*(?:—|–|-)\s*/u', '', $m[1] ?? $bname);
echo json_encode(['content' => $raw, 'title' => $title], JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
exit;
}
}
echo json_encode(['content' => '', 'title' => '']);
exit;
}
echo json_encode(['error' => 'unknown api']);
exit;
}
// Build navigation (only for HTML page rendering)
$nav = [];
foreach ($files as $file) {
$basename = basename($file, '.md');
$content = file_get_contents($file);
preg_match('/^#\s+(.+)$/mu', $content, $matches);
$title = $matches[1] ?? $basename;
$title = preg_replace('/^\d+\s*(?:—|–|-)\s*/u', '', $title);
$nav[] = ['slug' => $basename, 'title' => $title];
}
$navJson = json_encode($nav, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
$defaultSlug = $nav[0]['slug'] ?? '';
?>
<!DOCTYPE html>
<html dir="rtl" lang="ar">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EL-Captain Docs</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root {
--bg-base: #050506;
--bg-elevated: #0c0c0e;
--bg-surface: #121215;
--bg-card: #18181b;
--bg-hover: #1f1f23;
--bg-active: #27272a;
--border: rgba(255,255,255,0.06);
--border-hover: rgba(255,255,255,0.12);
--text-primary: #EDEDEF;
--text-secondary: #a1a1aa;
--text-tertiary: #71717a;
--text-muted: #52525b;
--accent: #5E6AD2;
--accent-hover: #6E7AE2;
--accent-glow: rgba(94,106,210,0.15);
--accent-subtle: rgba(94,106,210,0.08);
--orange: #F59E0B;
--green: #10b981;
--red: #ef4444;
--radius-sm: 6px;
--radius: 10px;
--radius-lg: 14px;
--sidebar-w: 280px;
--content-max: 820px;
--font: 'Cairo', system-ui, -apple-system, sans-serif;
--mono: 'JetBrains Mono', ui-monospace, monospace;
--transition: 200ms cubic-bezier(0.16, 1, 0.3, 1);
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
font-family: var(--font);
background: var(--bg-base);
color: var(--text-primary);
line-height: 1.7;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
}
/* ===== LAYOUT ===== */
.app {
display: flex;
min-height: 100vh;
}
/* ===== SIDEBAR ===== */
.sidebar {
position: fixed;
inset-inline-end: 0;
top: 0;
width: var(--sidebar-w);
height: 100vh;
background: var(--bg-elevated);
border-inline-start: 1px solid var(--border);
display: flex;
flex-direction: column;
z-index: 100;
overflow: hidden;
}
.sidebar-brand {
padding: 20px;
border-bottom: 1px solid var(--border);
}
.sidebar-brand h1 {
font-size: 15px;
font-weight: 700;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 10px;
}
.sidebar-brand h1 svg {
width: 22px;
height: 22px;
color: var(--accent);
}
.sidebar-brand p {
font-size: 12px;
color: var(--text-tertiary);
margin-top: 4px;
padding-inline-start: 32px;
}
/* Search */
.sidebar-search {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
}
.search-wrap {
position: relative;
}
.search-wrap svg {
position: absolute;
inset-inline-end: 10px;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
color: var(--text-muted);
pointer-events: none;
}
.search-wrap input {
width: 100%;
padding: 9px 36px 9px 10px;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-family: var(--font);
font-size: 13px;
outline: none;
transition: border-color var(--transition);
}
.search-wrap input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
}
.search-wrap input::placeholder { color: var(--text-muted); }
.search-kbd {
position: absolute;
inset-inline-start: 10px;
top: 50%;
transform: translateY(-50%);
font-size: 11px;
color: var(--text-muted);
background: var(--bg-hover);
border: 1px solid var(--border);
padding: 1px 5px;
border-radius: 3px;
font-family: var(--mono);
pointer-events: none;
}
/* Nav */
.sidebar-nav {
flex: 1;
overflow-y: auto;
padding: 8px 0;
}
.nav-link {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 16px;
margin: 1px 8px;
border-radius: var(--radius-sm);
color: var(--text-secondary);
text-decoration: none;
font-size: 13px;
font-weight: 500;
transition: all var(--transition);
cursor: pointer;
}
.nav-link:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.nav-link.active {
background: var(--accent-subtle);
color: var(--accent-hover);
}
.nav-num {
min-width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
color: var(--text-muted);
background: var(--bg-surface);
border-radius: 5px;
font-family: var(--mono);
}
.nav-link.active .nav-num {
background: var(--accent);
color: #fff;
}
/* ===== MAIN ===== */
.main {
flex: 1;
margin-inline-end: var(--sidebar-w);
min-height: 100vh;
}
/* ===== ARTICLE ===== */
.article {
max-width: var(--content-max);
margin: 0 auto;
padding: 48px 40px 80px;
}
/* Typography */
.article h1 {
font-size: 28px;
font-weight: 800;
color: var(--text-primary);
margin-bottom: 8px;
letter-spacing: -0.02em;
}
.article h1 + hr { margin-top: 16px; }
.article h2 {
font-size: 21px;
font-weight: 700;
color: var(--text-primary);
margin: 40px 0 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border);
}
.article h3 {
font-size: 17px;
font-weight: 600;
color: var(--orange);
margin: 28px 0 8px;
}
.article h4 {
font-size: 15px;
font-weight: 600;
color: var(--text-secondary);
margin: 20px 0 6px;
}
.article p {
font-size: 15px;
color: var(--text-secondary);
margin-bottom: 12px;
line-height: 1.8;
}
.article strong { color: var(--text-primary); font-weight: 600; }
.article em { color: var(--text-secondary); font-style: italic; }
.article a {
color: var(--accent-hover);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color var(--transition);
}
.article a:hover { border-bottom-color: var(--accent); }
.article hr {
border: none;
border-top: 1px solid var(--border);
margin: 32px 0;
}
/* Lists */
.article ul, .article ol {
margin: 8px 0 16px;
padding-inline-start: 22px;
}
.article li {
font-size: 15px;
color: var(--text-secondary);
margin-bottom: 4px;
line-height: 1.7;
}
.article li::marker { color: var(--accent); }
/* Blockquotes */
.article blockquote {
border-inline-start: 3px solid var(--accent);
padding: 12px 16px;
margin: 16px 0;
background: var(--accent-subtle);
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
font-size: 14px;
color: var(--text-secondary);
}
.article blockquote p { margin: 4px 0; color: var(--text-secondary); }
/* Tables */
.article table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
font-size: 13.5px;
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.article thead th {
background: var(--bg-hover);
padding: 10px 14px;
text-align: start;
font-weight: 600;
color: var(--text-primary);
font-size: 12.5px;
text-transform: none;
border-bottom: 1px solid var(--border);
}
.article tbody td {
padding: 9px 14px;
color: var(--text-secondary);
border-bottom: 1px solid var(--border);
}
.article tbody tr:last-child td { border-bottom: none; }
.article tbody tr:hover td { background: var(--bg-surface); }
/* Code */
.article code {
font-family: var(--mono);
font-size: 12.5px;
background: var(--bg-surface);
border: 1px solid var(--border);
padding: 2px 6px;
border-radius: 4px;
color: var(--orange);
}
.article pre {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px 20px;
margin: 16px 0;
overflow-x: auto;
direction: ltr;
text-align: left;
}
.article pre code {
background: none;
border: none;
padding: 0;
color: var(--text-secondary);
font-size: 13px;
line-height: 1.6;
}
/* Mermaid */
.mermaid-wrapper {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 28px 20px;
margin: 24px 0;
overflow-x: auto;
direction: ltr;
text-align: center;
}
/* ===== SEARCH RESULTS ===== */
.search-view {
max-width: var(--content-max);
margin: 0 auto;
padding: 48px 40px;
}
.search-view h2 {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 24px;
}
.search-view h2 span {
font-size: 13px;
color: var(--text-tertiary);
font-weight: 500;
background: var(--bg-surface);
padding: 3px 10px;
border-radius: 20px;
margin-inline-start: 8px;
}
.sr-item {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px 20px;
margin-bottom: 12px;
cursor: pointer;
transition: all var(--transition);
}
.sr-item:hover {
border-color: var(--accent);
transform: translateY(-1px);
}
.sr-item h3 {
font-size: 15px;
font-weight: 600;
color: var(--accent-hover);
margin-bottom: 8px;
}
.sr-match {
font-size: 13px;
color: var(--text-tertiary);
padding: 4px 10px;
background: var(--bg-base);
border-radius: var(--radius-sm);
margin-top: 6px;
border-inline-start: 2px solid var(--orange);
line-height: 1.6;
}
.sr-match mark {
background: rgba(245,158,11,0.2);
color: var(--orange);
padding: 0 2px;
border-radius: 2px;
}
/* ===== LOADING ===== */
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 80px 20px;
color: var(--text-muted);
font-size: 14px;
}
.loading::after {
content: '';
width: 20px;
height: 20px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
margin-inline-start: 10px;
animation: spin 0.6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ===== MOBILE ===== */
.mobile-btn {
display: none;
position: fixed;
top: 12px;
inset-inline-end: 12px;
z-index: 200;
width: 40px;
height: 40px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-primary);
cursor: pointer;
align-items: center;
justify-content: center;
}
.mobile-btn svg { width: 20px; height: 20px; }
.backdrop {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.6);
backdrop-filter: blur(4px);
z-index: 90;
}
@media (max-width: 860px) {
.sidebar {
transform: translateX(100%);
transition: transform 0.3s cubic-bezier(0.16,1,0.3,1);
}
.sidebar.open { transform: translateX(0); }
.main { margin-inline-end: 0; }
.article, .search-view { padding: 24px 16px 60px; }
.mobile-btn { display: flex; }
.backdrop.visible { display: block; }
.article h1 { font-size: 22px; }
.article h2 { font-size: 18px; }
}
/* Scrollbar */
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--bg-active); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
/* Print */
@media print {
.sidebar, .mobile-btn, .backdrop { display: none !important; }
.main { margin: 0 !important; }
body { background: #fff; color: #000; }
.article { max-width: 100%; }
}
</style>
</head>
<body>
<button class="mobile-btn" id="menuBtn" aria-label="القائمة">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg>
</button>
<div class="backdrop" id="backdrop"></div>
<div class="app">
<aside class="sidebar" id="sidebar">
<div class="sidebar-brand">
<h1>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
EL-Captain Docs
</h1>
<p>دليل نظام الكابتن الشامل</p>
</div>
<div class="sidebar-search">
<div class="search-wrap">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
<input type="text" id="searchInput" placeholder="بحث..." autocomplete="off">
<span class="search-kbd">/</span>
</div>
</div>
<nav class="sidebar-nav" id="navList">
<?php foreach ($nav as $i => $item): ?>
<a class="nav-link" data-slug="<?= htmlspecialchars($item['slug']) ?>">
<span class="nav-num"><?= str_pad($i, 2, '0', STR_PAD_LEFT) ?></span>
<?= htmlspecialchars($item['title']) ?>
</a>
<?php endforeach; ?>
</nav>
</aside>
<main class="main" id="main">
<div class="loading" id="loader">جارٍ التحميل</div>
</main>
</div>
<script src="https://cdn.jsdelivr.net/npm/marked@12/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
<script>
const NAV = <?= $navJson ?>;
const DEFAULT_SLUG = <?= json_encode($defaultSlug) ?>;
let currentSlug = '';
mermaid.initialize({
startOnLoad: false,
theme: 'dark',
themeVariables: {
primaryColor: '#5E6AD2',
primaryTextColor: '#EDEDEF',
primaryBorderColor: '#27272a',
lineColor: '#71717a',
secondaryColor: '#18181b',
tertiaryColor: '#0c0c0e',
background: '#121215',
mainBkg: '#18181b',
nodeBorder: '#27272a',
clusterBkg: '#0c0c0e',
clusterBorder: '#27272a',
titleColor: '#EDEDEF',
edgeLabelBackground: '#18181b',
fontFamily: 'Cairo, system-ui, sans-serif'
},
flowchart: { curve: 'basis', useMaxWidth: true, htmlLabels: true },
sequence: { useMaxWidth: true },
});
// Custom marked renderer
const renderer = new marked.Renderer();
renderer.code = function(obj) {
const code = typeof obj === 'object' ? obj.text : obj;
const lang = typeof obj === 'object' ? obj.lang : arguments[1];
if (lang === 'mermaid') {
return `<div class="mermaid-wrapper"><pre class="mermaid">${code}</pre></div>`;
}
return `<pre><code class="language-${lang || ''}">${escapeHtml(code)}</code></pre>`;
};
marked.setOptions({
renderer,
gfm: true,
breaks: false,
headerIds: true,
});
function escapeHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
// Load page
async function loadPage(slug, pushState = true) {
if (!slug) slug = DEFAULT_SLUG;
currentSlug = slug;
document.getElementById('main').innerHTML = '<div class="loading" id="loader">جارٍ التحميل</div>';
// Update active nav
document.querySelectorAll('.nav-link').forEach(el => {
el.classList.toggle('active', el.dataset.slug === slug);
});
const res = await fetch(`?api=content&slug=${encodeURIComponent(slug)}`);
const data = await res.json();
if (!data.content) {
document.getElementById('main').innerHTML = '<div class="article"><h1>الصفحة غير موجودة</h1></div>';
return;
}
const html = marked.parse(data.content);
document.getElementById('main').innerHTML = `<div class="article">${html}</div>`;
document.title = `${data.title} — EL-Captain Docs`;
if (pushState) {
history.pushState({ slug }, '', `?page=${encodeURIComponent(slug)}`);
}
// Render mermaid diagrams
await mermaid.run({ querySelector: '.mermaid' });
// Scroll to top
window.scrollTo(0, 0);
}
// Search
let searchTimeout;
const searchInput = document.getElementById('searchInput');
searchInput.addEventListener('input', () => {
clearTimeout(searchTimeout);
const q = searchInput.value.trim();
if (!q) {
loadPage(currentSlug || DEFAULT_SLUG, false);
return;
}
searchTimeout = setTimeout(async () => {
const res = await fetch(`?api=search&q=${encodeURIComponent(q)}`);
const results = await res.json();
renderSearch(q, results);
}, 250);
});
function renderSearch(q, results) {
let html = `<div class="search-view"><h2>نتائج البحث: "${escapeHtml(q)}" <span>${results.length} ملف</span></h2>`;
if (!results.length) {
html += '<p style="color:var(--text-tertiary)">لا توجد نتائج. حاول كلمات أخرى.</p>';
} else {
results.forEach(r => {
html += `<div class="sr-item" onclick="loadPage('${r.slug}'); searchInput.value='';">`;
html += `<h3>${escapeHtml(r.title)}</h3>`;
r.matches.slice(0, 3).forEach(m => {
const highlighted = escapeHtml(m).replace(new RegExp(escapeHtml(q), 'gi'), '<mark>$&</mark>');
html += `<div class="sr-match">${highlighted}</div>`;
});
html += '</div>';
});
}
html += '</div>';
document.getElementById('main').innerHTML = html;
}
// Nav clicks
document.querySelectorAll('.nav-link').forEach(el => {
el.addEventListener('click', (e) => {
e.preventDefault();
searchInput.value = '';
loadPage(el.dataset.slug);
closeMobile();
});
});
// History
window.addEventListener('popstate', (e) => {
if (e.state?.slug) loadPage(e.state.slug, false);
});
// Keyboard
document.addEventListener('keydown', (e) => {
if (e.key === '/' && document.activeElement !== searchInput) {
e.preventDefault();
searchInput.focus();
}
if (e.key === 'Escape') {
searchInput.blur();
searchInput.value = '';
if (currentSlug) loadPage(currentSlug, false);
closeMobile();
}
});
// Mobile
const sidebar = document.getElementById('sidebar');
const backdrop = document.getElementById('backdrop');
const menuBtn = document.getElementById('menuBtn');
menuBtn.addEventListener('click', () => {
sidebar.classList.toggle('open');
backdrop.classList.toggle('visible');
});
backdrop.addEventListener('click', closeMobile);
function closeMobile() {
sidebar.classList.remove('open');
backdrop.classList.remove('visible');
}
// Init
const params = new URLSearchParams(location.search);
const initSlug = params.get('page') || DEFAULT_SLUG;
loadPage(initSlug, false);
</script>
</body>
</html>
# 30-Day Survival Plan: 150,000 EGP
## The Constraint
- **Need:** 150,000 EGP/month (in the bank)
- **Time:** 30 days to first 150K
- **Markets:** Egypt + Saudi Arabia
- **Model:** Flat fee + percentage on collections
- **Avg academy:** 50 players × 500 EGP = 25,000 EGP/month collections
---
## THE FRAMING (This Is Everything)
> **"النظام ده تكلفة تطويره تجاوزت ٢٥٠,٠٠٠ ج.م — ولسه بنطوره.**
> إحنا مش بنبيع لايسنس. إحنا بنقدم النظام لأوائل المشتركين بسعر دعم فني فقط.
> لما نوصل للنسخة النهائية — السعر هيتضاعف. اللي داخل دلوقتي بيقفل على السعر ده للأبد."**
### What they get:
- Full ERP system (worth 250K+ to build)
- Auto-generated website for their academy (yourname.elcaptain.com)
- Custom domain (youracademy.com) for annual subscribers
- Continuous updates forever
- They only pay "support & hosting fees" — these plans
---
## EGYPT PRICING
| | Small (up to 100) | Medium (100-300) | Large (300+) |
|---|--:|--:|--:|
| **Monthly flat** | 699 | 1,499 | 3,499 |
| **% on collections** | 7% | 5% | 3% |
| **6-Month prepaid** | 3,499 | 7,499 | 17,499 |
| **Annual prepaid** | 5,999 | 13,499 | 29,999 |
| **Setup fee** | 1,000 | 2,500 | 5,000 |
| **Custom domain** | Annual only | Annual only | Included |
| **Academy website** | Included | Included | Included |
| **Year 2 flat** | 599 | 1,299 | 2,999 |
| **Year 3+ flat** | 499 | 999 | 2,499 |
### Egypt Revenue Per Customer (50 players × 500 = 25K collections)
| Tier | Flat | + % on 25K | = Monthly Revenue |
|---|--:|--:|--:|
| Small | 699 | 1,750 | **2,449** |
| Medium (200 players, 100K) | 1,499 | 5,000 | **6,499** |
| Large (400 players, 200K) | 3,499 | 6,000 | **9,499** |
### What the CUSTOMER sees (Small tier, 50 players):
```
اشتراكك الشهري: 699 ج.م (رسم دعم فني واستضافة)
نسبة تشغيل: 7% من التحصيلات = 1,750 ج.م
─────────────────
الإجمالي: 2,449 ج.م/شهر
= 49 ج.م لكل لاعب في الشهر
= أقل من ١٠٪ من إيرادك
مقابل نظام قيمته ٢٥٠,٠٠٠+ ج.م
+ موقع إلكتروني لأكاديميتك
+ تحديثات مستمرة
+ دعم فني مباشر
```
---
## SAUDI PRICING
1 SAR = 13 EGP. Saudi academies charge 500-2000 SAR/player.
Average: 50 players × 800 SAR = 40,000 SAR/month collections.
| | Small (up to 100) | Medium (100-300) | Large (300+) |
|---|--:|--:|--:|
| **Monthly flat** | 299 SAR | 699 SAR | 1,499 SAR |
| **% on collections** | 5% | 3.5% | 2% |
| **6-Month prepaid** | 1,499 SAR | 3,499 SAR | 7,499 SAR |
| **Annual prepaid** | 2,699 SAR | 5,999 SAR | 12,999 SAR |
| **Setup fee** | 499 SAR | 999 SAR | 1,999 SAR |
| **Custom domain** | Annual only | Annual only | Included |
| **Academy website** | Included | Included | Included |
| **Year 2 flat** | 249 SAR | 599 SAR | 1,299 SAR |
| **Year 3+ flat** | 199 SAR | 499 SAR | 999 SAR |
### Saudi Revenue Per Customer (in EGP)
| Tier | Flat (SAR→EGP) | + % on collections (EGP) | = Monthly (EGP) |
|---|--:|--:|--:|
| Small (50 players, 40K SAR) | 3,887 | 26,000 | **29,887** |
| Medium (200 players, 160K SAR) | 9,087 | 72,800 | **81,887** |
| Large (400 players, 400K SAR) | 19,487 | 104,000 | **123,487** |
**One Saudi small = 30K EGP/month. Five of those = survival.**
---
## PATH TO 150K/MONTH
### Scenario A: Egypt Only
| Type | Count | Revenue Each | Total |
|---|:-:|--:|--:|
| Small Egypt (50 players) | 40 | 2,449 | 97,960 |
| Medium Egypt | 15 | 6,499 | 97,485 |
| **Total** | **55** | | **195,445** |
Or more realistically mixed:
| Type | Count | Revenue Each | Total |
|---|:-:|--:|--:|
| Small Egypt | 30 | 2,449 | 73,470 |
| Medium Egypt | 15 | 6,499 | 97,485 |
| **Total** | **45** | | **170,955** |
### Scenario B: Egypt + Saudi (RECOMMENDED)
| Type | Count | Revenue Each (EGP) | Total |
|---|:-:|--:|--:|
| Small Egypt | 25 | 2,449 | 61,225 |
| Medium Egypt | 10 | 6,499 | 64,990 |
| Small Saudi | 1 | 29,887 | 29,887 |
| **Total** | **36** | | **156,102** |
### Scenario C: Saudi Heavy (FASTEST)
| Type | Count | Revenue Each (EGP) | Total |
|---|:-:|--:|--:|
| Small Egypt | 15 | 2,449 | 36,735 |
| Medium Egypt | 5 | 6,499 | 32,495 |
| Small Saudi | 3 | 29,887 | 89,661 |
| **Total** | **23** | | **158,891** |
---
## REVENUE VISUALIZATION
```mermaid
pie title Scenario B - 36 Customers = 156K
"Small Egypt x25 = 61K" : 61225
"Medium Egypt x10 = 65K" : 64990
"Small Saudi x1 = 30K" : 29887
```
```mermaid
pie title Scenario C - 23 Customers = 159K
"Small Egypt x15 = 37K" : 36735
"Medium Egypt x5 = 32K" : 32495
"Small Saudi x3 = 90K" : 89661
```
---
## THE PITCH (Exact Words)
### Opening Frame:
> "النظام ده مش SaaS عادي. ده نظام ERP رياضي كامل — تكلفة تطويره لحد دلوقتي تجاوزت ربع مليون جنيه.
> لو رحت أي شركة تطلب نظام زي ده — هيقولوك ٢٥٠-٥٠٠ ألف + صيانة شهرية.
> إحنا مش بنبيعه. إحنا بنديهولك كامل — وتدفع بس رسوم الدعم والاستضافة."
### The Breakdown:
> "يعني الشهر ده هتدفع ٦٩٩ ج.م رسم دعم + ٧٪ من تحصيلاتك لتغطية تكاليف التشغيل والتطوير المستمر.
> عندك ٥٠ لاعب بيدفعوا ٥٠٠؟ يبقى إجمالي تكلفتك ٢,٤٤٩ ج.م.
> يعني ٤٩ جنيه بس على كل لاعب. أقل من كوباية قهوة."
### The Website Hook:
> "وكمان — النظام بيعمل لك موقع إلكتروني لأكاديميتك أوتوماتيك.
> أهالي اللاعبين يدخلوا يشوفوا الجدول، يدفعوا أونلاين، يتابعوا الحضور.
> لو عايز الموقع على دومين خاص باسم أكاديميتك — ده متاح مع الاشتراك السنوي."
### Early Adopter Urgency:
> "إحنا لسه في مرحلة النمو. اللي بيشترك دلوقتي بيقفل على السعر ده للأبد.
> لما نكمّل كل المودولات ونوصل للنسخة النهائية — السعر هيتضاعف.
> بس اللي دخل كـ early adopter — سعره ثابت. ده وعد."
---
## VALUE STACK (What 250K Gets Them)
| Module | Stand-alone Cost | They Get It |
|--------|----------------:|:-----------:|
| Financial system (invoices, payments, double-entry) | 50,000 | Included |
| Attendance engine | 25,000 | Included |
| Training programs + scheduling | 35,000 | Included |
| POS system | 30,000 | Included |
| Inventory management | 20,000 | Included |
| Parent portal + notifications | 25,000 | Included |
| Academy website (auto-generated) | 15,000 | Included |
| Custom domain setup | 5,000 | Annual plan |
| Pricing engine (smart discounts) | 20,000 | Included |
| Facility management + booking | 15,000 | Included |
| HR + payroll | 15,000 | Included |
| Reports + analytics | 10,000 | Included |
| **Total development value** | **265,000** | **2,449/mo** |
> "كل ده بـ ٤٩ جنيه لكل لاعب في الشهر. بدل ٢٦٥ ألف مرة واحدة."
---
## CUSTOM DOMAIN DETAILS
| Plan | Website | Domain |
|------|---------|--------|
| Monthly | yourname.elcaptain.com | Subdomain only |
| 6-Month | yourname.elcaptain.com | Subdomain only |
| Annual | youracademy.com | Custom domain included |
| Large (any plan) | youracademy.com | Custom domain included |
Cost to us: ~200 EGP/year per domain. Perceived value: 5,000+.
---
## OBJECTION HANDLING (Updated)
| They Say | You Say |
|----------|---------|
| "غالي" | "لو رحت لأي مطور يعملك نظام زي ده — هيقولك ٢٥٠ ألف minimum. إنت بتدفع ٤٩ جنيه بس لكل لاعب" |
| "الـ ٧٪ كتير" | "الـ ٧٪ بتغطي سيرفرات + تحديثات + دعم ٢٤ ساعة. لو حصّلت ٢٥ ألف، يبقى ١,٧٥٠ بس — أقل من نص يوم شغل ريسبشن" |
| "مش محتاج" | "تمام. بس الموقع الإلكتروني لأكاديميتك + بوابة أولياء الأمور — دول لوحدهم بـ ١٥ ألف لو عملتهم برا" |
| "هفكر" | "العرض ده لأوائل المشتركين بس. السعر هيتضاعف بعد ما نخلّص التطوير. دلوقتي قافل على السعر ده للأبد." |
| "عندي نظام" | "إيه هو؟ عنده موقع لأكاديميتك؟ بوابة أولياء أمور؟ محرك تسعير ذكي؟ POS؟ ده نظام كامل مش feature واحدة." |
| "أنا ٥٠ لاعب بس" | "ممتاز — يبقى تكلفتك ٤٩ جنيه لكل لاعب. ولما توصل ١٠٠ — نفس النظام يشيل معاك. مش هتحتاج تغير أي حاجة." |
---
## SAUDI PITCH (Formal Arabic)
### Opening:
> "نظام El-Captain نظام ERP رياضي متكامل — تكلفة تطويره تجاوزت مليون ريال.
> نقدمه للأكاديميات كخدمة بدلاً من بيعه كمنتج.
> رسوم الاشتراك تغطي الدعم الفني والاستضافة والتحديثات المستمرة فقط."
### Value:
> "النظام ينشئ لكم موقع إلكتروني احترافي لأكاديميتكم تلقائياً.
> أولياء الأمور يدخلون يتابعون الحضور ويسددون الرسوم إلكترونياً.
> مع الاشتراك السنوي: دومين خاص باسم أكاديميتكم."
---
## 30-DAY WAR PLAN
```mermaid
gantt
title 30 Days to 150K
dateFormat YYYY-MM-DD
axisFormat %d
section PREP
Build Egypt list :p1, 2026-07-07, 2d
Build Saudi list :p2, 2026-07-07, 2d
Prepare demo :p3, 2026-07-08, 1d
Write scripts :p4, 2026-07-08, 1d
Setup payments :p5, 2026-07-09, 1d
section EGYPT
WhatsApp blitz :e1, 2026-07-10, 25d
Follow up calls :e2, 2026-07-11, 24d
Run demos :e3, 2026-07-12, 23d
Field visits :e4, 2026-07-12, 20d
section SAUDI
Outreach :s1, 2026-07-10, 25d
LinkedIn :s2, 2026-07-10, 20d
Zoom demos :s3, 2026-07-14, 20d
Close deals :s4, 2026-07-20, 15d
section TARGET
150K collected :milestone, 2026-08-05, 0d
```
---
## WEEKLY TARGETS
| Week | Egypt Deals | Egypt Rev | Saudi Deals | Saudi Rev | Running Total |
|:----:|:-----------:|---------:|:-----------:|---------:|-------------:|
| 1 | 7 | 17,143 | 0 | 0 | 17,143 |
| 2 | 10 | 24,490 | 1 | 29,887 | 71,520 |
| 3 | 10 | 24,490 | 1 | 29,887 | 125,897 |
| 4 | 8 | 19,592 | 1 | 29,887 | 175,376 |
| **Total** | **35** | **85,715** | **3** | **89,661** | **175,376** |
---
## DAILY ROUTINE
| Time | Activity |
|------|----------|
| 8:00-9:00 | 30 Egypt WhatsApp messages |
| 9:00-9:30 | 10 Saudi messages |
| 9:30-10:00 | Follow up yesterday |
| 10:00-12:00 | Egypt demos 3-5 |
| 12:00-13:00 | Saudi Zoom demos 1-2 |
| 14:00-16:00 | In-person visits 2 academies |
| 16:00-17:00 | Proposals + collect payments |
| 17:00-18:00 | Onboard new signups |
| 20:00-21:00 | Reply evening messages |
---
## SUMMARY TABLE
| | Egypt Small | Egypt Medium | Egypt Large | Saudi Small | Saudi Medium | Saudi Large |
|---|--:|--:|--:|--:|--:|--:|
| **Flat/mo** | 699 | 1,499 | 3,499 | 299 SAR | 699 SAR | 1,499 SAR |
| **%** | 7% | 5% | 3% | 5% | 3.5% | 2% |
| **Avg players** | 50 | 200 | 400 | 50 | 200 | 400 |
| **Avg collections** | 25K | 100K | 200K | 40K SAR | 160K SAR | 400K SAR |
| **Total/mo (local)** | 2,449 | 6,499 | 9,499 | 2,299 SAR | 6,299 SAR | 9,499 SAR |
| **Total/mo (EGP)** | 2,449 | 6,499 | 9,499 | 29,887 | 81,887 | 123,487 |
| **Per player/mo** | 49 | 32 | 24 | 46 SAR | 31 SAR | 24 SAR |
| **Website** | Subdomain | Subdomain | Custom | Subdomain | Subdomain | Custom |
| **Custom domain** | Annual only | Annual only | Included | Annual only | Annual only | Included |
---
## THE FRAME THAT SELLS
```
┌─────────────────────────────────────────────────────┐
│ نظام قيمته ٢٥٠,٠٠٠+ ج.م │
│ + موقع إلكتروني لأكاديميتك │
│ + بوابة أولياء أمور │
│ + تحديثات مستمرة مدى الحياة │
│ ───────────────────────────────────── │
│ كل ده بـ ٤٩ ج.م لكل لاعب في الشهر │
│ (رسوم دعم فني واستضافة فقط) │
│ │
│ ⚡ سعر Early Adopter — مش هيتكرر │
└─────────────────────────────────────────────────────┘
```
---
## NON-NEGOTIABLE RULES
1. Money before onboarding. Always.
2. Frame it as "support fees" not "subscription" — they're getting a 250K system.
3. The 7% is "operating costs" not commission.
4. Saudi first — one Saudi deal = twelve Egyptian small deals.
5. Push annual for custom domain — anchors them for 12 months.
6. Every demo shows the website: "ده موقع أكاديميتك — جاهز في ٥ دقايق."
7. Early adopter framing: "السعر ده مش هيتكرر."
8. Never call it "subscription." Call it "رسوم دعم واستضافة."
---
> **They're not buying software. They're getting a 250K system for the price of support fees. And a free website. And they're locking in the price forever because they're early adopters. The 7% covers operating costs. Simple.**
---
## COST STRUCTURE
### WhatsApp Business API (ALL notifications go through here — NO SMS)
#### Pricing (per 24h conversation window, Egypt)
| Category | Who opens | Cost (USD) | Cost (EGP) | Notes |
|---|---|---:|---:|---|
| Utility (transactional) | We initiate | $0.0220 | 1.10 | Attendance, invoices, reminders |
| Marketing (promo) | We initiate | $0.1073 | 5.37 | Offers, announcements |
| Service | Parent messages us | FREE | 0 | First 1,000/month free |
**Within one 24h conversation = unlimited messages. Not per-message.**
#### Cost Per Academy (50 players)
| Use case | Conversations/month | Cost (EGP) |
|---|---:|---:|
| Attendance notifications (1 per parent per session day) | ~50 | 55 |
| Payment reminders | ~15 | 16.5 |
| Enrollment confirmations | ~5 | 5.5 |
| Schedule changes | ~3 | 3.3 |
| **Total** | **~73** | **~80** |
**80 EGP/customer/month for ALL notifications. vs 9,414 if SMS.**
#### Technical Setup
**Option A: Meta Cloud API (RECOMMENDED — free hosting)**
1. Create a Meta Business Account → business.facebook.com
2. Create a WhatsApp Business App in Meta Developer Portal
3. Verify your business (Egyptian commercial registration)
4. Get a dedicated phone number (new SIM, don't use personal)
5. Register number via the API (number gets locked to API — can't use WhatsApp app on it)
6. Create message templates (Arabic) → submit for Meta approval (24-48h)
7. Get API token → call `graph.facebook.com/v18.0/{phone-id}/messages`
**Infrastructure needed:**
- Webhook endpoint (Laravel route) to receive delivery receipts + incoming messages
- Queue worker to batch outgoing messages (rate limit: 80 msg/sec at Tier 1)
- Template storage in DB (notification_templates table — already designed)
**Rate limits (grow with usage):**
| Tier | Limit | How to reach |
|---|---|---|
| Tier 1 (start) | 1,000 conversations/day | Default |
| Tier 2 | 10,000/day | Good quality rating + volume |
| Tier 3 | 100,000/day | Sustained volume |
**Option B: BSP (Business Solution Provider) like Twilio/360dialog/WATI**
- Easier setup but adds margin (~30-50% markup on Meta rates)
- Better dashboard, no webhook coding needed
- Good for MVP if you want it running in 1 day
- Migrate to direct Cloud API later to cut costs
#### Laravel Integration
```
POST https://graph.facebook.com/v18.0/{PHONE_NUMBER_ID}/messages
Authorization: Bearer {ACCESS_TOKEN}
Content-Type: application/json
{
"messaging_product": "whatsapp",
"to": "201012345678",
"type": "template",
"template": {
"name": "attendance_notification",
"language": {"code": "ar"},
"components": [{
"type": "body",
"parameters": [
{"type": "text", "text": "أحمد"},
{"type": "text", "text": "حاضر"},
{"type": "text", "text": "السباحة - مجموعة المبتدئين"}
]
}]
}
}
```
#### Template Examples (submit to Meta for approval)
| Name | Body |
|---|---|
| `attendance_notification` | "مرحباً، ابنكم {{1}} سجّل {{2}} في حصة {{3}} اليوم." |
| `payment_reminder` | "تذكير: مبلغ {{1}} ج.م مستحق على حساب {{2}}. آخر موعد: {{3}}." |
| `enrollment_confirmed` | "تم تسجيل {{1}} في برنامج {{2}} بنجاح. بداية: {{3}}." |
| `schedule_change` | "تنبيه: تم تغيير موعد حصة {{1}} من {{2}} إلى {{3}}." |
#### Setup Costs (one-time)
| Item | Cost |
|---|---|
| Meta Business verification | Free |
| Dedicated SIM card | 50 EGP |
| Template approval | Free |
| Development time | Already built into platform |
| **Total setup** | **50 EGP** |
#### RULES
1. NO SMS. Ever. WhatsApp only.
2. All notifications go through template messages (pre-approved)
3. One conversation per parent per day (batch all notifications into that window)
4. Parents can reply → triggers free Service conversation → handle in-app
5. Store delivery status (sent/delivered/read) in notification_logs
6. If WhatsApp delivery fails → show in-app notification + mark for manual follow-up
### Hosting & Infrastructure (Fixed Monthly)
| Item | Cost (EGP) |
|------|---:|
| AWS EC2 (current server) | 2,500 |
| Domain (al-arcade.com, amortized) | 100 |
| SSL (wildcard via CapRover) | 0 |
| PostgreSQL (self-hosted) | 0 |
| Backups (S3/snapshots) | 250 |
| Email (poste.io, self-hosted) | 0 |
| **Total fixed** | **2,850** |
Fixed cost — same for 1 or 50 customers. At 36 customers = **79 EGP/customer/month**.
### Per-Customer Unit Economics (Small Egypt, 50 players)
| | Amount (EGP) |
|---|---:|
| Revenue (flat + 7% on 25K) | 2,449 |
| WhatsApp API (~73 conversations/mo) | -80 |
| Hosting share (÷36 customers) | -79 |
| **Net margin** | **2,290** |
| **Margin %** | **93.5%** |
### Break-Even Analysis
| Fixed costs (monthly) | 2,850 |
|---|---:|
| Min customers to cover fixed costs | **2** (at 2,290 margin each) |
| Customers to reach 150K net | **~66** Egypt small OR **36 mixed** (Scenario B) |
### Cost at Scale (Scenario B: 36 customers)
| | Monthly (EGP) |
|---|---:|
| Total revenue | 156,102 |
| WhatsApp API (36 × 80) | -2,880 |
| Hosting (fixed) | -2,850 |
| **Net profit** | **150,372** |
| **Net margin** | **96%** |
### Scaling: Large Academies (300-1000 players)
**Problem:** If you WhatsApp "ابنك حضر" every session day, costs explode at scale.
| Academy size | Naive approach (every session) | Cost (EGP) | % of Revenue | Verdict |
|---|---:|---:|---:|---|
| 300 players (~220 parents) | 3,080 convos/mo | 3,388 | 38% | DEAD |
| 500 players (~350 parents) | 5,600 convos/mo | 6,160 | 49% | DEAD |
| 1000 players (~700 parents) | 12,600 convos/mo | 13,860 | 75% | DEAD |
**Solution: Smart Notification Routing**
| Notification | Channel | Why |
|---|---|---|
| Kid is PRESENT | In-app + push ONLY | Routine — nobody needs WhatsApp for "everything is fine" |
| Kid is ABSENT/LATE | WhatsApp | Urgent — parent needs to know NOW |
| Payment due/overdue | WhatsApp | Drives collection |
| Enrollment confirmation | WhatsApp | One-time, important |
| Schedule change/cancellation | WhatsApp | Affects their plans |
| Daily digest (opt-in) | WhatsApp | ONE message per parent per day max |
**Recalculated with absence-only strategy:**
| Academy size | Absence convos | Payment | Other | Total/mo | Cost (EGP) | % of Revenue |
|---|---:|---:|---:|---:|---:|---:|
| 50 players (Small) | 40 | 15 | 8 | 63 | 69 | 3% |
| 300 players (Medium) | 600 | 200 | 50 | 850 | 935 | 10% |
| 500 players | 1,000 | 350 | 80 | 1,430 | 1,573 | 13% |
| 1000 players (Large) | 2,000 | 700 | 150 | 2,850 | 3,135 | 17% |
**Sustainable across ALL tiers.**
**Rate limit implications:**
| Academy size | Daily convos (peak) | Required Tier |
|---|---:|---|
| Small (50) | ~5 | Tier 1 (1,000/day) ✓ |
| Medium (300) | ~40 | Tier 1 (1,000/day) ✓ |
| Large (1000) | ~130 | Tier 1 (1,000/day) ✓ |
| Platform total (36 customers) | ~200 | Tier 1 (1,000/day) ✓ |
| Platform total (100 customers) | ~600 | Tier 1 (1,000/day) ✓ |
| Platform total (500 customers) | ~3,000 | Tier 2 (10,000/day) |
Tier 1 covers everything until 150+ customers. Auto-upgrades with volume.
### NON-NEGOTIABLE COST RULES
1. **NO SMS. Zero. WhatsApp Business API for all notifications.**
2. **Absence-only strategy: WhatsApp is for PROBLEMS, not routine confirmations.**
3. Batch notifications per parent per day into one conversation window (saves money)
4. Parents reply = free Service conversation — encourage it
5. If WhatsApp undelivered → in-app only. No fallback to SMS.
6. Monitor conversations per academy — flag above 2× expected for their tier.
7. Marketing conversations (offers, announcements) charged at 5.37 EGP — use sparingly.
8. "Daily attendance summary" is an OPT-IN premium feature — charge academy extra if they want it.
# نظام الكابتن لإدارة الأكاديميات الرياضية — الدليل الشامل
## نظرة عامة
نظام ERP متكامل مصمم خصيصاً للأكاديميات والأندية الرياضية في مصر. يدعم أي نشاط رياضي (كرة قدم، سباحة، جمباز، كاراتيه...) من خلال إعدادات مرنة بدون تغيير في الكود.
---
## فهرس الوحدات (Modules)
| # | الوحدة | الملف | الوصف |
|---|--------|-------|-------|
| 01 | تسجيل الدخول والهوية | [01-الهوية-والمنظمة.md](01-الهوية-والمنظمة.md) | تسجيل الدخول، الأدوار، الفروع، المستخدمين |
| 02 | المشتركين وأولياء الأمور | [02-المشتركين.md](02-المشتركين.md) | تسجيل المشتركين، الحالات، أولياء الأمور، الاستيراد |
| 03 | البرامج التدريبية والمجموعات | [03-البرامج-والمجموعات.md](03-البرامج-والمجموعات.md) | إنشاء البرامج، المجموعات، الجداول، الحصص |
| 04 | التسجيل (Enrollment) | [04-التسجيل.md](04-التسجيل.md) | تسجيل المشتركين في المجموعات، قوائم الانتظار، النقل |
| 05 | الحضور والغياب | [05-الحضور.md](05-الحضور.md) | تسجيل الحضور، التأخير، الغياب التلقائي، التنبيهات |
| 06 | المنشآت والمساحات | [06-المنشآت.md](06-المنشآت.md) | إدارة الملاعب، التخطيطات، الحجوزات، كشف التعارض |
| 07 | التعيينات (Assignments) | [07-التعيينات.md](07-التعيينات.md) | تعيين المدربين للمجموعات والحصص |
| 08 | المالية والفواتير | [08-المالية.md](08-المالية.md) | الفواتير، المدفوعات، القيد المزدوج، المحافظ |
| 09 | محرك التسعير | [09-التسعير.md](09-التسعير.md) | الأسعار الأساسية، قواعد الخصم، العروض، الكوبونات |
| 10 | نقطة البيع (POS) | [10-نقطة-البيع.md](10-نقطة-البيع.md) | محطة البيع، الورديات، الإيصالات |
| 11 | المخزون | [11-المخزون.md](11-المخزون.md) | المنتجات، المخازن، الحركات، الجرد |
| 12 | الموارد البشرية | [12-الموارد-البشرية.md](12-الموارد-البشرية.md) | الموظفين، المدربين، الرواتب، السلف |
| 13 | الإشعارات والمستندات | [13-الإشعارات-والمستندات.md](13-الإشعارات-والمستندات.md) | قوالب الإشعارات، المستندات، الموافقات |
| 14 | التقييمات | [14-التقييمات.md](14-التقييمات.md) | معايير التقييم، تقييم المشتركين |
| 15 | التقارير والإعدادات | [15-التقارير-والإعدادات.md](15-التقارير-والإعدادات.md) | التقارير المالية، الحضور، الإعدادات، لوحات التحكم |
| 16 | مكتب الاستقبال | [16-مكتب-الاستقبال.md](16-مكتب-الاستقبال.md) | ويزارد التسجيل الجديد، تسجيل موجود، تحصيل دفعة |
| 17 | مخططات الدورات الشاملة | [17-مخططات-الدورات.md](17-مخططات-الدورات.md) | 13 مخطط Mermaid: كل الدورات في مكان واحد |
---
## الأدوار المتاحة في النظام
| الدور | المستوى | الوصف |
|-------|---------|-------|
| مدير النظام (super_admin) | 100 | تحكم كامل بجميع الأكاديميات |
| مالك الأكاديمية (academy_owner) | 90 | تحكم كامل بأكاديمية واحدة |
| مدير الأكاديمية (academy_admin) | 80 | إدارة يومية كاملة |
| مدير الفرع (branch_manager) | 70 | إدارة فرع واحد |
| رئيس المدربين (head_trainer) | 60 | إشراف على المدربين |
| مدرب (trainer) | 50 | تسجيل حضور + تقييم |
| موظف استقبال (receptionist) | 40 | تسجيل مشتركين + تحصيل |
| محاسب (accountant) | 35 | عمليات مالية فقط |
| إدخال بيانات (data_entry) | 30 | إدخال بيانات فقط |
| ولي أمر (parent) | 5 | متابعة أبنائه فقط |
---
## دورات العمل الأساسية (Business Cycles)
### الدورة الكاملة لمشترك جديد:
```
تسجيل ← اختيار برنامج ← اختيار مجموعة ← حساب السعر ← إنشاء فاتورة ← دفع ← تفعيل ← حضور ← تقييم ← تخرج/تجديد
```
### الدورة المالية اليومية:
```
فتح وردية ← عمليات بيع/تحصيل ← إقفال وردية ← مطابقة
```
### دورة الحضور:
```
إنشاء حصة ← توليد سجلات متوقعة ← تسجيل الحضور ← غياب تلقائي ← تنبيهات ← إيقاف تلقائي
```
---
## المهام المجدولة (Automated Jobs)
| المهمة | التوقيت | الوظيفة |
|--------|---------|---------|
| غياب تلقائي | كل ساعة | تحويل "متوقع" إلى "غائب" بعد انتهاء الحصة بساعتين |
| فحص حدود الحضور | 06:00 يومياً | إيقاف تلقائي عند 5 غيابات متتالية |
| تنبيهات الحضور | 20:00 يومياً | إرسال تنبيهات لأولياء الأمور |
| فواتير متأخرة | 01:00 يومياً | تحويل فواتير منتهية الأجل إلى "متأخرة" |
| توليد حصص | 02:00 يومياً | إنشاء حصص الأسبوع القادم |
| تنظيف السجلات | أسبوعياً | حذف سجلات التدقيق الأقدم من سنة |
| انتهاء المستندات | 06:00 يومياً | تحديث حالة المستندات المنتهية |
| ملخص يومي | 07:00 يومياً | إرسال ملخص للإدارة |
| تهنئة أعياد الميلاد | 08:00 يومياً | إرسال رسائل تهنئة |
| كشف تعثر الأقساط | 08:00 يومياً | تنبيه عند تأخر قسط |
| مخزون منخفض | 09:00 يومياً | تنبيه عند وصول مخزون للحد الأدنى |
| تذكير اشتراكات تنتهي | 10:00 يومياً | تذكير قبل 7 و 3 أيام |
| تذكير أقساط | 11:00 يومياً | تذكير قبل 3 و 1 يوم |
| فواتير متأخرة (تذكير) | الاثنين + الخميس | إرسال تذكير المتأخرات |
| مصالحة أعداد المجموعات | 03:00 يومياً | تصحيح عدادات المجموعات |
| إنهاء اشتراكات منتهية | 00:30 يومياً | تعطيل الاشتراكات المنتهية |
| مصالحة مالية | أسبوعياً (الأحد 04:00) | فحص تطابق القيود |
| تقرير أسبوعي لأولياء الأمور | السبت 12:00 | إرسال تقرير أسبوعي |
| تنبيه امتلاء المجموعات | 08:00 يومياً | تنبيه عند وصول 90% من السعة |
---
## كيف تقرأ هذا الدليل
كل وحدة (Module) تحتوي على:
1. **وصف الشاشات** — كل شاشة بالتفصيل مع الحقول والأزرار
2. **دورات العمل** — مخططات Mermaid توضح تسلسل العمليات
3. **أمثلة عملية** — سيناريوهات واقعية مع أرقام
4. **لماذا يحتاج عملك هذا** — المبرر التجاري لكل ميزة
5. **التكاملات** — كيف تتصل هذه الوحدة بالوحدات الأخرى
# 01 — الهوية والمنظمة
## نظرة عامة
هذه الوحدة هي أساس النظام بالكامل. تتحكم في:
- من يمكنه الدخول (المصادقة)
- ماذا يمكنه فعله (الصلاحيات)
- أين يعمل (الفروع)
- تحت أي مظلة (الأكاديمية)
---
## الشاشات المنفذة
### 1. شاشة تسجيل الدخول (`/login`)
**الوصف:** شاشة واحدة لجميع المستخدمين — الإدارة، المدربين، الاستقبال، أولياء الأمور.
**الحقول:**
| الحقل | النوع | مطلوب | ملاحظات |
|--------|------|-------|---------|
| البريد الإلكتروني | email | ✓ | |
| كلمة المرور | password | ✓ | |
| تذكرني | checkbox | ✗ | |
**السلوك بعد الدخول:**
- المدرب/رئيس المدربين → لوحة المدرب (`/trainer`)
- موظف الاستقبال → مكتب الاستقبال (`/receptionist`)
- ولي الأمر → لوحة ولي الأمر (`/guardian`)
- باقي الأدوار → لوحة التحكم الرئيسية (`/`)
**سجل الدخول:** كل عملية دخول تُسجّل في جدول `login_history` (IP، المتصفح، التوقيت).
```mermaid
flowchart TD
A[شاشة تسجيل الدخول] --> B{التحقق من البيانات}
B -->|خطأ| C[رسالة خطأ]
B -->|نجاح| D{تحديد الدور}
D -->|مدرب| E[لوحة المدرب]
D -->|استقبال| F[مكتب الاستقبال]
D -->|ولي أمر| G[لوحة ولي الأمر]
D -->|إدارة| H[لوحة التحكم الرئيسية]
```
---
### 2. لوحة التحكم الرئيسية (`/`)
**من يراها:** مدير النظام، مالك الأكاديمية، مدير الأكاديمية، مدير الفرع، المحاسب.
**المحتوى:**
#### الصف الأول — إحصائيات اليوم (4 بطاقات):
| البطاقة | القيمة | المصدر |
|---------|--------|--------|
| حصص اليوم | عدد | الحصص المجدولة + الجارية |
| مدربين حاضرين | عدد | سجلات حضور المدربين اليوم |
| مدفوعات اليوم | مبلغ (ج.م) | مجموع المدفوعات المؤكدة |
| مشتركين نشطين | عدد | حالة = active |
#### الصف الثاني — يحتاج انتباه:
| التنبيه | الحالة |
|---------|--------|
| فواتير متأخرة | عدد الفواتير overdue |
| كشوف رواتب معلقة | draft أو pending_approval |
| مستندات تنتظر موافقة | حالة = pending |
| مخزون منخفض | أصناف وصلت للحد الأدنى |
| شهادات طبية تنتهي قريباً | تنتهي خلال 7 أيام |
| حصص بدون تسجيل حضور | انتهت ولم يُسجّل حضورها |
#### الصف الثالث:
- **يسار:** جدول حصص اليوم (الوقت، المجموعة، المدرب، الملعب، الحالة)
- **يمين:** آخر 10 مدفوعات (يظهر فقط لمن لديه صلاحية مالية)
**تصفية بالفرع:** إذا اختار المستخدم فرع من أعلى الصفحة، تُعرض بيانات هذا الفرع فقط.
---
### 3. إدارة الفروع (`/branches`)
**من يراها:** من لديه صلاحية `branches.list`
**شاشة القائمة:**
| العمود | الوصف |
|--------|-------|
| الاسم (عربي) | name_ar |
| الاسم (إنجليزي) | name |
| العنوان | address |
| الهاتف | phone |
| الحالة | active/inactive |
| الإجراءات | تعديل |
**شاشة الإنشاء/التعديل (`/branches/create`, `/branches/{id}/edit`):**
| الحقل | النوع | مطلوب | ملاحظات |
|--------|------|-------|---------|
| الاسم بالعربية | text | ✓ | |
| الاسم بالإنجليزية | text | ✗ | |
| العنوان | text | ✗ | |
| رقم الهاتف | text | ✗ | |
| البريد الإلكتروني | email | ✗ | |
| مفعّل | toggle | — | افتراضي: نعم |
**لماذا يحتاج عملك هذا:**
- أكاديمية بها 3 فروع → كل فرع له مشتركين ومدربين وورديات مالية منفصلة
- مدير الفرع يرى بيانات فرعه فقط
- التقارير يمكن تصفيتها بالفرع أو عرض الكل
**مثال عملي:**
> أكاديمية "الكابتن" لديها فرع المعادي وفرع المقطم وفرع التجمع. كل فرع له ملاعب مختلفة ومدربين مختلفين. مدير فرع المعادي لا يرى مشتركي المقطم.
---
### 4. محوّل الفروع (Branch Switcher)
**موقعه:** في شريط التنقل العلوي (Topbar)
**السلوك:**
- يظهر قائمة منسدلة بالفروع المتاحة للمستخدم
- اختيار "كل الفروع" يُظهر بيانات الأكاديمية كاملة
- اختيار فرع محدد يُصفّي جميع الشاشات بهذا الفرع
- التحديد يُحفظ في الجلسة
---
### 5. إدارة المستخدمين (`/users`)
**من يراها:** من لديه صلاحية `users.list`
**شاشة القائمة:**
- بحث بالاسم أو البريد
- تصفية بالحالة (active, inactive, suspended, pending)
- تصفية بالدور
| العمود | الوصف |
|--------|-------|
| الاسم | name |
| البريد | email |
| الدور | الدور الأساسي |
| الفرع | branch_id |
| الحالة | الشارة الملونة |
| آخر دخول | last_login_at |
| الإجراءات | تعديل |
**شاشة الإنشاء/التعديل (`/users/create`, `/users/{id}/edit`):**
| الحقل | النوع | مطلوب | ملاحظات |
|--------|------|-------|---------|
| الاسم بالكامل | text | ✓ | |
| البريد الإلكتروني | email | ✓ | فريد على مستوى الأكاديمية |
| كلمة المرور | password | ✓ (إنشاء فقط) | |
| رقم الهاتف | text | ✗ | |
| الدور | select | ✓ | لا يمكن اختيار دور أعلى من دورك |
| الفرع | select | ✗ | |
| الحالة | select | ✓ | active, inactive, suspended, pending |
**قاعدة هرمية الأدوار:** لا يمكن لمستخدم أن يُنشئ أو يُعدّل مستخدم بدور أعلى من دوره. مثلاً: مدير الفرع (70) لا يمكنه إنشاء مدير أكاديمية (80).
```mermaid
flowchart TD
A[مدير النظام - 100] --> B[مالك الأكاديمية - 90]
B --> C[مدير الأكاديمية - 80]
C --> D[مدير الفرع - 70]
D --> E[رئيس المدربين - 60]
E --> F[مدرب - 50]
D --> G[موظف الاستقبال - 40]
D --> H[محاسب - 35]
D --> I[إدخال بيانات - 30]
A --> J[ولي أمر - 5]
```
---
### 6. إدارة الأدوار والصلاحيات (`/roles`)
**من يراها:** من لديه صلاحية `roles.list`
**شاشة القائمة:**
| العمود | الوصف |
|--------|-------|
| اسم الدور | name_ar |
| المستوى | level (رقمي) |
| عدد المستخدمين | العدد المرتبط |
| الإجراءات | تعديل |
**شاشة الإنشاء/التعديل (`/roles/create`, `/roles/{id}/edit`):**
| الحقل | النوع | مطلوب |
|--------|------|-------|
| الاسم بالعربية | text | ✓ |
| الاسم بالإنجليزية | text | ✓ |
| المعرّف (slug) | text | ✓ |
| المستوى | number | ✓ |
| الصلاحيات | checkboxes مجمّعة | ✓ |
**تنسيق الصلاحيات:** `module.action` — مثل:
- `participants.list` / `participants.create` / `participants.view` / `participants.update` / `participants.delete`
- `invoices.list` / `invoices.create` / `invoices.view` / `invoices.cancel`
- `attendance.list` / `attendance.mark`
- `pos.sell` / `pos.list`
- `settings.view` / `settings.manage`
**نقاط التطبيق (4 طبقات):**
1. Middleware على الـ Route → يمنع الدخول أصلاً
2. `$this->authorize()` في Livewire → حماية إضافية
3. PermissionService::applyScope() → تصفية البيانات حسب النطاق
4. `@can()` في Blade → إخفاء الأزرار/الروابط
**أمثلة على النطاقات (Scopes):**
| النطاق | المعنى | مثال |
|--------|--------|------|
| academy | يرى كل بيانات الأكاديمية | مدير الأكاديمية |
| branch | يرى بيانات فرعه فقط | مدير الفرع |
| own | يرى ما أنشأه فقط | إدخال بيانات |
| own_groups | يرى مجموعاته فقط | مدرب |
| own_children | يرى أبناءه فقط | ولي أمر |
---
### 7. الملف الشخصي (`/profile`)
**من يراها:** أي مستخدم مسجّل دخوله
**المحتوى:**
- تغيير الاسم
- تغيير كلمة المرور
- عرض الدور والفرع
---
### 8. تفضيلات الإشعارات (`/profile/notifications`)
**من يراها:** أي مستخدم مسجّل دخوله
**المحتوى:**
- تفعيل/تعطيل قنوات الإشعار لكل نوع (بريد، SMS)
- ملاحظة: الإشعارات داخل النظام لا يمكن تعطيلها
---
### 9. سجل النشاط (`/admin/activity-log`)
**من يراها:** من لديه صلاحية `audit.view`
**المحتوى:**
- تاريخ كل عملية (إنشاء، تعديل، حذف)
- المستخدم الذي نفّذ العملية
- القيم القديمة والجديدة
- عنوان IP
---
### 10. لوحة مدير النظام (`/admin`)
**من يراها:** من لديه صلاحية `super_admin.access`
**المحتوى:**
- إدارة الأكاديميات (تفعيل/تعطيل)
- إعدادات النظام العامة
- عرض شامل عبر الأكاديميات
---
### 11. ويزارد الإعداد الأولي (`/setup`)
**من يراها:** من لديه صلاحية `settings.manage`
**الخطوات (12 خطوة):**
1. معلومات الأكاديمية الأساسية
2. إعداد الفروع
3. الأنشطة الرياضية
4. البرامج التدريبية
5. المجموعات
6. إعدادات الحضور
7. إعدادات مالية
8. إعدادات الإشعارات
9. إعداد المستخدمين والأدوار
10. إعداد المنشآت
11. إعداد المخزون
12. مراجعة وتأكيد
**لماذا يحتاج عملك هذا:**
> بدلاً من إعداد كل شيء يدوياً شاشة بشاشة، الويزارد يأخذك بترتيب منطقي ويضمن عدم نسيان أي إعداد أساسي.
---
### 12. إعدادات الأكاديمية (`/settings`)
**من يراها:** من لديه صلاحية `settings.view`
**المحتوى:**
- اسم الأكاديمية (عربي + إنجليزي)
- الشعار
- اللون الأساسي
- معلومات التواصل
- العنوان
---
### 13. إعدادات العلامة التجارية (`/settings/branding`)
**من يراها:** من لديه صلاحية `settings.manage`
**المحتوى:**
- رفع الشعار
- ألوان الواجهة
- النص في ذيل الصفحة
---
### 14. إعدادات النظام (`/settings/system`)
**من يراها:** من لديه صلاحية `settings.manage`
**أمثلة على الإعدادات:**
| الإعداد | النوع | الوصف |
|---------|-------|-------|
| فترة السماح للتأخير (مشتركين) | رقم (دقائق) | افتراضي 15 |
| فترة السماح للتأخير (مدربين) | رقم (دقائق) | افتراضي 10 |
| عدد الغيابات قبل الإيقاف | رقم | افتراضي 5 |
| نسبة الحضور الأدنى | نسبة % | افتراضي 75% |
| الحد الأقصى للخصم | نسبة % | افتراضي 50% |
| العملة | text | ج.م |
| المنطقة الزمنية | select | Africa/Cairo |
---
### 15. البحث العام (Global Search)
**موقعه:** في شريط التنقل العلوي
**يبحث في:**
- المشتركين (بالاسم، الهاتف، الرقم القومي)
- البرامج
- المجموعات
- الفواتير
---
### 16. تبديل الوضع الداكن (Dark Mode)
**موقعه:** في شريط التنقل العلوي
**السلوك:** يبدّل بين الوضع الفاتح والداكن
---
### 17. تبديل اللغة (Language Switcher)
**موقعه:** في شريط التنقل العلوي
**اللغات:** العربية (افتراضي) + الإنجليزية
---
## دورة العمل — إعداد أكاديمية جديدة
```mermaid
flowchart TD
A[تسجيل دخول مدير النظام] --> B[إنشاء أكاديمية]
B --> C[تشغيل ويزارد الإعداد]
C --> D[بيانات الأكاديمية]
D --> E[إنشاء الفروع]
E --> F[تعريف الأنشطة]
F --> G[إنشاء برامج]
G --> H[إنشاء مجموعات]
H --> I[إعدادات الحضور والمالية]
I --> J[إنشاء مستخدمين]
J --> K[✓ جاهز للعمل]
```
---
## دورة العمل — إنشاء مستخدم جديد
```mermaid
sequenceDiagram
participant Admin as المدير
participant System as النظام
participant User as المستخدم الجديد
Admin->>System: إنشاء مستخدم (اسم، بريد، دور، فرع)
System->>System: التحقق: هل الدور أقل من دور المنشئ؟
System->>System: التحقق: هل البريد فريد في الأكاديمية؟
System->>System: إنشاء المستخدم + ربطه بالدور
System-->>Admin: تم الإنشاء بنجاح
User->>System: تسجيل الدخول بالبيانات
System->>System: تحديد الشاشة حسب الدور
System-->>User: توجيه للوحة المناسبة
```
---
## أمثلة عملية
### مثال 1: أكاديمية بفرع واحد
> أكاديمية صغيرة فيها مالك واحد يدير كل شيء. يُنشئ حسابه كـ academy_owner ويختار "كل الفروع" دائماً.
### مثال 2: أكاديمية بـ 3 فروع
> أكاديمية كبيرة. لكل فرع مدير (branch_manager) لا يرى إلا فرعه. المالك يرى الكل من لوحة التحكم.
> - فرع المعادي: 2 مدربين، 80 مشترك
> - فرع المقطم: 3 مدربين، 120 مشترك
> - فرع التجمع: 1 مدرب، 40 مشترك
### مثال 3: مدرب يسجّل دخوله
> يسجّل الدخول → يُحوّل تلقائياً إلى `/trainer` → يرى حصص اليوم فقط + زر تسجيل الحضور.
### مثال 4: ولي أمر يسجّل دخوله
> يسجّل الدخول → يُحوّل إلى `/guardian` → يرى بيانات أبنائه فقط (حضور، تقييمات، فواتير).
---
## لماذا يحتاج عملك هذا
| الميزة | الفائدة |
|--------|---------|
| Multi-tenancy بـ academy_id | أكاديمية واحدة لا ترى بيانات أخرى أبداً |
| هرمية الأدوار | لا يمكن لموظف أن يُعطي نفسه صلاحيات أعلى |
| تصفية بالفرع | مدير الفرع يركّز على عمله بدون تشتت |
| توجيه حسب الدور | كل شخص يرى ما يحتاجه فقط — بدون تعقيد |
| سجل الدخول | تتبع من دخل ومتى — للأمان والمحاسبة |
| ويزارد الإعداد | أكاديمية جديدة جاهزة في 30 دقيقة بدلاً من أيام |
---
## التكاملات مع الوحدات الأخرى
| الوحدة | العلاقة |
|--------|---------|
| المشتركين | كل مشترك مرتبط بـ academy_id + branch_id |
| المالية | الوردية مرتبطة بالمستخدم + الفرع |
| الحضور | تسجيل الحضور يتطلب صلاحية attendance.mark |
| التقارير | تُصفّى بالفرع النشط |
| الإشعارات | تُرسل حسب تفضيلات المستخدم |
# 02 — المشتركين وأولياء الأمور
## نظرة عامة
المشترك هو الشخص الذي يتدرب في الأكاديمية. يمكن أن يكون طفلاً (مرتبط بولي أمر) أو بالغاً.
النظام يفصل بين "الشخص" (Person) و"المشترك" (Participant) — نفس الشخص يمكن أن يكون مشتركاً في أكثر من أكاديمية.
---
## الشاشات المنفذة
### 1. قائمة المشتركين (`/participants`)
**من يراها:** من لديه صلاحية `participants.list`
**الفلاتر:**
| الفلتر | النوع | الخيارات |
|--------|------|----------|
| بحث | نص حر | اسم، هاتف، رقم المشترك |
| الحالة | select | مسجل، نشط، مجمد، موقوف، غير نشط، متخرج، محول، منسحب، محظور |
| النشاط | select | قائمة الأنشطة المفعّلة |
| مستوى المهارة | select | مبتدئ، متوسط، متقدم، محترف |
**أعمدة الجدول:**
| العمود | الوصف |
|--------|-------|
| رقم المشترك | participant_number (يُولّد تلقائياً) |
| الاسم | person.name_ar |
| النشاط الأساسي | primary_activity.name_ar |
| الحالة | شارة ملونة |
| مستوى المهارة | beginner/intermediate/advanced/professional |
| تاريخ التسجيل | created_at |
| الإجراءات | عرض، تعديل |
**التصدير:** زر تصدير Excel متاح (`/export/participants`)
---
### 2. تسجيل مشترك جديد (`/participants/create`)
**من يراها:** من لديه صلاحية `participants.create`
**الحقول — بيانات الشخص (إذا جديد):**
| الحقل | النوع | مطلوب | ملاحظات |
|--------|------|-------|---------|
| الاسم بالعربية | text | ✓ | |
| الاسم بالإنجليزية | text | ✓ | |
| الرقم القومي | text | ✗ | 14 رقم — يستنتج منه تاريخ الميلاد والجنس |
| تاريخ الميلاد | date | ✗ | |
| الجنس | select | ✓ | ذكر / أنثى |
| الهاتف | text | ✗ | |
| البريد الإلكتروني | email | ✗ | |
**الحقول — بيانات المشترك:**
| الحقل | النوع | مطلوب | ملاحظات |
|--------|------|-------|---------|
| الفرع | select | ✗ | يأخذ الفرع النشط تلقائياً |
| مصدر التسجيل | select | ✓ | walk_in, online, referral, social_media, event, transfer, other |
| النشاط الأساسي | select | ✗ | قائمة الأنشطة المفعّلة |
| ولي الأمر الأساسي | select | ✗ | |
| مستوى المهارة | select | ✗ | مبتدئ، متوسط، متقدم، محترف |
| نوع العضوية | select | ✗ | عضو / غير عضو |
| رقم العضوية | text | مشروط | مطلوب إذا نوع العضوية = عضو — فريد بالفرع |
| إفادة طبية | toggle | — | |
| تاريخ انتهاء الإفادة | date | ✗ | |
| رقم التيشيرت | number | ✗ | 0-999 |
| مقاس التيشيرت | text | ✗ | |
| مقاس الحذاء | text | ✗ | |
| اليد المسيطرة | select | ✗ | يسار، يمين، كلاهما |
| القدم المسيطرة | select | ✗ | يسار، يمين، كلاهما |
| الطول (سم) | number | ✗ | 50-250 |
| الوزن (كجم) | number | ✗ | 10-200 |
| ملاحظات | textarea | ✗ | |
**ميزة الاكتشاف التلقائي:** عند إدخال الرقم القومي يتم استنتاج تاريخ الميلاد والجنس تلقائياً (خدمة NationalIdService).
**ميزة كشف التكرار:** النظام يبحث عن مشتركين بنفس الاسم أو الرقم القومي ويحذر قبل الإنشاء (DuplicateDetectionService).
```mermaid
flowchart TD
A[موظف الاستقبال يبدأ تسجيل جديد] --> B{شخص موجود؟}
B -->|نعم| C[اختيار الشخص من القائمة]
B -->|لا| D[إدخال بيانات شخص جديد]
C --> E[إدخال بيانات المشترك]
D --> E
E --> F{كشف التكرار}
F -->|وُجد تطابق| G[تحذير + خيار المتابعة]
F -->|لا يوجد| H[حفظ]
G --> H
H --> I[مشترك جديد بحالة 'مسجل']
```
---
### 3. ملف المشترك (`/participants/{id}`)
**من يراها:** من لديه صلاحية `participants.view`
**التبويبات:**
#### تبويب المعلومات الأساسية:
- الاسم، الجنس، تاريخ الميلاد، العمر
- رقم المشترك، رقم العضوية
- النشاط الأساسي، مستوى المهارة
- مصدر التسجيل
- ولي الأمر الأساسي
- البيانات البدنية (طول، وزن، يد/قدم مسيطرة)
- الإفادة الطبية وتاريخ انتهائها
#### تبويب الاشتراكات النشطة:
- المجموعات المسجّل فيها حالياً
- البرنامج لكل مجموعة
- المدرب الرئيسي
- جدول الحصص
#### تبويب الحضور (ملخص):
| المؤشر | القيمة |
|--------|--------|
| إجمالي الحصص | عدد |
| حاضر | عدد |
| متأخر | عدد |
| غائب | عدد |
| جزئي | عدد |
| نسبة الحضور | % |
**معادلة نسبة الحضور:**
```
(حاضر + متأخر + جزئي) ÷ (الإجمالي - ملغى - معفى) × 100
```
#### تبويب المدفوعات (آخر 10):
- تاريخ الدفع، المبلغ، طريقة الدفع، الفاتورة المرتبطة
#### تبويب المحفظة:
- الرصيد الحالي (إن وُجدت محفظة)
#### تغيير الحالة:
زر يفتح نافذة منبثقة تطلب:
- الحالة الجديدة (يظهر فقط الحالات المسموح الانتقال إليها)
- سبب التغيير (مطلوب — 3 حروف على الأقل)
---
### 4. تعديل مشترك (`/participants/{id}/edit`)
**من يراها:** من لديه صلاحية `participants.update`
نفس نموذج الإنشاء مع تعبئة البيانات الحالية.
---
### 5. تجميد مشترك (Freeze)
**المكوّن:** `FreezeParticipant`
**الحقول:**
- تاريخ بدء التجميد
- تاريخ نهاية التجميد (اختياري)
- سبب التجميد
**السلوك:**
- يُغيّر الحالة إلى "مجمد"
- يوقف توليد سجلات حضور مستقبلية
- يوقف الفوترة (لا تُنشأ فواتير تجديد)
---
### 6. تغيير حالة جماعي (`/participants/bulk-status`)
**من يراها:** من لديه صلاحية `participants.update`
**السلوك:**
- اختيار مجموعة مشتركين
- اختيار الحالة الجديدة
- إدخال السبب
- تنفيذ التغيير لكل المحددين
---
### 7. استيراد مشتركين (`/participants/import`)
**من يراها:** من لديه صلاحية `participants.create`
**السلوك:**
- رفع ملف Excel/CSV
- معاينة البيانات قبل الاستيراد
- كشف الأخطاء والتكرارات
- استيراد الصالح فقط
---
### 8. طباعة كارت المشترك (`/participants/{id}/card`)
**من يراها:** من لديه صلاحية `participants.view`
**المحتوى:**
- اسم المشترك
- رقم المشترك
- النشاط
- الفرع
- شعار الأكاديمية
- صورة (إن وُجدت)
---
### 9. شهادة الحضور (`/participants/{id}/certificate/{enrollment}`)
**من يراها:** من لديه صلاحية `participants.view`
**المحتوى:**
- شهادة حضور للاشتراك المحدد
- اسم المشترك، البرنامج، الفترة، نسبة الحضور
---
## حالات المشترك — آلة الحالة
```mermaid
stateDiagram-v2
[*] --> مسجل
مسجل --> نشط : تفعيل (بعد الدفع أو يدوي)
مسجل --> منسحب : انسحاب
نشط --> مجمد : تجميد مؤقت
نشط --> موقوف : 5 غيابات متتالية (تلقائي) أو يدوي
نشط --> غير_نشط : عدم تجديد
نشط --> متخرج : إنهاء البرنامج
نشط --> منسحب : طلب انسحاب
نشط --> محظور : قرار إداري
مجمد --> نشط : إنهاء التجميد
مجمد --> منسحب : انسحاب أثناء التجميد
مجمد --> غير_نشط : عدم تجديد
موقوف --> نشط : رفع الإيقاف
موقوف --> منسحب : انسحاب
موقوف --> محظور : تصعيد
غير_نشط --> نشط : إعادة تفعيل
متخرج --> نشط : تسجيل في برنامج جديد
منسحب --> نشط : إعادة تسجيل
```
**الحالات النهائية:**
- `محول` (transferred) — لا رجعة
- `محظور` (blacklisted) — لا رجعة (إلا بتدخل super_admin)
---
## سجل تغيير الحالة (Status Timeline)
**المكوّن:** `StatusTimeline`
يعرض تسلسل زمني لكل تغييرات الحالة:
- التاريخ والوقت
- من الحالة → إلى الحالة
- السبب
- من نفّذ التغيير
---
## تاريخ الاشتراكات (Enrollment History)
**المكوّن:** `EnrollmentHistory`
يعرض كل الاشتراكات (نشطة + منتهية + ملغاة):
- البرنامج
- المجموعة
- تاريخ البدء والانتهاء
- الحالة
---
## الأشخاص (People) — الطبقة الأساسية
### قائمة الأشخاص (`/people`)
- بحث بالاسم أو الرقم القومي أو الهاتف
- الأعمدة: الاسم، الهاتف، البريد، تاريخ الميلاد
### إنشاء/تعديل شخص (`/people/create`, `/people/{id}/edit`)
| الحقل | النوع | مطلوب |
|--------|------|-------|
| الاسم بالعربية | text | ✓ |
| الاسم بالإنجليزية | text | ✗ |
| الرقم القومي | text | ✗ |
| تاريخ الميلاد | date | ✗ |
| الجنس | select | ✓ |
| الهاتف | text | ✗ |
| البريد | email | ✗ |
| العنوان | text | ✗ |
### ملف الشخص (`/people/{id}`)
- بياناته الأساسية
- المشتركين المرتبطين (إذا كان أكثر من مشترك لنفس الشخص)
- أبناؤه (إذا كان ولي أمر)
---
## أولياء الأمور (Guardians)
ولي الأمر هو شخص (Person) مرتبط بمشترك واحد أو أكثر عبر جدول وسيط `guardian_participant`.
**العلاقة:**
- ولي أمر واحد ← عدة مشتركين (أبناؤه)
- مشترك واحد ← عدة أولياء أمور (أب + أم مثلاً)
- ولي أمر أساسي (primary_guardian_id) على سجل المشترك
**لوحة ولي الأمر (`/guardian`):**
- يرى بيانات أبنائه فقط
- حضور كل ابن
- فواتير كل ابن
- تقييمات كل ابن
---
## المستندات المرتبطة بالمشترك
**المكوّن:** `ParticipantDocuments`
يُظهر المستندات المرفوعة للمشترك:
- إفادة طبية
- شهادة ميلاد
- صورة شخصية
- أي مستند آخر
**تنبيه الشهادة الطبية:** `MedicalCertificateAlert` — يظهر تنبيه إذا الشهادة الطبية تنتهي خلال 7 أيام أو منتهية.
---
## دورة العمل — تسجيل مشترك جديد كاملة
```mermaid
sequenceDiagram
participant R as موظف الاستقبال
participant S as النظام
participant G as ولي الأمر
participant P as المشترك
R->>S: إنشاء شخص جديد (اسم، رقم قومي، هاتف)
S->>S: استنتاج العمر والجنس من الرقم القومي
S->>S: كشف التكرار
S-->>R: لا يوجد تكرار
R->>S: إنشاء مشترك (نشاط، فرع، ولي أمر)
S-->>R: مشترك بحالة "مسجل"
R->>S: تسجيل في مجموعة (Enrollment)
S->>S: حساب السعر (محرك التسعير)
S->>S: إنشاء فاتورة
S-->>R: فاتورة بمبلغ X ج.م
R->>S: تحصيل الدفعة
S->>S: تسجيل الدفع + قيد مزدوج
S->>S: تفعيل الاشتراك
S->>S: تغيير حالة المشترك → "نشط"
S->>S: توليد سجلات الحضور المتوقعة
S-->>G: إشعار: تم تسجيل ابنك
```
---
## أمثلة عملية
### مثال 1: طفل عمره 8 سنوات يسجّل في كرة قدم
> 1. ولي الأمر يأتي مع ابنه
> 2. الاستقبال يُسجّل بيانات الطفل (رقم قومي → العمر 8 سنوات تلقائياً)
> 3. يختار نشاط "كرة قدم"
> 4. يُضاف ولي الأمر كـ primary_guardian
> 5. الحالة: "مسجل"
> 6. بعد الدفع: الحالة → "نشط"
### مثال 2: مشترك يتجمد 3 أشهر (سفر)
> 1. المشترك يطلب تجميد
> 2. المدير يفتح ملف المشترك → تجميد
> 3. يُحدد فترة 3 أشهر + سبب "سفر"
> 4. الحالة → "مجمد"
> 5. لا يُحسب حضور/غياب خلال الفترة
> 6. لا تُصدر فواتير تجديد
> 7. بعد 3 أشهر: الحالة → "نشط" تلقائياً أو يدوياً
### مثال 3: 5 غيابات متتالية → إيقاف تلقائي
> 1. المشترك يغيب 5 حصص متتالية
> 2. المهمة المجدولة `attendance:enforce-thresholds` تكتشف ذلك
> 3. الحالة → "موقوف" تلقائياً
> 4. إشعار لولي الأمر: "تم إيقاف ابنك بسبب الغياب المتكرر"
> 5. إشعار للإدارة
> 6. لا يمكن للمشترك حضور حصص حتى يُرفع الإيقاف
### مثال 4: مشترك بعضوية نادي
> 1. عضوية نوعها "member" + رقم عضوية "M-2024-0001"
> 2. الرقم فريد داخل الفرع
> 3. يمكن أن يحصل على أسعار مختلفة (قاعدة تسعير حسب العضوية)
### مثال 5: نقل مشترك من فرع لآخر
> عبر خدمة TransferService:
> 1. يُنقل من فرع المعادي إلى فرع التجمع
> 2. تُلغى اشتراكاته الحالية
> 3. يُعاد تسجيله في مجموعات الفرع الجديد
> 4. الحالة تبقى "نشط"
---
## لماذا يحتاج عملك هذا
| الميزة | الفائدة |
|--------|---------|
| فصل Person عن Participant | نفس الشخص يمكن أن يكون مشتركاً + ولي أمر + موظف |
| آلة الحالة الصارمة | لا يمكن الانتقال لحالة غير منطقية (مثلاً: من "مسجل" إلى "متخرج") |
| الإيقاف التلقائي | يحفّز المشتركين على الالتزام بدون تدخل يدوي |
| كشف التكرار | يمنع تسجيل نفس الشخص مرتين بالخطأ |
| رقم العضوية | ربط النظام بأنظمة النادي الموجودة |
| البيانات البدنية | ضروري لرياضات مثل الجمباز والسباحة والكاراتيه |
| الاستيراد بالجملة | نقل 500 مشترك من نظام قديم بملف Excel واحد |
| طباعة الكارت | بطاقة هوية للمشترك عند الدخول للملعب |
---
## التكاملات مع الوحدات الأخرى
| الوحدة | العلاقة |
|--------|---------|
| التسجيل (Enrollment) | المشترك يُسجّل في مجموعات |
| المالية | الفواتير والمدفوعات مرتبطة بالمشترك |
| الحضور | سجلات الحضور مرتبطة بالمشترك |
| التقييمات | درجات التقييم مرتبطة بالمشترك |
| المحفظة | محفظة إلكترونية لكل مشترك |
| المستندات | مستندات مرفوعة (شهادة طبية، إلخ) |
| الإشعارات | إشعارات لولي الأمر عن ابنه |
| نقطة البيع | شراء منتجات |
# 03 — البرامج التدريبية والمجموعات
## نظرة عامة
**البرنامج التدريبي** = المنهج/الخطة (مثل: "كرة قدم - مبتدئين 6-8 سنوات")
**المجموعة** = التطبيق الفعلي للبرنامج (مثل: "مجموعة السبت والثلاثاء - المعادي")
**الجدول** = مواعيد المجموعة الأسبوعية
**الحصة** = جلسة تدريبية واحدة في يوم محدد
```
برنامج → مجموعة → جدول أسبوعي → حصص يومية
```
---
## الشاشات المنفذة
### 1. إدارة الأنشطة (`/activities`)
**من يراها:** من لديه صلاحية `activities.list`
**الوصف:** النشاط هو الرياضة نفسها (كرة قدم، سباحة، جمباز...). كل برنامج مرتبط بنشاط واحد.
**شاشة القائمة:**
| العمود | الوصف |
|--------|-------|
| الاسم بالعربية | name_ar |
| الاسم بالإنجليزية | name |
| التصنيف | category (فردي/جماعي/مائي/قتالي...) |
| مفعّل | is_active |
| الإجراءات | تعديل |
**شاشة الإنشاء/التعديل (`/activities/create`, `/activities/{id}/edit`):**
| الحقل | النوع | مطلوب | ملاحظات |
|--------|------|-------|---------|
| الاسم بالعربية | text | ✓ | |
| الاسم بالإنجليزية | text | ✓ | |
| التصنيف | select | ✓ | team_sport, individual_sport, aquatic, combat, fitness, dance, gymnastics, other |
| الوصف بالعربية | textarea | ✗ | |
| الوصف بالإنجليزية | textarea | ✗ | |
| مفعّل | toggle | — | |
---
### 2. قائمة البرامج التدريبية (`/programs`)
**من يراها:** من لديه صلاحية `programs.list`
**الفلاتر:**
- بحث بالاسم
- تصفية بالنشاط
- تصفية بالفرع
- تصفية بالحالة
**أعمدة الجدول:**
| العمود | الوصف |
|--------|-------|
| الاسم | name_ar |
| النشاط | activity.name_ar |
| الفرع | branch.name_ar |
| الحصص/الأسبوع | sessions_per_week |
| مدة الحصة | session_duration_minutes |
| السعة | min - max |
| الحالة | draft/active/archived |
| التسجيل مفتوح | ✓/✗ |
---
### 3. إنشاء برنامج — نموذج (`/programs/create`)
**من يراها:** من لديه صلاحية `programs.create`
**الحقول:**
| الحقل | النوع | مطلوب | ملاحظات |
|--------|------|-------|---------|
| الاسم بالعربية | text | ✓ | |
| الاسم بالإنجليزية | text | ✓ | |
| المعرّف (slug) | text | ✓ | يُولّد من الاسم |
| النشاط | select | ✓ | |
| الفرع | select | ✗ | |
| الوصف بالعربية | textarea | ✗ | |
| الوصف بالإنجليزية | textarea | ✗ | |
| مستوى المهارة | select | — | all, beginner, intermediate, advanced, professional |
| العمر الأدنى | number | ✗ | |
| العمر الأقصى | number | ✗ | |
| الجنس | select | — | all, male, female |
| حصص في الأسبوع | number | ✓ | افتراضي: 2 |
| مدة الحصة (دقائق) | number | ✓ | افتراضي: 60 |
| إجمالي الحصص | number | ✗ | للبرامج المحددة |
| مدة البرنامج (أسابيع) | number | ✗ | |
| الحد الأدنى للمشتركين | number | ✓ | افتراضي: 1 |
| الحد الأقصى للمشتركين | number | ✓ | افتراضي: 20 |
| السماح بقائمة الانتظار | toggle | — | افتراضي: نعم |
| نسبة الحضور المطلوبة | number | ✗ | % |
| تقييم مطلوب | toggle | — | |
| التسجيل مفتوح | toggle | — | |
| آخر موعد للتسجيل | date | ✗ | |
| تاريخ بدء البرنامج | date | ✗ | |
| تاريخ انتهاء البرنامج | date | ✗ | |
| سياسة التجديد | select | ✓ | manual_renew, auto_renew, no_renew |
| سياسة الإلغاء | textarea | ✗ | |
| سياسة الاسترداد | textarea | ✗ | |
| الحالة | select | ✓ | draft, active, archived |
| مميز | toggle | — | |
| سعر الأعضاء | money | ✗ | ج.م |
| سعر غير الأعضاء | money | ✗ | ج.م |
---
### 4. إنشاء برنامج — ويزارد (`/programs/wizard`)
**ويزارد من 3 خطوات:**
**الخطوة 1: البيانات الأساسية**
- الاسم، النشاط، الفرع، المستوى، العمر، الجنس
**الخطوة 2: هيكل البرنامج**
- حصص/أسبوع، مدة الحصة، إجمالي الحصص، السعة
**الخطوة 3: السياسات والتسعير**
- التجديد، الإلغاء، الأسعار
---
### 5. قائمة المجموعات (`/groups`)
**من يراها:** من لديه صلاحية `groups.list`
**الفلاتر:**
- بحث بالاسم أو الكود
- تصفية بالبرنامج
- تصفية بالفرع
- تصفية بالحالة
**أعمدة الجدول:**
| العمود | الوصف |
|--------|-------|
| الكود | code |
| الاسم | name_ar |
| البرنامج | program.name_ar |
| المدرب الرئيسي | head_trainer.name |
| السعة | current_count / max_capacity |
| الموسم | season |
| الحالة | شارة ملونة |
| الإجراءات | تعديل |
---
### 6. إنشاء مجموعة — نموذج (`/groups/create`)
**من يراها:** من لديه صلاحية `groups.create`
**الحقول:**
| الحقل | النوع | مطلوب | ملاحظات |
|--------|------|-------|---------|
| البرنامج | select | ✓ | عند الاختيار: يملأ السعة والفرع تلقائياً |
| الفرع | select | ✗ | |
| الاسم بالإنجليزية | text | ✓ | |
| الاسم بالعربية | text | ✓ | |
| الكود | text | ✗ | حتى 10 أحرف |
| المدرب الرئيسي | select | ✗ | |
| الحد الأقصى للسعة | number | ✓ | 1-500 |
| الموسم | text | ✗ | مثل: "2024-2025" |
| تاريخ البداية | date | ✗ | |
| تاريخ الانتهاء | date | ✗ | بعد أو يساوي تاريخ البداية |
| ملاحظات | textarea | ✗ | حتى 1000 حرف |
---
### 7. إنشاء مجموعة — ويزارد (`/groups/wizard`)
**ويزارد متعدد الخطوات:**
1. اختيار البرنامج والفرع
2. بيانات المجموعة (اسم، كود، مدرب)
3. إعداد الجدول الأسبوعي (أيام + أوقات)
4. تأكيد ومراجعة
---
### 8. الجدول الأسبوعي (`/schedule`)
**من يراها:** من لديه صلاحية `schedules.view`
**الوصف:** عرض أسبوعي يُظهر كل الحصص مرتبة بالأيام والأوقات.
**التصفية:** بالفرع، بالمجموعة، بالمدرب، بالملعب
**المعلومات لكل حصة:**
- المجموعة
- المدرب
- الملعب
- الوقت (من - إلى)
- الحالة (مجدولة / جارية / مكتملة / ملغاة)
---
### 9. إعادة جدولة حصة (Reschedule)
**المكوّن:** `RescheduleSession`
**الحقول:**
- التاريخ الجديد
- الوقت الجديد
- السبب
**السلوك:**
- الحصة الأصلية → حالة "rescheduled"
- تُنشأ حصة جديدة بالتاريخ/الوقت الجديد
- تُرسل إشعارات للمشتركين
---
### 10. طباعة جدول المجموعة (`/groups/{id}/print`)
**من يراها:** من لديه صلاحية `groups.list`
**المحتوى:**
- اسم المجموعة والبرنامج
- جدول الأيام والأوقات
- المدرب
- الملعب
- قابل للطباعة
---
## حالات المجموعة
```mermaid
stateDiagram-v2
[*] --> تكوين
تكوين --> نشطة : بدء التدريب
تكوين --> ملغاة : إلغاء قبل البدء
نشطة --> ممتلئة : وصول السعة القصوى
نشطة --> متوقفة : إيقاف مؤقت
نشطة --> مكتملة : انتهاء البرنامج
نشطة --> ملغاة : إلغاء
ممتلئة --> نشطة : فتح مكان (انسحاب مشترك)
متوقفة --> نشطة : استئناف
```
| الحالة | المعنى |
|--------|--------|
| forming | تكوين — لم تبدأ بعد |
| active | نشطة — تدريب جارٍ |
| full | ممتلئة — لا أماكن |
| on_hold | متوقفة مؤقتاً |
| completed | مكتملة — انتهى البرنامج |
| cancelled | ملغاة |
---
## حالات الحصة
```mermaid
stateDiagram-v2
[*] --> مجدولة
مجدولة --> جارية : بدء الحصة
مجدولة --> ملغاة : إلغاء
مجدولة --> معاد_جدولتها : نقل لموعد آخر
جارية --> مكتملة : انتهاء الحصة
```
| الحالة | المعنى |
|--------|--------|
| scheduled | مجدولة — لم تبدأ |
| in_progress | جارية — الحصة الآن |
| completed | مكتملة |
| cancelled | ملغاة |
| rescheduled | أُعيدت جدولتها |
---
## توليد الحصص التلقائي
**المهمة المجدولة:** `sessions:generate-upcoming` (يومياً 02:00)
**الآلية:**
1. لكل مجموعة نشطة لها جدول أسبوعي:
2. تولّد حصص الأسبوع القادم
3. تتخطى أيام العطل (جدول `holidays`)
4. تتخطى الحصص الموجودة بالفعل (لا تكرار)
```mermaid
flowchart TD
A[مهمة يومية 02:00] --> B[لكل مجموعة نشطة]
B --> C[قراءة الجدول الأسبوعي]
C --> D[لكل يوم في الأسبوع القادم]
D --> E{هل هو عطلة؟}
E -->|نعم| F[تخطي]
E -->|لا| G{هل الحصة موجودة؟}
G -->|نعم| F
G -->|لا| H[إنشاء حصة جديدة]
H --> I[توليد سجلات حضور متوقعة]
I --> J[إنشاء حجز مساحة - إن وُجد تعيين]
```
---
## دورة العمل — إنشاء برنامج كامل
```mermaid
sequenceDiagram
participant M as المدير
participant S as النظام
M->>S: إنشاء نشاط "كرة قدم"
S-->>M: ✓ تم
M->>S: إنشاء برنامج "كرة قدم - مبتدئين 6-8"
Note over S: النشاط: كرة قدم<br>العمر: 6-8<br>المستوى: beginner<br>حصص/أسبوع: 3<br>مدة: 60 دقيقة<br>سعة: 15-20
S-->>M: ✓ تم
M->>S: إنشاء مجموعة "سبت-اثنين-أربعاء"
Note over S: البرنامج: كرة قدم مبتدئين<br>المدرب: كابتن أحمد<br>السعة: 20
S-->>M: ✓ تم
M->>S: إنشاء جدول أسبوعي
Note over S: السبت 16:00-17:00<br>الاثنين 16:00-17:00<br>الأربعاء 16:00-17:00
S-->>M: ✓ تم
M->>S: تفعيل البرنامج
S->>S: توليد حصص الأسبوع القادم
S-->>M: ✓ البرنامج جاهز لاستقبال مشتركين
```
---
## أمثلة عملية
### مثال 1: أكاديمية كرة قدم بـ 3 فئات عمرية
> - برنامج "براعم 4-6 سنوات" — حصتين/أسبوع × 45 دقيقة — سعة 12
> - برنامج "أشبال 7-9 سنوات" — 3 حصص/أسبوع × 60 دقيقة — سعة 18
> - برنامج "ناشئين 10-14 سنة" — 3 حصص/أسبوع × 90 دقيقة — سعة 20
>
> كل برنامج له مجموعتين أو أكثر بأيام مختلفة.
### مثال 2: أكاديمية سباحة — برنامج محدد المدة
> - برنامج "تعليم سباحة - مستوى 1" — 12 حصة إجمالي — 3/أسبوع — 4 أسابيع
> - بعد انتهاء 12 حصة: الحالة → "مكتمل"
> - يمكن الانتقال لمستوى 2
### مثال 3: مجموعة امتلأت
> - المجموعة سعتها 20 — وصل عدد المشتركين 20
> - الحالة → "ممتلئة" تلقائياً
> - المشترك الجديد يُضاف لقائمة الانتظار (إذا مفعّلة)
> - عند انسحاب مشترك: الحالة → "نشطة" + إشعار لأول شخص في الانتظار
### مثال 4: إعادة جدولة حصة بسبب المطر
> 1. المدرب يطلب إلغاء حصة الأربعاء
> 2. المدير يستخدم "إعادة جدولة" → الخميس 16:00
> 3. الحصة الأصلية → "rescheduled"
> 4. حصة جديدة الخميس → "scheduled"
> 5. إشعار لجميع المشتركين
### مثال 5: عطلة رسمية
> 1. تُسجّل في جدول العطلات: "6 أكتوبر"
> 2. عند توليد الحصص: يوم 6 أكتوبر يُتخطّى تلقائياً
> 3. لا تُنشأ حصص ولا سجلات حضور لذلك اليوم
---
## العطلات (Holidays)
**جدول `holidays`:**
| الحقل | الوصف |
|--------|-------|
| name_ar | اسم العطلة |
| date | التاريخ |
| affects | يؤثر على: all, specific_activities, specific_branches |
| is_recurring | سنوي متكرر؟ |
**التأثير:**
- لا تُولّد حصص في أيام العطل
- العطل الجارية تُلغى حصصها (إذا وُلّدت قبل إضافة العطلة)
---
## ويزارد الجدول المرئي (`/facilities/schedule-builder`)
**الوصف:** واجهة مرئية لبناء الجدول الأسبوعي — سحب وإفلات الحصص على شبكة الأيام/الأوقات.
**الميزات:**
- عرض أسبوعي بصري
- كشف التعارض الفوري (مدرب مشغول / ملعب محجوز)
- تلوين حسب المجموعة أو النشاط
---
## لماذا يحتاج عملك هذا
| الميزة | الفائدة |
|--------|---------|
| فصل البرنامج عن المجموعة | نفس البرنامج يمكن أن يعمل بعدة مجموعات بأيام مختلفة |
| توليد الحصص التلقائي | لا حاجة لإنشاء 100 حصة يدوياً — يكفي الجدول الأسبوعي |
| العطلات | لا حاجة لإلغاء حصص يدوياً كل عطلة |
| حدود العمر والمستوى | يمنع تسجيل مشترك في برنامج غير مناسب له |
| سياسة التجديد | يحدد ماذا يحدث عند انتهاء الاشتراك |
| الويزارد المرئي | بناء جدول بسهولة بدون أخطاء |
| تنبيه امتلاء المجموعة | الإدارة تعرف متى تحتاج فتح مجموعة جديدة |
| طباعة الجدول | يُعلّق في لوحة الإعلانات أو يُرسل لأولياء الأمور |
---
## التكاملات مع الوحدات الأخرى
| الوحدة | العلاقة |
|--------|---------|
| المشتركين | يُسجّلون في المجموعات عبر Enrollment |
| الحضور | لكل حصة سجلات حضور متوقعة |
| المنشآت | كل حصة مرتبطة بملعب/مساحة |
| التعيينات | المدرب مُعيّن للمجموعة أو لحصة محددة |
| التسعير | السعر مرتبط بالبرنامج |
| المالية | الفاتورة تُنشأ عند التسجيل في مجموعة |
# 04 — التسجيل في المجموعات (Enrollment)
## نظرة عامة
التسجيل هو ربط مشترك بمجموعة تدريبية. يشمل:
- التحقق من الأهلية (العمر، الجنس، المستوى، التوفر)
- حساب السعر
- إنشاء فاتورة
- تفعيل الاشتراك بعد الدفع
- إدارة قائمة الانتظار
- النقل بين المجموعات
---
## الشاشات المنفذة
### 1. قائمة التسجيلات (`/enrollments`)
**من يراها:** من لديه صلاحية `enrollments.list`
**الفلاتر:**
- بحث بالاسم أو رقم المشترك
- تصفية بالبرنامج
- تصفية بالمجموعة
- تصفية بالحالة
**أعمدة الجدول:**
| العمود | الوصف |
|--------|-------|
| المشترك | اسم المشترك |
| المجموعة | اسم المجموعة |
| البرنامج | اسم البرنامج |
| تاريخ البدء | start_date |
| تاريخ الانتهاء | end_date |
| حالة الدفع | paid/pending/partial/overdue/waived |
| الحالة | شارة ملونة |
| الإجراءات | — |
**التصدير:** زر تصدير Excel (`/export/enrollments`)
---
### 2. تسجيل مشترك في مجموعة (`/enrollments/create`)
**من يراها:** من لديه صلاحية `enrollments.create`
**الحقول:**
| الحقل | النوع | مطلوب | ملاحظات |
|--------|------|-------|---------|
| المشترك | select/search | ✓ | بحث بالاسم أو الرقم |
| المجموعة | select | ✓ | تُعرض المجموعات المتاحة فقط |
| تاريخ البدء | date | ✗ | افتراضي: اليوم |
| حالة الدفع | select | ✓ | paid, pending, partial, overdue, waived |
| ملاحظات | textarea | ✗ | حتى 1000 حرف |
**التحققات عند الحفظ:**
1. هل المشترك مسجّل بالفعل في هذه المجموعة؟ → رفض
2. هل المجموعة ممتلئة؟ → إضافة لقائمة الانتظار (أو رفض إذا لا توجد قائمة انتظار)
3. هل المشترك مؤهل (عمر، جنس، مستوى)؟
4. هل المشترك موقوف أو محظور؟ → رفض
```mermaid
flowchart TD
A[طلب تسجيل] --> B{مسجّل بالفعل؟}
B -->|نعم| C[رفض: مسجل بالفعل]
B -->|لا| D{المشترك مؤهل؟}
D -->|لا| E[رفض: غير مؤهل]
D -->|نعم| F{المجموعة ممتلئة؟}
F -->|لا| G[تسجيل مباشر]
F -->|نعم| H{قائمة انتظار مفعّلة؟}
H -->|نعم| I[إضافة لقائمة الانتظار]
H -->|لا| J[رفض: لا أماكن]
G --> K[حساب السعر]
K --> L[إنشاء فاتورة]
L --> M{الدفع تم؟}
M -->|نعم| N[حالة: نشط]
M -->|لا| O[حالة: معلّق]
N --> P[توليد سجلات حضور]
```
---
### 3. إدارة قائمة الانتظار (`/enrollments/waitlist`)
**من يراها:** من لديه صلاحية `enrollments.list`
**الفلاتر:**
- بحث بالاسم
- تصفية بالمجموعة
- تصفية بالبرنامج
**أعمدة الجدول:**
| العمود | الوصف |
|--------|-------|
| المشترك | الاسم |
| المجموعة | اسم المجموعة |
| تاريخ الإضافة | created_at |
| الترتيب | الأولوية في القائمة |
| الإجراءات | تأكيد التسجيل، حذف |
**السلوك عند فتح مكان:**
1. أول شخص في القائمة يحصل على إشعار
2. لديه مهلة محددة (configurable) للرد
3. إذا لم يرد: ينتقل للتالي
4. عند القبول: يُنقل من waitlisted إلى pending ثم active بعد الدفع
---
### 4. نقل بين المجموعات (`/enrollments/transfer`)
**من يراها:** من لديه صلاحية `enrollments.create`
**الحقول:**
- المشترك
- المجموعة الحالية
- المجموعة الجديدة
- سبب النقل
**السلوك:**
1. الاشتراك القديم → ملغى
2. اشتراك جديد في المجموعة الجديدة
3. سجلات الحضور المستقبلية تُنقل
4. عداد المجموعة القديمة -1
5. عداد المجموعة الجديدة +1
---
### 5. ويزارد نقل مشترك (`/enrollments/transfer-wizard`)
**من يراها:** من لديه صلاحية `enrollments.create`
**خطوات الويزارد:**
1. اختيار المشترك والاشتراك الحالي
2. اختيار المجموعة الجديدة
3. مراجعة التأثيرات (حضور، مالية)
4. تأكيد النقل
---
## حالات التسجيل (Enrollment Status)
```mermaid
stateDiagram-v2
[*] --> معلّق
معلّق --> نشط : بعد الدفع أو يدوي
معلّق --> ملغى : إلغاء قبل التفعيل
معلّق --> قائمة_انتظار : المجموعة ممتلئة
قائمة_انتظار --> معلّق : فتح مكان + قبول
قائمة_انتظار --> ملغى : انسحاب
نشط --> مكتمل : انتهاء البرنامج
نشط --> ملغى : إلغاء
نشط --> منتهي : انتهاء المدة بدون تجديد
```
| الحالة | المعنى |
|--------|--------|
| pending | معلّق — بانتظار الدفع |
| active | نشط — يحضر التدريب |
| completed | مكتمل — أنهى البرنامج |
| cancelled | ملغى — إلغاء يدوي |
| expired | منتهي — انتهت المدة |
| waitlisted | في قائمة الانتظار |
---
## إنهاء الاشتراكات المنتهية
**المهمة المجدولة:** `enrollments:deactivate-expired` (يومياً 00:30)
**الآلية:**
- اشتراكات حالتها "نشط" وتاريخ انتهائها مرّ
- تُحوّل إلى "منتهي"
- تُزال سجلات الحضور المستقبلية
- عداد المجموعة يُنقّص
---
## تنبيه اشتراكات تنتهي قريباً
**المهمة المجدولة:** `reminders:expiring-enrollments` (يومياً 10:00 و 10:30)
- قبل 7 أيام: تذكير أول
- قبل 3 أيام: تذكير ثاني
---
## دورة العمل — تسجيل مشترك مع قائمة انتظار
```mermaid
sequenceDiagram
participant R as الاستقبال
participant S as النظام
participant W as ولي الأمر (في الانتظار)
R->>S: طلب تسجيل مشترك في مجموعة ممتلئة
S-->>R: المجموعة ممتلئة — إضافة لقائمة الانتظار؟
R->>S: نعم
S->>S: إضافة لقائمة الانتظار (ترتيب 3)
S-->>R: ✓ أُضيف — حالة: waitlisted
Note over S: --- بعد أسبوع: مشترك آخر ينسحب ---
S->>S: عداد المجموعة: 19/20
S->>S: فحص قائمة الانتظار
S->>W: إشعار: "يوجد مكان متاح — هل تريد التأكيد؟"
W->>S: نعم أريد التسجيل
S->>S: نقل من waitlisted → pending
S->>S: إنشاء فاتورة
R->>S: تحصيل الدفعة
S->>S: pending → active
S->>S: توليد سجلات الحضور
```
---
## أمثلة عملية
### مثال 1: تسجيل عادي ناجح
> 1. اختيار مشترك "أحمد" + مجموعة "السبت-الاثنين"
> 2. التحقق: مؤهل (عمر 7، المجموعة ليست ممتلئة)
> 3. حساب السعر: 1500 ج.م/شهر
> 4. إنشاء فاتورة
> 5. الدفع الفوري → حالة "نشط"
> 6. توليد 12 سجل حضور (4 أسابيع × 3 حصص)
### مثال 2: تسجيل بدون دفع
> 1. الاستقبال يسجّل المشترك بحالة دفع "pending"
> 2. حالة الاشتراك: "pending" (لا يحضر بعد)
> 3. لا تُولّد سجلات حضور حتى يدفع
> 4. بعد أسبوع: الأب يأتي ويدفع
> 5. الاشتراك يتفعّل → سجلات الحضور تُولّد
### مثال 3: نقل مشترك لمجموعة أخرى
> 1. "سارة" مسجلة في مجموعة السبت/الاثنين
> 2. الأم تطلب نقلها لمجموعة الأحد/الثلاثاء
> 3. نقل: إلغاء القديم + تسجيل جديد
> 4. سجلات الحضور المستقبلية تُحذف من القديمة وتُنشأ في الجديدة
> 5. التاريخ السابق محفوظ
### مثال 4: مشترك موقوف يحاول التسجيل
> 1. "محمد" حالته "موقوف" (5 غيابات متتالية)
> 2. الاستقبال يحاول تسجيله في مجموعة
> 3. النظام يرفض: "لا يمكن تسجيل مشترك موقوف"
> 4. يجب رفع الإيقاف أولاً
---
## لماذا يحتاج عملك هذا
| الميزة | الفائدة |
|--------|---------|
| قائمة الانتظار | لا تفقد عملاء محتملين عندما المجموعة ممتلئة |
| الربط بالدفع | الاشتراك يتفعّل فقط بعد الدفع — لا مجاملات |
| النقل | مرونة لتلبية طلبات أولياء الأمور بدون إعادة بناء |
| الإنهاء التلقائي | لا اشتراكات "شبح" — تُنظّف تلقائياً |
| التذكير بالتجديد | يذكّر أولياء الأمور قبل الانتهاء — أقل عملاء مفقودين |
| التصدير | تقرير لمن يدير عدة مجموعات يريد نظرة شاملة |
---
## التكاملات
| الوحدة | العلاقة |
|--------|---------|
| المشتركين | المشترك هو صاحب التسجيل |
| المجموعات | المجموعة هي هدف التسجيل |
| المالية | إنشاء فاتورة عند التسجيل |
| التسعير | حساب السعر عند التسجيل |
| الحضور | توليد سجلات حضور عند التفعيل |
| الإشعارات | تنبيهات قائمة الانتظار والتجديد |
# 05 — الحضور والغياب
## نظرة عامة
نظام الحضور يتتبع حضور المشتركين والمدربين في كل حصة تدريبية. يتكون من:
1. **توليد تلقائي** — عند إنشاء حصة، تُنشأ سجلات "متوقع" لكل مشترك/مدرب
2. **تسجيل يدوي** — المدرب أو الإدارة يسجّل الحضور فعلياً
3. **غياب تلقائي** — بعد انتهاء الحصة بساعتين، المتوقعون يُحوّلون لـ "غائب"
4. **إنفاذ** — 5 غيابات متتالية = إيقاف تلقائي
---
## الشاشات المنفذة
### 1. قائمة الحضور (`/attendance`)
**من يراها:** من لديه صلاحية `attendance.list`
**الفلاتر:**
- بحث بالاسم
- تصفية بالمجموعة
- تصفية بالتاريخ
- تصفية بالحالة
**أعمدة الجدول:**
| العمود | الوصف |
|--------|-------|
| الاسم | اسم المشترك/المدرب |
| النوع | مشترك أو مدرب |
| الحصة | تاريخ ووقت الحصة |
| المجموعة | اسم المجموعة |
| الحالة | شارة ملونة |
| وقت الحضور الفعلي | actual_check_in |
| دقائق التأخير | late_minutes |
| ملاحظات | notes |
---
### 2. تسجيل حضور حصة (`/attendance/{session}`)
**من يراها:** من لديه صلاحية `attendance.mark`
**الوصف:** شاشة تعرض جميع السجلات المتوقعة لحصة معينة مع أزرار تسجيل الحضور.
**المعلومات:**
- اسم المجموعة
- التاريخ والوقت
- المدرب
- ملاحظات الحصة (قابلة للتعديل)
**لكل سجل:**
| العنصر | الوصف |
|--------|-------|
| اسم المشترك/المدرب | + صورة إن وُجدت |
| الحالة الحالية | شارة ملونة |
| أزرار التغيير | حاضر، متأخر، غائب، مستأذن، جزئي |
| ملاحظة فردية | حقل نص — يُحفظ منفرداً |
**الأزرار العامة:**
- "الكل حاضر" — يسجّل حضور كل المتوقعين دفعة واحدة
- حفظ ملاحظات الحصة
**السلوك عند تسجيل "حاضر":**
1. وقت الحضور الفعلي = الآن
2. إذا الوقت الحالي > وقت بدء الحصة + فترة السماح → "متأخر" تلقائياً
3. يُحسب `late_minutes`
```mermaid
flowchart TD
A[المدرب يضغط 'حاضر'] --> B{الوقت الحالي}
B -->|قبل أو خلال فترة السماح| C[حالة: حاضر]
B -->|بعد فترة السماح| D[حالة: متأخر]
D --> E[حساب دقائق التأخير]
C --> F[تسجيل actual_check_in = now]
E --> F
```
---
### 3. تسجيل حضور سريع (`/attendance/quick`)
**من يراها:** من لديه صلاحية `attendance.mark`
**الوصف:** واجهة مختصرة لتسجيل الحضور بسرعة — اختيار الحصة ثم التسجيل.
**مناسبة لـ:** المدرب الذي لديه عدة حصص في اليوم ويريد تسجيل سريع.
---
### 4. لوحة المدرب (`/trainer`)
**من يراها:** المدرب ورئيس المدربين
**المحتوى:**
- حصص اليوم الخاصة بهذا المدرب
- زر "تسجيل الحضور" لكل حصة
- ملخص: كم حصة اليوم، كم مشترك، كم أنهيت
---
## حالات الحضور (10 حالات)
```mermaid
stateDiagram-v2
[*] --> متوقع : عند إنشاء الحصة
متوقع --> حاضر : تسجيل حضور
متوقع --> متأخر : حضور بعد فترة السماح
متوقع --> مستأذن : إذن مسبق
متوقع --> غائب : تلقائي بعد ساعتين أو يدوي
متوقع --> لم_يحضر : غياب بدون إخطار
متوقع --> انصرف_مبكراً : حضر ثم غادر
متوقع --> جزئي : حضر جزء من الحصة
متوقع --> ملغى : الحصة أُلغيت
متوقع --> معفى : إعفاء خاص
```
| الحالة | بالإنجليزية | المعنى | يُحسب إيجاباً؟ |
|--------|------------|--------|----------------|
| متوقع | expected | لم يُسجّل بعد | — |
| حاضر | present | حضر في الموعد | ✓ |
| متأخر | late | حضر بعد فترة السماح | ✓ |
| مستأذن | excused | إذن مسبق (مرض، ظرف) | لا يُحسب |
| غائب | absent | لم يحضر | ✗ |
| لم يحضر | no_show | غياب بدون إخطار مسبق | ✗ |
| انصرف مبكراً | left_early | حضر ثم غادر | يعتمد |
| جزئي | partial | حضر جزء | ✓ |
| ملغى | cancelled | الحصة ملغاة | لا يُحسب |
| معفى | exempt | إعفاء خاص | لا يُحسب |
---
## فترات السماح (Grace Periods)
| الدور | الافتراضي | قابل للتعديل |
|-------|-----------|-------------|
| مشتركين | 15 دقيقة | ✓ (إعدادات النظام) |
| مدربين | 10 دقائق | ✓ (إعدادات النظام) |
**مثال:**
> الحصة تبدأ 16:00. فترة السماح 15 دقيقة.
> - حضور 16:14 → "حاضر"
> - حضور 16:16 → "متأخر" + late_minutes = 16
---
## الغياب التلقائي
**المهمة المجدولة:** `attendance:auto-absent` (كل ساعة)
**الآلية:**
```
السجلات حيث:
- الحالة = "متوقع"
- وقت انتهاء الحصة + ساعتين < الآن
- لم يُسجّل يدوياً
→ تُحوّل إلى "غائب"
→ تُعلّم كـ "يحتاج مراجعة إدارية"
```
---
## إنفاذ حدود الحضور
**المهمة المجدولة:** `attendance:enforce-thresholds` (يومياً 06:00)
**القواعد:**
### 1. غيابات متتالية (افتراضي: 5)
```
إذا آخر 5 سجلات متتالية = "غائب" أو "لم يحضر"
→ المشترك يُوقف تلقائياً (status → suspended)
→ إشعار لولي الأمر
→ إشعار للإدارة
```
### 2. نسبة حضور أقل من الحد (افتراضي: 75%)
```
إذا نسبة الحضور < 75%
→ إشعار تحذيري لولي الأمر
→ إشعار للإدارة
→ لا يُوقف (تحذير فقط)
```
```mermaid
flowchart TD
A[مهمة يومية 06:00] --> B[لكل مشترك نشط]
B --> C{5 غيابات متتالية؟}
C -->|نعم| D[إيقاف تلقائي]
D --> E[إشعار ولي الأمر]
D --> F[إشعار الإدارة]
C -->|لا| G{نسبة الحضور < 75%؟}
G -->|نعم| H[إشعار تحذيري]
G -->|لا| I[لا إجراء]
```
---
## تنبيهات الحضور
**المهمة المجدولة:** `attendance:send-alerts` (يومياً 20:00)
**الوصف:** إرسال تنبيهات لأولياء الأمور عن حضور/غياب أبنائهم اليوم.
---
## توليد سجلات الحضور
**متى يحدث:**
1. عند إنشاء حصة جديدة (يومياً بواسطة session generator)
2. عند تفعيل اشتراك جديد (لجميع الحصص المستقبلية)
3. عند إنشاء تعيين مدرب جديد
**ماذا يُولّد:**
- سجل واحد لكل مشترك نشط في المجموعة
- سجل واحد لكل مدرب مُعيّن للمجموعة/الحصة
- الحالة الابتدائية: "expected"
**متى تُحذف:**
- عند إلغاء اشتراك (حصص مستقبلية فقط — الماضي يبقى)
- عند إلغاء حصة (جميع سجلاتها → "cancelled")
- عند إنهاء تعيين مدرب
---
## معادلة نسبة الحضور
```
نسبة الحضور = (حاضر + متأخر + جزئي) ÷ (الإجمالي - ملغى - معفى) × 100
```
**ملاحظات:**
- "مستأذن" (excused) لا يُحسب لا إيجاباً ولا سلباً
- "غائب" و"لم يحضر" يُحسبان سلباً
- "ملغى" و"معفى" يُستبعدان من القسمة
---
## تقرير الحضور (`/reports/attendance`)
**من يراها:** من لديه صلاحية `reports.attendance`
**الفلاتر:**
- الفترة الزمنية (من - إلى)
- المجموعة
- المشترك
- المدرب
**المخرجات:**
- نسبة الحضور لكل مشترك
- عدد أيام الغياب
- عدد أيام التأخير
- ترتيب حسب الحضور
---
## دورة الحضور الكاملة
```mermaid
sequenceDiagram
participant Job as مهمة مجدولة
participant S as النظام
participant T as المدرب
participant G as ولي الأمر
Job->>S: توليد حصة يوم السبت 16:00
S->>S: إنشاء 18 سجل حضور "متوقع"
Note over T: يوم السبت 15:55 - المدرب يبدأ
T->>S: فتح شاشة تسجيل الحضور
S-->>T: قائمة 18 مشترك (كلهم "متوقع")
T->>S: "أحمد" → حاضر (15:58)
S->>S: قبل فترة السماح → "حاضر"
T->>S: "سارة" → حاضر (16:18)
S->>S: بعد فترة السماح → "متأخر" + 18 دقيقة
T->>S: "محمد" → مستأذن (مريض)
S->>S: حالة: "مستأذن"
T->>S: الباقي لم يأتوا
Note over S: بعد ساعتين (18:00)
Job->>S: مهمة الغياب التلقائي
S->>S: من بقي "متوقع" → "غائب"
Note over S: اليوم التالي 06:00
Job->>S: فحص حدود الحضور
S->>S: "خالد" — 5 غيابات متتالية!
S->>S: إيقاف "خالد" تلقائياً
S->>G: إشعار: ابنك أُوقف بسبب الغياب
```
---
## أمثلة عملية
### مثال 1: مدرب يسجّل حضور مجموعة 15 مشترك
> 1. يفتح `/trainer` → يرى حصته الساعة 16:00
> 2. يضغط "تسجيل الحضور"
> 3. يرى 15 اسم (كلهم "متوقع")
> 4. يضغط "الكل حاضر" ← 15 سجل → "حاضر"
> 5. يُعدّل "محمد" إلى "غائب" لأنه لم يأتِ
> 6. يُعدّل "سارة" إلى "مستأذن" (قال والدها مريضة)
> 7. النتيجة: 13 حاضر، 1 غائب، 1 مستأذن
### مثال 2: تأخير مع حساب الدقائق
> - الحصة تبدأ 16:00، فترة السماح 15 دقيقة
> - "عمر" يُسجّل حضوره 16:25
> - النظام: حالة = "متأخر"، late_minutes = 25
### مثال 3: حصة ملغاة
> 1. المدير يُلغي حصة الأربعاء (بسبب المطر)
> 2. جميع سجلات الحضور → "ملغى"
> 3. لا تؤثر على نسبة الحضور
> 4. إشعار للمشتركين وأولياء الأمور
### مثال 4: إيقاف تلقائي
> - "خالد" غاب 5 حصص متتالية (سبت، اثنين، أربعاء، سبت، اثنين)
> - المهمة المجدولة تكتشف ذلك
> - حالة المشترك → "موقوف"
> - رسالة لوالده: "تم إيقاف خالد مؤقتاً بسبب الغياب المتكرر. يرجى التواصل مع الإدارة."
> - للعودة: يجب أن يتواصل ولي الأمر ويُرفع الإيقاف يدوياً
### مثال 5: مشترك حضوره 60% (تحذير)
> - نسبة الحضور الأدنى = 75%
> - "لينا" حضرت 9 من 15 حصة = 60%
> - رسالة تحذيرية لولي الأمر: "نسبة حضور لينا أقل من الحد المطلوب"
> - لا يُوقف — فقط تحذير
---
## لماذا يحتاج عملك هذا
| الميزة | الفائدة |
|--------|---------|
| توليد تلقائي | لا حاجة لإنشاء سجلات يدوياً لكل مشترك |
| الغياب التلقائي | لا يُنسى أحد — إذا المدرب لم يسجّل، النظام يُغيّب الجميع بعد ساعتين |
| الإيقاف التلقائي | يحفّز الالتزام بدون تدخل يدوي يومي |
| التنبيهات | ولي الأمر يعرف فوراً إذا ابنه لم يحضر |
| نسبة الحضور | مؤشر واضح لكل مشترك — يُستخدم في التقييم والشهادات |
| فترة السماح | عدالة — تأخير 5 دقائق لا يُعتبر غياب |
| سجل التأخير | يكشف الأنماط (هل هذا المشترك دائماً متأخر؟) |
---
## التكاملات
| الوحدة | العلاقة |
|--------|---------|
| البرامج والمجموعات | الحصة مصدر سجلات الحضور |
| التسجيل | تفعيل/إلغاء اشتراك → توليد/حذف سجلات |
| المشتركين | الإيقاف التلقائي يغيّر حالة المشترك |
| التعيينات | تعيين مدرب → سجل حضور للمدرب |
| الإشعارات | تنبيهات لأولياء الأمور والإدارة |
| التقارير | تقرير حضور شامل |
| الشهادات | نسبة الحضور تظهر في شهادة المشترك |
| الموارد البشرية | حضور المدرب → حساب المكافأة |
# 06 — المنشآت والمساحات
## نظرة عامة
المنشأة = ملعب أو حمام سباحة أو صالة. لكل منشأة تخطيطات (layouts) مختلفة حسب الوقت، والتخطيط مقسّم إلى أجزاء (segments) يمكن حجزها.
```
منشأة → تخطيط (حسب اليوم/الوقت) → أجزاء (قابلة للحجز)
```
---
## الشاشات المنفذة
### 1. قائمة المنشآت (`/facilities`)
**من يراها:** من لديه صلاحية `facilities.list`
**الفلاتر:**
| الفلتر | النوع | الخيارات |
|--------|------|----------|
| بحث | نص | اسم أو كود |
| الحالة | select | active, maintenance, closed, reserved, unavailable |
| النوع | select | indoor_field, outdoor_field, pool, gym, court, track, other |
| الفرع | select | قائمة الفروع |
**أعمدة الجدول:**
| العمود | الوصف |
|--------|-------|
| الكود | code |
| الاسم | name_ar |
| النوع | شارة |
| الفرع | branch.name_ar |
| المساحة | area_sqm م² |
| تكلفة الإيجار | monthly_rental_cost ج.م/شهر |
| الحالة | شارة ملونة |
| الإجراءات | تعديل، حذف، تخطيطات |
---
### 2. إنشاء/تعديل منشأة (`/facilities/create`, `/facilities/{id}/edit`)
**الحقول:**
| الحقل | النوع | مطلوب | ملاحظات |
|--------|------|-------|---------|
| الاسم بالعربية | text | ✓ | |
| الاسم بالإنجليزية | text | ✗ | |
| الكود | text | ✗ | |
| النوع | select | ✓ | indoor_field, outdoor_field, pool, gym, court, track, other |
| الفرع | select | ✗ | |
| المساحة (م²) | number | ✗ | |
| السعة القصوى | number | ✗ | |
| أوقات العمل (من) | time | ✗ | |
| أوقات العمل (إلى) | time | ✗ | |
| تكلفة إيجار شهرية | money | ✗ | بالقروش |
| الوصف | textarea | ✗ | |
| الحالة | select | ✓ | active, maintenance, closed |
---
### 3. ويزارد إنشاء منشأة (`/facilities/wizard`)
**خطوات:**
1. البيانات الأساسية (اسم، نوع، فرع)
2. المواصفات (مساحة، سعة، أوقات العمل)
3. التخطيط الأولي (نوع التقسيم + الأبعاد)
4. مراجعة وتأكيد
---
### 4. إدارة التخطيطات (`/facilities/{id}/layouts`)
**من يراها:** من لديه صلاحية `facilities.manage_layouts`
**المفهوم الأساسي:** المنشأة الواحدة يمكن أن يكون لها تخطيطات مختلفة حسب اليوم والوقت.
**مثال:**
> ملعب كرة قدم:
> - صباحاً (8:00-14:00): مقسّم 3×2 = 6 مساحات صغيرة (تدريب أطفال)
> - مساءً (14:00-22:00): مقسّم 2×1 = ملعبين كبيرين (مباريات)
**حقول التخطيط:**
| الحقل | النوع | مطلوب | ملاحظات |
|--------|------|-------|---------|
| الاسم بالعربية | text | ✓ | |
| الاسم بالإنجليزية | text | ✗ | |
| نوع التخطيط | select | ✓ | grid, lanes, zones, custom |
| متكرر أسبوعياً | toggle | — | |
| يوم الأسبوع | select | مشروط | إذا متكرر — 0(أحد) إلى 6(سبت) |
| تاريخ محدد | date | مشروط | إذا غير متكرر |
| وقت البداية | time | ✓ | |
| وقت النهاية | time | ✓ | |
| الأبعاد | حسب النوع | ✓ | |
**أنواع التخطيط:**
| النوع | الإعداد | النتيجة |
|-------|---------|---------|
| grid | صفوف × أعمدة | مثل: 3×2 = 6 مربعات |
| lanes | عدد الحارات | مثل: 6 حارات سباحة |
| zones | تعريف يدوي | مناطق بأسماء مخصصة |
| custom | تعريف يدوي | بدون قيود هندسية |
**توليد الأجزاء التلقائي:**
- Grid(3,2) → R1C1, R1C2, R2C1, R2C2, R3C1, R3C2
- Lanes(6) → Lane 1, Lane 2, ..., Lane 6
- Zones → جزء لكل تعريف
---
### 5. ويزارد تعيين المساحات (`/facilities/space-assignment`)
**من يراها:** من لديه صلاحية `facilities.manage_layouts`
**الوظيفة:** ربط مجموعة تدريبية بأجزاء محددة من المنشأة في أوقات محددة.
---
### 6. منشئ الجدول المرئي (`/facilities/schedule-builder`)
**من يراها:** من لديه صلاحية `schedules.manage`
**الوصف:** واجهة مرئية تعرض المنشأة كشبكة (أيام × أوقات) مع الحجوزات الحالية ملوّنة.
---
## حالات المنشأة
| الحالة | المعنى | التأثير |
|--------|--------|---------|
| active | مفعّلة — تعمل | يمكن الحجز |
| maintenance | صيانة | تُلغى الحجوزات الجديدة — القائمة تُلغى |
| closed | مغلقة | لا حجوزات |
| reserved | محجوزة (بالكامل) | لا حجوزات جديدة |
| unavailable | غير متاحة | لا حجوزات |
---
## كشف التعارض (Collision Detection)
**الخدمة:** `SpaceCollisionService`
**الآلية:**
```
1. إيجاد الحجوزات المؤكدة (confirmed) في نفس المنشأة ونفس اليوم
2. تصفية: التي تتداخل مع الوقت المطلوب
3. استبعاد نفس الحجز (إذا تعديل)
4. فحص تقاطع الأجزاء المطلوبة مع الأجزاء المحجوزة
5. إذا لا تقاطع → لا مشكلة ✓
6. إذا تقاطع → رفض + تفاصيل التعارض
```
```mermaid
flowchart TD
A[طلب حجز: منشأة X، يوم Y، 16:00-17:00، أجزاء 1,2,3] --> B[البحث عن حجوزات مؤكدة]
B --> C[تصفية التداخل الزمني]
C --> D{هل يوجد حجوزات متداخلة؟}
D -->|لا| E[✓ لا تعارض — يمكن الحجز]
D -->|نعم| F[فحص تقاطع الأجزاء]
F --> G{هل الأجزاء نفسها؟}
G -->|لا| E
G -->|نعم| H[✗ تعارض — رفض]
```
**أنواع الحجز:**
| النوع | التأثير |
|-------|---------|
| confirmed | يمنع الآخرين (حجز صلب) |
| tentative | يحذّر فقط (لا يمنع) |
| cancelled | غير مرئي — لا تأثير |
---
## حجز المساحة عند إنشاء حصة
```mermaid
sequenceDiagram
participant Job as مولّد الحصص
participant S as النظام
participant Col as خدمة التعارض
Job->>S: إنشاء حصة (مجموعة X، السبت 16:00)
S->>S: هل المجموعة لها مساحة محجوزة في الجدول؟
S->>Col: فحص: هل المساحة متاحة يوم السبت 16:00?
Col-->>S: ✓ متاحة
S->>S: إنشاء حجز (confirmed)
S->>S: إنشاء الحصة + ربطها بالحجز
```
**عند إلغاء الحصة:**
- الحجز المرتبط → cancelled
- المساحة تصبح متاحة فوراً
---
## أمثلة عملية
### مثال 1: ملعب كرة قدم بتخطيطين
> **صباحاً (8:00-14:00):** Grid 3×2 = 6 مساحات صغيرة
> - المساحة 1+2: مجموعة براعم 4-6
> - المساحة 3+4: مجموعة أشبال 7-9
> - المساحة 5+6: فارغة
>
> **مساءً (14:00-22:00):** Grid 2×1 = ملعبين
> - الملعب 1: مجموعة ناشئين 10-14
> - الملعب 2: إيجار خارجي
### مثال 2: حمام سباحة بـ 6 حارات
> - التخطيط: Lanes(6) → 6 حارات
> - الحارات 1-3: مجموعة تعليم مبتدئين (4:00-5:00)
> - الحارات 4-6: مجموعة متقدمين (4:00-5:00)
> - الحارات 1-6: مجموعة فريق المنافسات (5:00-7:00)
### مثال 3: محاولة حجز مساحة محجوزة
> 1. المدير يحاول تعيين مجموعة جديدة في الملعب 1، السبت 16:00
> 2. النظام يكتشف: مجموعة الناشئين محجوزة هناك 14:00-18:00
> 3. رفض: "تعارض مع حجز مجموعة الناشئين"
> 4. البدائل: اختيار ملعب آخر أو وقت آخر
### مثال 4: منشأة تدخل في صيانة
> 1. حالة المنشأة → maintenance
> 2. كل الحجوزات المستقبلية المؤكدة → cancelled
> 3. إشعارات للمجموعات المتأثرة
> 4. الحصص المرتبطة تحتاج إعادة تعيين لمنشأة أخرى
---
## لماذا يحتاج عملك هذا
| الميزة | الفائدة |
|--------|---------|
| التخطيطات المتعددة | نفس الملعب يُستخدم بطرق مختلفة صباحاً ومساءً |
| كشف التعارض | يستحيل حجز نفس المكان مرتين — لا فوضى |
| الحجز التلقائي | عند إنشاء الحصص، المساحة تُحجز تلقائياً |
| تكلفة الإيجار | حساب break-even: كم مشترك نحتاج لتغطية الإيجار؟ |
| أنواع التخطيط | يناسب أي رياضة: ملاعب، حمامات سباحة، صالات |
---
## التكاملات
| الوحدة | العلاقة |
|--------|---------|
| المجموعات | الجدول الأسبوعي مرتبط بمنشأة |
| الحصص | كل حصة مرتبطة بمنشأة + أجزاء |
| التعيينات | المدرب مُعيّن في مساحة محددة |
| المالية | تكلفة الإيجار تدخل في الحسابات |
| لوحة التحكم | عرض حصص اليوم مع المنشأة |
# 07 — التعيينات (Assignments)
## نظرة عامة
التعيين = ربط مدرب أو موظف بمجموعة تدريبية أو حصة محددة. يحدد:
- من يُدرّب من
- في أي فترة
- بأي نطاق (كامل، جزئي، مساعد، مشرف، مراقب)
---
## الشاشات المنفذة
### 1. قائمة التعيينات (`/assignments`)
**من يراها:** من لديه صلاحية `assignments.list`
**الفلاتر:**
- بحث بالاسم
- تصفية بالمجموعة
- تصفية بالحالة
**أعمدة الجدول:**
| العمود | الوصف |
|--------|-------|
| المُعيَّن | اسم المدرب/الموظف |
| النوع | trainer, head_trainer, assistant |
| المجموعة | اسم المجموعة |
| النطاق | full, partial, supervising, assisting, observing |
| تاريخ البداية | start_date |
| تاريخ النهاية | end_date |
| الحالة | active, suspended, completed, cancelled |
| الإجراءات | — |
---
### 2. إنشاء تعيين (`/assignments/create`)
**من يراها:** من لديه صلاحية `assignments.create`
**الحقول:**
| الحقل | النوع | مطلوب | ملاحظات |
|--------|------|-------|---------|
| المُعيَّن | select | ✓ | بحث بالاسم بين المدربين |
| نوع المُعيَّن | select | ✓ | trainer, assistant, head_trainer |
| المجموعة | select | ✓ | |
| النطاق | select | ✓ | full, partial, supervising, assisting, observing |
| تاريخ البداية | date | ✓ | |
| تاريخ النهاية | date | ✗ | |
| ملاحظات | textarea | ✗ | |
**النطاقات:**
| النطاق | المعنى |
|--------|--------|
| full | مسؤول كامل عن المجموعة |
| partial | مسؤول عن جزء (أيام محددة) |
| supervising | إشراف فقط — لا يُدرّب مباشرة |
| assisting | مساعد — يساعد المدرب الرئيسي |
| observing | مراقب — لا يتدخل |
---
### حالات التعيين
```mermaid
stateDiagram-v2
[*] --> نشط
نشط --> موقوف : إيقاف مؤقت
نشط --> مكتمل : انتهاء الفترة
نشط --> ملغى : إلغاء
موقوف --> نشط : استئناف
```
---
## كشف تعارض التعيينات
**الخدمة:** `ScheduleConflictService`
**التحققات عند إنشاء تعيين:**
1. هل المدرب مُعيّن بالفعل لمجموعة أخرى في نفس الوقت؟
2. هل المدرب متاح في هذه الأيام/الأوقات (جدول التوفر)؟
3. هل تجاوز الحد الأقصى لساعات العمل الأسبوعية؟
---
## العلاقة بالحضور
**عند إنشاء تعيين:**
- تُولّد سجلات حضور "expected" للمدرب في جميع حصص المجموعة المستقبلية
**عند إنهاء تعيين:**
- تُحذف سجلات الحضور المستقبلية للمدرب (الماضي يبقى)
**التجاوز على مستوى الحصة:**
- إذا حصة معينة لها مدرب مُعيّن خصيصاً (override) → المدرب المُعيّن على مستوى المجموعة لا يُتوقع حضوره لتلك الحصة
---
## أمثلة عملية
### مثال 1: تعيين مدرب رئيسي لمجموعة
> 1. كابتن أحمد → مجموعة "أشبال السبت/الاثنين" → نطاق: full
> 2. تُولّد سجلات حضور لأحمد في كل حصص المجموعة
> 3. يظهر في لوحة المدرب: حصصه اليوم
### مثال 2: مدرب مساعد في أيام محددة
> 1. كابتن سارة → نفس المجموعة → نطاق: partial
> 2. يوم السبت فقط
> 3. حضورها مطلوب يوم السبت فقط
### مثال 3: تعارض تعيين
> 1. كابتن أحمد مُعيّن لمجموعة "أشبال" السبت 16:00-17:00
> 2. المدير يحاول تعيينه لمجموعة "ناشئين" السبت 16:30-18:00
> 3. النظام يكتشف التعارض الزمني
> 4. تحذير: "تعارض مع تعيين موجود"
---
## لماذا يحتاج عملك هذا
| الميزة | الفائدة |
|--------|---------|
| ربط مدرب بمجموعة | واضح من يُدرّب من — لا خلافات |
| كشف التعارض | مستحيل تعيين مدرب في مكانين بنفس الوقت |
| نطاقات متعددة | يناسب أكاديميات بمدربين رئيسيين ومساعدين |
| الربط بالحضور | المدرب يُحاسب على حضوره تلقائياً |
| الربط بالرواتب | حضور المدرب = أساس حساب المكافأة |
---
## التكاملات
| الوحدة | العلاقة |
|--------|---------|
| الموارد البشرية | المدرب هو الشخص المُعيَّن |
| المجموعات | المجموعة هي هدف التعيين |
| الحضور | تعيين → سجلات حضور مدرب |
| الرواتب | حضور المدرب → مكافأة الحصة |
| المنشآت | التعيين مرتبط بمساحة في المنشأة |
# 08 — المالية والفواتير
## نظرة عامة
النظام المالي يعتمد على **القيد المزدوج** — كل عملية مالية تُنشئ قيدين (مدين + دائن). الأموال تُخزّن بال**قروش** (1 ج.م = 100 قرش) لتجنب مشاكل الأرقام العشرية.
```
فاتورة → دفعة → قيد مزدوج (مدين + دائن)
```
---
## الشاشات المنفذة
### 1. النظرة المالية العامة (`/financial-overview`)
**من يراها:** من لديه صلاحية `invoices.list`
**الفلاتر:**
- الفترة: اليوم / هذا الأسبوع / هذا الشهر / الشهر الماضي / هذا الربع
- الفرع
**المحتوى:**
| القسم | الوصف |
|-------|-------|
| الإيرادات | مجموع المدفوعات المؤكدة في الفترة |
| المصروفات | مشتريات مخزون + رواتب + إيجارات |
| الأرباح والخسائر شهرياً | رسم بياني |
| نقطة التعادل | كم مشترك نحتاج لتغطية التكاليف |
| الإيرادات حسب المصدر | اشتراكات / مبيعات / إيجارات |
| نسبة التحصيل | المحصّل ÷ المستحق × 100 |
| تكاليف المنشآت | إيجارات الملاعب الشهرية |
---
### 2. قائمة الفواتير (`/invoices`)
**من يراها:** من لديه صلاحية `invoices.list`
**الفلاتر:**
- بحث برقم الفاتورة أو اسم المشترك
- الحالة
- الفترة
**أعمدة الجدول:**
| العمود | الوصف |
|--------|-------|
| رقم الفاتورة | invoice_number |
| المشترك | billable.name_ar |
| المجموع | total_amount (ج.م) |
| المدفوع | paid_amount (ج.م) |
| المتبقي | due_amount (ج.م) |
| تاريخ الإصدار | issue_date |
| تاريخ الاستحقاق | due_date |
| الحالة | شارة ملونة |
| الإجراءات | عرض، طباعة |
**التصدير:** زر تصدير Excel (`/export/invoices`)
---
### 3. إنشاء فاتورة — ويزارد (`/invoices/wizard`)
**خطوات:**
1. اختيار المشترك
2. إضافة البنود (برنامج / منتج / خدمة)
3. تطبيق الخصم/الكوبون
4. تحديد تاريخ الاستحقاق وطريقة الدفع
5. مراجعة وإصدار
---
### 4. إنشاء فاتورة — نموذج (`/invoices/create`)
**الحقول:**
| الحقل | النوع | مطلوب |
|--------|------|-------|
| المشترك | select/search | ✓ |
| البنود | repeater | ✓ (بند واحد على الأقل) |
| تاريخ الإصدار | date | ✓ |
| تاريخ الاستحقاق | date | ✓ |
| ملاحظات | textarea | ✗ |
**بند الفاتورة:**
| الحقل | النوع | مطلوب |
|--------|------|-------|
| الوصف | text | ✓ |
| الكمية | number | ✓ |
| سعر الوحدة | money | ✓ |
| الخصم | money | ✗ |
---
### 5. تفاصيل فاتورة (`/invoices/{id}`)
**من يراها:** من لديه صلاحية `invoices.view`
**المحتوى:**
- معلومات الفاتورة (رقم، تاريخ، حالة)
- بيانات المشترك
- جدول البنود (وصف، كمية، سعر، إجمالي)
- الإجمالي الفرعي، الخصم، الضريبة، الإجمالي
- المدفوع، المتبقي
- سجل المدفوعات السابقة
**نموذج تسجيل دفعة (مدمج في الصفحة):**
| الحقل | النوع | مطلوب |
|--------|------|-------|
| المبلغ | money | ✓ | يعبأ تلقائياً بالمتبقي |
| طريقة الدفع | select | ✓ | cash, card, bank_transfer, wallet, online, cheque, other |
| تاريخ الدفع | date | ✓ | اليوم افتراضياً |
| ملاحظات | textarea | ✗ | |
**التحقق عند الدفع:**
- المبلغ > 0
- المبلغ ≤ المتبقي
- الفاتورة ليست ملغاة أو مسودة
---
### 6. طباعة فاتورة (`/invoices/{id}/print`)
صفحة قابلة للطباعة تحتوي:
- شعار الأكاديمية
- بيانات المشترك
- البنود
- الإجماليات
- QR code أو رابط عام
---
### 7. فاتورة عامة (`/public/invoice/{uuid}`)
**بدون تسجيل دخول** — رابط عام (signed URL) يُرسل لولي الأمر.
---
## حالات الفاتورة
```mermaid
stateDiagram-v2
[*] --> مسودة
مسودة --> مرسلة : إرسال
مسودة --> ملغاة : إلغاء
مرسلة --> مدفوعة_جزئياً : دفعة جزئية
مرسلة --> متأخرة : تجاوز تاريخ الاستحقاق
مرسلة --> ملغاة : إلغاء (فقط إذا المدفوع = 0)
متأخرة --> مدفوعة_جزئياً : دفعة جزئية
متأخرة --> مدفوعة : دفع كامل
مدفوعة_جزئياً --> مدفوعة : اكتمال الدفع
مدفوعة --> مستردة : عملية استرداد
```
**قواعد حاسمة:**
- لا يمكن إلغاء فاتورة بها مدفوعات (partially_paid)
- لا يمكن إلغاء فاتورة مدفوعة
- المسترد يمر عبر عملية استرداد رسمية (reversing entries)
---
## القيد المزدوج
**كل دفعة تُنشئ قيدين:**
```
دفعة نقدية 1500 ج.م:
مدين: حساب النقدية ← 150000 قرش
دائن: حساب المدينون ← 150000 قرش
```
**استرداد 500 ج.م:**
```
مدين: حساب المدينون ← 50000 قرش
دائن: حساب النقدية ← 50000 قرش
```
**القيود لا تُعدّل أبداً ولا تُحذف.** التصحيح = قيد عكسي جديد.
---
### 8. المحافظ الإلكترونية (`/wallets`)
**من يراها:** من لديه صلاحية `wallets.list`
**قائمة المحافظ:**
| العمود | الوصف |
|--------|-------|
| المالك | اسم المشترك |
| الرصيد | balance (ج.م) |
| المجمد | frozen_amount (ج.م) |
| المتاح | available_balance (ج.م) |
| الحالة | active, frozen, closed |
---
### 9. تفاصيل محفظة (`/wallets/{id}`)
**المحتوى:**
- الرصيد الحالي / المجمد / المتاح
- سجل المعاملات (إيداع، سحب، خصم، تجميد)
**عمليات:**
- **إيداع:** إضافة مبلغ (يتطلب وصف)
- **سحب:** خصم مبلغ (يتطلب وصف) — لا يمكن السحب أكثر من المتاح
**قاعدة:** الرصيد لا يصل لسالب أبداً. يُفحص قبل الخصم مع `lockForUpdate()`.
---
### 10. الورديات المالية (`/cash-sessions`)
**من يراها:** من لديه صلاحية `cash_sessions.list`
**القائمة:**
| العمود | الوصف |
|--------|-------|
| المستخدم | من فتح الوردية |
| تاريخ الفتح | opened_at |
| تاريخ الإقفال | closed_at |
| المبلغ الافتتاحي | opening_amount |
| المبلغ الختامي | closing_amount |
| الفرق | variance |
| عدد العمليات | transactions_count |
| الحالة | open/closed |
---
### 11. إدارة الوردية (`/cash-sessions/manage`)
**من يراها:** من لديه صلاحية `cash_sessions.manage`
**فتح وردية:**
- إدخال المبلغ الافتتاحي (كم كاش في الدرج)
**إقفال وردية:**
- إدخال المبلغ الختامي (كم كاش في الدرج الآن)
- النظام يحسب: المتوقع = الافتتاحي + مبيعات الكاش
- الفرق = الختامي - المتوقع
- إذا فرق سلبي (نقص) → يُسجّل ويحتاج تبرير
**قاعدة:** لا يمكن إجراء عمليات بيع/تحصيل بدون وردية مفتوحة.
```mermaid
flowchart TD
A[بداية اليوم] --> B[فتح وردية + مبلغ افتتاحي]
B --> C[عمليات بيع/تحصيل خلال اليوم]
C --> D[إقفال الوردية]
D --> E[إدخال المبلغ الختامي]
E --> F{الفرق}
F -->|صفر| G[✓ مطابقة]
F -->|موجب| H[⚠ زيادة — تحتاج تبرير]
F -->|سالب| I[✗ نقص — تحتاج تحقيق]
```
---
### 12. خطط الدفع (Installments) (`/payment-plans/create`)
**من يراها:** من لديه صلاحية `invoices.create`
**الحقول:**
| الحقل | النوع | مطلوب |
|--------|------|-------|
| الفاتورة | select | ✓ |
| عدد الأقساط | number | ✓ |
| تاريخ أول قسط | date | ✓ |
| الفاصل بين الأقساط | select | ✓ | أسبوعي / شهري |
**السلوك:**
- يُقسّم المبلغ المتبقي على عدد الأقساط
- يُنشئ سجل لكل قسط (تاريخ + مبلغ + حالة)
- القسط الأخير يأخذ الفرق (لتجنب مشاكل القسمة)
**تذكير الأقساط:** مهمة مجدولة تذكّر قبل 3 أيام وقبل يوم واحد
**كشف التعثر:** مهمة `payments:detect-defaults` — إذا قسط تجاوز تاريخه بدون دفع → إشعار
---
### 13. النظرة المالية — مصالحة
**المهمة الأسبوعية:** `financials:reconcile` (الأحد 04:00)
**تفحص:**
- مجموع المدين = مجموع الدائن (لكل حساب)
- مجموع المدفوع لكل فاتورة = مجموع الدفعات المؤكدة
- رصيد المحفظة = مجموع الإيداعات - مجموع السحب
---
## أمثلة عملية
### مثال 1: فاتورة اشتراك شهري
> 1. مشترك "أحمد" يُسجّل في مجموعة — السعر 1500 ج.م
> 2. تُنشأ فاتورة: إجمالي = 150,000 قرش
> 3. الأب يدفع كاش: 1500 ج.م
> 4. قيد: مدين نقدية 150000، دائن مدينون 150000
> 5. الفاتورة → "مدفوعة"
> 6. الاشتراك → "نشط"
### مثال 2: دفع جزئي + تقسيط
> 1. فاتورة 3000 ج.م
> 2. الأب يدفع 1000 ج.م الآن → الفاتورة "مدفوعة جزئياً"
> 3. يُنشأ خطة دفع: 2000 ÷ 2 = 1000 شهرياً
> 4. بعد شهر: تذكير بالقسط الأول
> 5. يدفع → 2000/3000 مدفوع
> 6. بعد شهرين: القسط الأخير → "مدفوعة بالكامل"
### مثال 3: دفع من المحفظة
> 1. رصيد محفظة "أحمد" = 2000 ج.م
> 2. فاتورة جديدة = 1500 ج.م
> 3. دفع بالمحفظة → خصم 1500 من المحفظة
> 4. الرصيد الجديد = 500 ج.م
> 5. قيد: مدين محفظة-مدينون 150000، دائن محفظة-أصل 150000
### مثال 4: وردية مالية يومية
> 1. 08:00 — فتح وردية: مبلغ افتتاحي 500 ج.م
> 2. خلال اليوم: 10 عمليات تحصيل = 8000 ج.م كاش
> 3. 17:00 — إقفال: المبلغ في الدرج = 8450 ج.م
> 4. المتوقع = 500 + 8000 = 8500 ج.م
> 5. الفرق = -50 ج.م (نقص)
> 6. يُسجّل النقص + يُطلب تبرير من الموظف
### مثال 5: فاتورة متأخرة
> 1. فاتورة مرسلة بتاريخ استحقاق 1 يوليو
> 2. اليوم 5 يوليو — لم تُدفع
> 3. المهمة المجدولة `invoices:mark-overdue`: الحالة → "متأخرة"
> 4. إرسال تذكير لولي الأمر (الاثنين والخميس)
---
## لماذا يحتاج عملك هذا
| الميزة | الفائدة |
|--------|---------|
| القيد المزدوج | دقة مالية 100% — لا مبالغ ضائعة |
| الأقساط | مرونة في التحصيل — لا تخسر العميل بسبب مبلغ كبير |
| المحفظة | دفع سريع بدون كاش — مناسب لأولياء الأمور |
| الوردية | محاسبة يومية — تعرف أين كل قرش |
| التذكيرات | تحصيل أفضل بدون اتصالات يدوية |
| الفاتورة العامة | ولي الأمر يرى فاتورته من أي مكان |
| المصالحة | يكشف أي خطأ مالي تلقائياً |
| عدم قابلية التعديل | لا أحد يستطيع "تعديل" قيد قديم — أمان مالي |
---
## التكاملات
| الوحدة | العلاقة |
|--------|---------|
| التسجيل | إنشاء فاتورة عند التسجيل |
| التسعير | حساب سعر الفاتورة |
| نقطة البيع | عمليات POS تُنشئ فواتير + مدفوعات |
| المخزون | مشتريات تُنشئ فواتير مشتريات |
| الموارد البشرية | رواتب تُنشئ قيود مالية |
| الإشعارات | تذكيرات الفواتير والأقساط |
| التقارير | تقرير مالي شامل |
# 09 — محرك التسعير
## نظرة عامة
**لا توجد أسعار ثابتة.** كل سعر يُحسب لحظياً عند البيع/التسجيل بناءً على:
1. السعر الأساسي
2. قواعد الخصم (13 نوع)
3. العروض والكوبونات
4. حدود الخصم
بعد الحساب، يُجمّد السعر على الفاتورة ولا يتغير أبداً.
---
## الشاشات المنفذة
### 1. الأسعار الأساسية (`/pricing/base-prices`)
**من يراها:** من لديه صلاحية `pricing.list`
**القائمة:**
| العمود | الوصف |
|--------|-------|
| العنصر | البرنامج أو المنتج |
| الفرع | branch (أو الكل) |
| السعر (أعضاء) | member_price ج.م |
| السعر (غير أعضاء) | non_member_price ج.م |
| من تاريخ | effective_from |
| إلى تاريخ | effective_to |
| مفعّل | is_active |
**إنشاء/تعديل (`/pricing/base-prices/create`, `/pricing/base-prices/{id}/edit`):**
| الحقل | النوع | مطلوب |
|--------|------|-------|
| النوع المسعّر | select | ✓ | program / product |
| العنصر | select | ✓ | |
| الفرع | select | ✗ | كل الفروع إذا فارغ |
| سعر الأعضاء | money | ✓ | |
| سعر غير الأعضاء | money | ✗ | |
| من تاريخ | date | ✓ | |
| إلى تاريخ | date | ✗ | |
| مفعّل | toggle | — | |
**قاعدة:** إذا لم يُوجد سعر أساسي نشط → البيع/التسجيل يُرفض مع رسالة "لا يوجد سعر محدد".
---
### 2. قواعد التسعير (`/pricing/rules`)
**من يراها:** من لديه صلاحية `pricing.list`
**القائمة:**
| العمود | الوصف |
|--------|-------|
| الاسم | name_ar |
| النوع | rule_type |
| التعديل | adjustment_type + قيمة |
| الأولوية | priority |
| قابل للتراكم | stackable ✓/✗ |
| مفعّل | is_active |
---
### 3. إنشاء قاعدة — ويزارد (`/pricing/rules/wizard`)
**خطوات:**
1. اختيار نوع القاعدة (من 13 نوع)
2. إعداد الشروط
3. تحديد التعديل (نسبة أو مبلغ ثابت)
4. إعدادات متقدمة (أولوية، تراكم، حدود)
---
### 4. إنشاء/تعديل قاعدة — نموذج (`/pricing/rules/create`, `/pricing/rules/{id}/edit`)
**الحقول:**
| الحقل | النوع | مطلوب | ملاحظات |
|--------|------|-------|---------|
| الاسم بالعربية | text | ✓ | |
| الاسم بالإنجليزية | text | ✗ | |
| نوع القاعدة | select | ✓ | 13 نوع |
| نوع التعديل | select | ✓ | percentage_discount, fixed_discount, fixed_price, percentage_increase |
| قيمة التعديل | number | ✓ | |
| الشروط | JSON/dynamic | ✓ | حسب نوع القاعدة |
| الأولوية | number | ✓ | رقم أقل = يُطبّق أولاً |
| قابل للتراكم | toggle | — | |
| الحد الأقصى للخصم | number | ✗ | نسبة % |
| من تاريخ | date | ✗ | |
| إلى تاريخ | date | ✗ | |
| الفرع | select | ✗ | |
| مفعّل | toggle | — | |
---
### 5. العروض والكوبونات (`/pricing/promotions`)
**القائمة:**
| العمود | الوصف |
|--------|-------|
| الكود | coupon_code |
| الاسم | name_ar |
| نوع الخصم | percentage / fixed |
| القيمة | discount_value |
| الاستخدام | used_count / max_uses |
| من - إلى | valid_from — valid_to |
| مفعّل | is_active |
**إنشاء عرض (`/pricing/promotions/create`):**
| الحقل | النوع | مطلوب |
|--------|------|-------|
| كود الكوبون | text | ✓ | فريد |
| الاسم بالعربية | text | ✓ | |
| نوع الخصم | select | ✓ | percentage / fixed_amount |
| قيمة الخصم | number | ✓ | |
| تاريخ البداية | date | ✗ | |
| تاريخ النهاية | date | ✗ | |
| الحد الأقصى للاستخدام | number | ✗ | |
| استخدام لكل مشترك | number | ✗ | |
| الحد الأدنى للمبلغ | money | ✗ | |
| مفعّل | toggle | — | |
---
### 6. فاحص الكوبون (Coupon Validator)
**المكوّن:** `CouponValidator` — يُستخدم داخل نقطة البيع والفواتير
**8 تحققات:**
1. هل الكود موجود؟
2. هل مفعّل؟
3. هل في فترة الصلاحية؟
4. هل تجاوز الحد الأقصى للاستخدام الكلي؟
5. هل هذا المشترك استخدمه أكثر من المسموح؟
6. هل المبلغ يتجاوز الحد الأدنى المطلوب؟
7. هل ينطبق على هذا الفرع؟
8. هل ينطبق على هذا البرنامج/المنتج؟
---
## أنواع قواعد التسعير (13 نوع)
| النوع | الشرح | مثال |
|-------|-------|------|
| age | حسب عمر المشترك | أقل من 6 سنوات: خصم 20% |
| membership_duration | حسب مدة العضوية | عضو لأكثر من سنة: خصم 10% |
| family_size | حسب عدد الأبناء | 3 أبناء فأكثر: خصم 15% |
| sibling_order | حسب ترتيب الأخ | الأخ الثاني: 10%، الثالث: 20% |
| classification | حسب تصنيف المشترك | VIP: خصم 25% |
| enrollment_timing | حسب توقيت التسجيل | تسجيل مبكر (قبل بدء الموسم): 10% |
| enrollment_volume | حسب عدد البرامج | مسجّل في 3 برامج+: خصم 15% |
| seasonal | موسمي | صيف: خصم 30% |
| gender | حسب الجنس | إناث: خصم 10% (تحفيز) |
| branch | حسب الفرع | فرع التجمع: خصم افتتاح 20% |
| day_time | حسب الوقت | حصص صباحية: أرخص بـ 200 ج.م |
| loyalty | ولاء | مشترك 3 سنوات: 25% |
| custom | مخصص | أي شرط آخر |
---
## أنواع التعديل
| النوع | المعنى | مثال |
|-------|--------|------|
| percentage_discount | خصم نسبي | 15% من السعر الحالي |
| fixed_discount | خصم ثابت | 200 ج.م |
| fixed_price | سعر ثابت (يحل محل الأصلي) | 800 ج.م بغض النظر |
| percentage_increase | زيادة نسبية | +10% (حصة خاصة) |
---
## خوارزمية حساب السعر (10 خطوات)
```mermaid
flowchart TD
A[1. إيجاد السعر الأساسي] --> B{وُجد؟}
B -->|لا| C[✗ رفض: لا يوجد سعر]
B -->|نعم| D[2. جمع القواعد المطبقة]
D --> E[3. تقييم الشروط ضد بيانات المشترك]
E --> F[4. تصفية غير المتراكمة — الأعلى أولوية يفوز]
F --> G[5. تطبيق القواعد بترتيب الأولوية]
G --> H[6. تطبيق حد أقصى لكل قاعدة]
H --> I[7. تطبيق عرض/كوبون]
I --> J[8. تطبيق حد أقصى عام - افتراضي 50%]
J --> K[9. فحص: لا يكون سالب]
K --> L[10. إرجاع النتيجة النهائية]
```
---
## التراكم vs عدم التراكم
**قابل للتراكم (stackable=true):**
- يُطبّق مع قواعد أخرى (تراكمي)
- مثال: خصم العمر 10% + خصم الأخ الثاني 10% = 20%
**غير قابل للتراكم (stackable=false):**
- إذا تعارض مع قاعدة غير قابلة للتراكم أخرى → الأعلى أولوية فقط
- مثال: خصم VIP 25% (غير متراكم) vs خصم موسمي 30% (غير متراكم) → 30% فقط (الأعلى)
- يمكن أن يتراكم مع قواعد متراكمة أخرى
---
## أمثلة عملية
### مثال 1: حساب سعر بسيط
> - السعر الأساسي: 1500 ج.م
> - لا توجد قواعد مطبقة
> - **النتيجة: 1500 ج.م**
### مثال 2: خصم عمر
> - السعر الأساسي: 1500 ج.م
> - قاعدة: عمر < 6 سنوات → خصم 20%
> - المشترك عمره 5 سنوات → ينطبق
> - **1500 - 300 = 1200 ج.م**
### مثال 3: خصم أخوة تراكمي
> - السعر الأساسي: 1500 ج.م
> - قاعدة 1: الأخ الثاني → خصم 10% (stackable)
> - قاعدة 2: عائلة 3 أبناء+ → خصم 5% (stackable)
> - المشترك: الأخ الثاني من 3 أبناء
> - **1500 - 150 (10%) - 75 (5% من 1500) = 1275 ج.م**
### مثال 4: تطبيق كوبون بعد القواعد
> - السعر بعد القواعد: 1200 ج.م
> - كوبون "SUMMER2024": خصم 100 ج.م ثابت
> - **1200 - 100 = 1100 ج.م**
> - فحص الحد الأقصى (50%): 1100 > 750 (50% من 1500) → ✓ مقبول
### مثال 5: وصول للحد الأقصى
> - السعر الأساسي: 1000 ج.م
> - قاعدة VIP: خصم 40%
> - كوبون: خصم 20%
> - المجموع: 60% خصم → لكن الحد الأقصى 50%
> - **النتيجة: 500 ج.م** (أقل خصم ممكن = 50%)
### مثال 6: سعر ثابت (override)
> - السعر الأساسي: 1500 ج.م
> - قاعدة "تصنيف منحة": fixed_price = 0
> - **النتيجة: 0 ج.م** (مجاني — طالب منحة)
---
## لماذا يحتاج عملك هذا
| الميزة | الفائدة |
|--------|---------|
| لا أسعار ثابتة | مرونة كاملة بدون تغيير كود |
| 13 نوع قاعدة | تغطي كل سيناريو تسعير ممكن |
| التراكم | خصومات متعددة تعمل معاً بشكل عادل |
| الحد الأقصى | حماية من خصومات مبالغ فيها بالخطأ |
| التجميد | تغيير الأسعار لا يؤثر على الفواتير القديمة |
| الكوبونات | أداة تسويقية قوية مع 8 تحققات |
| الأولويات | تحكم دقيق في ترتيب التطبيق |
---
## التكاملات
| الوحدة | العلاقة |
|--------|---------|
| التسجيل | حساب السعر عند التسجيل في مجموعة |
| نقطة البيع | حساب السعر عند البيع |
| المالية | النتيجة تذهب لبند الفاتورة |
| المشتركين | بيانات المشترك تُقيّم ضد شروط القواعد |
# 10 — نقطة البيع (POS)
## نظرة عامة
محطة بيع متكاملة لعمليات البيع اليومية: بيع منتجات، تسجيل اشتراكات، تحصيل مدفوعات. تعمل ضمن وردية مالية (Cash Session).
---
## الشاشات المنفذة
### 1. محطة البيع (`/pos`)
**من يراها:** من لديه صلاحية `pos.sell`
**الشرط الأساسي:** يجب أن تكون هناك وردية مفتوحة — وإلا تظهر رسالة "يجب فتح وردية أولاً".
**تقسيم الشاشة:**
#### الجانب الأيمن — المنتجات والخدمات:
- بحث سريع
- تصنيفات (tabs)
- بطاقات المنتجات (صورة + اسم + سعر)
- النقر → يضيف للسلة
#### الجانب الأيسر — السلة:
| العنصر | الوصف |
|--------|-------|
| قائمة البنود | اسم + كمية + سعر |
| الإجمالي الفرعي | subtotal |
| الخصم | discount |
| الإجمالي | total |
| حقل الكوبون | مع زر "تحقق" |
| اختيار المشترك | select (اختياري لمبيعات منتجات) |
| طريقة الدفع | cash / card / wallet / split |
| زر "إتمام البيع" | |
**عمليات السلة:**
- تعديل الكمية (+/-)
- حذف بند
- مسح السلة بالكامل
---
### 2. دورة عملية بيع واحدة (10 خطوات)
```mermaid
flowchart TD
A[1. تحديد المشترك - اختياري] --> B[2. إضافة بنود]
B --> C[3. حساب الأسعار - محرك التسعير]
C --> D[4. تطبيق كوبون - اختياري]
D --> E[5. حساب الإجماليات]
E --> F[6. اختيار طريقة الدفع]
F --> G[7. التحقق من الرصيد/المبلغ]
G --> H[8. تنفيذ العملية - transaction واحدة]
H --> I[9. إنشاء الإيصال]
I --> J[10. إطلاق الأحداث]
```
**الخطوة 8 تشمل (كلها في DB::transaction واحدة):**
- إنشاء pos_transaction + items
- إنشاء فاتورة + بنود
- إنشاء دفعة + قيد مزدوج
- خصم من المحفظة (إذا wallet)
- خصم من المخزون (إذا منتج tracked)
- إنشاء سجل تسجيل (إذا برنامج)
- تحديث عداد الوردية
---
### 3. الدفع المقسّم (Split Payment)
- جزء كاش + جزء كارت
- جزء محفظة + جزء كاش
- يجب أن يغطي المجموع = الإجمالي تماماً
---
### 4. تاريخ عمليات البيع (`/pos/history`)
**من يراها:** من لديه صلاحية `pos.list`
**الفلاتر:**
- الفترة
- طريقة الدفع
- الموظف
**أعمدة الجدول:**
| العمود | الوصف |
|--------|-------|
| رقم الإيصال | receipt_number |
| التاريخ | created_at |
| المشترك | اسم (أو "عميل عابر") |
| الإجمالي | total_amount |
| طريقة الدفع | method |
| الموظف | seller |
| الإجراءات | طباعة إيصال |
---
### 5. طباعة إيصال (`/receipts/pos/{id}`)
**المحتوى:**
- شعار الأكاديمية
- رقم الإيصال: `RCP-{BRANCH}-{YYYYMMDD}-{SEQ}`
- بنود البيع
- الإجماليات
- طريقة الدفع
- التاريخ والوقت
- اسم الموظف
---
### 6. إيصال عام (`/public/pos-receipt/{uuid}`)
رابط عام بدون تسجيل دخول — يُرسل للعميل عبر واتساب.
---
## الحراسات (Guards)
| الشرط | النتيجة |
|--------|---------|
| لا وردية مفتوحة | رفض: "يجب فتح وردية أولاً" |
| مشترك موقوف | رفض: "المشترك موقوف — يمكن فقط دفع المستحقات" |
| تسجيل مكرر | رفض: "مسجل بالفعل في هذا البرنامج" |
| مبلغ الدفع المقسّم ≠ الإجمالي | رفض: "المبلغ المدفوع أقل من الإجمالي" |
| السلة فارغة | تعطيل زر الإتمام |
| منتج نفذ | تحذير مع إمكانية override من المدير |
---
## إعدادات الإيصال (`/settings/receipts`)
**من يراها:** من لديه صلاحية `settings.manage`
**الحقول:**
- نص الترويسة
- نص التذييل
- إظهار الشعار
- إظهار QR code
- حجم الخط
- عرض الإيصال
---
## أمثلة عملية
### مثال 1: بيع تيشيرت
> 1. العميل يريد تيشيرت
> 2. الموظف يبحث "تيشيرت" → يضيفه (200 ج.م)
> 3. لا حاجة لتحديد مشترك (بيع منتج)
> 4. دفع كاش → إتمام
> 5. خصم 1 من المخزون
> 6. طباعة إيصال
### مثال 2: تسجيل مشترك + بيع حقيبة
> 1. اختيار المشترك "أحمد"
> 2. إضافة: برنامج "كرة قدم مبتدئين" (1500 ج.م)
> 3. إضافة: حقيبة (150 ج.م)
> 4. تطبيق كوبون "WELCOME": خصم 100 ج.م
> 5. الإجمالي: 1550 ج.م
> 6. دفع: 1000 كاش + 550 كارت (split)
> 7. النتيجة:
> - فاتورة مدفوعة
> - اشتراك نشط لأحمد
> - -1 حقيبة من المخزون
> - إيصال
### مثال 3: دفع من المحفظة
> 1. اختيار "سارة" — رصيد محفظتها 2000 ج.م
> 2. إضافة اشتراك: 1200 ج.م
> 3. طريقة الدفع: محفظة
> 4. التحقق: 2000 ≥ 1200 → ✓
> 5. خصم من المحفظة → رصيد جديد: 800 ج.م
> 6. إتمام البيع
---
## لماذا يحتاج عملك هذا
| الميزة | الفائدة |
|--------|---------|
| واجهة بيع سريعة | عملية واحدة في أقل من دقيقة |
| الدفع المقسّم | العميل يدفع جزء كاش وجزء فيزا |
| ربط بالمخزون | البيع يخصم تلقائياً — لا أخطاء يدوية |
| ربط بالاشتراكات | تسجيل + دفع في عملية واحدة |
| الإيصال الإلكتروني | يُرسل واتساب بدون طباعة ورق |
| الحراسات | تمنع الأخطاء قبل حدوثها |
---
## التكاملات
| الوحدة | العلاقة |
|--------|---------|
| المالية | كل عملية POS = فاتورة + دفعة + قيد |
| المخزون | بيع منتج = حركة مخزون خارجة |
| التسعير | حساب السعر يمر بمحرك التسعير |
| التسجيل | بيع برنامج = تسجيل مشترك |
| الورديات | عملية POS ترتبط بالوردية المفتوحة |
| الإشعارات | إرسال إيصال بعد البيع |
# 11 — المخزون
## نظرة عامة
نظام مخزون كامل يتتبع المنتجات، المخازن، الحركات، والجرد. القاعدة الحديدية: **لا تعديل مباشر على الكمية — كل تغيير يمر عبر حركة مخزون.**
---
## الشاشات المنفذة
### 1. قائمة المنتجات (`/inventory/products`)
**من يراها:** من لديه صلاحية `inventory.list`
**الفلاتر:**
- بحث بالاسم أو الباركود
- تصفية بالتصنيف
- تصفية بحالة المخزون (متوفر / منخفض / نفذ)
**أعمدة الجدول:**
| العمود | الوصف |
|--------|-------|
| الاسم | name_ar |
| التصنيف | category.name_ar |
| SKU | sku |
| سعر البيع | selling_price (ج.م) |
| الكمية | quantity_on_hand |
| الحد الأدنى | min_stock_level |
| تتبع المخزون | ✓/✗ |
| مفعّل | is_active |
| الإجراءات | تعديل |
---
### 2. إنشاء منتج — ويزارد (`/inventory/products/wizard`)
**خطوات:**
1. البيانات الأساسية (اسم، تصنيف، SKU)
2. التسعير (سعر الشراء، سعر البيع)
3. إعدادات المخزون (تتبع، حد أدنى، مخزن افتراضي)
4. مراجعة
---
### 3. إنشاء/تعديل منتج (`/inventory/products/create`, `/inventory/products/{id}/edit`)
**الحقول:**
| الحقل | النوع | مطلوب | ملاحظات |
|--------|------|-------|---------|
| الاسم بالعربية | text | ✓ | |
| الاسم بالإنجليزية | text | ✗ | |
| التصنيف | select | ✗ | |
| SKU | text | ✗ | فريد |
| الباركود | text | ✗ | |
| الوصف | textarea | ✗ | |
| سعر الشراء | money | ✗ | |
| سعر البيع | money | ✓ | |
| تتبع المخزون | toggle | — | |
| الحد الأدنى | number | ✗ | للتنبيه عند الوصول |
| مفعّل | toggle | — | |
---
### 4. قائمة المخازن (`/inventory/warehouses`)
**أعمدة:**
| العمود | الوصف |
|--------|-------|
| الاسم | name_ar |
| النوع | main, branch, temporary, transit |
| الفرع | branch.name_ar |
| الحالة | active/inactive |
**إنشاء/تعديل (`/inventory/warehouses/create`, `/inventory/warehouses/{id}/edit`):**
| الحقل | النوع | مطلوب |
|--------|------|-------|
| الاسم بالعربية | text | ✓ |
| الاسم بالإنجليزية | text | ✗ |
| النوع | select | ✓ |
| الفرع | select | ✗ |
| العنوان | text | ✗ |
| مفعّل | toggle | — |
---
### 5. حركات المخزون (`/inventory/movements`)
**من يراها:** من لديه صلاحية `inventory.list`
**الفلاتر:**
- المنتج
- المخزن
- نوع الحركة
- الاتجاه (in/out)
- الفترة
**أعمدة الجدول:**
| العمود | الوصف |
|--------|-------|
| التاريخ | created_at |
| المنتج | product.name_ar |
| المخزن | warehouse.name_ar |
| النوع | movement_type |
| الاتجاه | in/out |
| الكمية | quantity |
| قبل | quantity_before |
| بعد | quantity_after |
| المرجع | reference |
| المستخدم | created_by |
---
### 6. تعديل مخزون (`/inventory/adjustments`)
**من يراها:** من لديه صلاحية `inventory.adjust`
**الحقول:**
| الحقل | النوع | مطلوب |
|--------|------|-------|
| المنتج | select | ✓ |
| المخزن | select | ✓ |
| النوع | select | ✓ | adjustment_up, adjustment_down, damage, loss |
| الكمية | number | ✓ |
| السبب | textarea | ✓ |
---
### 7. ويزارد تعديل المخزون (`/inventory/adjustments/wizard`)
**خطوات:**
1. اختيار المنتج والمخزن
2. تحديد نوع التعديل والكمية
3. إدخال السبب
4. مراجعة وتأكيد
---
## أنواع حركات المخزون
| النوع | الاتجاه | المعنى |
|-------|---------|--------|
| purchase_received | in | استلام مشتريات |
| sale | out | بيع منتج |
| transfer_out | out | نقل لمخزن آخر |
| transfer_in | in | استلام من مخزن آخر |
| return_to_stock | in | مرتجع من عميل |
| return_to_supplier | out | مرتجع للمورد |
| adjustment_up | in | تعديل بالزيادة |
| adjustment_down | out | تعديل بالنقص |
| damage | out | تالف |
| loss | out | مفقود |
| consumption | out | استهلاك داخلي |
| kit_assembly | out | تجميع طقم |
| kit_disassembly | in | تفكيك طقم |
| initial_stock | in | رصيد افتتاحي |
| count_adjustment_up | in | فرق جرد بالزيادة |
| count_adjustment_down | out | فرق جرد بالنقص |
---
## قاعدة إنشاء الحركة
```mermaid
flowchart TD
A[طلب حركة] --> B[قفل سجل المخزون - lockForUpdate]
B --> C[تسجيل quantity_before]
C --> D{الاتجاه}
D -->|in| E[إضافة للكمية]
D -->|out| F{الكمية كافية؟}
F -->|نعم| G[خصم من الكمية]
F -->|لا| H[✗ InsufficientStockException]
E --> I[تسجيل quantity_after]
G --> I
I --> J[إنشاء سجل الحركة]
```
---
## تنبيه المخزون المنخفض
**المهمة المجدولة:** `inventory:notify-low-stock` (يومياً 09:00)
**الشرط:**
- `quantity_on_hand ≤ min_stock_level`
- المنتج مفعّل + تتبع المخزون مفعّل
**النتيجة:** إشعار للإدارة بقائمة المنتجات التي تحتاج إعادة طلب.
---
## المصالحة الأسبوعية
**الفحص:**
```
مجموع حركات (in) - مجموع حركات (out) = الكمية الحالية
```
إذا لا تتطابق → تحذير للإدارة (لا يُصلح تلقائياً).
---
## أمثلة عملية
### مثال 1: بيع منتج عبر POS
> 1. عميل يشتري 2 تيشيرت
> 2. POS يُنشئ حركة: type=sale, direction=out, quantity=2
> 3. المخزون: 50 → 48
> 4. الحركة تُسجّل: before=50, after=48
### مثال 2: استلام مشتريات
> 1. المخزن يستلم شحنة 100 كرة
> 2. حركة: type=purchase_received, direction=in, quantity=100
> 3. المخزون: 20 → 120
### مثال 3: جرد ووجود فرق
> 1. الجرد الفعلي: 47 تيشيرت
> 2. النظام يقول: 50 تيشيرت
> 3. تعديل: type=count_adjustment_down, quantity=3, reason="فرق جرد"
> 4. المخزون: 50 → 47
### مثال 4: منتج بدون تتبع مخزون
> - "خدمة تصوير" — سعرها 50 ج.م
> - track_inventory = false
> - عند البيع: لا تُنشأ حركة مخزون
> - مناسب للخدمات
### مثال 5: تنبيه مخزون منخفض
> - "كرة قدم" — الحد الأدنى: 10
> - الكمية الحالية: 8
> - المهمة المجدولة: إشعار "كرة قدم وصلت للحد الأدنى — أعد الطلب"
---
## لماذا يحتاج عملك هذا
| الميزة | الفائدة |
|--------|---------|
| تتبع الحركات | تعرف أين ذهبت كل قطعة |
| القفل (lockForUpdate) | لا race conditions — بيعين متزامنين لا يفسدان الكمية |
| التنبيه | لا تنفذ البضاعة بدون علمك |
| المخازن المتعددة | فرعين بمخازن مختلفة |
| المصالحة | يكشف السرقة أو الأخطاء |
| عدم التعديل المباشر | كل تغيير موثّق — audit trail كامل |
---
## التكاملات
| الوحدة | العلاقة |
|--------|---------|
| نقطة البيع | بيع منتج → حركة خروج |
| المالية | مشتريات → فاتورة مورد + حركة دخول |
| التنبيهات | مخزون منخفض → إشعار |
| التقارير | تقرير حركة المخزون |
# 12 — الموارد البشرية
## نظرة عامة
إدارة الموظفين والمدربين بالكامل: بيانات شخصية، عقود، مؤهلات، جداول توفر، مكافآت، رواتب، سلف.
```
موظف → مدرب (اختياري) → مؤهلات + جدول توفر → تعيينات → حضور → مكافآت → كشف راتب
```
---
## الشاشات المنفذة
### 1. قائمة الموظفين (`/hr/employees`)
**من يراها:** من لديه صلاحية `employees.list`
**أعمدة:**
| العمود | الوصف |
|--------|-------|
| الاسم | person.name_ar |
| المسمى الوظيفي | job_title |
| نوع التوظيف | full_time, part_time, contract, freelance |
| الفرع | branch.name_ar |
| تاريخ التعيين | hire_date |
| الراتب | base_salary ج.م |
| الحالة | active, on_leave, suspended, terminated |
---
### 2. ويزارد إنشاء موظف (`/hr/employees/wizard`)
**خطوات:**
1. بيانات الشخص (اسم، هاتف، بريد، رقم قومي)
2. بيانات الوظيفة (مسمى، نوع، فرع، تاريخ تعيين)
3. الراتب والمزايا (أساسي، بدلات، تكرار الصرف)
4. مراجعة وتأكيد
---
### 3. نموذج موظف (`/hr/employees/create`, `/hr/employees/{id}/edit`)
**الحقول:**
| الحقل | النوع | مطلوب |
|--------|------|-------|
| الشخص | select/create | ✓ |
| المسمى الوظيفي | text | ✓ |
| نوع التوظيف | select | ✓ |
| الفرع | select | ✗ |
| تاريخ التعيين | date | ✓ |
| تاريخ انتهاء العقد | date | ✗ |
| الراتب الأساسي | money | ✗ |
| تكرار الصرف | select | ✓ | monthly, weekly, bi_weekly |
| طريقة الصرف | select | ✓ | cash, bank_transfer, wallet |
| رقم الحساب البنكي | text | ✗ |
| الحالة | select | ✓ |
---
### 4. قائمة المدربين (`/hr/trainers`)
**من يراها:** من لديه صلاحية `trainers.list`
**أعمدة:**
| العمود | الوصف |
|--------|-------|
| الاسم | person.name_ar |
| التخصص | specialization |
| سنوات الخبرة | experience_years |
| نموذج المكافأة | per_session, monthly_fixed, percentage |
| معدل الحصة | per_session_rate ج.م |
| المجموعات | عدد المجموعات المُعيّن لها |
| الحالة | active, inactive, on_leave, terminated |
---
### 5. ويزارد إنشاء مدرب (`/hr/trainers/wizard`)
**خطوات:**
1. بيانات الشخص
2. بيانات التدريب (تخصص، خبرة، مؤهلات)
3. نموذج المكافأة (لكل حصة / شهري / نسبة)
4. جدول التوفر
5. مراجعة
---
### 6. نموذج مدرب (`/hr/trainers/create`, `/hr/trainers/{id}/edit`)
**الحقول:**
| الحقل | النوع | مطلوب |
|--------|------|-------|
| الشخص | select/create | ✓ |
| الموظف (ربط) | select | ✗ | يمكن أن يكون مدرب بدون أن يكون موظف |
| التخصص | text | ✗ |
| سنوات الخبرة | number | ✗ |
| مستوى التصنيف | select | ✗ |
| نموذج المكافأة | select | ✓ | per_session, monthly_fixed, percentage |
| معدل الحصة | money | مشروط | مطلوب إذا per_session |
| الراتب الشهري | money | مشروط | مطلوب إذا monthly_fixed |
| نسبة الإيرادات | number | مشروط | مطلوب إذا percentage |
| الحالة | select | ✓ |
---
### 7. مؤهلات المدرب
**المكوّن:** ضمن نموذج المدرب
| الحقل | النوع |
|--------|------|
| اسم المؤهل | text |
| الجهة المانحة | text |
| تاريخ الحصول | date |
| تاريخ الانتهاء | date |
| رقم الشهادة | text |
---
### 8. جدول توفر المدرب
**المكوّن:** ضمن إعداد المدرب
| الحقل | النوع |
|--------|------|
| يوم الأسبوع | select |
| من الساعة | time |
| إلى الساعة | time |
| نوع | available, unavailable, preferred |
---
### 9. لوحة الرواتب (`/hr/payroll`)
**من يراها:** من لديه صلاحية `payroll.manage`
**المحتوى:**
- الفترة الحالية (من - إلى)
- حالة الفترة (draft, processing, approved, paid)
- إجمالي الرواتب المستحقة
- قائمة كشوف الرواتب (payslips)
**لكل كشف:**
| العمود | الوصف |
|--------|-------|
| المدرب/الموظف | الاسم |
| الراتب الأساسي | base_amount |
| المكافآت | total_earnings |
| الخصومات | total_deductions |
| صافي المستحق | net_amount |
| الحالة | draft, pending_approval, approved, paid |
---
### 10. تفاصيل كشف راتب (`/hr/payroll/payslip/{id}`)
**المحتوى:**
- بيانات الموظف/المدرب
- الفترة
- بنود الاستحقاق (earnings):
- راتب أساسي
- مكافأة حصص (عدد × معدل)
- بدلات
- بنود الخصم (deductions):
- سلف مسترجعة
- غرامات
- تأمينات
- صافي المستحق
- حالة الصرف
---
### 11. مكافآت المدرب (`/hr/payroll/trainer/{id}/compensations`)
**من يراها:** من لديه صلاحية `payroll.manage`
**المحتوى:**
- قائمة المكافآت المُستحقة (لكل حصة حضرها)
- الحالة: pending, approved, paid, cancelled
- المجموع
**الآلية (تلقائية):**
```mermaid
flowchart TD
A[المدرب يحضر حصة] --> B[حدث: AttendanceMarked]
B --> C[Listener: GenerateTrainerCompensation]
C --> D[إنشاء سجل مكافأة]
D --> E[المبلغ = per_session_rate]
E --> F[حالة: pending]
```
---
### 12. السلف (`/hr/advances`)
**من يراها:** من لديه صلاحية `payroll.manage`
**أعمدة:**
| العمود | الوصف |
|--------|-------|
| المدرب | الاسم |
| المبلغ | amount |
| تاريخ الطلب | requested_date |
| طريقة السداد | one_time, installments |
| عدد الأقساط | installment_count |
| المسدد | repaid_amount |
| المتبقي | remaining |
| الحالة | pending, approved, active, repaid, rejected |
**إنشاء سلفة:**
| الحقل | النوع | مطلوب |
|--------|------|-------|
| المدرب | select | ✓ |
| المبلغ | money | ✓ |
| السبب | textarea | ✗ |
| طريقة السداد | select | ✓ |
| عدد أقساط السداد | number | مشروط |
**السداد:** يُخصم تلقائياً من كشف الراتب الشهري (كبند خصم).
---
### 13. تاريخ معدلات المدرب
**جدول `trainer_rate_history`:** يسجّل كل تغيير في معدل مكافأة المدرب:
- التاريخ
- المعدل القديم
- المعدل الجديد
- السبب
---
## نموذج المكافأة — 3 أنواع
| النموذج | الحساب | مناسب لـ |
|---------|--------|----------|
| per_session | حصص حضرها × معدل الحصة | مدربين part-time |
| monthly_fixed | مبلغ ثابت شهرياً | مدربين full-time |
| percentage | نسبة من إيرادات المجموعة | مدربين شراكة |
---
## دورة الراتب الشهري
```mermaid
sequenceDiagram
participant S as النظام
participant HR as الموارد البشرية
participant T as المدرب
Note over S: يومياً: المدرب يحضر حصص
S->>S: كل حصة → مكافأة pending
Note over HR: نهاية الشهر
HR->>S: فتح فترة رواتب جديدة
S->>S: تجميع مكافآت الشهر
S->>S: خصم السلف المستحقة
S->>S: إنشاء كشف راتب (draft)
HR->>S: مراجعة + اعتماد
S->>S: حالة: approved
HR->>S: صرف
S->>S: حالة: paid
S-->>T: إشعار: تم صرف راتبك
```
---
## أمثلة عملية
### مثال 1: مدرب بمكافأة لكل حصة
> - كابتن أحمد: per_session_rate = 150 ج.م
> - حضر 24 حصة هذا الشهر
> - المستحق: 24 × 150 = 3600 ج.م
> - عليه سلفة قسط 500 ج.م
> - الصافي: 3100 ج.م
### مثال 2: مدرب براتب ثابت
> - كابتن سارة: monthly_fixed = 5000 ج.م
> - حضرت 20 من 24 حصة
> - لا خصم حضور (ثابت)
> - الصافي: 5000 ج.م
### مثال 3: سلفة مقسّطة
> 1. كابتن محمد يطلب سلفة 3000 ج.م
> 2. الموافقة: سداد على 6 أشهر = 500/شهر
> 3. كل كشف راتب: يُخصم 500 ج.م تلقائياً
> 4. بعد 6 أشهر: السلفة مسددة بالكامل
### مثال 4: إلغاء حصة وتأثيرها على المكافأة
> 1. حصة الأربعاء أُلغيت (مطر)
> 2. Listener: HandleSessionCancelled
> 3. مكافأة المدرب لهذه الحصة → cancelled
> 4. لا يُحسب ضمن الراتب
---
## لماذا يحتاج عملك هذا
| الميزة | الفائدة |
|--------|---------|
| ربط الحضور بالمكافأة | عدالة — المدرب يُحاسب على ما حضره فعلاً |
| 3 نماذج مكافأة | مرونة — يناسب كل أنواع التعاقد |
| السلف | خدمة المدربين + سداد منظم |
| كشف الراتب | وثيقة رسمية لكل شهر |
| تاريخ المعدلات | شفافية — كل تغيير في الأجر موثّق |
| المؤهلات | تتبع صلاحية الشهادات |
| جدول التوفر | تجنب تعيين مدرب في وقت غير متاح |
---
## التكاملات
| الوحدة | العلاقة |
|--------|---------|
| التعيينات | المدرب مُعيّن لمجموعات |
| الحضور | حضور المدرب → مكافأة |
| المالية | كشف الراتب → قيود مالية |
| الإشعارات | إشعار صرف الراتب |
| المستندات | مؤهلات ووثائق الموظف |
# 13 — الإشعارات والمستندات
## نظرة عامة
### الإشعارات:
نظام إشعارات متعدد القنوات (داخل النظام، بريد إلكتروني، SMS) مع قوالب قابلة للتخصيص وتفضيلات لكل مستخدم.
### المستندات:
نظام رفع وإدارة المستندات (شهادات طبية، إثبات هوية، شهادات مؤهلات) مع دورة موافقة.
---
## شاشات الإشعارات
### 1. مركز الإشعارات (`/notifications`)
**من يراها:** أي مستخدم مسجّل
**المحتوى:**
- قائمة إشعارات المستخدم (مقروءة وغير مقروءة)
- زر "تحديد الكل كمقروء"
- تصفية: الكل / غير مقروءة فقط
---
### 2. قوالب الإشعارات (`/notifications/templates`)
**من يراها:** من لديه صلاحية `notifications.manage`
**القائمة:**
| العمود | الوصف |
|--------|-------|
| الحدث | event_type |
| القناة | channel (in_app, email, sms) |
| اللغة | locale |
| العنوان | subject |
| مفعّل | is_active |
---
### 3. إنشاء/تعديل قالب (`/notifications/templates/create`, `/notifications/templates/{id}/edit`)
**الحقول:**
| الحقل | النوع | مطلوب | ملاحظات |
|--------|------|-------|---------|
| الحدث | select | ✓ | enrollment_created, payment_received, etc. |
| القناة | select | ✓ | in_app, email, sms |
| اللغة | select | ✓ | ar, en |
| العنوان | text | ✓ | يدعم متغيرات {{name}}, {{amount}} |
| المحتوى | textarea | ✓ | يدعم متغيرات |
| مفعّل | toggle | — | |
**المتغيرات المتاحة:**
- `{{participant_name}}` — اسم المشترك
- `{{amount}}` — المبلغ
- `{{date}}` — التاريخ
- `{{group_name}}` — اسم المجموعة
- `{{academy_name}}` — اسم الأكاديمية
**قاعدة:** كل المتغيرات في القالب يجب أن تُملأ عند الإرسال — إذا ناقصة → exception.
---
### 4. سجل الإشعارات (`/notifications/logs`)
**من يراها:** من لديه صلاحية `notifications.manage`
**أعمدة:**
| العمود | الوصف |
|--------|-------|
| التاريخ | created_at |
| المستلم | recipient |
| القناة | channel |
| الحدث | event_type |
| الحالة | sent, failed, pending |
| سبب الفشل | error_message |
---
### 5. رسائل جماعية (`/messaging`)
**من يراها:** من لديه صلاحية `notifications.manage`
**الحقول:**
| الحقل | النوع | مطلوب |
|--------|------|-------|
| المستلمين | multi-select | ✓ | بالمجموعة / البرنامج / يدوي |
| القناة | select | ✓ | sms, email, in_app |
| العنوان | text | مشروط | مطلوب للبريد |
| المحتوى | textarea | ✓ | |
---
### 6. تفضيلات الإشعارات (`/profile/notifications`)
**من يراها:** أي مستخدم
**المحتوى:**
- لكل نوع حدث: تفعيل/تعطيل البريد والـ SMS
- ملاحظة: الإشعارات داخل النظام لا يمكن تعطيلها
---
## تدفق الإشعارات
```mermaid
flowchart TD
A[حدث في النظام] --> B[Listener]
B --> C[NotificationService]
C --> D[تحديد المستلمين]
D --> E[قراءة تفضيلات كل مستلم]
E --> F{القناة مفعّلة؟}
F -->|نعم| G[تحميل القالب]
F -->|لا| H[تخطي]
G --> I[تعبئة المتغيرات]
I --> J[إرسال]
J --> K[تسجيل في notification_logs]
```
---
## أنواع الأحداث المُشعَر عنها
| الحدث | المستلم | القنوات |
|-------|---------|---------|
| تسجيل مشترك جديد | ولي الأمر + إدارة | in_app, email |
| دفعة مستلمة | ولي الأمر | in_app, email, sms |
| فاتورة متأخرة | ولي الأمر | in_app, sms |
| اقتراب موعد قسط | ولي الأمر | in_app, sms |
| غياب مشترك | ولي الأمر | in_app, sms |
| إيقاف مشترك | ولي الأمر + إدارة | in_app, email, sms |
| تجميد مشترك | ولي الأمر | in_app |
| تقييم جديد | ولي الأمر | in_app, email |
| إلغاء حصة | المشتركون + أولياء الأمور | in_app, sms |
| فتح مكان (قائمة انتظار) | ولي الأمر | in_app, sms |
| اكتمال اشتراك | ولي الأمر | in_app, email |
| مخزون منخفض | الإدارة | in_app |
| إقفال وردية | الإدارة | in_app |
| تعثر خطة دفع | ولي الأمر + إدارة | in_app, sms |
| صرف راتب | المدرب | in_app |
| عيد ميلاد | المشترك/ولي الأمر | in_app, sms |
| تقرير أسبوعي | ولي الأمر | email |
| شهادة طبية تنتهي | الإدارة | in_app |
---
## شاشات المستندات
### 1. رفع مستند (`/documents/upload/{type}/{id}`)
**ويزارد من 3 خطوات:**
1. اختيار نوع المستند
2. رفع الملف
3. إدخال بيانات إضافية (تاريخ انتهاء، رقم)
**أنواع المستندات:**
| النوع | الوصف |
|-------|-------|
| medical_certificate | شهادة طبية |
| birth_certificate | شهادة ميلاد |
| national_id | بطاقة هوية |
| photo | صورة شخصية |
| qualification | مؤهل/شهادة |
| contract | عقد |
| other | أخرى |
---
### 2. قائمة طلبات الموافقة (`/documents/approvals`)
**من يراها:** من لديه صلاحية `documents.approve`
**أعمدة:**
| العمود | الوصف |
|--------|-------|
| المستند | اسم الملف + النوع |
| الكيان | المشترك أو الموظف |
| رافع المستند | اسم المستخدم |
| تاريخ الرفع | created_at |
| الحالة | pending |
| الإجراءات | مراجعة |
---
### 3. مراجعة مستند (`/documents/{id}/review`)
**من يراها:** من لديه صلاحية `documents.approve`
**المحتوى:**
- عرض المستند (صورة أو PDF)
- بيانات المستند (نوع، تاريخ انتهاء، رقم)
- أزرار: قبول / رفض
- سبب الرفض (إذا رُفض)
---
### 4. مستندات المشترك
**المكوّن:** `ParticipantDocuments` (ضمن ملف المشترك)
- قائمة المستندات المرتبطة
- حالة كل مستند (pending, approved, rejected, expired)
- تنبيه الشهادة الطبية (إذا منتهية أو تنتهي قريباً)
---
## حالات المستند
```mermaid
stateDiagram-v2
[*] --> معلّق
معلّق --> معتمد : موافقة
معلّق --> مرفوض : رفض
معتمد --> منتهي : تجاوز تاريخ الانتهاء
مرفوض --> معلّق : إعادة رفع
```
---
## انتهاء المستندات
**المهمة المجدولة:** `documents:expire` (يومياً 06:00)
**الآلية:**
- مستندات معتمدة + تاريخ انتهاء < اليوم → حالة "expired"
- إشعار للإدارة
---
## أمثلة عملية
### مثال 1: رفع شهادة طبية
> 1. ولي الأمر يرفع شهادة طبية لابنه
> 2. الحالة: "pending" (تنتظر موافقة)
> 3. الإدارة تراجع → "approved" + تاريخ انتهاء: بعد 6 أشهر
> 4. قبل الانتهاء بأسبوع: تنبيه "شهادة أحمد الطبية تنتهي قريباً"
> 5. بعد الانتهاء: "expired" + تنبيه
### مثال 2: رسالة جماعية
> 1. المدير يريد إرسال تذكير لمشتركي مجموعة الأحد
> 2. يختار: مستلمين = مجموعة "أحد-ثلاثاء"، قناة = SMS
> 3. يكتب الرسالة: "تذكير: غداً حصة تدريب الساعة 4 مساءً"
> 4. يُرسل → تُسجّل في logs
### مثال 3: تفضيلات إشعارات
> - ولي أمر يعطّل SMS لإشعارات الحضور (يكتفي بداخل النظام)
> - لكن يُبقي SMS للفواتير المتأخرة (مهم)
> - النظام يحترم تفضيلاته
---
## لماذا يحتاج عملك هذا
| الميزة | الفائدة |
|--------|---------|
| قوالب مخصصة | كل أكاديمية تصيغ رسائلها بأسلوبها |
| تفضيلات المستخدم | لا إزعاج — كل شخص يختار ما يريد |
| سجل الإشعارات | إثبات أن الرسالة أُرسلت (لحل النزاعات) |
| المستندات + انتهاء | لا مشترك بشهادة طبية منتهية يتدرب |
| دورة الموافقة | لا مستند يُعتمد بدون مراجعة |
| الرسائل الجماعية | تواصل سريع مع مجموعة كاملة |
---
## التكاملات
| الوحدة | العلاقة |
|--------|---------|
| المشتركين | مستندات + إشعارات لأولياء الأمور |
| الموارد البشرية | مستندات ومؤهلات الموظفين |
| المالية | إشعارات فواتير وأقساط |
| الحضور | إشعارات غياب وإيقاف |
| التسجيل | إشعارات تسجيل وانتظار |
# 14 — التقييمات
## نظرة عامة
نظام تقييم المشتركين بناءً على معايير قابلة للتخصيص. المدرب يُقيّم كل مشترك دورياً (شهرياً / ربع سنوي / نهاية البرنامج) ويُشارك التقييم مع ولي الأمر.
---
## الشاشات المنفذة
### 1. معايير التقييم (`/evaluations/criteria`)
**من يراها:** من لديه صلاحية `evaluations.manage`
**الوصف:** تعريف المعايير التي يُقيّم عليها المشتركون.
**أمثلة على المعايير:**
| المعيار | النشاط | الوزن |
|---------|--------|-------|
| اللياقة البدنية | كل الأنشطة | 20% |
| المهارات التقنية | كرة قدم | 30% |
| التكتيك | كرة قدم | 20% |
| العمل الجماعي | كرة قدم | 15% |
| السلوك والانضباط | كل الأنشطة | 15% |
| تقنية السباحة | سباحة | 40% |
| التحمل | سباحة | 30% |
**حقول المعيار:**
| الحقل | النوع | مطلوب |
|--------|------|-------|
| الاسم بالعربية | text | ✓ |
| الاسم بالإنجليزية | text | ✗ |
| النشاط | select | ✗ | فارغ = ينطبق على الكل |
| الوزن | number | ✗ | نسبة % |
| الحد الأقصى للدرجة | number | ✓ | مثل: 10 |
| الترتيب | number | ✗ | |
| مفعّل | toggle | — | |
---
### 2. قائمة التقييمات (`/evaluations`)
**من يراها:** من لديه صلاحية `evaluations.list`
**الفلاتر:**
- المشترك
- المجموعة
- الفترة
- الحالة (draft, submitted, shared)
**أعمدة:**
| العمود | الوصف |
|--------|-------|
| المشترك | participant.name_ar |
| المجموعة | group.name_ar |
| المقيّم | evaluator.name |
| التاريخ | evaluation_date |
| الدرجة الكلية | total_score / max_score |
| النسبة | % |
| الحالة | draft / submitted / shared |
| الإجراءات | عرض، تعديل، مشاركة |
---
### 3. إنشاء/تعديل تقييم (`/evaluations/create`, `/evaluations/{id}/edit`)
**الحقول:**
| الحقل | النوع | مطلوب |
|--------|------|-------|
| المشترك | select | ✓ |
| المجموعة | select | ✓ |
| تاريخ التقييم | date | ✓ |
| الفترة | text | ✗ | مثل: "يناير 2025" |
| ملاحظات عامة | textarea | ✗ |
| توصيات | textarea | ✗ |
**+ لكل معيار:**
| الحقل | النوع |
|--------|------|
| الدرجة | number (0 → الحد الأقصى) |
| ملاحظة | text |
---
### 4. عرض تقييم (`/evaluations/{id}`)
**المحتوى:**
- بيانات المشترك والمجموعة
- التاريخ والفترة
- جدول الدرجات (معيار + درجة + ملاحظة)
- الدرجة الكلية والنسبة
- ملاحظات عامة وتوصيات المدرب
- زر "مشاركة مع ولي الأمر"
---
## دورة التقييم
```mermaid
stateDiagram-v2
[*] --> مسودة
مسودة --> مُقدَّم : المدرب يقدّم
مُقدَّم --> مُشارَك : مشاركة مع ولي الأمر
```
**عند المشاركة:**
- إشعار لولي الأمر: "تقييم جديد لابنك"
- يظهر في لوحة ولي الأمر
---
## أمثلة عملية
### مثال 1: تقييم شهري لمجموعة كرة قدم
> المدرب يُقيّم 15 مشترك نهاية الشهر:
> - اللياقة: 8/10
> - المهارات التقنية: 7/10
> - التكتيك: 6/10
> - العمل الجماعي: 9/10
> - السلوك: 10/10
> - المجموع: 40/50 = 80%
> - توصية: "يحتاج تركيز أكثر على الجانب التكتيكي"
### مثال 2: تقييم سباحة
> - تقنية السباحة: 9/10
> - التحمل: 7/10
> - اللياقة: 8/10
> - السلوك: 10/10
> - توصية: "جاهز للانتقال للمستوى المتقدم"
### مثال 3: ولي أمر يرى التقييم
> 1. يسجّل دخوله كولي أمر
> 2. يرى إشعار: "تقييم جديد لمحمد"
> 3. يفتح التقييم → يرى الدرجات والتوصيات
> 4. يعرف نقاط القوة والضعف لابنه
---
## لماذا يحتاج عملك هذا
| الميزة | الفائدة |
|--------|---------|
| معايير قابلة للتخصيص | كل نشاط له معاييره الخاصة |
| تقييم دوري | تتبع تطور المشترك على مدار الوقت |
| مشاركة مع ولي الأمر | شفافية + تواصل = رضا العميل |
| التوصيات | المدرب يوجّه المشترك للتحسن |
| ربط بالشهادة | نسبة التقييم تظهر في شهادة الحضور |
---
## التكاملات
| الوحدة | العلاقة |
|--------|---------|
| المشتركين | التقييم مرتبط بمشترك محدد |
| المجموعات | التقييم ضمن مجموعة محددة |
| المدربين | المدرب هو المقيّم |
| الإشعارات | مشاركة التقييم ترسل إشعار |
| لوحة ولي الأمر | ولي الأمر يرى تقييمات أبنائه |
# 15 — التقارير والإعدادات
## نظرة عامة
تقارير شاملة (مالية + حضور) مع صفحات إعدادات قابلة للتخصيص لكل أكاديمية. بالإضافة إلى سجل التدقيق وأدوات SuperAdmin.
---
## شاشات التقارير
### 1. صفحة التقارير الرئيسية (`/reports`)
**من يراها:** من لديه صلاحية `reports.view`
**المحتوى:** بطاقات تنقل لكل تقرير متاح:
- التقرير المالي
- تقرير الحضور
- طباعة التقرير المالي اليومي
---
### 2. التقرير المالي (`/reports/financial`)
**من يراها:** من لديه صلاحية `reports.view`
**الفلاتر:**
- الفترة (من - إلى)
- الفرع
**المحتوى:**
| القسم | الوصف |
|-------|-------|
| إجمالي الإيرادات | مجموع المدفوعات المؤكدة |
| إجمالي الفواتير الصادرة | عدد + مبلغ |
| فواتير مدفوعة | عدد + مبلغ |
| فواتير متأخرة | عدد + مبلغ |
| نسبة التحصيل | المحصّل ÷ المستحق |
| توزيع طرق الدفع | كاش / كارت / محفظة / تحويل |
| أعلى 10 مشتركين ديون | قائمة |
---
### 3. تقرير الحضور (`/reports/attendance`)
**من يراها:** من لديه صلاحية `reports.attendance`
**الفلاتر:**
- الفترة (من - إلى)
- المجموعة
- المشترك
**المحتوى:**
| القسم | الوصف |
|-------|-------|
| نسبة الحضور العامة | % |
| توزيع الحالات | حاضر / متأخر / غائب / مستأذن |
| أعلى حضور | أفضل 10 مشتركين |
| أقل حضور | أسوأ 10 مشتركين |
| حضور المدربين | نسبة لكل مدرب |
---
### 4. طباعة التقرير المالي اليومي (`/reports/daily-print`)
**من يراها:** من لديه صلاحية `reports.view`
**صفحة قابلة للطباعة:**
- تاريخ اليوم
- إجمالي المدفوعات
- تفصيل حسب طريقة الدفع
- تفصيل حسب البند (اشتراكات / منتجات)
- الورديات وأرصدتها
---
### 5. التصدير
| الرابط | المحتوى |
|--------|---------|
| `/export/participants` | قائمة المشتركين (Excel) |
| `/export/payments` | المدفوعات (Excel) |
| `/export/invoices` | الفواتير (Excel) |
| `/export/enrollments` | التسجيلات (Excel) |
---
## شاشات الإعدادات
### 6. إعدادات الأكاديمية (`/settings`)
**من يراها:** من لديه صلاحية `settings.view`
**الحقول:**
- اسم الأكاديمية (عربي + إنجليزي)
- الشعار
- العنوان
- هاتف التواصل
- البريد الإلكتروني
- الموقع الإلكتروني
---
### 7. إعدادات العلامة التجارية (`/settings/branding`)
**من يراها:** من لديه صلاحية `settings.manage`
**الحقول:**
- الشعار الرئيسي
- اللون الأساسي
- اللون الثانوي
- نص الذيل
- أيقونة الـ Favicon
---
### 8. إعدادات الإيصالات (`/settings/receipts`)
**من يراها:** من لديه صلاحية `settings.manage`
**الحقول:**
- ترويسة الإيصال
- تذييل الإيصال
- إظهار الشعار
- إظهار QR
- حجم الخط
- عرض الورقة
---
### 9. إعدادات النظام (`/settings/system`)
**من يراها:** من لديه صلاحية `settings.manage`
**أمثلة (محفوظة في جدول `system_settings`):**
| المفتاح | النوع | الوصف |
|---------|-------|-------|
| grace_period_participants | integer | فترة سماح التأخير (دقائق) |
| grace_period_trainers | integer | فترة سماح المدربين (دقائق) |
| consecutive_absence_threshold | integer | غيابات قبل الإيقاف |
| min_attendance_percent | integer | نسبة الحضور الأدنى |
| max_discount_percent | integer | حد أقصى للخصم |
| currency | string | العملة |
| timezone | string | المنطقة الزمنية |
| sms_gateway | string | بوابة SMS |
| auto_generate_sessions_days | integer | عدد أيام توليد الحصص مسبقاً |
| enrollment_auto_activate | boolean | تفعيل تلقائي بعد الدفع |
| waitlist_response_hours | integer | مهلة الرد على قائمة الانتظار |
---
## سجل التدقيق
### 10. سجل التدقيق (`/audit`)
**من يراها:** من لديه صلاحية `audit.list`
**الفلاتر:**
- المستخدم
- الكيان (participants, invoices, etc.)
- العملية (created, updated, deleted)
- الفترة
**أعمدة:**
| العمود | الوصف |
|--------|-------|
| التاريخ | created_at |
| المستخدم | user.name |
| العملية | action |
| الكيان | auditable_type |
| القيم القديمة | old_values (JSON) |
| القيم الجديدة | new_values (JSON) |
| IP | ip_address |
**خصائص:**
- لا يمكن تعديل أو حذف سجلات التدقيق (IMMUTABLE)
- السجلات المالية تُحفظ للأبد
- السجلات غير المالية تُحذف بعد سنة (`audit:cleanup --days=365`)
---
## لوحة SuperAdmin
### 11. لوحة مدير النظام (`/admin`)
**من يراها:** من لديه صلاحية `super_admin.access`
**المحتوى:**
- قائمة الأكاديميات
- تفعيل/تعطيل أكاديمية
- إعدادات النظام العامة
- عدد المستخدمين لكل أكاديمية
---
### 12. إعدادات النظام العامة (`/admin/system-settings`)
**من يراها:** من لديه صلاحية `settings.manage`
**إعدادات على مستوى المنصة (ليست لأكاديمية واحدة).**
---
## لوحات متخصصة
### 13. لوحة المدرب (`/trainer`)
**من يراها:** المدرب ورئيس المدربين
**المحتوى:**
- حصص اليوم (مع أزرار تسجيل الحضور)
- عدد المشتركين في كل حصة
- حصص تم تسجيل حضورها ✓
- ملخص إحصائي
---
### 14. لوحة ولي الأمر (`/guardian`)
**من يراها:** من لديه دور "parent"
**المحتوى:**
- قائمة الأبناء
- لكل ابن:
- الحالة الحالية
- نسبة الحضور
- آخر تقييم
- الفواتير المعلقة
- الاشتراكات النشطة
---
## التقرير الأسبوعي لأولياء الأمور
**المهمة المجدولة:** `reports:parent-weekly` (السبت 12:00)
**المحتوى:**
- ملخص حضور الأسبوع لكل ابن
- أي غيابات
- أي تقييمات جديدة
- أي فواتير مستحقة
---
## أمثلة عملية
### مثال 1: مالك أكاديمية يراجع أداء الشهر
> 1. يفتح `/reports/financial`
> 2. يختار "هذا الشهر"
> 3. يرى: إيرادات 150,000 ج.م، نسبة تحصيل 85%
> 4. يرى فواتير متأخرة: 15 فاتورة بقيمة 22,500 ج.م
> 5. يقرر: تكثيف التحصيل
### مثال 2: رئيس مدربين يراجع الحضور
> 1. يفتح `/reports/attendance`
> 2. يصفّي: مجموعة "ناشئين"
> 3. يرى: نسبة الحضور 72% (أقل من 75%)
> 4. يحدد المشتركين الأقل حضوراً
> 5. يتواصل مع أولياء أمورهم
### مثال 3: ضبط إعدادات جديدة
> 1. المدير يريد تغيير فترة السماح من 15 إلى 10 دقائق
> 2. يفتح `/settings/system`
> 3. يغيّر `grace_period_participants` = 10
> 4. من الآن: أي حضور بعد 10 دقائق = "متأخر"
---
## لماذا يحتاج عملك هذا
| الميزة | الفائدة |
|--------|---------|
| التقرير المالي | نظرة شاملة على صحة العمل |
| تقرير الحضور | كشف المشتركين غير الملتزمين |
| التصدير | مشاركة البيانات مع المحاسب أو الشريك |
| الإعدادات المرنة | كل أكاديمية تضبط النظام حسب احتياجها |
| سجل التدقيق | لا أحد يستطيع التلاعب بدون أثر |
| التقرير الأسبوعي | تواصل دوري مع أولياء الأمور = ثقة |
| لوحات متخصصة | كل شخص يرى ما يهمه فقط |
# 16 — مكتب الاستقبال
## نظرة عامة
واجهة مخصصة لموظف الاستقبال تجمع العمليات اليومية الأكثر شيوعاً في مكان واحد: تسجيل مشترك جديد، تسجيل موجود في مجموعة، تحصيل دفعة.
---
## الشاشات المنفذة
### 1. لوحة الاستقبال (`/receptionist`)
**من يراها:** من لديه صلاحية `participants.list` (موظف الاستقبال عادةً)
**المحتوى:**
- 3 بطاقات كبيرة (اختصارات):
1. **تسجيل جديد** → ويزارد التسجيل الكامل
2. **تسجيل في مجموعة** → ويزارد تسجيل مشترك موجود
3. **تحصيل دفعة** → ويزارد التحصيل
- إحصائيات سريعة:
- مشتركين تم تسجيلهم اليوم
- مدفوعات اليوم
- حصص اليوم
---
### 2. ويزارد التسجيل الجديد (`/receptionist/new-registration`)
**من يراها:** من لديه صلاحية `participants.create`
**الوصف:** ويزارد شامل يأخذ موظف الاستقبال من الصفر إلى مشترك مفعّل في خطوات واضحة.
**الخطوات:**
#### الخطوة 1: بيانات المشترك
- الاسم بالعربية (مطلوب)
- الاسم بالإنجليزية
- الرقم القومي → يستنتج تاريخ الميلاد والجنس
- تاريخ الميلاد
- الجنس
- الهاتف
#### الخطوة 2: ولي الأمر
- بحث عن ولي أمر موجود أو إنشاء جديد
- اسم ولي الأمر
- هاتف ولي الأمر
- صلة القرابة
#### الخطوة 3: اختيار البرنامج والمجموعة
- عرض البرامج المتاحة (مصفّاة حسب العمر والجنس)
- عرض المجموعات لكل برنامج (مع السعة المتبقية)
- اختيار المجموعة
#### الخطوة 4: حساب السعر والدفع
- عرض السعر المحسوب (من محرك التسعير)
- حقل كوبون (اختياري)
- اختيار طريقة الدفع
- خيار: دفع الآن أو تأجيل
#### الخطوة 5: مراجعة وتأكيد
- ملخص كل البيانات
- زر "تأكيد التسجيل"
```mermaid
flowchart TD
A[الخطوة 1: بيانات المشترك] --> B[الخطوة 2: ولي الأمر]
B --> C[الخطوة 3: البرنامج والمجموعة]
C --> D[الخطوة 4: السعر والدفع]
D --> E[الخطوة 5: مراجعة وتأكيد]
E --> F{إتمام}
F --> G[إنشاء شخص + مشترك + ولي أمر]
F --> H[إنشاء اشتراك]
F --> I[إنشاء فاتورة]
F --> J[تسجيل دفعة - إذا دفع الآن]
F --> K[تفعيل - إذا دُفع]
```
---
### 3. ويزارد تسجيل مشترك موجود (`/receptionist/enroll-existing`)
**من يراها:** من لديه صلاحية `enrollments.create`
**الوصف:** لمشترك مسجّل بالفعل يريد الانضمام لمجموعة جديدة أو تجديد اشتراك.
**الخطوات:**
#### الخطوة 1: البحث عن المشترك
- بحث بالاسم أو الهاتف أو رقم المشترك
- عرض بيانات المشترك المُختار
#### الخطوة 2: اختيار المجموعة
- عرض المجموعات المتاحة
- إظهار اشتراكاته الحالية (لتجنب التكرار)
#### الخطوة 3: السعر والدفع
- حساب السعر
- كوبون (اختياري)
- طريقة الدفع
#### الخطوة 4: تأكيد
---
### 4. ويزارد تحصيل دفعة (`/receptionist/collect-payment`)
**من يراها:** من لديه صلاحية `invoices.create`
**الوصف:** لتحصيل دفعة على فاتورة قائمة (بدون إنشاء اشتراك جديد).
**الخطوات:**
#### الخطوة 1: البحث عن المشترك
- بحث بالاسم أو الهاتف
#### الخطوة 2: اختيار الفاتورة
- عرض الفواتير المعلقة/المتأخرة للمشترك
- المبلغ المتبقي لكل فاتورة
#### الخطوة 3: إدخال الدفعة
- المبلغ (يعبأ بالمتبقي تلقائياً)
- طريقة الدفع
- ملاحظات
#### الخطوة 4: تأكيد وإيصال
- طباعة إيصال
- أو إرسال إيصال إلكتروني
---
## سيناريوهات يومية
### سيناريو 1: عميل جديد يأتي للسؤال ويسجّل
> 1. الأب يأتي مع ابنه (7 سنوات)
> 2. الاستقبال يضغط "تسجيل جديد"
> 3. يُدخل اسم الطفل + الرقم القومي → العمر 7 يظهر تلقائياً
> 4. يُدخل بيانات الأب كولي أمر
> 5. يعرض البرامج: "أشبال 7-9 سنوات" متاح
> 6. يعرض المجموعات: "سبت-اثنين" بها 3 أماكن
> 7. السعر: 1500 ج.م — كوبون "FRIEND": خصم 100 ج.م → 1400 ج.م
> 8. الأب يدفع كاش
> 9. تأكيد: المشترك نشط + فاتورة مدفوعة + إيصال
> 10. **إجمالي الوقت: 3 دقائق**
### سيناريو 2: مشترك حالي يجدد اشتراكه
> 1. "أحمد" اشتراكه ينتهي الأسبوع القادم
> 2. الأب يأتي للتجديد
> 3. الاستقبال يضغط "تسجيل في مجموعة"
> 4. يبحث "أحمد" → يظهر
> 5. يختار نفس المجموعة (تجديد)
> 6. السعر: 1500 ج.م — خصم ولاء (سنة+): 10% → 1350 ج.م
> 7. دفع + تأكيد
### سيناريو 3: ولي أمر يأتي لدفع فاتورة متأخرة
> 1. الاستقبال يضغط "تحصيل دفعة"
> 2. يبحث باسم الأب أو الابن
> 3. يظهر: فاتورة #INV-2024-0045 — متأخرة — متبقي 750 ج.م
> 4. الأب يدفع 750 ج.م كاش
> 5. الفاتورة → "مدفوعة"
> 6. طباعة إيصال
### سيناريو 4: المجموعة ممتلئة
> 1. الأب يريد تسجيل ابنه في مجموعة "سبت-اثنين"
> 2. الويزارد يُظهر: "ممتلئة (20/20)"
> 3. خيار: إضافة لقائمة الانتظار
> 4. الأب يوافق → "تمت الإضافة — سنُبلغكم عند فتح مكان"
---
## لماذا يحتاج عملك هذا
| الميزة | الفائدة |
|--------|---------|
| ويزارد شامل | 3 دقائق من البداية للنهاية — بدلاً من 5 شاشات مختلفة |
| بحث ذكي | يجد المشترك بسرعة بأي طريقة |
| حساب سعر فوري | لا حاجة لسؤال المدير عن السعر |
| طباعة إيصال فوري | احترافية — العميل يغادر بإيصال |
| كشف التكرار | لا يُسجّل نفس الطفل مرتين |
| التصفية الذكية | يُظهر فقط البرامج المناسبة للعمر |
| لا حاجة لتدريب طويل | الويزارد يوجّه الموظف خطوة بخطوة |
---
## التكاملات
| الوحدة | العلاقة |
|--------|---------|
| المشتركين | إنشاء مشترك جديد |
| التسجيل | إنشاء اشتراك |
| التسعير | حساب السعر + تطبيق الكوبون |
| المالية | إنشاء فاتورة + تسجيل دفعة |
| الورديات | العملية ترتبط بالوردية المفتوحة |
| الإشعارات | إرسال إيصال + إشعار تسجيل |
# 17 — مخططات الدورات الشاملة (Mermaid)
## 1. الدورة الكاملة لمشترك جديد
```mermaid
flowchart TD
subgraph تسجيل
A[عميل جديد يأتي] --> B[إنشاء شخص Person]
B --> C[إنشاء مشترك Participant]
C --> D[ربط ولي أمر Guardian]
end
subgraph اشتراك
D --> E[اختيار برنامج]
E --> F[اختيار مجموعة]
F --> G{ممتلئة؟}
G -->|لا| H[إنشاء Enrollment]
G -->|نعم| I[قائمة انتظار]
end
subgraph مالية
H --> J[حساب السعر - PricingService]
J --> K[إنشاء فاتورة]
K --> L[تحصيل دفعة]
L --> M[قيد مزدوج مدين/دائن]
end
subgraph تفعيل
M --> N[الاشتراك → نشط]
N --> O[المشترك → نشط]
O --> P[توليد سجلات حضور]
P --> Q[حجز مساحة - إن وُجد]
end
subgraph تدريب
Q --> R[المشترك يحضر حصص]
R --> S[تسجيل حضور يومي]
S --> T[تقييم دوري]
T --> U{انتهى البرنامج؟}
U -->|نعم| V[تخرج أو تجديد]
U -->|لا| R
end
```
---
## 2. الدورة المالية اليومية
```mermaid
flowchart TD
subgraph بداية_اليوم
A[موظف الاستقبال يسجّل الدخول] --> B[فتح وردية مالية]
B --> C[إدخال المبلغ الافتتاحي]
end
subgraph عمليات_اليوم
C --> D{نوع العملية}
D -->|تسجيل جديد| E[ويزارد الاستقبال]
D -->|بيع منتج| F[نقطة البيع POS]
D -->|تحصيل فاتورة| G[تحصيل دفعة]
D -->|إيداع محفظة| H[إيداع Wallet]
E --> I[فاتورة + دفعة + قيد]
F --> I
G --> I
H --> I
I --> J[تحديث عداد الوردية]
J --> D
end
subgraph نهاية_اليوم
J --> K[إقفال الوردية]
K --> L[إدخال المبلغ الختامي]
L --> M[حساب الفرق]
M --> N{تطابق؟}
N -->|نعم| O[✓ مطابقة]
N -->|لا| P[⚠ فرق يحتاج تبرير]
end
```
---
## 3. دورة الحضور اليومية
```mermaid
flowchart TD
subgraph توليد
A[مهمة 02:00 - توليد حصص] --> B[لكل مجموعة نشطة]
B --> C[إنشاء حصة]
C --> D[توليد سجلات 'متوقع']
D --> E[حجز مساحة]
end
subgraph تسجيل
F[المدرب يفتح شاشة الحضور] --> G[قائمة المشتركين]
G --> H{لكل مشترك}
H --> I[حاضر ← قبل فترة السماح]
H --> J[متأخر ← بعد فترة السماح]
H --> K[مستأذن ← إذن مسبق]
H --> L[لم يأتِ ← يبقى 'متوقع']
end
subgraph تلقائي
M[مهمة كل ساعة] --> N{انتهت الحصة + ساعتين؟}
N -->|نعم| O[متوقع → غائب تلقائي]
N -->|لا| P[انتظار]
end
subgraph إنفاذ
Q[مهمة 06:00 يومياً] --> R{5 غيابات متتالية؟}
R -->|نعم| S[إيقاف المشترك]
S --> T[إشعار ولي الأمر]
R -->|لا| U{نسبة < 75%؟}
U -->|نعم| V[تحذير ولي الأمر]
U -->|لا| W[لا إجراء]
end
```
---
## 4. دورة محرك التسعير
```mermaid
flowchart TD
A[طلب حساب سعر] --> B[البحث عن سعر أساسي نشط]
B --> C{وُجد؟}
C -->|لا| D[✗ رفض: لا يوجد سعر محدد]
C -->|نعم| E[جمع القواعد المطبقة]
E --> F[تقييم شروط كل قاعدة]
F --> G[تصفية: غير المتراكمة - الأعلى أولوية يفوز]
G --> H[تطبيق بالترتيب]
H --> I[قاعدة 1: مثلاً خصم عمر 10%]
I --> J[قاعدة 2: مثلاً خصم أخوة 15%]
J --> K[تطبيق حد أقصى لكل قاعدة]
K --> L{كوبون؟}
L -->|نعم| M[8 تحققات على الكوبون]
M --> N[تطبيق خصم الكوبون]
L -->|لا| O[تخطي]
N --> P[فحص الحد الأقصى العام - 50%]
O --> P
P --> Q[فحص: لا يكون سالب]
Q --> R[النتيجة النهائية ← الفاتورة]
```
---
## 5. دورة عملية POS
```mermaid
sequenceDiagram
participant C as العميل
participant R as الاستقبال
participant POS as نقطة البيع
participant Pricing as محرك التسعير
participant Inv as المخزون
participant Fin as المالية
C->>R: يريد شراء منتج + اشتراك
R->>POS: إضافة بنود للسلة
POS->>Pricing: حساب سعر كل بند
Pricing-->>POS: الأسعار المحسوبة
R->>POS: تطبيق كوبون
POS->>POS: التحقق من الكوبون (8 checks)
POS-->>R: الإجمالي بعد الخصم
R->>POS: إتمام البيع (كاش)
POS->>POS: DB::transaction بدأت
POS->>Fin: إنشاء فاتورة + دفعة + قيد مزدوج
POS->>Inv: حركة مخزون (خصم المنتج)
POS->>POS: إنشاء اشتراك + تفعيل
POS->>POS: تحديث عداد الوردية
POS->>POS: إنشاء إيصال
POS->>POS: DB::transaction انتهت ✓
POS-->>R: ✓ تم + إيصال
R-->>C: إيصال مطبوع
```
---
## 6. دورة إنشاء المجموعة وتوليد الحصص
```mermaid
flowchart TD
subgraph إعداد
A[إنشاء برنامج] --> B[إنشاء مجموعة]
B --> C[تعيين مدرب]
C --> D[إنشاء جدول أسبوعي]
D --> E[ربط بمنشأة]
end
subgraph توليد_يومي
F[مهمة 02:00] --> G[لكل مجموعة نشطة بجدول]
G --> H[لكل يوم في الأسبوع القادم]
H --> I{عطلة؟}
I -->|نعم| J[تخطي]
I -->|لا| K{موجودة بالفعل؟}
K -->|نعم| J
K -->|لا| L[إنشاء حصة]
L --> M[توليد حضور المشتركين]
M --> N[توليد حضور المدرب]
N --> O[حجز المساحة]
end
subgraph أثناء_الحصة
P[الحصة تبدأ] --> Q[المدرب يسجل الحضور]
Q --> R[الحصة تنتهي]
R --> S[حالة: مكتملة]
end
```
---
## 7. دورة المحفظة
```mermaid
flowchart TD
A[ولي الأمر يودع 2000 ج.م] --> B[lockForUpdate على المحفظة]
B --> C[إنشاء wallet_transaction: deposit]
C --> D[الرصيد: 0 → 2000]
E[عملية شراء 1500 ج.م] --> F[lockForUpdate]
F --> G{الرصيد ≥ المطلوب؟}
G -->|نعم| H[إنشاء wallet_transaction: debit]
H --> I[الرصيد: 2000 → 500]
G -->|لا| J[✗ رصيد غير كافٍ]
K[تجميد 300 ج.م] --> L[frozen_amount: 0 → 300]
L --> M[available = balance - frozen = 500 - 300 = 200]
```
---
## 8. دورة الموارد البشرية الشهرية
```mermaid
flowchart TD
subgraph يومياً
A[المدرب يحضر حصة] --> B[حدث: AttendanceMarked]
B --> C[Listener: GenerateTrainerCompensation]
C --> D[مكافأة: pending]
end
subgraph نهاية_الشهر
E[فتح فترة رواتب] --> F[تجميع مكافآت الشهر]
F --> G[خصم أقساط السلف]
G --> H[إنشاء كشف راتب draft]
H --> I[مراجعة وتعديل]
I --> J[اعتماد: approved]
J --> K[صرف: paid]
K --> L[إشعار المدرب]
end
```
---
## 9. دورة قائمة الانتظار
```mermaid
flowchart TD
A[طلب تسجيل في مجموعة ممتلئة] --> B[إضافة لقائمة الانتظار]
B --> C[ترتيب: حسب وقت الطلب]
D[مشترك ينسحب من المجموعة] --> E[عداد المجموعة -1]
E --> F[حدث: WaitlistSpotAvailable]
F --> G[أول شخص في القائمة]
G --> H[إرسال إشعار: مكان متاح]
H --> I{الرد؟}
I -->|قبول| J[نقل: waitlisted → pending]
J --> K[إنشاء فاتورة]
K --> L[بعد الدفع: active]
I -->|رفض/انتهاء المهلة| M[الانتقال للتالي في القائمة]
M --> G
```
---
## 10. دورة المستندات
```mermaid
flowchart TD
A[رفع مستند] --> B[حالة: pending]
B --> C[المسؤول يراجع]
C --> D{القرار}
D -->|قبول| E[حالة: approved]
D -->|رفض| F[حالة: rejected + سبب]
E --> G[المستند فعّال]
G --> H{تاريخ انتهاء؟}
H -->|نعم| I[مهمة يومية تفحص]
I --> J{انتهى؟}
J -->|نعم| K[حالة: expired + تنبيه]
J -->|لا| L[تحذير قبل 7 أيام]
H -->|لا| M[فعّال دائماً]
F --> N[إعادة رفع] --> B
```
---
## 11. دورة كشف تعارض المساحات
```mermaid
flowchart TD
A[طلب حجز مساحة] --> B[تحديد: منشأة + يوم + وقت + أجزاء]
B --> C[البحث عن حجوزات مؤكدة]
C --> D[تصفية: نفس المنشأة + نفس اليوم]
D --> E[تصفية: تداخل زمني]
E --> F{وُجد تداخل؟}
F -->|لا| G[✓ لا تعارض - يمكن الحجز]
F -->|نعم| H[فحص تقاطع الأجزاء]
H --> I{نفس الأجزاء مطلوبة؟}
I -->|لا| G
I -->|نعم| J[✗ تعارض]
J --> K[إرجاع تفاصيل: من يحجز + متى + أي أجزاء]
```
---
## 12. دورة الفاتورة الكاملة
```mermaid
stateDiagram-v2
[*] --> draft : إنشاء
draft --> sent : إرسال لولي الأمر
draft --> cancelled : إلغاء
sent --> partially_paid : دفعة جزئية
sent --> overdue : تجاوز الاستحقاق (تلقائي)
sent --> cancelled : إلغاء (مدفوع=0 فقط)
overdue --> partially_paid : دفعة جزئية
overdue --> paid : دفع كامل
partially_paid --> paid : اكتمال الدفع
paid --> refunded : استرداد
note right of cancelled : لا يمكن إلغاء فاتورة بها مدفوعات
note right of paid : التعديل = استرداد + فاتورة جديدة
note right of refunded : قيد عكسي جديد
```
---
## 13. نظرة شاملة — تدفق البيانات بين الوحدات
```mermaid
flowchart LR
subgraph الهوية
Users[مستخدمين]
Roles[أدوار]
Branches[فروع]
end
subgraph الأشخاص
People[أشخاص]
Participants[مشتركين]
Guardians[أولياء أمور]
end
subgraph التدريب
Programs[برامج]
Groups[مجموعات]
Sessions[حصص]
Enrollments[اشتراكات]
end
subgraph المالية
Invoices[فواتير]
Payments[مدفوعات]
Transactions[قيود]
Wallets[محافظ]
end
subgraph العمليات
Attendance[حضور]
Assignments[تعيينات]
Facilities[منشآت]
Inventory[مخزون]
POS[نقطة بيع]
HR[موارد بشرية]
end
subgraph الدعم
Notifications[إشعارات]
Documents[مستندات]
Evaluations[تقييمات]
Reports[تقارير]
Settings[إعدادات]
end
Participants --> Enrollments
Enrollments --> Groups
Groups --> Sessions
Sessions --> Attendance
Enrollments --> Invoices
Invoices --> Payments
Payments --> Transactions
POS --> Invoices
POS --> Inventory
Assignments --> Attendance
Attendance --> HR
HR --> Transactions
Sessions --> Facilities
```
# El-Captain Pricing Strategy
## Market Reality
```mermaid
graph LR
A[Average Academy] --> B[50 Players]
B --> C[500 EGP/month each]
C --> D[25,000 EGP/month collections]
D --> E[~10,000 EGP owner profit]
```
| Metric | Value |
|--------|-------|
| Average academy size | 50 players |
| Average subscription/player | 500 EGP |
| Monthly collections | 25,000 EGP |
| Owner monthly profit | ~10,000 EGP |
| Our survival number | 150,000 EGP/month |
---
## Trial Model
> **10 days free trial on ALL tiers. Full features. No credit card required.**
>
> After 10 days: pay or lose access. Data preserved for 30 days if they come back.
```mermaid
flowchart LR
A["Sign Up"] --> B["10 Day Trial<br/>(full features)"]
B -->|"Day 7: reminder"| C{Converts?}
C -->|Yes| D["Paying Customer"]
C -->|No| E["Access Locked<br/>Data kept 30 days"]
E -->|"Comes back"| D
```
---
## Pricing Tiers
```mermaid
graph TD
subgraph BASIC["أساسي — 399 ج.م/شهر"]
B1[150 مشترك]
B2[3 مستخدمين]
B3[كل الأساسيات]
B4[إشعارات أولياء أمور]
B5[10 يوم تجربة مجانية]
end
subgraph BIZ["بزنس — 1,199 ج.م/شهر"]
Z1[مشتركين بلا حد]
Z2[10 مستخدمين]
Z3[POS + مخازن]
Z4[SMS + تطبيق أولياء]
Z5[محرك تسعير ذكي]
end
subgraph ENT["إنتربرايز — 3,499 ج.م/شهر"]
E1[فروع متعددة]
E2[API + تكامل]
E3[مدير حساب مخصص]
E4[SLA + ضمان تشغيل]
end
BASIC -->|"grows past 150 players"| BIZ
BIZ -->|"opens 2nd branch"| ENT
```
---
## Tier Details
### أساسي (Essential) — 399 ج.م/شهر
| | |
|---|---|
| **Price** | 399 ج.م/شهر |
| **Trial** | 10 days full access |
| **Target** | 90% of the market (up to 150 players) |
| **Limit** | 150 players, 3 users |
| **Includes** | Attendance, invoices, payments, programs, groups, schedule, parent notifications, WhatsApp support, basic reports |
| **Pitch** | "أقل من اشتراك لاعب واحد" |
| **% of academy revenue** | 1.6% — invisible |
---
### بزنس (Business) — 1,199 ج.م/شهر
| | |
|---|---|
| **Price** | 1,199 ج.م/شهر |
| **Trial** | 10 days full access |
| **Target** | Growing academies (100–500 players) |
| **Limit** | Unlimited players, 10 users |
| **Includes** | Everything in أساسي + smart pricing engine, POS, inventory, facilities/lanes, SMS + email, parent app, advanced reports + export, multi-role permissions |
| **Pitch** | "أقل من ربع راتب ريسبشن" |
| **% of academy revenue** | 4.8% for avg academy, drops as they grow |
---
### إنتربرايز (Enterprise) — 3,499 ج.م/شهر
| | |
|---|---|
| **Price** | 3,499 ج.م/شهر |
| **Trial** | 10 days full access |
| **Target** | Chains & clubs (300+ players, multi-branch) |
| **Limit** | Everything unlimited |
| **Includes** | Everything in بزنس + unlimited branches, API & external integrations, dedicated account manager, team training, SLA + 99.9% uptime guarantee, optional private hosting |
| **Pitch** | "أقل من نص راتب مدير عمليات" |
---
## Conversion Funnel
```mermaid
flowchart TD
A["Trial Signup<br/>(10 days, any tier)"] --> B["Day 3: First value moment"]
B --> C["Day 7: Upgrade reminder<br/>'3 days left'"]
C --> D{Converts?}
D -->|Yes| E["أساسي (399)"]
D -->|Yes| F["بزنس (1,199)"]
D -->|Yes| G["إنتربرايز (3,499)"]
D -->|No| H["Access Locked<br/>Day 10"]
H --> I["Follow-up: Day 14, 21, 30"]
I -->|"Comes back"| E
E --> J{Growing past 150?}
J -->|Yes| F
F --> K{Multi-branch?}
K -->|Yes| G
```
---
## Revenue Model — Path to 150K (Plan A: Volume)
```mermaid
pie title Target Revenue Mix (153K EGP/month)
"أساسي × 100 = 39,900" : 39900
"بزنس × 80 = 95,920" : 95920
"إنتربرايز × 5 = 17,495" : 17495
```
| Tier | Price | Customers | Revenue |
|------|------:|:---------:|--------:|
| أساسي | 399 | 100 | 39,900 |
| بزنس | 1,199 | 80 | 95,920 |
| إنتربرايز | 3,499 | 5 | 17,495 |
| **Total** | | **185 paying** | **153,315** |
---
## Growth Scenarios
```mermaid
gantt
title Revenue Growth Milestones
dateFormat YYYY-MM
axisFormat %b %Y
section Survival
Break-even (150K) :milestone, m1, 2026-10, 0d
section أساسي (399)
First 30 customers :a1, 2026-07, 30d
Reach 60 customers :a2, after a1, 30d
Reach 100 customers :a3, after a2, 60d
section بزنس (1,199)
First 10 upgrades :b1, 2026-08, 30d
Reach 40 customers :b2, after b1, 60d
Reach 80 customers :b3, after b2, 60d
section إنتربرايز (3,499)
First 2 enterprise deals :e1, 2026-09, 30d
Reach 5 customers :e2, after e1, 60d
```
---
## Psychology Behind Each Number
```mermaid
mindmap
root((Pricing Psychology))
399 ج.م
Less than 1 player fee
"If it saves ONE kid from quitting = ROI"
< 2% of revenue = invisible cost
Below "needs manager approval" threshold
1,199 ج.م
1/4 of a receptionist salary
Works 24/7 no sick days
Below "let me think about it" threshold
Psychological: 1,199 not 1,200
3,499 ج.م
Half an operations manager salary
Does the work of 3 people
Includes human touch (account manager)
"If you need to ask the price..."
10-Day Trial
Long enough to enter real data
Short enough to create urgency
"Day 7 reminder" triggers FOMO
Data hostage: they invested time
```
---
## Competitive Positioning
```mermaid
quadrantChart
title Price vs Value (Egyptian Market)
x-axis "Low Price" --> "High Price"
y-axis "Basic Features" --> "Full Platform"
quadrant-1 "Sweet Spot"
quadrant-2 "Too Expensive"
quadrant-3 "Cheap but Useless"
quadrant-4 "Overpriced Junk"
"El-Captain أساسي": [0.15, 0.7]
"El-Captain بزنس": [0.35, 0.95]
"Excel + WhatsApp": [0.05, 0.1]
"Generic CRM": [0.5, 0.4]
"Custom Developer": [0.8, 0.6]
"Hiring Staff Instead": [0.7, 0.3]
```
---
## No-Brainer Proof Points
| Objection | Response |
|-----------|----------|
| "مش محتاج نظام" | "كام لاعب نسيت تحصّل منه الشهر ده؟ لو واحد بس = ٥٠٠ > ٣٩٩" |
| "غالي" | "بتدفع كام لريسبشن؟ ٦٠٠٠؟ ده ربع السعر وبيشتغل ٢٤ ساعة" |
| "هجرب بعدين" | "١٠ أيام مجاناً. سجل دلوقتي. لو مش عاجبك — ما تدفعش" |
| "خايف بياناتي" | "سيرفرات أوروبية + نسخ احتياطي يومي + بياناتك ملكك تصدّرها أي وقت" |
| "مش هيشتغل مع أكاديميتي" | "مصمم خصيصاً للأكاديميات. مش ERP عام حد عدّله" |
---
## Key Principles
> **1. Zero percentages. Flat pricing only.**
>
> Egyptian small business owners HATE commission models — it feels like a business partner taking a cut, not a tool you're paying for. Flat monthly fee = predictable, budgetable, invisible after month 1.
> **2. No free tier. Trial only.**
>
> Free tiers attract freeloaders who never pay and consume support. A 10-day trial creates urgency, demonstrates real value, and filters for serious buyers.
---
## Trial Conversion Tactics
| Day | Action |
|:---:|--------|
| 0 | Welcome email + onboarding checklist |
| 1 | "Add your first 5 players" push |
| 3 | "You've logged 12 attendance records! Imagine losing this" |
| 5 | Mid-trial check-in (WhatsApp) |
| 7 | **"3 DAYS LEFT"** — urgency email + "subscribe now, save 10%" |
| 9 | "Tomorrow your access expires" — show data they'll lose |
| 10 | **Access locked.** Data preserved. |
| 14 | "Your data is still here. Come back?" |
| 21 | Last follow-up. "We're deleting in 9 days." |
| 30 | Data deleted. Account closed. |
---
## Landing Page One-Liner
> **جرّب ١٠ أيام مجاناً. ابدأ بـ ٣٩٩ ج.م بس — أقل من اشتراك لاعب واحد.**
---
---
# PLAN B: Break Even with 20 Customers Only
## The Problem
185 customers is the safe model. But what if we can only close **20 customers** in month 1?
**150,000 ÷ 20 = 7,500 EGP average per customer.**
A small academy paying 7,500/month for software? That's 30% of their profit. Not happening with flat pricing alone. We need a **hybrid + services model** targeting medium-large academies.
---
## Strategy: Software + Revenue Share + Services
```mermaid
graph TD
subgraph REVENUE["Revenue Streams with 20 Customers"]
A["SaaS Subscription<br/>Monthly flat fee"] --> T[150K Target]
B["Revenue Share<br/>% on collections PROCESSED through us"] --> T
C["Setup & Onboarding<br/>One-time fee"] --> T
D["SMS & Notifications<br/>Usage-based markup"] --> T
E["Payment Processing<br/>We become their payment gateway"] --> T
end
```
---
## Revised Target: Medium-Large Academies
If we can only get 20, we don't go after the 50-player academies. We go after the **150-300 player** ones.
**Average medium academy:** 150 players × 500 EGP = **75,000 EGP/month collections**
```mermaid
graph LR
A[150 Players] --> B[75K Collections/month]
B --> C[Owner profit: ~30K]
C --> D[Can afford: 5-8K for software]
```
---
## Plan B Tier Structure
| Tier | Price | Target | Customers | Revenue |
|------|-------|--------|:---------:|--------:|
| بزنس | 3,999 + 3% | 100-200 players (75K collections) | 12 | 74,988 |
| إنتربرايز | 6,999 + 2% | 300+ players (150K collections) | 5 | 49,995 |
| VIP شريك | 14,999 flat | Clubs/chains (500K+ collections) | 3 | 44,997 |
| **Total** | | | **20** | **169,980** |
---
## Breakdown Per Tier
### بزنس — 3,999 ج.م + 3% of collections
| | |
|---|---|
| **Base fee** | 3,999 ج.م/شهر |
| **Commission** | 3% on payments processed through platform |
| **Trial** | 10 days full access |
| **Target** | Academies with 100-200 players |
| **Their collections** | ~75K/month |
| **Their total cost** | 3,999 + 2,250 = **6,249/month** |
| **As % of revenue** | 8.3% |
| **Setup fee** | 2,000 (one-time) |
| **Includes** | Full platform, 10 users, SMS pack (300/month), onboarding call, data migration |
| **Pitch** | "بندير لك كل التحصيلات. الـ ٣٪ أقل من أي بوابة دفع." |
### إنتربرايز — 6,999 ج.م + 2% of collections
| | |
|---|---|
| **Base fee** | 6,999 ج.م/شهر |
| **Commission** | 2% on payments processed |
| **Trial** | 10 days full access |
| **Target** | Academies with 300+ players |
| **Their collections** | ~150K/month |
| **Their total cost** | 6,999 + 3,000 = **9,999/month** |
| **As % of revenue** | 6.7% |
| **Setup fee** | 5,000 (one-time) |
| **Includes** | Everything + unlimited branches, dedicated account manager, priority support, custom reports, API access |
| **Pitch** | "مدير عمليات رقمي بأقل من ثلث تكلفة الحقيقي." |
### VIP شريك — 14,999 ج.م flat (no %)
| | |
|---|---|
| **Base fee** | 14,999 ج.م/شهر |
| **Commission** | 0% (flat all-inclusive) |
| **Trial** | 10 days full access |
| **Target** | Clubs and academy chains (500+ players, multi-location) |
| **Their collections** | 300K-1M/month |
| **As % of revenue** | 1.5-5% — gets cheaper as they grow |
| **Setup fee** | 10,000 (one-time, includes training) |
| **Includes** | Everything + private instance, SLA, custom development hours (5hrs/month), quarterly business review, team training (up to 20 people) |
| **Pitch** | "شريك تقني كامل. سعر ثابت. بدون مفاجآت." |
---
## Revenue Waterfall (20 Customers)
```mermaid
sankey-beta
بزنس Subscription, Monthly Revenue, 47988
بزنس Commission 3%, Monthly Revenue, 27000
إنتربرايز Subscription, Monthly Revenue, 34995
إنتربرايز Commission 2%, Monthly Revenue, 15000
VIP Flat Fee, Monthly Revenue, 44997
Setup Fees Month 1, Month 1 Bonus, 69000
```
| Stream | Monthly | Notes |
|--------|--------:|-------|
| بزنس × 12 subscriptions | 47,988 | 3,999 × 12 |
| بزنس × 12 commission (3% of 75K) | 27,000 | 2,250 × 12 |
| إنتربرايز × 5 subscriptions | 34,995 | 6,999 × 5 |
| إنتربرايز × 5 commission (2% of 150K) | 15,000 | 3,000 × 5 |
| VIP × 3 flat | 44,997 | 14,999 × 3 |
| **Monthly recurring** | **169,980** | |
| Setup fees (month 1 only) | +69,000 | 12×2K + 5×5K + 3×10K |
| **Month 1 total** | **238,980** | |
---
## Why This Works With Only 20
```mermaid
mindmap
root((20 Customers Model))
Higher ARPU
Target medium/large academies
Not the 50-player micro ones
They have budget and pain
They NEED software not WANT it
Hybrid Revenue
Flat fee = predictable base
Commission = grows with them
Setup fee = cash upfront month 1
SMS = ongoing micro-revenue
Service Layer
"We run your back-office"
Not just software but a partnership
Onboarding call builds loyalty
Account manager reduces churn
Sales Approach
Direct outreach not ads
10 calls/day = 200 in a month
15% close rate = 30 opportunities = 20 deals
WhatsApp demos not landing pages
```
---
## Sales Funnel for 20 Deals
```mermaid
flowchart TD
A["200 Targeted Outreach<br/>(academies with 100+ players)"] --> B["60 Respond<br/>(30% response rate)"]
B --> C["40 Book Demo<br/>(67% demo rate)"]
C --> D["25 Interested<br/>(63% interest)"]
D --> E["20 Close<br/>(80% close rate on interested)"]
style E fill:#059669,color:#fff
```
| Stage | Count | Activity |
|-------|:-----:|----------|
| Week 1 | 200 outreach | WhatsApp/cold call medium-large academies |
| Week 2 | 60 responses | Book 15-min demos |
| Week 3 | 40 demos | Show THEIR data in the system |
| Week 4 | 20 closes | Offer: "setup free if you sign today" |
---
## Comparison: Plan A vs Plan B
```mermaid
graph LR
subgraph PLAN_A["Plan A: Volume Play"]
A1["185 customers"]
A2["399-3,499 ج.م/customer"]
A3["~830 avg ARPU"]
A4["Takes 3-6 months"]
A5["Needs marketing budget"]
end
subgraph PLAN_B["Plan B: 20 Whales"]
B1["20 customers"]
B2["6,249-14,999 ج.م/customer"]
B3["~8,500 avg ARPU"]
B4["Achievable in 1 month"]
B5["Needs direct sales hustle"]
end
PLAN_A -.->|"After stabilizing"| MERGE["Combine Both"]
PLAN_B -.->|"Revenue funds growth"| MERGE
```
| | Plan A (Volume) | Plan B (20 Whales) |
|---|---|---|
| **Customers needed** | 185 | 20 |
| **Avg revenue/customer** | 830 | 8,500 |
| **Time to 150K** | 3-6 months | 1 month |
| **Sales approach** | Inbound + ads + content | Outbound + demos + hustle |
| **Risk** | Slow ramp | High churn if not delivered |
| **Best for** | Long-term stability | Immediate survival |
---
## The Execution Plan (4 Weeks to 150K)
```mermaid
gantt
title Plan B: 4 Weeks to Break-Even
dateFormat YYYY-MM-DD
axisFormat %d %b
section Prep (Days 1-3)
Build target list of 200 medium+ academies :prep1, 2026-07-07, 3d
Prepare demo environment with sample data :prep2, 2026-07-07, 2d
Create WhatsApp pitch templates :prep3, 2026-07-08, 1d
section Outreach (Days 4-14)
Cold outreach batch 1 (100 academies) :out1, 2026-07-10, 5d
Cold outreach batch 2 (100 academies) :out2, 2026-07-15, 5d
Follow-up non-responders :out3, 2026-07-17, 3d
section Demos (Days 10-21)
Run demos (target 40) :demo1, 2026-07-17, 11d
Customize demo per academy size :demo2, 2026-07-17, 11d
section Closing (Days 15-28)
Send proposals :close1, 2026-07-22, 7d
Handle objections + negotiate :close2, 2026-07-24, 5d
Sign contracts + collect setup fees :close3, 2026-07-26, 5d
Onboard first batch :close4, 2026-07-28, 4d
section Revenue
First payments hit :milestone, 2026-07-31, 0d
```
---
## Sweetener: "Sign This Week" Offers
| Incentive | Cost to us | Perceived value |
|-----------|-----------|-----------------|
| Free setup (waive 2-5K fee) | 0 (it's our time) | High |
| First month 50% off | ~3K revenue sacrifice | Gets them in the door |
| Free data migration from Excel | 2 hours work | "They'll do my data entry?!" |
| 3 months commitment = 1 month free | ~8K deferred | Lock-in + cash flow |
| Free SMS pack for 3 months | ~750 cost | Sticky feature |
---
## Risk Mitigation
| Risk | Mitigation |
|------|-----------|
| Can't find 200 medium academies | Target sports clubs, swimming schools, martial arts chains — not just football |
| They say "too expensive" | Show ROI: "you lost 3 players last month from bad follow-up = 1,500 gone. Our fee = 6,249. We save you 4+ players = pays for itself" |
| Churn after month 1 | 3-month minimum commitment with setup fee as anchor |
| Takes longer than 4 weeks | Plan A runs in parallel — trial signups convert later |
| Commission model rejected | Offer flat-only option at higher base: 5,999 instead of 3,999+3% |
---
## Bottom Line
> **Plan B is the survival play. 20 medium-large academies, direct sales, 4 weeks, done.**
>
> Once the 150K/month is secured, THEN layer in Plan A (399/month volume play) for long-term scale.
>
> You don't need 200 customers to survive. You need 20 of the RIGHT customers.
>
> **No free tier. Ever. 10-day trial only. If they won't pay 399/month after seeing the value — they were never going to pay.**
# WhatsApp Business API — Full Setup Tutorial
## Overview
El-Captain uses the **Meta Cloud API** (free hosting, pay per conversation) for all customer notifications. No SMS. No third-party BSPs.
**End result:** Your Laravel app sends templated WhatsApp messages to parents/admins via a simple HTTP POST.
---
## Prerequisites
- Facebook personal account (yours)
- A new SIM card (dedicated number for API — can't use WhatsApp app on it after)
- Egyptian commercial registration or tax ID (for business verification)
- Credit card or PayPal for Meta billing (pay-as-you-go, no minimum)
---
## Step 1: Create Meta Business Account
1. Go to https://business.facebook.com
2. Click "Create Account"
3. Fill in:
- Business name: `El-Captain Sports Management` (or the academy's name if per-customer)
- Your name
- Business email
4. Confirm email
5. You now have a Business Manager ID
---
## Step 2: Create a Meta App
1. Go to https://developers.facebook.com
2. Click "My Apps" → "Create App"
3. Select **"Other"** use case
4. Select **"Business"** app type
5. App name: `El-Captain WhatsApp`
6. Connect to your Business Account from Step 1
7. On the app dashboard, click **"Add Product"** → find **WhatsApp** → click **"Set Up"**
---
## Step 3: Add Phone Number
1. In the WhatsApp section of your app, go to **"Getting Started"**
2. Meta gives you a **test number** — good for development (sends to 5 verified numbers only)
3. For production: click **"Add Phone Number"**
4. Enter your dedicated number (the new SIM)
5. Verify via SMS or voice call to that SIM
6. Choose a display name (this is what recipients see): e.g. `El-Captain` or the academy name
7. Display name goes through Meta review (24-48h typically)
**Important:**
- Once registered, that number is locked to the API. The WhatsApp app will stop working on it.
- Use a clean number with no prior WhatsApp history for fastest approval.
- The display name must match your business or be clearly associated with it.
---
## Step 4: Business Verification
Meta requires business verification before you can send messages to non-test numbers.
1. Go to Business Manager → **Settings****Security Center****Start Verification**
2. Upload ONE of:
- Commercial Registration (السجل التجاري)
- Tax Card (البطاقة الضريبية)
- Utility bill with business name + address
3. Enter business details exactly as they appear on the document
4. Meta reviews in 2-5 business days
5. Status shows "Verified" when approved
**If rejected:** Usually a name mismatch. Business name in Meta must match the document exactly (including Arabic/English).
---
## Step 5: Generate Permanent Access Token
The "Getting Started" page gives you a temporary token (expires in 24h). For production:
1. Go to Business Manager → **Settings****Users****System Users**
2. Click "Add" → name it `whatsapp-api` → set role to **Admin**
3. Click on the system user → **"Generate Token"**
4. Select your WhatsApp app
5. Check these permissions:
- `whatsapp_business_management`
- `whatsapp_business_messaging`
6. Click "Generate Token"
7. **Copy the token immediately** — it won't show again
Store in your `.env`:
```
WHATSAPP_TOKEN=your_permanent_token_here
WHATSAPP_PHONE_ID=your_phone_number_id
WHATSAPP_BUSINESS_ID=your_business_id
```
---
## Step 6: Create Message Templates
Templates are pre-approved message formats. Meta must approve each one before you can use it.
1. In the WhatsApp section → **"Message Templates"**
2. Click **"Create Template"**
3. Category: **Utility** (for transactional — cheaper than Marketing)
4. Name: `attendance_notification` (lowercase, underscores only)
5. Language: **Arabic**
6. Body:
### Template: attendance_notification
```
مرحباً، ابنكم {{1}} سجّل {{2}} في حصة {{3}} اليوم.
```
Variables: child name, status (حاضر/غايب/متأخر), group name
### Template: payment_reminder
```
تذكير: مبلغ {{1}} ج.م مستحق على حساب {{2}}.
آخر موعد للسداد: {{3}}.
للاستفسار تواصل معنا.
```
Variables: amount, child name, due date
### Template: enrollment_confirmed
```
تم تسجيل {{1}} في برنامج {{2}} بنجاح.
تاريخ البداية: {{3}}.
نتمنى له تجربة ممتعة!
```
Variables: child name, program name, start date
### Template: schedule_change
```
تنبيه: تم تغيير موعد حصة {{1}} من {{2}} إلى {{3}}.
نعتذر عن أي إزعاج.
```
Variables: group name, old time, new time
### Template: absence_alert
```
تنبيه: ابنكم {{1}} غاب عن حصة {{2}} اليوم {{3}}.
في حالة وجود عذر يرجى التواصل مع الإدارة.
```
Variables: child name, group name, date
### Template: payment_received
```
تم استلام مبلغ {{1}} ج.م من حساب {{2}} بنجاح.
رقم الإيصال: {{3}}
الرصيد المتبقي: {{4}} ج.م
شكراً لكم.
```
Variables: amount, child name, receipt number, remaining balance
---
**Submit each template.** Meta reviews within 24 hours usually. Status:
- **Approved** → ready to use
- **Rejected** → reason given, edit and resubmit
- **Pending** → still in review
**Common rejection reasons:**
- Template contains a URL (not allowed in utility templates without URL button)
- Placeholder text is too vague
- Missing example values in submission
**Tip:** When submitting, provide example values for each `{{N}}` — Meta uses these to understand context.
---
## Step 7: Set Up Webhook (Receive Messages + Delivery Status)
1. In your app → WhatsApp → **Configuration**
2. Callback URL: `https://your-app.caprover.al-arcade.com/api/whatsapp/webhook`
3. Verify Token: generate a random string, store it in `.env` as `WHATSAPP_VERIFY_TOKEN`
4. Subscribe to:
- `messages` (incoming messages from parents)
- `message_status` (sent/delivered/read receipts)
### Laravel webhook route:
```php
// routes/api.php
Route::get('whatsapp/webhook', [WhatsAppWebhookController::class, 'verify']);
Route::post('whatsapp/webhook', [WhatsAppWebhookController::class, 'handle']);
```
### Verification (GET request from Meta):
```php
public function verify(Request $request)
{
$mode = $request->query('hub_mode');
$token = $request->query('hub_verify_token');
$challenge = $request->query('hub_challenge');
if ($mode === 'subscribe' && $token === config('services.whatsapp.verify_token')) {
return response($challenge, 200);
}
return response('Forbidden', 403);
}
```
### Handling incoming (POST):
```php
public function handle(Request $request)
{
$payload = $request->all();
// Message status updates (sent/delivered/read)
$statuses = data_get($payload, 'entry.0.changes.0.value.statuses', []);
foreach ($statuses as $status) {
NotificationLog::where('external_id', $status['id'])
->update(['delivery_status' => $status['status']]);
}
// Incoming messages from parents
$messages = data_get($payload, 'entry.0.changes.0.value.messages', []);
foreach ($messages as $message) {
// Store or route to support queue
}
return response('OK', 200);
}
```
---
## Step 8: Send Messages from Laravel
### Service Class:
```php
namespace App\Domain\Notification\Services;
use Illuminate\Support\Facades\Http;
class WhatsAppService
{
private string $baseUrl;
private string $token;
public function __construct()
{
$phoneId = config('services.whatsapp.phone_id');
$this->baseUrl = "https://graph.facebook.com/v21.0/{$phoneId}/messages";
$this->token = config('services.whatsapp.token');
}
public function sendTemplate(
string $to,
string $templateName,
array $parameters = [],
string $language = 'ar'
): ?string {
$components = [];
if (!empty($parameters)) {
$components[] = [
'type' => 'body',
'parameters' => array_map(
fn($value) => ['type' => 'text', 'text' => (string) $value],
$parameters
),
];
}
$response = Http::withToken($this->token)
->post($this->baseUrl, [
'messaging_product' => 'whatsapp',
'to' => $this->formatNumber($to),
'type' => 'template',
'template' => [
'name' => $templateName,
'language' => ['code' => $language],
'components' => $components,
],
]);
if ($response->successful()) {
return $response->json('messages.0.id');
}
logger()->error('WhatsApp send failed', [
'to' => $to,
'template' => $templateName,
'error' => $response->json(),
]);
return null;
}
private function formatNumber(string $phone): string
{
$phone = preg_replace('/[\s\-()]/', '', $phone);
// 01012345678 → 201012345678
if (str_starts_with($phone, '0')) {
$phone = '20' . substr($phone, 1);
}
// +201012345678 → 201012345678
$phone = ltrim($phone, '+');
return $phone;
}
}
```
### Config:
```php
// config/services.php
'whatsapp' => [
'token' => env('WHATSAPP_TOKEN'),
'phone_id' => env('WHATSAPP_PHONE_ID'),
'business_id' => env('WHATSAPP_BUSINESS_ID'),
'verify_token' => env('WHATSAPP_VERIFY_TOKEN'),
],
```
### Usage (from a Listener):
```php
class SendAttendanceNotification implements ShouldQueue
{
public function handle(AttendanceMarked $event): void
{
$record = $event->attendanceRecord;
$participant = $record->participant;
$guardian = $participant->primaryGuardian;
if (!$guardian || !$guardian->phone) {
return;
}
$messageId = app(WhatsAppService::class)->sendTemplate(
to: $guardian->phone,
templateName: 'attendance_notification',
parameters: [
$participant->name_ar,
$record->status->label(),
$record->session->group->name_ar,
]
);
NotificationLog::create([
'academy_id' => $participant->academy_id,
'channel' => 'whatsapp',
'recipient_phone' => $guardian->phone,
'template_name' => 'attendance_notification',
'external_id' => $messageId,
'delivery_status' => $messageId ? 'sent' : 'failed',
]);
}
}
```
---
## Step 9: Go Live Checklist
- [ ] Business verified (green checkmark in Business Manager)
- [ ] Phone number approved + display name live
- [ ] Permanent System User token generated
- [ ] At least 3 templates approved (attendance, payment_reminder, enrollment)
- [ ] Webhook URL registered and responding 200
- [ ] `.env` variables set on production (CapRover env vars)
- [ ] Test message sent and received successfully
- [ ] NotificationLog recording delivery statuses
- [ ] Rate limit handling (queue with throttle)
---
## Smart Notification Routing (CRITICAL for Cost Control)
**The #1 mistake:** WhatsApping "ابنك حضر" every session day. At scale this DESTROYS margins.
### Routing Rules
| Event | Channel | Logic |
|---|---|---|
| Attendance: PRESENT | In-app + push only | Routine — no WhatsApp needed |
| Attendance: ABSENT | WhatsApp | Urgent — parent must know |
| Attendance: LATE | WhatsApp | Parent needs to know |
| Payment due (3 days before) | WhatsApp | Drives collection |
| Payment overdue | WhatsApp | Urgent follow-up |
| Enrollment confirmed | WhatsApp | One-time confirmation |
| Schedule change | WhatsApp | Affects their plans |
| Session cancelled | WhatsApp | Immediate impact |
| Daily digest (opt-in) | WhatsApp | ONE message per parent/day MAX |
| General updates | In-app only | Not worth a conversation |
### Implementation in Laravel
```php
// In AttendanceMarkedListener — route by status
public function handle(AttendanceMarked $event): void
{
$record = $event->attendanceRecord;
// Only WhatsApp for PROBLEMS — not routine presence
if (!in_array($record->status->value, ['absent', 'late', 'no_show'])) {
// In-app notification only
$this->createInAppNotification($record);
return;
}
// Absence/Late → WhatsApp
$this->sendWhatsAppAlert($record);
}
```
### Why This Works (Math)
A 300-player academy has ~220 unique parents. Average absence rate is 15%.
**Naive (every attendance):** 220 parents × 14 session days = 3,080 conversations = 3,388 EGP (38% of revenue) ❌
**Smart (absence-only):** 33 absences/day × 24 days + payments + other = 850 conversations = 935 EGP (10% of revenue) ✓
### Premium Add-on: Daily Summary
Some academies WANT "ابنك حضر" every day. Offer it as a toggle:
- Default: OFF (absence-only — free tier)
- Premium: ON (+200 EGP/month for academy, covers the extra conversations)
- Implementation: End-of-day cron batches all attendance into ONE WhatsApp per parent
---
## Rate Limits & Scaling
| Tier | Daily conversation limit | How to unlock |
|------|---:|---|
| Unverified | 250 | Default before business verification |
| Tier 1 | 1,000 | After business verification |
| Tier 2 | 10,000 | Send 1,000+ in 7 days with good quality |
| Tier 3 | 100,000 | Send 10,000+ in 7 days with good quality |
| Tier 4 | Unlimited | Send 100,000+ with good quality |
**Quality rating** = low block/report rate. Keep templates relevant and expected (absence-only strategy helps quality — parents never feel spammed).
### Platform-Wide Rate Limit Planning
| Platform size | Daily convos (peak) | Required Tier |
|---|---:|---|
| 36 customers (Scenario B) | ~200 | Tier 1 ✓ |
| 100 customers | ~600 | Tier 1 ✓ |
| 500 customers | ~3,000 | Tier 2 |
| 1000+ customers | ~6,000 | Tier 2 ✓ |
**Tier 1 (1,000/day) covers the platform until ~150 customers.** Auto-upgrades with sustained volume.
---
## Cost Summary (Smart Routing — Absence-Only)
| Customers | Avg convos/customer/month | Total convos | Monthly cost (EGP) |
|---:|---:|---:|---:|
| 10 (all small) | 63 | 630 | 693 |
| 36 (mixed) | 75 | 2,700 | 2,970 |
| 100 (mixed) | 80 | 8,000 | 8,800 |
### Cost Per Academy By Size (Smart Routing)
| Academy size | Convos/month | Cost (EGP) | Revenue | % of Revenue |
|---:|---:|---:|---:|---:|
| 50 players (Small) | 63 | 69 | 2,449 | 3% |
| 200 players (Medium) | 450 | 495 | 6,499 | 8% |
| 300 players | 850 | 935 | 8,999 | 10% |
| 500 players | 1,430 | 1,573 | 12,499 | 13% |
| 1000 players (Large) | 2,850 | 3,135 | 18,499 | 17% |
Formula: `conversations × 1.10 EGP` (utility rate). All sustainable.
---
## Billing Setup
1. Go to Business Manager → **Billing****Payment Settings**
2. Add credit card or PayPal
3. Set billing threshold (or monthly auto-charge)
4. Invoices come from Meta monthly
---
## Troubleshooting
| Problem | Solution |
|---|---|
| "Number not registered" error | Number must have WhatsApp app installed first, THEN register via API (it takes over) |
| Template rejected | Check: no URLs in body (use URL buttons instead), provide example values, category matches content |
| Messages not delivered | Check recipient has WhatsApp + internet. Check your quality rating isn't "Red" |
| Webhook not receiving | Verify HTTPS, valid SSL, responding 200 within 5 seconds |
| Token expired | System User tokens don't expire. If using temporary token, switch to System User. |
| Rate limited | Queue messages with delay. Upgrade tier by sustained volume. |
| "Re-engagement" error | Can't initiate conversation after 24h without a new template. That's expected — use template. |
---
## Multi-Tenant Considerations
For El-Captain (multi-tenant SaaS):
**Option A: Single number for all academies (SIMPLER)**
- One WhatsApp number: "El-Captain"
- Templates include academy name: "أكاديمية {{academy}} — ابنكم {{child}}..."
- Cheaper (one setup)
- Parents see "El-Captain" as sender
**Option B: Per-academy numbers (PREMIUM)**
- Each academy gets their own number + display name
- Sell as premium feature (setup fee: 500 EGP)
- Requires separate Meta Business verification per academy (complex)
- Better UX — parent sees "أكاديمية النجم" not "El-Captain"
**Recommendation:** Start with Option A. Move to Option B as a paid addon for Large tier.
---
## Environment Variables (.env)
```
WHATSAPP_TOKEN=EAAxxxxxxxxxxxxxxxxxxxxxxxx
WHATSAPP_PHONE_ID=1234567890123456
WHATSAPP_BUSINESS_ID=9876543210987654
WHATSAPP_VERIFY_TOKEN=random_string_for_webhook_verification
WHATSAPP_API_VERSION=v21.0
```
captainVersion: 4
services:
# PostgreSQL Database
$$cap_appname-db:
image: postgres:16-alpine
volumes:
- $$cap_appname-db-data:/var/lib/postgresql/data
restart: always
environment:
POSTGRES_DB: $$cap_pg_db
POSTGRES_USER: $$cap_pg_user
POSTGRES_PASSWORD: $$cap_pg_password
caproverExtra:
notExposeAsWebApp: 'true'
# El Captain App (Laravel + Nginx + Queue + Scheduler)
$$cap_appname:
depends_on:
- $$cap_appname-db
restart: always
environment:
APP_NAME: $$cap_academy_name_ar
APP_ENV: production
APP_KEY: $$cap_app_key
APP_DEBUG: 'false'
APP_URL: http://$$cap_appname.$$cap_root_domain
APP_LOCALE: ar
APP_FALLBACK_LOCALE: en
APP_FAKER_LOCALE: ar_EG
APP_MAINTENANCE_DRIVER: file
BCRYPT_ROUNDS: '12'
LOG_CHANNEL: stack
LOG_STACK: single
LOG_LEVEL: error
DB_CONNECTION: pgsql
DB_HOST: srv-captain--$$cap_appname-db
DB_PORT: '5432'
DB_DATABASE: $$cap_pg_db
DB_USERNAME: $$cap_pg_user
DB_PASSWORD: $$cap_pg_password
SESSION_DRIVER: database
SESSION_LIFETIME: '120'
BROADCAST_CONNECTION: log
FILESYSTEM_DISK: local
QUEUE_CONNECTION: database
CACHE_STORE: database
MAIL_MAILER: $$cap_mail_mailer
MAIL_HOST: $$cap_mail_host
MAIL_PORT: $$cap_mail_port
MAIL_USERNAME: $$cap_mail_username
MAIL_PASSWORD: $$cap_mail_password
MAIL_ENCRYPTION: $$cap_mail_encryption
MAIL_FROM_ADDRESS: $$cap_mail_from_address
MAIL_FROM_NAME: $$cap_academy_name_ar
MAIL_VERIFY_PEER: 'false'
TRUSTED_PROXIES: '*'
PLATFORM_SERVICE_FEE_PERCENT: $$cap_platform_fee
ADMIN_EMAIL: $$cap_admin_email
ADMIN_PASSWORD: $$cap_admin_password
ADMIN_NAME: $$cap_admin_name
ACADEMY_NAME_AR: $$cap_academy_name_ar
ACADEMY_NAME_EN: $$cap_academy_name_en
RUN_SEED_ON_FIRST_DEPLOY: 'true'
volumes:
- $$cap_appname-storage:/var/www/html/storage/app
caproverExtra:
containerHttpPort: '80'
caproverOneClickApp:
variables:
- id: $$cap_app_version
label: App Version
defaultValue: latest
description: 'Docker image tag. Use "latest" for most recent stable.'
validRegex: /^([^\s^\/])+$/
- id: $$cap_academy_name_ar
label: اسم الأكاديمية (عربي)
defaultValue: أكاديمية الكابتن
description: 'اسم الأكاديمية بالعربي يظهر في الفواتير والإيصالات'
- id: $$cap_academy_name_en
label: Academy Name (English)
defaultValue: El Captain Academy
description: 'English academy name.'
- id: $$cap_admin_email
label: Admin Email
defaultValue: admin@example.com
description: 'Login email for the first admin user.'
validRegex: /^[^@]+@[^@]+\.[^@]+$/
- id: $$cap_admin_password
label: Admin Password
defaultValue: $$cap_gen_random_hex(12)
description: 'Password for admin login (min 8 chars).'
validRegex: /.{8,}/
- id: $$cap_admin_name
label: Admin Name
defaultValue: مدير النظام
description: 'Full name of the admin user.'
- id: $$cap_app_key
label: App Key (Laravel)
defaultValue: ''
description: 'Leave empty to auto-generate on first boot.'
- id: $$cap_pg_db
label: Database Name
defaultValue: elcaptain
validRegex: /^[a-zA-Z][a-zA-Z0-9_]*$/
- id: $$cap_pg_user
label: Database User
defaultValue: elcaptain
validRegex: /^[a-zA-Z][a-zA-Z0-9_]*$/
- id: $$cap_pg_password
label: Database Password
defaultValue: $$cap_gen_random_hex(16)
description: 'Strong password for PostgreSQL.'
validRegex: /.{8,}/
- id: $$cap_platform_fee
label: Platform Fee %
defaultValue: '0'
description: 'Service fee % added to receipts (0 = disabled). Revenue for the platform provider.'
- id: $$cap_mail_mailer
label: Mail Driver
defaultValue: log
description: 'smtp, log, mailgun, ses. Use "log" to skip email setup.'
- id: $$cap_mail_host
label: SMTP Host
defaultValue: ''
description: 'Leave empty if using "log" driver.'
- id: $$cap_mail_port
label: SMTP Port
defaultValue: '587'
- id: $$cap_mail_username
label: SMTP Username
defaultValue: ''
- id: $$cap_mail_password
label: SMTP Password
defaultValue: ''
- id: $$cap_mail_encryption
label: SMTP Encryption
defaultValue: tls
description: 'tls, ssl, or empty.'
- id: $$cap_mail_from_address
label: From Email
defaultValue: noreply@example.com
description: 'Sender address for outgoing emails.'
instructions:
start: |-
## نظام الكابتن لإدارة الأكاديميات الرياضية
El Captain Sports Management — نظام ERP متكامل للأكاديميات الرياضية
**سيتم إنشاء:**
1. قاعدة بيانات PostgreSQL 16 (خاصة، غير معروضة للويب)
2. تطبيق الكابتن (Laravel + Nginx + Queue Worker + Scheduler)
يتم تشغيل الميغرشن والإعداد الأولي تلقائياً عند أول تشغيل.
---
**المميزات:** فواتير، حضور وغياب، نقطة بيع، مخزون، جدولة، مجموعات، مدفوعات، محفظة، تقارير، إشعارات
end: |-
## ✅ تم التثبيت بنجاح!
**رابط التطبيق:** http://$$cap_appname.$$cap_root_domain
**بيانات الدخول:**
- الإيميل: `$$cap_admin_email`
- كلمة المرور: `$$cap_admin_password`
---
⚠️ **مهم:** قم بتفعيل HTTPS من لوحة CapRover ثم سجل الدخول.
أول تشغيل يستغرق 2-3 دقائق (إنشاء الجداول والبيانات الأساسية).
displayName: El Captain Sports Management
isOfficial: true
description: نظام إدارة أكاديميات رياضية متكامل — فواتير، حضور، نقطة بيع، مخزون، جدولة، مجموعات، مدفوعات
documentation: https://github.com/mahmoudaglan/el-captain-sports-management
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6"> <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
<h1 class="text-2xl font-bold text-gray-900">{{ __('المدربين') }}</h1> <h1 class="text-2xl font-bold text-gray-900">{{ __('المدربين') }}</h1>
@can('trainers.create') @can('trainers.create')
<a href="{{ route('trainers.create') }}" wire:navigate <a href="{{ route('trainers.wizard') }}" wire:navigate
class="inline-flex items-center gap-2 px-4 py-2.5 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors"> class="inline-flex items-center gap-2 px-4 py-2.5 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
{{ __('إضافة مدرب') }} {{ __('إضافة مدرب') }}
......
...@@ -81,7 +81,7 @@ class="transition-all duration-700"/> ...@@ -81,7 +81,7 @@ class="transition-all duration-700"/>
<h3 class="text-base font-semibold text-[#0F172A] mb-3">{{ __('سجل الحضور') }}</h3> <h3 class="text-base font-semibold text-[#0F172A] mb-3">{{ __('سجل الحضور') }}</h3>
<div wire:loading.class="opacity-50 pointer-events-none"> <div wire:loading.class="opacity-50 pointer-events-none">
@if(empty($attendanceRecords)) @if($records->isEmpty())
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-8 text-center"> <div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-8 text-center">
<svg class="w-10 h-10 text-gray-200 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-10 h-10 text-gray-200 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
...@@ -102,13 +102,13 @@ class="transition-all duration-700"/> ...@@ -102,13 +102,13 @@ class="transition-all duration-700"/>
]; ];
@endphp @endphp
@foreach($attendanceRecords as $record) @foreach($records as $record)
@php $config = $statusConfig[$record['status'] ?? ''] ?? ['label' => $record['status'] ?? '', 'bg' => 'bg-gray-100', 'text' => 'text-gray-700']; @endphp @php $config = $statusConfig[$record->status ?? ''] ?? ['label' => $record->status ?? '', 'bg' => 'bg-gray-100', 'text' => 'text-gray-700']; @endphp
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-4"> <div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-sm font-medium text-[#0F172A]">{{ $record['session'] ?? '' }}</p> <p class="text-sm font-medium text-[#0F172A]">{{ $record->session?->group?->program?->name_ar ?? $record->session?->group?->name_ar ?? '' }}</p>
<p class="text-xs text-[#64748B] mt-0.5" dir="ltr">{{ $record['date'] ?? '' }} - {{ $record['time'] ?? '' }}</p> <p class="text-xs text-[#64748B] mt-0.5" dir="ltr">{{ $record->session?->session_date ?? '' }} - {{ $record->session?->start_time ?? '' }}</p>
</div> </div>
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium {{ $config['bg'] }} {{ $config['text'] }}"> <span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium {{ $config['bg'] }} {{ $config['text'] }}">
{{ __($config['label']) }} {{ __($config['label']) }}
...@@ -117,6 +117,10 @@ class="transition-all duration-700"/> ...@@ -117,6 +117,10 @@ class="transition-all duration-700"/>
</div> </div>
@endforeach @endforeach
</div> </div>
<div class="mt-4">
{{ $records->links() }}
</div>
@endif @endif
</div> </div>
</div> </div>
......
<div> <div>
{{-- Hero Card: Outstanding Balance --}} {{-- Hero Card: Outstanding Balance --}}
@php @php
$balanceColor = ($outstandingBalance ?? 0) > 0 $balanceColor = ($totalOutstanding ?? 0) > 0
? (($hasOverdue ?? false) ? 'from-red-500 to-red-600' : 'from-amber-500 to-amber-600') ? (($overdueCount ?? 0) > 0 ? 'from-red-500 to-red-600' : 'from-amber-500 to-amber-600')
: 'from-green-500 to-green-600'; : 'from-green-500 to-green-600';
$balanceTextColor = 'text-white';
@endphp @endphp
<div class="bg-gradient-to-br {{ $balanceColor }} rounded-2xl shadow-md p-6 mb-4 text-white"> <div class="bg-gradient-to-br {{ $balanceColor }} rounded-2xl shadow-md p-6 mb-4 text-white">
<p class="text-sm opacity-90">{{ __('الرصيد المستحق') }}</p> <p class="text-sm opacity-90">{{ __('الرصيد المستحق') }}</p>
<p class="text-3xl font-bold mt-1" dir="ltr"> <p class="text-3xl font-bold mt-1" dir="ltr">
{{ number_format(($outstandingBalance ?? 0) / 100, 2) }} {{ number_format(($totalOutstanding ?? 0) / 100, 2) }}
<span class="text-base font-normal opacity-80">{{ __('ج.م') }}</span> <span class="text-base font-normal opacity-80">{{ __('ج.م') }}</span>
</p> </p>
@if(($outstandingBalance ?? 0) > 0 && ($hasOverdue ?? false)) @if(($totalOutstanding ?? 0) > 0 && ($overdueCount ?? 0) > 0)
<p class="text-xs mt-2 opacity-90"> <p class="text-xs mt-2 opacity-90">
<svg class="w-3.5 h-3.5 inline-block me-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-3.5 h-3.5 inline-block me-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg> </svg>
{{ __('يوجد مبالغ متأخرة عن موعد السداد') }} {{ __('يوجد مبالغ متأخرة عن موعد السداد') }}
</p> </p>
@elseif(($outstandingBalance ?? 0) == 0) @elseif(($totalOutstanding ?? 0) == 0)
<p class="text-xs mt-2 opacity-90">{{ __('لا يوجد مبالغ مستحقة - ممتاز!') }}</p> <p class="text-xs mt-2 opacity-90">{{ __('لا يوجد مبالغ مستحقة - ممتاز!') }}</p>
@endif @endif
</div> </div>
...@@ -28,14 +27,13 @@ ...@@ -28,14 +27,13 @@
<div class="grid grid-cols-2 gap-3 mb-6"> <div class="grid grid-cols-2 gap-3 mb-6">
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-4"> <div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-4">
<div class="flex items-center gap-2 mb-1"> <div class="flex items-center gap-2 mb-1">
<svg class="w-4 h-4 text-[#2563EB]" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 text-[#DC2626]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg> </svg>
<span class="text-xs text-[#64748B]">{{ __('رصيد المحفظة') }}</span> <span class="text-xs text-[#64748B]">{{ __('فواتير متأخرة') }}</span>
</div> </div>
<p class="text-lg font-bold text-[#0F172A]" dir="ltr"> <p class="text-lg font-bold text-[#DC2626]" dir="ltr">
{{ number_format(($walletBalance ?? 0) / 100, 2) }} {{ $overdueCount ?? 0 }}
<span class="text-xs font-normal text-[#64748B]">{{ __('ج.م') }}</span>
</p> </p>
</div> </div>
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-4"> <div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-4">
...@@ -43,10 +41,10 @@ ...@@ -43,10 +41,10 @@
<svg class="w-4 h-4 text-[#059669]" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 text-[#059669]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg> </svg>
<span class="text-xs text-[#64748B]">{{ __('مدفوع هذا الشهر') }}</span> <span class="text-xs text-[#64748B]">{{ __('إجمالي المدفوع') }}</span>
</div> </div>
<p class="text-lg font-bold text-[#059669]" dir="ltr"> <p class="text-lg font-bold text-[#059669]" dir="ltr">
{{ number_format(($paidThisMonth ?? 0) / 100, 2) }} {{ number_format(($totalPaid ?? 0) / 100, 2) }}
<span class="text-xs font-normal text-[#64748B]">{{ __('ج.م') }}</span> <span class="text-xs font-normal text-[#64748B]">{{ __('ج.م') }}</span>
</p> </p>
</div> </div>
...@@ -56,17 +54,17 @@ ...@@ -56,17 +54,17 @@
<div class="flex gap-2 overflow-x-auto pb-2 mb-4 scrollbar-hide"> <div class="flex gap-2 overflow-x-auto pb-2 mb-4 scrollbar-hide">
@php @php
$filters = [ $filters = [
'all' => 'الكل', '' => 'الكل',
'pending' => 'مستحقة', 'sent' => 'مستحقة',
'paid' => 'مدفوعة', 'paid' => 'مدفوعة',
'overdue' => 'متأخرة', 'overdue' => 'متأخرة',
]; ];
@endphp @endphp
@foreach($filters as $key => $label) @foreach($filters as $key => $label)
<button <button
wire:click="$set('filter', '{{ $key }}')" wire:click="$set('statusFilter', '{{ $key }}')"
class="px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-all min-h-[44px] class="px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-all min-h-[44px]
{{ ($filter ?? 'all') === $key {{ $statusFilter === $key
? 'bg-[#2563EB] text-white' ? 'bg-[#2563EB] text-white'
: 'bg-white text-[#64748B] border border-gray-200 hover:border-[#2563EB] hover:text-[#2563EB]' }}" : 'bg-white text-[#64748B] border border-gray-200 hover:border-[#2563EB] hover:text-[#2563EB]' }}"
> >
...@@ -77,7 +75,7 @@ class="px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-a ...@@ -77,7 +75,7 @@ class="px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-a
{{-- Invoice List --}} {{-- Invoice List --}}
<div wire:loading.class="opacity-50 pointer-events-none"> <div wire:loading.class="opacity-50 pointer-events-none">
@if(empty($invoices) || (is_countable($invoices) && count($invoices) === 0)) @if($invoices->isEmpty())
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-8 text-center"> <div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-8 text-center">
<svg class="w-12 h-12 text-gray-200 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-12 h-12 text-gray-200 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 14l6-6m-5.5.5h.01m4.99 5h.01M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16l3.5-2 3.5 2 3.5-2 3.5 2z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 14l6-6m-5.5.5h.01m4.99 5h.01M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16l3.5-2 3.5 2 3.5-2 3.5 2z"/>
...@@ -100,13 +98,16 @@ class="px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-a ...@@ -100,13 +98,16 @@ class="px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-a
@endphp @endphp
@foreach($invoices as $invoice) @foreach($invoices as $invoice)
@php $statusConf = $invoiceStatusConfig[$invoice['status'] ?? ''] ?? ['label' => $invoice['status'] ?? '', 'bg' => 'bg-gray-100', 'text' => 'text-gray-700']; @endphp @php
<a href="{{ route('parent.invoice-detail', ['invoice' => $invoice['id'] ?? '']) }}" wire:navigate $invoiceStatus = $invoice->status->value ?? $invoice->status ?? '';
$statusConf = $invoiceStatusConfig[$invoiceStatus] ?? ['label' => $invoiceStatus, 'bg' => 'bg-gray-100', 'text' => 'text-gray-700'];
@endphp
<a href="{{ route('parent.invoice-detail', ['invoice' => $invoice->uuid]) }}" wire:navigate
class="block bg-white rounded-2xl shadow-sm border border-gray-100 p-4 hover:shadow-md transition-shadow"> class="block bg-white rounded-2xl shadow-sm border border-gray-100 p-4 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between"> <div class="flex items-start justify-between">
<div> <div>
<p class="text-sm font-semibold text-[#0F172A]" dir="ltr">{{ $invoice['number'] ?? '' }}</p> <p class="text-sm font-semibold text-[#0F172A]" dir="ltr">{{ $invoice->invoice_number ?? '' }}</p>
<p class="text-xs text-[#64748B] mt-0.5">{{ $invoice['date'] ?? '' }}</p> <p class="text-xs text-[#64748B] mt-0.5">{{ $invoice->issue_date?->format('Y-m-d') ?? '' }}</p>
</div> </div>
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-[10px] font-medium {{ $statusConf['bg'] }} {{ $statusConf['text'] }}"> <span class="inline-flex items-center px-2.5 py-1 rounded-full text-[10px] font-medium {{ $statusConf['bg'] }} {{ $statusConf['text'] }}">
{{ __($statusConf['label']) }} {{ __($statusConf['label']) }}
...@@ -114,17 +115,21 @@ class="block bg-white rounded-2xl shadow-sm border border-gray-100 p-4 hover:sha ...@@ -114,17 +115,21 @@ class="block bg-white rounded-2xl shadow-sm border border-gray-100 p-4 hover:sha
</div> </div>
<div class="flex items-center justify-between mt-3 pt-3 border-t border-gray-50"> <div class="flex items-center justify-between mt-3 pt-3 border-t border-gray-50">
<span class="text-sm font-bold text-[#0F172A]" dir="ltr"> <span class="text-sm font-bold text-[#0F172A]" dir="ltr">
{{ number_format(($invoice['total'] ?? 0) / 100, 2) }} {{ __('ج.م') }} {{ number_format(($invoice->total_amount ?? 0) / 100, 2) }} {{ __('ج.م') }}
</span> </span>
@if(($invoice['balance_due'] ?? 0) > 0) @if(($invoice->due_amount ?? 0) > 0)
<span class="text-xs text-[#DC2626] font-medium" dir="ltr"> <span class="text-xs text-[#DC2626] font-medium" dir="ltr">
{{ __('متبقي:') }} {{ number_format(($invoice['balance_due'] ?? 0) / 100, 2) }} {{ __('ج.م') }} {{ __('متبقي:') }} {{ number_format($invoice->due_amount / 100, 2) }} {{ __('ج.م') }}
</span> </span>
@endif @endif
</div> </div>
</a> </a>
@endforeach @endforeach
</div> </div>
<div class="mt-4">
{{ $invoices->links() }}
</div>
@endif @endif
</div> </div>
</div> </div>
...@@ -46,6 +46,17 @@ class="text-sm text-gray-600 hover:text-gray-800">{{ __('← العودة للق ...@@ -46,6 +46,17 @@ class="text-sm text-gray-600 hover:text-gray-800">{{ __('← العودة للق
</select> </select>
@error('branch_id') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror @error('branch_id') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div> </div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('المدرب الافتراضي') }}</label>
<select wire:model="default_trainer_id" class="w-full px-4 py-2.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="">{{ __('بدون مدرب افتراضي') }}</option>
@foreach($trainers as $trainer)
<option value="{{ $trainer->id }}">{{ $trainer->name }}</option>
@endforeach
</select>
@error('default_trainer_id') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
<p class="text-xs text-gray-500 mt-1">{{ __('يُعيّن تلقائياً للمجموعات الجديدة') }}</p>
</div>
<div class="sm:col-span-2"> <div class="sm:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('الوصف بالعربية') }}</label> <label class="block text-sm font-medium text-gray-700 mb-1">{{ __('الوصف بالعربية') }}</label>
<textarea wire:model="description_ar" rows="3" class="w-full px-4 py-2.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"></textarea> <textarea wire:model="description_ar" rows="3" class="w-full px-4 py-2.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"></textarea>
......
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