Commit 59719212 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add retroactive enrollment wizard for backdating participant registrations

New wizard at /receptionist/retroactive-enrollment lets staff enroll
participants who started in previous months. Flow: select participant →
choose program → set actual start date → mark each month as unpaid or
paid-outside-system → optionally pay all outstanding now. Creates
individual invoices per month with proper metadata. Supports admin price
override for the total. Accessible from receptionist dashboard with
enrollments.create permission.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent c15edf88
<?php
namespace App\Livewire\Receptionist;
use App\Domain\Financial\Enums\InvoiceStatus;
use App\Domain\Financial\Models\Invoice;
use App\Domain\Financial\Services\InvoiceService;
use App\Domain\Financial\Services\PaymentService;
use App\Domain\Identity\Models\Branch;
use App\Domain\Participant\Models\Participant;
use App\Domain\Pricing\Services\PricingService;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Models\Activity;
use App\Domain\Training\Models\TrainingProgram;
use App\Domain\Training\Services\EnrollmentService;
use Carbon\Carbon;
use Carbon\CarbonPeriod;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('تسجيل لاعب سابق')]
class RetroactiveEnrollmentWizard extends Component
{
use UsesBranchScope;
public ?int $branchId = null;
public int $currentStep = 1;
public int $totalSteps = 5;
// Step 1: Search existing participant
public string $search = '';
public ?int $selected_participant_id = null;
public ?string $selected_participant_name = null;
// Step 2: Program selection
public ?int $selected_activity_id = null;
public ?int $selected_program_id = null;
// Step 3: Retroactive dates + months owed
public string $actual_start_date = '';
public array $monthStatuses = []; // ['2026-05' => 'unpaid', '2026-06' => 'paid_outside', ...]
// Admin override
public bool $priceOverrideEnabled = false;
public string $priceOverrideInput = '';
public string $priceOverrideReason = '';
// Step 4: Payment for outstanding months
public bool $pay_now = false;
public string $payment_method = 'cash';
public string $payment_notes = '';
// Result
public bool $completed = false;
public ?string $enrollment_summary = null;
public int $invoices_created = 0;
public int $payments_recorded = 0;
public ?string $participant_uuid = null;
public function mount(): void
{
$this->authorize('enrollments.create');
$this->branchId = $this->getActiveBranchId() ?? Branch::where('is_active', true)->first()?->id;
}
public function rules(): array
{
return match ($this->currentStep) {
1 => ['selected_participant_id' => 'required|exists:participants,id'],
2 => [
'selected_activity_id' => 'required|exists:activities,id',
'selected_program_id' => 'required|exists:training_programs,id',
],
3 => ['actual_start_date' => 'required|date|before_or_equal:today'],
4 => ['payment_method' => 'required_if:pay_now,true|in:cash,card,bank_transfer,wallet,online,cheque,other'],
default => [],
};
}
public function messages(): array
{
return [
'selected_participant_id.required' => 'يرجى اختيار المشترك',
'selected_activity_id.required' => 'يرجى اختيار النشاط',
'selected_program_id.required' => 'يرجى اختيار البرنامج',
'actual_start_date.required' => 'يرجى تحديد تاريخ البدء الفعلي',
'actual_start_date.before_or_equal' => 'تاريخ البدء لا يمكن أن يكون في المستقبل',
];
}
public function selectParticipant(int $id, string $name): void
{
$this->selected_participant_id = $id;
$this->selected_participant_name = $name;
}
public function updatedSelectedActivityId(): void
{
$this->selected_program_id = null;
}
public function updatedActualStartDate(): void
{
$this->calculateMonthsOwed();
}
public function calculateMonthsOwed(): void
{
if (!$this->actual_start_date || !$this->selected_program_id) {
$this->monthStatuses = [];
return;
}
$start = Carbon::parse($this->actual_start_date)->startOfMonth();
$now = now()->startOfMonth();
$months = [];
$current = $start->copy();
while ($current->lte($now)) {
$key = $current->format('Y-m');
$months[$key] = 'unpaid';
$current->addMonth();
}
// Preserve existing selections
foreach ($months as $key => $default) {
if (!isset($this->monthStatuses[$key])) {
$this->monthStatuses[$key] = $default;
}
}
// Remove months that are no longer in range
$this->monthStatuses = array_intersect_key($this->monthStatuses, $months);
}
public function setMonthStatus(string $month, string $status): void
{
if (isset($this->monthStatuses[$month])) {
$this->monthStatuses[$month] = $status;
}
}
#[Computed]
public function monthlyPrice(): int
{
if (!$this->selected_participant_id || !$this->selected_program_id) {
return 0;
}
try {
$participant = Participant::find($this->selected_participant_id);
$program = TrainingProgram::find($this->selected_program_id);
if (!$participant || !$program) return 0;
$pricingService = app(PricingService::class);
$result = $pricingService->calculate(
priceable: $program,
participant: $participant,
branchId: $this->branchId,
);
return $result->finalAmount;
} catch (\Throwable) {
return 0;
}
}
#[Computed]
public function unpaidMonthsCount(): int
{
return collect($this->monthStatuses)->filter(fn ($s) => $s === 'unpaid')->count();
}
#[Computed]
public function totalOwed(): int
{
if ($this->priceOverrideEnabled && $this->priceOverrideInput !== '') {
return max(0, (int) round((float) $this->priceOverrideInput * 100));
}
return $this->monthlyPrice * $this->unpaidMonthsCount;
}
public function nextStep(): void
{
$this->validate();
if ($this->currentStep === 2) {
$this->calculateMonthsOwed();
}
$this->currentStep = min($this->currentStep + 1, $this->totalSteps);
}
public function previousStep(): void
{
$this->currentStep = max($this->currentStep - 1, 1);
}
public function goToStep(int $step): void
{
if ($step < $this->currentStep) {
$this->currentStep = $step;
}
}
public function confirm(): void
{
$actor = auth()->user();
try {
DB::transaction(function () use ($actor) {
$participant = Participant::findOrFail($this->selected_participant_id);
$program = TrainingProgram::findOrFail($this->selected_program_id);
$enrollmentService = app(EnrollmentService::class);
$invoiceService = app(InvoiceService::class);
$paymentService = app(PaymentService::class);
// 1. Create enrollment with retroactive start date
$enrollment = $enrollmentService->enrollInProgram(
$participant,
$program,
$actor,
[
'start_date' => $this->actual_start_date,
'payment_status' => 'pending',
]
);
// 2. Calculate effective per-month price
$perMonth = $this->monthlyPrice;
// Admin override distributes total evenly
$unpaidCount = $this->unpaidMonthsCount;
$overrideActive = $this->priceOverrideEnabled && $this->priceOverrideInput !== '' && $actor->is_super_admin;
if ($overrideActive && $unpaidCount > 0) {
$overrideTotal = max(0, (int) round((float) $this->priceOverrideInput * 100));
$perMonth = (int) floor($overrideTotal / $unpaidCount);
Log::channel('audit')->info('retroactive_enrollment_price_override', [
'actor_id' => $actor->id,
'actor_name' => $actor->name,
'participant_name' => $this->selected_participant_name,
'program' => $program->name_ar,
'original_per_month' => $this->monthlyPrice,
'override_total' => $overrideTotal,
'effective_per_month' => $perMonth,
'months_count' => $unpaidCount,
'reason' => $this->priceOverrideReason,
]);
}
// 3. Generate invoices per month
$invoicesCreated = 0;
$paymentsRecorded = 0;
foreach ($this->monthStatuses as $monthKey => $status) {
$monthDate = Carbon::parse($monthKey . '-01');
$isCurrentMonth = $monthDate->isSameMonth(now());
if ($status === 'paid_outside') {
// Create invoice marked as paid (record that payment happened outside)
$invoice = $invoiceService->create([
'academy_id' => $participant->academy_id,
'branch_id' => $this->branchId,
'billable_type' => $participant->getMorphClass(),
'billable_id' => $participant->id,
'total_amount' => $perMonth,
'subtotal_amount' => $perMonth,
'discount_amount' => 0,
'tax_amount' => 0,
'due_date' => $monthDate->copy()->addDays(7)->toDateString(),
'contact_name' => $participant->person?->name_ar,
'notes' => 'اشتراك ' . $program->name_ar . ' — ' . $this->getArabicMonth($monthDate),
'metadata' => ['retroactive' => true, 'month' => $monthKey],
], [
[
'description' => 'اشتراك: ' . $program->name_ar . ' (' . $this->getArabicMonth($monthDate) . ')',
'quantity' => 1,
'unit_price' => $perMonth,
'discount_amount' => 0,
'tax_amount' => 0,
],
], $actor);
// Record as paid
$invoice->update([
'status' => InvoiceStatus::Paid,
'paid_amount' => $perMonth,
'due_amount' => 0,
'paid_at' => $monthDate->copy()->addDays(1),
'metadata' => array_merge($invoice->metadata ?? [], [
'paid_outside_system' => true,
'recorded_by' => $actor->name,
'recorded_at' => now()->toIso8601String(),
]),
]);
$invoicesCreated++;
} elseif ($status === 'unpaid') {
// Create outstanding invoice
$invoice = $invoiceService->create([
'academy_id' => $participant->academy_id,
'branch_id' => $this->branchId,
'billable_type' => $participant->getMorphClass(),
'billable_id' => $participant->id,
'total_amount' => $perMonth,
'subtotal_amount' => $perMonth,
'discount_amount' => 0,
'tax_amount' => 0,
'due_date' => $monthDate->copy()->addDays(7)->toDateString(),
'contact_name' => $participant->person?->name_ar,
'notes' => 'اشتراك ' . $program->name_ar . ' — ' . $this->getArabicMonth($monthDate),
'metadata' => ['retroactive' => true, 'month' => $monthKey],
], [
[
'description' => 'اشتراك: ' . $program->name_ar . ' (' . $this->getArabicMonth($monthDate) . ')',
'quantity' => 1,
'unit_price' => $perMonth,
'discount_amount' => 0,
'tax_amount' => 0,
],
], $actor);
$invoice->update(['status' => InvoiceStatus::Sent]);
$invoicesCreated++;
// Pay now if selected
if ($this->pay_now && $perMonth > 0) {
$paymentService->recordPayment([
'invoice_id' => $invoice->id,
'branch_id' => $this->branchId,
'amount' => $perMonth,
'method' => $this->payment_method,
'direction' => 'inbound',
'currency' => 'EGP',
'payment_date' => now()->toDateString(),
'notes' => 'دفع بأثر رجعي — ' . $this->getArabicMonth($monthDate),
], $actor);
$paymentsRecorded++;
}
}
}
// 4. Set next_billing_date to 1st of next month
$enrollment->update([
'next_billing_date' => now()->addMonth()->startOfMonth()->toDateString(),
'last_billed_at' => now()->toDateString(),
]);
$this->invoices_created = $invoicesCreated;
$this->payments_recorded = $paymentsRecorded;
$this->participant_uuid = $participant->uuid;
$this->enrollment_summary = $program->name_ar;
$this->completed = true;
$this->currentStep = 5;
});
session()->flash('success', __('تم تسجيل اللاعب بنجاح بأثر رجعي'));
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
} catch (\Throwable $e) {
Log::error('Retroactive enrollment failed', ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
session()->flash('error', __('حدث خطأ غير متوقع'));
}
}
public function render()
{
$searchResults = collect();
if (strlen($this->search) >= 2 && !$this->selected_participant_id) {
$searchResults = Participant::query()
->with('person')
->where('branch_id', $this->branchId)
->where('status', 'active')
->where(function ($q) {
$search = $this->search;
$q->where('participant_number', 'ilike', "%{$search}%")
->orWhereHas('person', function ($pq) use ($search) {
$pq->where('name_ar', 'ilike', "%{$search}%")
->orWhere('name', 'ilike', "%{$search}%")
->orWhere('phone', 'like', "%{$search}%");
});
})
->limit(10)
->get();
}
$activities = Activity::where('is_active', true)->orderBy('name_ar')->get();
$programs = collect();
if ($this->selected_activity_id) {
$programs = TrainingProgram::where('activity_id', $this->selected_activity_id)
->where('status', 'active')
->where('registration_open', true)
->orderBy('name_ar')
->get();
}
return view('livewire.receptionist.retroactive-enrollment-wizard', [
'searchResults' => $searchResults,
'activities' => $activities,
'programs' => $programs,
'isSuperAdmin' => auth()->user()?->is_super_admin ?? false,
]);
}
private function getArabicMonth(Carbon $date): string
{
$months = [
1 => 'يناير', 2 => 'فبراير', 3 => 'مارس', 4 => 'أبريل',
5 => 'مايو', 6 => 'يونيو', 7 => 'يوليو', 8 => 'أغسطس',
9 => 'سبتمبر', 10 => 'أكتوبر', 11 => 'نوفمبر', 12 => 'ديسمبر',
];
return $months[$date->month] . ' ' . $date->year;
}
}
...@@ -126,6 +126,18 @@ class="group flex flex-col items-center gap-2.5 bg-white rounded-xl shadow-sm bo ...@@ -126,6 +126,18 @@ class="group flex flex-col items-center gap-2.5 bg-white rounded-xl shadow-sm bo
</a> </a>
@endcan @endcan
@can('enrollments.create')
<a href="{{ route('receptionist.retroactive-enrollment') }}" wire:navigate
class="group flex flex-col items-center gap-2.5 bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-5 hover:shadow-md hover:border-purple-300 active:scale-[0.97] transition-all">
<div class="w-12 h-12 rounded-xl bg-purple-100 flex items-center justify-center group-hover:bg-purple-200 transition-colors shrink-0">
<svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<span class="text-xs sm:text-sm font-semibold text-gray-700 text-center leading-tight">{{ __('تسجيل لاعب سابق') }}</span>
</a>
@endcan
@can('pos.sell') @can('pos.sell')
<a href="{{ route('pos.terminal') }}" wire:navigate <a href="{{ route('pos.terminal') }}" wire:navigate
class="group flex flex-col items-center gap-2.5 bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-5 hover:shadow-md hover:border-rose-300 active:scale-[0.97] transition-all"> class="group flex flex-col items-center gap-2.5 bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-5 hover:shadow-md hover:border-rose-300 active:scale-[0.97] transition-all">
......
<div>
{{-- Header --}}
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-xl sm:text-2xl font-bold text-gray-800">{{ __('تسجيل لاعب سابق') }}</h1>
<p class="text-sm text-gray-500 mt-1">{{ __('تسجيل مشترك بدأ قبل كده وتسوية الفواتير السابقة') }}</p>
</div>
<a href="{{ route('receptionist.dashboard') }}" wire:navigate
class="inline-flex items-center gap-2 px-4 py-2 text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 text-sm font-medium transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
{{ __('إلغاء') }}
</a>
</div>
{{-- Flash Messages --}}
@if(session('error'))
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-xl text-red-700 text-sm">{{ session('error') }}</div>
@endif
@if(session('success'))
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-xl text-green-700 text-sm">{{ session('success') }}</div>
@endif
{{-- Step Indicator --}}
@if($currentStep < 5)
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-6">
<div class="flex items-center justify-between">
@php
$steps = [
1 => 'اختيار المشترك',
2 => 'اختيار البرنامج',
3 => 'الشهور المستحقة',
4 => 'الدفع والتأكيد',
];
@endphp
@foreach($steps as $num => $label)
<div class="flex items-center {{ !$loop->last ? 'flex-1' : '' }}">
<button wire:click="goToStep({{ $num }})" @if($num >= $currentStep) disabled @endif
class="flex items-center gap-2 {{ $num < $currentStep ? 'cursor-pointer' : 'cursor-default' }}">
<div class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold transition-colors
{{ $num === $currentStep ? 'bg-indigo-600 text-white' : '' }}
{{ $num < $currentStep ? 'bg-green-500 text-white' : '' }}
{{ $num > $currentStep ? 'bg-gray-200 text-gray-500' : '' }}">
@if($num < $currentStep)
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
@else
{{ $num }}
@endif
</div>
<span class="text-xs font-medium hidden sm:inline
{{ $num === $currentStep ? 'text-indigo-600' : '' }}
{{ $num < $currentStep ? 'text-green-600' : '' }}
{{ $num > $currentStep ? 'text-gray-400' : '' }}">
{{ __($label) }}
</span>
</button>
@if(!$loop->last)
<div class="flex-1 h-0.5 mx-3 {{ $num < $currentStep ? 'bg-green-500' : 'bg-gray-200' }}"></div>
@endif
</div>
@endforeach
</div>
</div>
@endif
{{-- Step Content --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6">
{{-- Step 1: Select Participant --}}
@if($currentStep === 1)
<div>
<h2 class="text-lg font-semibold text-gray-800 mb-6">{{ __('اختيار المشترك') }}</h2>
<div class="mb-6">
<input type="text" wire:model.live.debounce.300ms="search"
class="w-full px-4 py-3 min-h-16 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-lg"
placeholder="{{ __('بحث بالاسم أو الهاتف أو رقم المشترك...') }}">
</div>
@if($selected_participant_id)
<div class="mb-4 p-4 bg-indigo-50 border border-indigo-200 rounded-xl flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-indigo-200 flex items-center justify-center">
<svg class="w-5 h-5 text-indigo-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
</div>
<span class="font-medium text-indigo-800">{{ $selected_participant_name }}</span>
</div>
<button wire:click="$set('selected_participant_id', null)" class="text-indigo-600 hover:text-indigo-800 text-sm">
{{ __('تغيير') }}
</button>
</div>
@endif
@if(strlen($search) >= 2 && !$selected_participant_id)
<div class="space-y-2" wire:loading.class="opacity-50">
@forelse($searchResults as $result)
<button wire:click="selectParticipant({{ $result->id }}, '{{ $result->person?->name_ar }}')"
class="w-full p-4 min-h-16 border border-gray-200 rounded-xl text-start hover:border-indigo-300 hover:bg-indigo-50 transition-all flex items-center justify-between">
<div>
<p class="font-medium text-gray-800">{{ $result->person?->name_ar }}</p>
<div class="flex items-center gap-3 mt-1 text-xs text-gray-500">
@if($result->participant_number)
<span dir="ltr">{{ $result->participant_number }}</span>
@endif
@if($result->person?->phone)
<span dir="ltr">{{ $result->person->phone }}</span>
@endif
</div>
</div>
<span class="px-2 py-0.5 text-xs rounded-full bg-green-100 text-green-700">{{ __('نشط') }}</span>
</button>
@empty
<div class="text-center py-8 text-gray-500">
<p>{{ __('لا توجد نتائج') }}</p>
</div>
@endforelse
</div>
@endif
@error('selected_participant_id') <p class="mt-2 text-sm text-red-600">{{ $message }}</p> @enderror
<div class="flex justify-end mt-8">
<button wire:click="nextStep" @if(!$selected_participant_id) disabled @endif
class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 text-base font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
{{ __('التالي') }}
<svg class="w-5 h-5 rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
</button>
</div>
</div>
@endif
{{-- Step 2: Program Selection --}}
@if($currentStep === 2)
<div>
<h2 class="text-lg font-semibold text-gray-800 mb-6">{{ __('اختيار البرنامج') }}</h2>
{{-- Activity --}}
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __('النشاط') }}</label>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
@foreach($activities as $activity)
<label class="relative cursor-pointer">
<input type="radio" wire:model.live="selected_activity_id" value="{{ $activity->id }}" class="peer sr-only">
<div class="p-4 min-h-16 border border-gray-200 rounded-xl text-center transition-all
peer-checked:border-indigo-500 peer-checked:bg-indigo-50 hover:border-gray-300">
<p class="font-medium text-gray-800 text-sm">{{ $activity->name_ar }}</p>
</div>
</label>
@endforeach
</div>
@error('selected_activity_id') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
{{-- Program --}}
@if($selected_activity_id)
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __('البرنامج') }}</label>
@if($programs->isEmpty())
<p class="text-sm text-gray-500 p-4 bg-gray-50 rounded-lg">{{ __('لا توجد برامج متاحة لهذا النشاط') }}</p>
@else
<div class="space-y-2">
@foreach($programs as $program)
<label class="relative cursor-pointer block">
<input type="radio" wire:model.live="selected_program_id" value="{{ $program->id }}" class="peer sr-only">
<div class="p-4 min-h-16 border border-gray-200 rounded-xl transition-all
peer-checked:border-indigo-500 peer-checked:bg-indigo-50 hover:border-gray-300">
<p class="font-medium text-gray-800">{{ $program->name_ar }}</p>
@if($program->description)
<p class="text-xs text-gray-500 mt-1">{{ Str::limit($program->description, 80) }}</p>
@endif
</div>
</label>
@endforeach
</div>
@endif
@error('selected_program_id') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
@endif
<div class="flex justify-between mt-8">
<button wire:click="previousStep"
class="inline-flex items-center gap-2 px-6 py-3 min-h-16 text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 text-base font-medium">
<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="M15 19l-7-7 7-7"/>
</svg>
{{ __('السابق') }}
</button>
<button wire:click="nextStep" @if(!$selected_program_id) disabled @endif
class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 text-base font-medium disabled:opacity-50 disabled:cursor-not-allowed">
{{ __('التالي') }}
<svg class="w-5 h-5 rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
</button>
</div>
</div>
@endif
{{-- Step 3: Months Owed --}}
@if($currentStep === 3)
<div>
<h2 class="text-lg font-semibold text-gray-800 mb-6">{{ __('الشهور المستحقة') }}</h2>
{{-- Start Date --}}
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __('تاريخ البدء الفعلي') }}</label>
<input type="date" wire:model.live="actual_start_date" dir="ltr" max="{{ now()->toDateString() }}"
class="w-full max-w-xs px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 text-base">
<p class="text-xs text-gray-500 mt-1">{{ __('التاريخ اللي اللاعب بدأ فيه فعلياً') }}</p>
@error('actual_start_date') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
{{-- Monthly Price Info --}}
@if($this->monthlyPrice > 0)
<div class="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg flex items-center justify-between">
<span class="text-sm text-blue-800">{{ __('سعر الاشتراك الشهري') }}</span>
<span class="font-bold text-blue-800" dir="ltr">{{ number_format($this->monthlyPrice / 100, 2) }} {{ __('ج.م') }}</span>
</div>
@endif
{{-- Months Grid --}}
@if(count($monthStatuses) > 0)
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-3">{{ __('حدد حالة كل شهر') }}</label>
<div class="space-y-2">
@foreach($monthStatuses as $monthKey => $status)
@php
$monthDate = \Carbon\Carbon::parse($monthKey . '-01');
$monthLabel = $this->getArabicMonth($monthDate);
@endphp
<div class="flex items-center justify-between p-3 rounded-lg border
{{ $status === 'paid_outside' ? 'border-green-200 bg-green-50' : 'border-red-200 bg-red-50' }}">
<span class="text-sm font-medium text-gray-800">{{ $monthLabel }}</span>
<div class="flex items-center gap-2">
<button wire:click="setMonthStatus('{{ $monthKey }}', 'unpaid')"
class="px-3 py-1.5 rounded-lg text-xs font-medium transition-colors
{{ $status === 'unpaid' ? 'bg-red-600 text-white' : 'bg-white text-red-600 border border-red-300 hover:bg-red-50' }}">
{{ __('لم يدفع') }}
</button>
<button wire:click="setMonthStatus('{{ $monthKey }}', 'paid_outside')"
class="px-3 py-1.5 rounded-lg text-xs font-medium transition-colors
{{ $status === 'paid_outside' ? 'bg-green-600 text-white' : 'bg-white text-green-600 border border-green-300 hover:bg-green-50' }}">
{{ __('دفع خارج النظام') }}
</button>
</div>
</div>
@endforeach
</div>
</div>
{{-- Summary --}}
<div class="p-4 bg-gray-50 border border-gray-200 rounded-xl mb-4">
<div class="flex items-center justify-between mb-2">
<span class="text-sm text-gray-600">{{ __('شهور لم تُدفع') }}</span>
<span class="font-bold text-red-700">{{ $this->unpaidMonthsCount }} {{ __('شهر') }}</span>
</div>
<div class="flex items-center justify-between mb-2">
<span class="text-sm text-gray-600">{{ __('شهور مدفوعة خارج النظام') }}</span>
<span class="font-bold text-green-700">{{ count($monthStatuses) - $this->unpaidMonthsCount }} {{ __('شهر') }}</span>
</div>
<div class="border-t border-gray-200 pt-2 mt-2 flex items-center justify-between">
<span class="text-base font-bold text-gray-800">{{ __('إجمالي المطلوب') }}</span>
<span class="text-xl font-bold text-gray-800" dir="ltr">{{ number_format($this->totalOwed / 100, 2) }} {{ __('ج.م') }}</span>
</div>
</div>
{{-- Admin Override --}}
@if($isSuperAdmin)
<div class="border border-dashed border-red-300 rounded-xl overflow-hidden" x-data="{ open: @entangle('priceOverrideEnabled') }">
<button type="button" @click="open = !open; $wire.set('priceOverrideEnabled', open)"
class="w-full flex items-center justify-between p-4 text-start hover:bg-red-50 transition-colors">
<div class="flex items-center gap-2">
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
</svg>
<span class="text-sm font-semibold text-red-700">{{ __('تجاوز الإجمالي (أدمن)') }}</span>
</div>
<svg class="w-4 h-4 text-red-400 transition-transform" :class="open ? 'rotate-180' : ''" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<div x-show="open" x-cloak x-transition class="p-4 pt-0 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('الإجمالي المخصص (ج.م) للشهور غير المدفوعة') }}</label>
<input type="number" wire:model.live="priceOverrideInput" dir="ltr" step="0.01" min="0"
class="w-full max-w-xs px-4 py-3 border border-red-300 rounded-lg focus:ring-2 focus:ring-red-400 text-lg font-mono"
placeholder="0.00">
@if($priceOverrideInput !== '' && $this->totalOwed !== $this->monthlyPrice * $this->unpaidMonthsCount)
<p class="mt-1 text-xs text-red-600">
{{ __('بدلاً من') }} {{ number_format($this->monthlyPrice * $this->unpaidMonthsCount / 100, 2) }} {{ __('ج.م') }}
</p>
@endif
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('سبب التعديل') }}</label>
<input type="text" wire:model="priceOverrideReason"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-400 text-sm"
placeholder="{{ __('مثال: خصم خاص، اتفاقية') }}">
</div>
</div>
</div>
@endif
@endif
<div class="flex justify-between mt-8">
<button wire:click="previousStep"
class="inline-flex items-center gap-2 px-6 py-3 min-h-16 text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 text-base font-medium">
<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="M15 19l-7-7 7-7"/>
</svg>
{{ __('السابق') }}
</button>
<button wire:click="nextStep" @if(!$actual_start_date) disabled @endif
class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 text-base font-medium disabled:opacity-50 disabled:cursor-not-allowed">
{{ __('التالي') }}
<svg class="w-5 h-5 rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
</button>
</div>
</div>
@endif
{{-- Step 4: Payment & Confirm --}}
@if($currentStep === 4)
<div>
<h2 class="text-lg font-semibold text-gray-800 mb-6">{{ __('الدفع والتأكيد') }}</h2>
{{-- Summary --}}
<div class="p-5 bg-gray-50 border border-gray-200 rounded-xl mb-6 space-y-3">
<div class="flex items-center justify-between">
<span class="text-gray-600">{{ __('المشترك') }}</span>
<span class="font-medium">{{ $selected_participant_name }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600">{{ __('تاريخ البدء') }}</span>
<span class="font-medium" dir="ltr">{{ $actual_start_date }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600">{{ __('إجمالي الشهور') }}</span>
<span class="font-medium">{{ count($monthStatuses) }} {{ __('شهر') }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600">{{ __('شهور غير مدفوعة') }}</span>
<span class="font-bold text-red-700">{{ $this->unpaidMonthsCount }} {{ __('شهر') }}</span>
</div>
<div class="border-t border-gray-200 pt-3 flex items-center justify-between">
<span class="text-lg font-bold text-gray-800">{{ __('إجمالي المطلوب') }}</span>
<span class="text-2xl font-bold text-gray-800" dir="ltr">{{ number_format($this->totalOwed / 100, 2) }} {{ __('ج.م') }}</span>
</div>
</div>
{{-- Pay Now? --}}
@if($this->unpaidMonthsCount > 0 && $this->totalOwed > 0)
<div class="mb-6 p-4 border border-gray-200 rounded-xl">
<label class="flex items-center gap-3 cursor-pointer">
<input type="checkbox" wire:model.live="pay_now" class="w-5 h-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
<span class="text-sm font-medium text-gray-800">{{ __('دفع كل المبلغ المستحق الآن') }}</span>
</label>
@if($pay_now)
<div class="mt-4 grid grid-cols-3 sm:grid-cols-4 gap-3">
@foreach(['cash' => 'نقدي', 'card' => 'بطاقة', 'bank_transfer' => 'تحويل', 'wallet' => 'محفظة'] as $method => $label)
<label class="relative cursor-pointer">
<input type="radio" wire:model="payment_method" value="{{ $method }}" class="peer sr-only">
<div class="p-3 min-h-12 flex items-center justify-center border border-gray-300 rounded-xl transition-all text-xs font-medium
peer-checked:border-indigo-500 peer-checked:bg-indigo-50 peer-checked:text-indigo-700 hover:border-gray-400">
{{ __($label) }}
</div>
</label>
@endforeach
</div>
@endif
</div>
@endif
<div class="flex justify-between mt-8">
<button wire:click="previousStep"
class="inline-flex items-center gap-2 px-6 py-3 min-h-16 text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 text-base font-medium">
<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="M15 19l-7-7 7-7"/>
</svg>
{{ __('السابق') }}
</button>
<button wire:click="confirm" wire:loading.attr="disabled"
class="inline-flex items-center gap-2 px-8 py-3 min-h-16 bg-green-600 text-white rounded-lg hover:bg-green-700 text-base font-bold transition-colors disabled:opacity-50">
<span wire:loading.remove wire:target="confirm">{{ __('تأكيد التسجيل') }}</span>
<span wire:loading wire:target="confirm" class="inline-flex items-center gap-2">
<svg class="animate-spin w-5 h-5" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
</svg>
{{ __('جارٍ التسجيل...') }}
</span>
</button>
</div>
</div>
@endif
{{-- Step 5: Success --}}
@if($currentStep === 5)
<div class="text-center py-8">
<div class="w-20 h-20 mx-auto bg-green-100 rounded-full flex items-center justify-center mb-6">
<svg class="w-10 h-10 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
</div>
<h2 class="text-2xl font-bold text-green-700 mb-2">{{ __('تم التسجيل بنجاح!') }}</h2>
<p class="text-gray-600 mb-6">{{ __('تم تسجيل اللاعب بأثر رجعي وإنشاء الفواتير') }}</p>
<div class="inline-block bg-gray-50 border border-gray-200 rounded-xl p-6 mb-6 text-start">
<div class="space-y-3">
<div>
<p class="text-xs text-gray-500">{{ __('المشترك') }}</p>
<p class="font-medium text-gray-800">{{ $selected_participant_name }}</p>
</div>
<div>
<p class="text-xs text-gray-500">{{ __('البرنامج') }}</p>
<p class="font-medium text-gray-800">{{ $enrollment_summary }}</p>
</div>
<div class="flex gap-6">
<div>
<p class="text-xs text-gray-500">{{ __('فواتير تم إنشاؤها') }}</p>
<p class="text-lg font-bold text-gray-800">{{ $invoices_created }}</p>
</div>
@if($payments_recorded > 0)
<div>
<p class="text-xs text-gray-500">{{ __('مدفوعات تم تسجيلها') }}</p>
<p class="text-lg font-bold text-green-700">{{ $payments_recorded }}</p>
</div>
@endif
</div>
</div>
</div>
<div class="flex items-center justify-center gap-4 mt-6">
@if($participant_uuid)
<a href="{{ route('participants.show', $participant_uuid) }}" wire:navigate
class="inline-flex items-center gap-2 px-6 py-3 min-h-16 border border-indigo-300 text-indigo-700 rounded-lg hover:bg-indigo-50 text-base font-medium">
{{ __('عرض المشترك') }}
</a>
@endif
<a href="{{ route('receptionist.retroactive-enrollment') }}" wire:navigate
class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 text-base font-medium">
{{ __('تسجيل آخر') }}
</a>
</div>
</div>
@endif
</div>
</div>
...@@ -524,6 +524,8 @@ ...@@ -524,6 +524,8 @@
->middleware('permission:enrollments.create'); ->middleware('permission:enrollments.create');
Route::get('/receptionist/collect-payment', CollectPaymentWizard::class)->name('receptionist.collect-payment') Route::get('/receptionist/collect-payment', CollectPaymentWizard::class)->name('receptionist.collect-payment')
->middleware('permission:invoices.create'); ->middleware('permission:invoices.create');
Route::get('/receptionist/retroactive-enrollment', \App\Livewire\Receptionist\RetroactiveEnrollmentWizard::class)->name('receptionist.retroactive-enrollment')
->middleware('permission:enrollments.create');
// Financial — Payment Plans // Financial — Payment Plans
Route::get('/payment-plans/create', \App\Livewire\Financial\PaymentPlanCreate::class)->name('payment-plans.create') Route::get('/payment-plans/create', \App\Livewire\Financial\PaymentPlanCreate::class)->name('payment-plans.create')
......
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