Commit fb56f022 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add 4 dashboard/financial features: compact renewals, retroactive wizard fix,...

Add 4 dashboard/financial features: compact renewals, retroactive wizard fix, club revenue, expense shortcuts

- Compact overdue renewals alert into summary card with toggle detail list
- Rewrite retroactive enrollment wizard to CREATE new participants (not search existing)
- Add external revenue import form for club lump-sum payments
- Add quick-action buttons to financial overview for expenses/revenue
- Migration adds external_revenue to expenses category CHECK constraint
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 172a4836
......@@ -14,6 +14,7 @@
case Medical = 'medical';
case Utilities = 'utilities';
case Other = 'other';
case ExternalRevenue = 'external_revenue';
public function label(): string
{
......@@ -28,6 +29,7 @@ public function label(): string
self::Medical => 'طبي',
self::Utilities => 'مرافق وخدمات',
self::Other => 'أخرى',
self::ExternalRevenue => 'إيراد خارجي',
};
}
}
......@@ -85,6 +85,64 @@ public function recordExpense(array $data, User $actor): Expense
});
}
public function recordExternalRevenue(array $data, User $actor): Expense
{
return DB::transaction(function () use ($data, $actor) {
$academyId = app('current_academy')?->id ?? $actor->academy_id;
$expense = Expense::create([
'academy_id' => $academyId,
'branch_id' => $data['branch_id'] ?? null,
'category' => 'external_revenue',
'amount' => $data['amount'],
'description' => $data['description'],
'recipient_name' => $data['source'] ?? null,
'payment_method' => $data['payment_method'],
'receipt_reference' => $data['reference_number'] ?? null,
'expense_date' => $data['revenue_date'],
'notes' => $data['notes'] ?? null,
'created_by' => $actor->id,
]);
$revenueAccount = FinancialAccount::where('academy_id', $academyId)
->where('code', '4060')
->first();
if (!$revenueAccount) {
throw new DomainException('حساب الإيرادات الخارجية غير موجود — يرجى إعداد شجرة الحسابات');
}
$cashAccountCode = match ($data['payment_method']) {
'bank_transfer', 'cheque' => '1010',
default => '1000',
};
$cashAccount = FinancialAccount::where('academy_id', $academyId)
->where('code', $cashAccountCode)
->first();
if (!$cashAccount) {
throw new DomainException('حساب النقدية/البنك غير موجود — يرجى إعداد شجرة الحسابات');
}
Transaction::create([
'academy_id' => $academyId,
'debit_account_id' => $cashAccount->id,
'credit_account_id' => $revenueAccount->id,
'reference_type' => get_class($expense),
'reference_id' => $expense->id,
'amount' => $data['amount'],
'currency' => 'EGP',
'type' => TransactionType::PaymentReceived,
'description' => $data['description'],
'transaction_date' => $data['revenue_date'],
'created_by' => $actor->id,
]);
return $expense;
});
}
private function createExpenseTransaction(
int $academyId,
string $expenseAccountCode,
......
......@@ -12,18 +12,17 @@
class OverdueRenewalsAlert extends Component
{
public bool $expanded = false;
public bool $showList = false;
public function toggleExpanded(): void
public function toggleList(): void
{
$this->expanded = !$this->expanded;
$this->showList = !$this->showList;
}
public function render()
{
$today = now()->toDateString();
// 1. Participants with unpaid renewal invoices (already billed, not yet paid)
$unpaidInvoices = Invoice::whereIn('status', [InvoiceStatus::Sent, InvoiceStatus::PartiallyPaid, InvoiceStatus::Overdue])
->where('due_amount', '>', 0)
->where('billable_type', Participant::class)
......@@ -32,7 +31,6 @@ public function render()
->orderBy('due_date')
->get();
// 2. Also find enrollments overdue but NO invoice generated yet
$overdueEnrollments = Enrollment::where('status', EnrollmentStatus::Active)
->whereNotNull('next_billing_date')
->where('next_billing_date', '<=', $today)
......@@ -54,46 +52,50 @@ public function render()
->exists();
});
// Build unified list
$invoiceCount = $unpaidInvoices->count();
$enrollmentCount = $overdueEnrollments->count();
$totalOverdue = $invoiceCount + $enrollmentCount;
$totalAmount = $unpaidInvoices->sum('due_amount');
$items = collect();
if ($this->showList) {
foreach ($unpaidInvoices as $invoice) {
$participant = $invoice->billable;
if (!$participant) {
continue;
}
$items->push([
'participant_name' => $participant->person?->name_ar ?? $invoice->contact_name ?? '-',
'participant_phone' => $participant->person?->phone ?? null,
'participant_uuid' => $participant->uuid,
'amount' => $invoice->due_amount,
'program_name' => $this->extractProgramName($invoice->notes),
'days_overdue' => $invoice->due_date?->isPast() ? (int) now()->diffInDays($invoice->due_date) : 0,
'type' => 'invoice',
]);
}
foreach ($unpaidInvoices as $invoice) {
$participant = $invoice->billable;
if (!$participant) {
continue;
foreach ($overdueEnrollments as $enrollment) {
$items->push([
'participant_name' => $enrollment->participant?->person?->name_ar ?? '-',
'participant_phone' => $enrollment->participant?->person?->phone ?? null,
'participant_uuid' => $enrollment->participant?->uuid,
'amount' => null,
'program_name' => $enrollment->program?->name_ar ?? '-',
'days_overdue' => (int) now()->diffInDays($enrollment->next_billing_date),
'type' => 'no_invoice',
]);
}
$items->push([
'participant_name' => $participant->person?->name_ar ?? $invoice->contact_name ?? '-',
'participant_phone' => $participant->person?->phone ?? null,
'participant_uuid' => $participant->uuid,
'amount' => $invoice->due_amount,
'program_name' => $this->extractProgramName($invoice->notes),
'days_overdue' => $invoice->due_date?->isPast() ? (int) now()->diffInDays($invoice->due_date) : 0,
'type' => 'invoice',
]);
}
foreach ($overdueEnrollments as $enrollment) {
$items->push([
'participant_name' => $enrollment->participant?->person?->name_ar ?? '-',
'participant_phone' => $enrollment->participant?->person?->phone ?? null,
'participant_uuid' => $enrollment->participant?->uuid,
'amount' => null,
'program_name' => $enrollment->program?->name_ar ?? '-',
'days_overdue' => (int) now()->diffInDays($enrollment->next_billing_date),
'type' => 'no_invoice',
]);
$items = $items->sortByDesc('days_overdue')->values();
}
$items = $items->sortByDesc('days_overdue')->values();
$totalOverdue = $items->count();
$displayList = $this->expanded ? $items : $items->take(15);
$totalAmount = $unpaidInvoices->sum('due_amount');
return view('livewire.dashboard.overdue-renewals-alert', [
'items' => $displayList,
'items' => $items,
'totalOverdue' => $totalOverdue,
'totalAmount' => $totalAmount,
'invoiceCount' => $invoiceCount,
'enrollmentCount' => $enrollmentCount,
]);
}
......
<?php
namespace App\Livewire\Financial;
use App\Domain\Financial\Services\ExpenseService;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('تسجيل إيراد خارجي')]
class ExternalRevenueForm extends Component
{
use UsesBranchScope;
public string $source = '';
public string $amount_display = '';
public string $description = '';
public string $payment_method = 'cash';
public string $reference_number = '';
public ?string $revenue_date = null;
public string $notes = '';
public function mount(): void
{
$this->authorize('expenses.create');
$this->revenue_date = now()->toDateString();
}
public function rules(): array
{
return [
'source' => 'required|string|max:255',
'amount_display' => 'required|numeric|min:0.01',
'description' => 'required|string|max:500',
'payment_method' => 'required|in:cash,card,bank_transfer,wallet,online,cheque,other',
'reference_number' => 'nullable|string|max:100',
'revenue_date' => 'required|date',
'notes' => 'nullable|string',
];
}
public function messages(): array
{
return [
'source.required' => 'مصدر الإيراد مطلوب',
'amount_display.required' => 'المبلغ مطلوب',
'amount_display.min' => 'المبلغ يجب أن يكون أكبر من صفر',
'description.required' => 'وصف الإيراد مطلوب',
'payment_method.required' => 'اختر طريقة الاستلام',
'revenue_date.required' => 'تاريخ الإيراد مطلوب',
];
}
public function save(ExpenseService $service): void
{
$this->validate();
try {
$service->recordExternalRevenue([
'branch_id' => $this->getActiveBranchId(),
'source' => $this->source,
'amount' => (int) round((float) $this->amount_display * 100),
'description' => $this->description,
'payment_method' => $this->payment_method,
'reference_number' => $this->reference_number ?: null,
'revenue_date' => $this->revenue_date,
'notes' => $this->notes ?: null,
], auth()->user());
session()->flash('success', __('تم تسجيل الإيراد الخارجي بنجاح'));
$this->redirect(route('financial.overview'), navigate: true);
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
}
}
public function render()
{
return view('livewire.financial.external-revenue-form');
}
}
......@@ -3,11 +3,14 @@
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\Identity\Models\Guardian;
use App\Domain\Identity\Models\Person;
use App\Domain\Identity\Services\EgyptianNidDecoder;
use App\Domain\Identity\Services\PersonService;
use App\Domain\Participant\Services\ParticipantService;
use App\Domain\Pricing\Services\PricingService;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Services\PlatformFeeService;
......@@ -16,7 +19,6 @@
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;
......@@ -33,27 +35,36 @@ class RetroactiveEnrollmentWizard extends Component
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 $totalSteps = 6;
// Step 1: Participant info (create new)
public string $participant_name_ar = '';
public ?string $participant_date_of_birth = null;
public string $participant_gender = 'male';
public string $participant_phone = '';
public string $participant_national_id = '';
public string $participant_medical_notes = '';
public bool $participant_nid_decoded = false;
// Step 2: Guardian info
public string $guardian_name_ar = '';
public string $guardian_phone = '';
public string $guardian_relation = 'father';
// Step 3: Program selection
public ?int $selected_activity_id = null;
public ?int $selected_program_id = null;
// Step 3: Retroactive dates + months owed
// Step 4: Retroactive dates + months owed
public string $actual_start_date = '';
public array $monthStatuses = []; // ['2026-05' => 'unpaid', '2026-06' => 'paid_outside', ...]
public array $monthStatuses = [];
// Admin override
public bool $priceOverrideEnabled = false;
public string $priceOverrideInput = '';
public string $priceOverrideReason = '';
// Step 4: Payment for outstanding months
// Step 5: Payment for outstanding months
public bool $pay_now = false;
public string $payment_method = 'cash';
public string $payment_notes = '';
......@@ -64,6 +75,7 @@ class RetroactiveEnrollmentWizard extends Component
public int $invoices_created = 0;
public int $payments_recorded = 0;
public ?string $participant_uuid = null;
public ?string $participant_number = null;
public function mount(): void
{
......@@ -74,13 +86,24 @@ public function mount(): void
public function rules(): array
{
return match ($this->currentStep) {
1 => ['selected_participant_id' => 'required|exists:participants,id'],
1 => [
'participant_name_ar' => 'required|string|min:3|max:100',
'participant_date_of_birth' => 'nullable|date|before:today',
'participant_gender' => 'required|in:male,female',
'participant_phone' => 'nullable|string|max:20',
'participant_national_id' => 'nullable|string|size:14',
],
2 => [
'guardian_name_ar' => 'required|string|min:3|max:100',
'guardian_phone' => 'required|string|min:10|max:20',
'guardian_relation' => 'required|in:father,mother,brother,sister,uncle,aunt,grandfather,grandmother,other',
],
3 => [
'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'],
4 => ['actual_start_date' => 'required|date|before_or_equal:today'],
5 => ['payment_method' => 'required_if:pay_now,true|in:cash,card,bank_transfer,wallet,online,cheque,other'],
default => [],
};
}
......@@ -88,7 +111,13 @@ public function rules(): array
public function messages(): array
{
return [
'selected_participant_id.required' => 'يرجى اختيار المشترك',
'participant_name_ar.required' => 'يرجى إدخال اسم اللاعب',
'participant_name_ar.min' => 'اسم اللاعب قصير جداً',
'participant_date_of_birth.before' => 'تاريخ الميلاد غير صحيح',
'participant_national_id.size' => 'الرقم القومي يجب أن يكون 14 رقماً',
'guardian_name_ar.required' => 'يرجى إدخال اسم ولي الأمر',
'guardian_phone.required' => 'يرجى إدخال هاتف ولي الأمر',
'guardian_phone.min' => 'رقم الهاتف قصير جداً',
'selected_activity_id.required' => 'يرجى اختيار النشاط',
'selected_program_id.required' => 'يرجى اختيار البرنامج',
'actual_start_date.required' => 'يرجى تحديد تاريخ البدء الفعلي',
......@@ -96,10 +125,23 @@ public function messages(): array
];
}
public function selectParticipant(int $id, string $name): void
public function updatedParticipantNationalId(): void
{
$this->selected_participant_id = $id;
$this->selected_participant_name = $name;
$nid = trim($this->participant_national_id);
if (strlen($nid) !== 14) {
$this->participant_nid_decoded = false;
return;
}
try {
$decoder = app(EgyptianNidDecoder::class);
$info = $decoder->decode($nid);
$this->participant_date_of_birth = $info['date_of_birth'];
$this->participant_gender = $info['gender'];
$this->participant_nid_decoded = true;
} catch (\Throwable) {
$this->participant_nid_decoded = false;
}
}
public function updatedSelectedActivityId(): void
......@@ -130,14 +172,12 @@ public function calculateMonthsOwed(): void
$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);
}
......@@ -151,19 +191,18 @@ public function setMonthStatus(string $month, string $status): void
#[Computed]
public function monthlyPrice(): int
{
if (!$this->selected_participant_id || !$this->selected_program_id) {
if (!$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;
if (!$program) return 0;
$pricingService = app(PricingService::class);
$result = $pricingService->calculate(
priceable: $program,
participant: $participant,
participant: null,
branchId: $this->branchId,
);
return $result->finalAmount;
......@@ -191,7 +230,7 @@ public function nextStep(): void
{
$this->validate();
if ($this->currentStep === 2) {
if ($this->currentStep === 4) {
$this->calculateMonthsOwed();
}
......@@ -216,13 +255,66 @@ public function confirm(): void
try {
DB::transaction(function () use ($actor) {
$participant = Participant::findOrFail($this->selected_participant_id);
$program = TrainingProgram::findOrFail($this->selected_program_id);
$personService = app(PersonService::class);
$participantService = app(ParticipantService::class);
$enrollmentService = app(EnrollmentService::class);
$invoiceService = app(InvoiceService::class);
$paymentService = app(PaymentService::class);
// 1. Create enrollment with retroactive start date
// 1. Create guardian Person (or find by phone)
$guardianPerson = Person::where('phone', $this->guardian_phone)->first();
if (!$guardianPerson) {
$guardianPerson = $personService->create([
'name_ar' => $this->guardian_name_ar,
'name' => $this->guardian_name_ar,
'phone' => $this->guardian_phone,
], $actor);
}
// 2. Create or find Guardian record
$guardian = Guardian::firstOrCreate(
['person_id' => $guardianPerson->id],
[
'academy_id' => app('current_academy')->id,
'relationship_type' => $this->guardian_relation,
'is_emergency_contact' => true,
'is_financial_responsible' => true,
'can_pickup' => true,
]
);
// 3. Create participant Person record
$participantPerson = $personService->create([
'name_ar' => $this->participant_name_ar,
'name' => $this->participant_name_ar,
'date_of_birth' => $this->participant_date_of_birth,
'gender' => $this->participant_gender,
'phone' => $this->participant_phone ?: null,
'national_id' => $this->participant_national_id ?: null,
'medical_notes' => $this->participant_medical_notes ?: null,
], $actor);
// 4. Create Participant record
$participant = $participantService->register([
'person_id' => $participantPerson->id,
'branch_id' => $this->branchId,
'registration_source' => 'walk_in',
'primary_guardian_id' => $guardian->id,
'primary_activity_id' => $this->selected_activity_id,
], $actor);
// 5. Link guardian to participant
$participant->guardians()->attach($guardian->id, [
'relationship_type' => $this->guardian_relation,
'is_primary' => true,
'is_emergency_contact' => true,
'can_pickup' => true,
'receives_notifications' => true,
'can_authorize_payment' => true,
]);
// 6. Enroll in program with retroactive start date
$program = TrainingProgram::findOrFail($this->selected_program_id);
$enrollment = $enrollmentService->enrollInProgram(
$participant,
$program,
......@@ -233,10 +325,9 @@ public function confirm(): void
]
);
// 2. Calculate effective per-month price
// 7. 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) {
......@@ -246,7 +337,7 @@ public function confirm(): void
Log::channel('audit')->info('retroactive_enrollment_price_override', [
'actor_id' => $actor->id,
'actor_name' => $actor->name,
'participant_name' => $this->selected_participant_name,
'participant_name' => $this->participant_name_ar,
'program' => $program->name_ar,
'original_per_month' => $this->monthlyPrice,
'override_total' => $overrideTotal,
......@@ -256,7 +347,7 @@ public function confirm(): void
]);
}
// 3. Generate invoices per month (no platform fee on individual past invoices)
// 8. Generate invoices per month
$invoicesCreated = 0;
$paymentsRecorded = 0;
$totalRetroactiveRevenue = 0;
......@@ -276,7 +367,7 @@ public function confirm(): void
'tax_amount' => 0,
'service_fee_amount' => 0,
'due_date' => $monthDate->copy()->addDays(7)->toDateString(),
'contact_name' => $participant->person?->name_ar,
'contact_name' => $participantPerson->name_ar,
'notes' => 'اشتراك ' . $program->name_ar . ' — ' . $this->getArabicMonth($monthDate),
'metadata' => ['retroactive' => true, 'month' => $monthKey, 'fee_deferred_to_registration_month' => true],
], [
......@@ -315,7 +406,7 @@ public function confirm(): void
'tax_amount' => 0,
'service_fee_amount' => 0,
'due_date' => $monthDate->copy()->addDays(7)->toDateString(),
'contact_name' => $participant->person?->name_ar,
'contact_name' => $participantPerson->name_ar,
'notes' => 'اشتراك ' . $program->name_ar . ' — ' . $this->getArabicMonth($monthDate),
'metadata' => ['retroactive' => true, 'month' => $monthKey, 'fee_deferred_to_registration_month' => true],
], [
......@@ -348,7 +439,7 @@ public function confirm(): void
}
}
// 4. Platform fee invoice — charged TODAY for ALL retroactive months
// 9. Platform fee invoice — charged TODAY for ALL retroactive months
$platformFeeService = app(PlatformFeeService::class);
if ($platformFeeService->isActive() && $totalRetroactiveRevenue > 0) {
$totalPlatformFee = $platformFeeService->calculate($totalRetroactiveRevenue);
......@@ -366,7 +457,7 @@ public function confirm(): void
'tax_amount' => 0,
'service_fee_amount' => $totalPlatformFee,
'due_date' => now()->addDays(7)->toDateString(),
'contact_name' => $participant->person?->name_ar,
'contact_name' => $participantPerson->name_ar,
'notes' => 'رسوم خدمة — تسجيل بأثر رجعي (' . $monthCount . ' شهور) — ' . $program->name_ar,
'metadata' => [
'retroactive_platform_fee' => true,
......@@ -390,7 +481,7 @@ public function confirm(): void
}
}
// 5. Set next_billing_date to 1st of next month
// 10. Set next_billing_date to 1st of next month
$enrollment->update([
'next_billing_date' => now()->addMonth()->startOfMonth()->toDateString(),
'last_billed_at' => now()->toDateString(),
......@@ -399,9 +490,10 @@ public function confirm(): void
$this->invoices_created = $invoicesCreated;
$this->payments_recorded = $paymentsRecorded;
$this->participant_uuid = $participant->uuid;
$this->participant_number = $participant->participant_number;
$this->enrollment_summary = $program->name_ar;
$this->completed = true;
$this->currentStep = 5;
$this->currentStep = 6;
});
session()->flash('success', __('تم تسجيل اللاعب بنجاح بأثر رجعي'));
......@@ -415,25 +507,6 @@ public function confirm(): void
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();
......@@ -446,7 +519,6 @@ public function render()
}
return view('livewire.receptionist.retroactive-enrollment-wizard', [
'searchResults' => $searchResults,
'activities' => $activities,
'programs' => $programs,
'isSuperAdmin' => auth()->user()?->is_super_admin ?? false,
......
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
DB::statement("ALTER TABLE expenses DROP CONSTRAINT IF EXISTS expenses_category_check");
DB::statement("ALTER TABLE expenses ADD CONSTRAINT expenses_category_check CHECK (category IN ('maintenance', 'supplies', 'transport', 'food_beverage', 'sports_equipment', 'printing', 'cleaning', 'medical', 'utilities', 'other', 'external_revenue'))");
}
public function down(): void
{
DB::statement("ALTER TABLE expenses DROP CONSTRAINT IF EXISTS expenses_category_check");
DB::statement("ALTER TABLE expenses ADD CONSTRAINT expenses_category_check CHECK (category IN ('maintenance', 'supplies', 'transport', 'food_beverage', 'sports_equipment', 'printing', 'cleaning', 'medical', 'utilities', 'other'))");
}
};
......@@ -31,6 +31,7 @@ public function run(): void
['code' => '4030', 'name' => 'Facility Rental', 'name_ar' => 'إيجار الملاعب', 'type' => 'revenue', 'category' => 'operating', 'is_system' => true],
['code' => '4040', 'name' => 'Private Sessions', 'name_ar' => 'الحصص الخاصة', 'type' => 'revenue', 'category' => 'operating', 'is_system' => true],
['code' => '4050', 'name' => 'Tournament Fees', 'name_ar' => 'رسوم البطولات', 'type' => 'revenue', 'category' => 'operating', 'is_system' => true],
['code' => '4060', 'name' => 'External Revenue', 'name_ar' => 'إيرادات خارجية', 'type' => 'revenue', 'category' => 'operating', 'is_system' => true],
// Expenses
['code' => '5000', 'name' => 'Trainer Salaries', 'name_ar' => 'رواتب المدربين', 'type' => 'expense', 'category' => 'operating', 'is_system' => true],
......
<div>
@if($totalOverdue > 0)
<div class="bg-gradient-to-l from-red-50 to-red-100 rounded-xl shadow-sm border-2 border-red-300 p-5 mb-6">
{{-- Header --}}
<div class="flex items-center justify-between mb-4">
<div class="bg-gradient-to-l from-red-50 to-red-100 rounded-xl shadow-sm border border-red-200 p-4 mb-4">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div class="flex items-center gap-3">
<div class="w-12 h-12 bg-red-200 rounded-xl flex items-center justify-center animate-pulse">
<svg class="w-7 h-7 text-red-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="w-10 h-10 bg-red-200 rounded-lg flex items-center justify-center shrink-0">
<svg class="w-5 h-5 text-red-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg>
</div>
<div>
<h2 class="text-lg font-bold text-red-900">{{ __('اشتراكات لم تُجدد') }}</h2>
<p class="text-sm text-red-700">
<span class="font-bold text-xl">{{ $totalOverdue }}</span>
{{ __('مشترك لم يجدد اشتراكه حتى الآن') }}
<h2 class="text-sm font-bold text-red-900">{{ __('اشتراكات لم تُجدد') }}</h2>
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 mt-0.5">
<span class="text-lg font-bold text-red-800">{{ $totalOverdue }}</span>
<span class="text-xs text-red-700">{{ __('مشترك') }}</span>
@if($totalAmount > 0)
<span class="font-bold">{{ number_format($totalAmount / 100, 2) }} {{ __('ج.م') }}</span> {{ __('مستحقة') }}
<span class="text-sm font-bold text-red-800">{{ number_format($totalAmount / 100, 0) }} {{ __('ج.م') }}</span>
<span class="text-xs text-red-600">{{ __('مستحقة') }}</span>
@endif
</div>
<p class="text-[11px] text-red-600 mt-0.5">
{{ $invoiceCount }} {{ __('فاتورة غير مدفوعة') }}
&bull;
{{ $enrollmentCount }} {{ __('بدون فاتورة') }}
</p>
</div>
</div>
<a href="{{ route('receptionist.collect-payment') }}" wire:navigate
class="inline-flex items-center gap-2 px-5 py-3 bg-red-700 text-white rounded-xl hover:bg-red-800 text-sm font-bold transition-colors shadow-sm">
<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 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
{{ __('تحصيل مدفوعات') }}
</a>
<div class="flex items-center gap-2">
<button wire:click="toggleList"
class="inline-flex items-center gap-1.5 px-3 py-2 text-red-700 bg-white border border-red-200 rounded-lg hover:bg-red-50 text-xs font-medium transition-colors">
<svg class="w-3.5 h-3.5 transition-transform {{ $showList ? '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>
{{ $showList ? __('إخفاء') : __('عرض التفاصيل') }}
</button>
<a href="{{ route('receptionist.collect-payment') }}" wire:navigate
class="inline-flex items-center gap-1.5 px-3 py-2 bg-red-700 text-white rounded-lg hover:bg-red-800 text-xs font-bold transition-colors">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8V7m0 10v1"/>
</svg>
{{ __('تحصيل مدفوعات') }}
</a>
</div>
</div>
</div>
{{-- Participants list --}}
<div class="bg-white rounded-xl border border-red-200 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-red-50 border-b border-red-200">
<tr>
<th class="text-start px-4 py-2.5 font-semibold text-red-800">{{ __('المشترك') }}</th>
<th class="text-start px-4 py-2.5 font-semibold text-red-800">{{ __('البرنامج') }}</th>
<th class="text-center px-4 py-2.5 font-semibold text-red-800">{{ __('المبلغ') }}</th>
<th class="text-center px-4 py-2.5 font-semibold text-red-800">{{ __('تأخير') }}</th>
<th class="text-center px-4 py-2.5 font-semibold text-red-800">{{ __('إجراء') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-red-100">
@foreach($items as $item)
<tr class="hover:bg-red-50 transition-colors">
<td class="px-4 py-3">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-full {{ $item['type'] === 'invoice' ? 'bg-red-100' : 'bg-amber-100' }} flex items-center justify-center shrink-0">
<svg class="w-4 h-4 {{ $item['type'] === 'invoice' ? 'text-red-600' : 'text-amber-600' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
</div>
<div class="min-w-0">
<p class="font-medium text-gray-800 truncate">{{ $item['participant_name'] }}</p>
@if($item['participant_phone'])
<p class="text-xs text-gray-500" dir="ltr">{{ $item['participant_phone'] }}</p>
@endif
</div>
</div>
</td>
<td class="px-4 py-3 text-gray-700">{{ $item['program_name'] }}</td>
<td class="px-4 py-3 text-center">
@if($item['amount'])
<span class="font-bold text-red-700">{{ number_format($item['amount'] / 100, 2) }}</span>
<span class="text-xs text-gray-500">{{ __('ج.م') }}</span>
@else
<span class="text-xs text-amber-600 font-medium">{{ __('بدون فاتورة') }}</span>
@endif
</td>
<td class="px-4 py-3 text-center">
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-bold
{{ $item['days_overdue'] > 7 ? 'bg-red-200 text-red-800' : 'bg-amber-100 text-amber-800' }}">
{{ $item['days_overdue'] }} {{ __('يوم') }}
</span>
</td>
<td class="px-4 py-3 text-center">
@if($item['participant_uuid'])
<a href="{{ route('receptionist.collect-payment') }}?participant={{ $item['participant_uuid'] }}" wire:navigate
class="inline-flex items-center gap-1 px-3 py-1.5 bg-green-600 text-white rounded-lg hover:bg-green-700 text-xs font-bold transition-colors">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8V7m0 10v1"/>
{{-- Expandable Detail List --}}
@if($showList)
<div class="bg-white rounded-xl shadow-sm border border-red-200 overflow-hidden mb-4" wire:loading.class="opacity-50" wire:target="toggleList">
<table class="w-full text-sm">
<thead class="bg-red-50 border-b border-red-200">
<tr>
<th class="text-start px-4 py-2.5 font-semibold text-red-800">{{ __('المشترك') }}</th>
<th class="text-start px-4 py-2.5 font-semibold text-red-800">{{ __('البرنامج') }}</th>
<th class="text-center px-4 py-2.5 font-semibold text-red-800">{{ __('المبلغ') }}</th>
<th class="text-center px-4 py-2.5 font-semibold text-red-800">{{ __('تأخير') }}</th>
<th class="text-center px-4 py-2.5 font-semibold text-red-800">{{ __('إجراء') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-red-100">
@foreach($items as $item)
<tr class="hover:bg-red-50/50 transition-colors">
<td class="px-4 py-2.5">
<div class="flex items-center gap-2">
<div class="w-7 h-7 rounded-full {{ $item['type'] === 'invoice' ? 'bg-red-100' : 'bg-amber-100' }} flex items-center justify-center shrink-0">
<svg class="w-3.5 h-3.5 {{ $item['type'] === 'invoice' ? 'text-red-600' : 'text-amber-600' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
{{ __('جدّد') }}
</a>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{-- Show more / less --}}
@if($totalOverdue > 15)
<div class="mt-3 text-center">
<button wire:click="toggleExpanded" class="text-sm text-red-700 hover:text-red-900 font-medium">
@if($expanded)
{{ __('عرض أقل') }}
@else
{{ __('عرض الكل') }} ({{ $totalOverdue }})
@endif
</button>
</div>
@endif
</div>
<div class="min-w-0">
<p class="font-medium text-gray-800 truncate text-xs">{{ $item['participant_name'] }}</p>
@if($item['participant_phone'])
<p class="text-[10px] text-gray-500" dir="ltr">{{ $item['participant_phone'] }}</p>
@endif
</div>
</div>
</td>
<td class="px-4 py-2.5 text-gray-700 text-xs">{{ $item['program_name'] }}</td>
<td class="px-4 py-2.5 text-center">
@if($item['amount'])
<span class="font-bold text-red-700 text-xs">{{ number_format($item['amount'] / 100, 0) }}</span>
<span class="text-[10px] text-gray-500">{{ __('ج.م') }}</span>
@else
<span class="text-[10px] text-amber-600 font-medium">{{ __('بدون فاتورة') }}</span>
@endif
</td>
<td class="px-4 py-2.5 text-center">
<span class="inline-flex items-center px-1.5 py-0.5 rounded-full text-[10px] font-bold
{{ $item['days_overdue'] > 7 ? 'bg-red-200 text-red-800' : 'bg-amber-100 text-amber-800' }}">
{{ $item['days_overdue'] }} {{ __('يوم') }}
</span>
</td>
<td class="px-4 py-2.5 text-center">
@if($item['participant_uuid'])
<a href="{{ route('receptionist.collect-payment') }}?participant={{ $item['participant_uuid'] }}" wire:navigate
class="inline-flex items-center gap-1 px-2.5 py-1 bg-green-600 text-white rounded-lg hover:bg-green-700 text-[10px] font-bold transition-colors">
{{ __('جدّد') }}
</a>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
@endif
</div>
<div>
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4 sm:mb-6">
<h1 class="text-xl sm:text-2xl font-bold text-gray-800">{{ __('تسجيل إيراد خارجي') }}</h1>
<a href="{{ route('financial.overview') }}" wire:navigate
class="inline-flex items-center gap-1 text-sm text-gray-600 hover:text-gray-800">
<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="M11 17l-5-5m0 0l5-5m-5 5h12"/></svg>
{{ __('النظرة المالية') }}
</a>
</div>
@if(session('error'))
<div class="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">{{ session('error') }}</div>
@endif
<form wire:submit="save" class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6">
{{-- Source --}}
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('مصدر الإيراد') }} <span class="text-red-500">*</span></label>
<input type="text" wire:model="source"
class="w-full rounded-lg border-gray-300 text-sm py-2.5"
placeholder="{{ __('مثال: النادي الأهلي، شركة راعية') }}">
@error('source') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
{{-- Amount --}}
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('المبلغ (ج.م)') }} <span class="text-red-500">*</span></label>
<input type="number" wire:model="amount_display" dir="ltr" step="0.01" min="0"
class="w-full rounded-lg border-gray-300 text-sm py-2.5" placeholder="0.00">
@error('amount_display') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
{{-- Description --}}
<div class="md:col-span-2">
<label class="block text-sm text-gray-600 mb-1">{{ __('الوصف') }} <span class="text-red-500">*</span></label>
<input type="text" wire:model="description"
class="w-full rounded-lg border-gray-300 text-sm py-2.5"
placeholder="{{ __('مثال: دفعة النادي عن شهر يوليو') }}">
@error('description') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
{{-- Payment Method --}}
<div class="md:col-span-2">
<label class="block text-sm text-gray-600 mb-2">{{ __('طريقة الاستلام') }} <span class="text-red-500">*</span></label>
<div class="grid grid-cols-2 sm:grid-cols-5 gap-2">
<label class="relative cursor-pointer">
<input type="radio" wire:model="payment_method" value="cash" class="peer sr-only">
<div class="peer-checked:ring-2 peer-checked:ring-green-500 peer-checked:border-green-500 peer-checked:bg-green-50 flex flex-col items-center gap-1.5 p-3 rounded-xl border-2 border-gray-200 hover:border-green-300 transition-all">
<svg class="w-7 h-7 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z"/>
</svg>
<span class="text-xs font-medium text-gray-700">{{ __('كاش') }}</span>
</div>
</label>
<label class="relative cursor-pointer">
<input type="radio" wire:model="payment_method" value="bank_transfer" class="peer sr-only">
<div class="peer-checked:ring-2 peer-checked:ring-purple-500 peer-checked:border-purple-500 peer-checked:bg-purple-50 flex flex-col items-center gap-1.5 p-3 rounded-xl border-2 border-gray-200 hover:border-purple-300 transition-all">
<svg class="w-7 h-7 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 14v3m4-3v3m4-3v3M3 21h18M3 10h18M3 7l9-4 9 4M4 10h16v11H4V10z"/>
</svg>
<span class="text-xs font-medium text-gray-700">{{ __('تحويل بنكي') }}</span>
</div>
</label>
<label class="relative cursor-pointer">
<input type="radio" wire:model="payment_method" value="cheque" class="peer sr-only">
<div class="peer-checked:ring-2 peer-checked:ring-amber-500 peer-checked:border-amber-500 peer-checked:bg-amber-50 flex flex-col items-center gap-1.5 p-3 rounded-xl border-2 border-gray-200 hover:border-amber-300 transition-all">
<svg class="w-7 h-7 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
<span class="text-xs font-medium text-gray-700">{{ __('شيك') }}</span>
</div>
</label>
<label class="relative cursor-pointer">
<input type="radio" wire:model="payment_method" value="other" class="peer sr-only">
<div class="peer-checked:ring-2 peer-checked:ring-gray-500 peer-checked:border-gray-500 peer-checked:bg-gray-50 flex flex-col items-center gap-1.5 p-3 rounded-xl border-2 border-gray-200 hover:border-gray-300 transition-all">
<svg class="w-7 h-7 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h.01M12 12h.01M19 12h.01M6 12a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0z"/>
</svg>
<span class="text-xs font-medium text-gray-700">{{ __('أخرى') }}</span>
</div>
</label>
</div>
@error('payment_method') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
{{-- Reference Number --}}
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('رقم المرجع / الإيصال') }}</label>
<input type="text" wire:model="reference_number" dir="ltr"
class="w-full rounded-lg border-gray-300 text-sm py-2.5">
@error('reference_number') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
{{-- Date --}}
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('تاريخ الإيراد') }} <span class="text-red-500">*</span></label>
<input type="date" wire:model="revenue_date" dir="ltr"
class="w-full rounded-lg border-gray-300 text-sm py-2.5">
@error('revenue_date') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
{{-- Notes --}}
<div class="md:col-span-2">
<label class="block text-sm text-gray-600 mb-1">{{ __('ملاحظات') }}</label>
<textarea wire:model="notes" rows="2"
class="w-full rounded-lg border-gray-300 text-sm py-2.5"
placeholder="{{ __('أي تفاصيل إضافية...') }}"></textarea>
</div>
</div>
{{-- Submit --}}
<div class="mt-6 flex justify-end">
<button type="submit" wire:loading.attr="disabled" wire:target="save"
class="inline-flex items-center gap-2 px-6 py-2.5 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition text-sm font-medium disabled:opacity-50">
<span wire:loading.remove wire:target="save">{{ __('تسجيل الإيراد') }}</span>
<span wire:loading wire:target="save">{{ __('جارٍ الحفظ...') }}</span>
</button>
</div>
</form>
</div>
......@@ -23,6 +23,26 @@
</div>
</div>
{{-- Quick Actions --}}
@can('expenses.create')
<div class="flex flex-wrap gap-2 mb-4">
<a href="{{ route('expenses.create') }}" wire:navigate
class="inline-flex items-center gap-1.5 px-3 py-2 bg-red-50 border border-red-200 text-red-700 rounded-lg hover:bg-red-100 text-xs 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="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
{{ __('تسجيل مصروف') }}
</a>
<a href="{{ route('revenue.external.create') }}" wire:navigate
class="inline-flex items-center gap-1.5 px-3 py-2 bg-emerald-50 border border-emerald-200 text-emerald-700 rounded-lg hover:bg-emerald-100 text-xs 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="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
{{ __('تسجيل إيراد خارجي') }}
</a>
</div>
@endcan
{{-- Loading overlay --}}
<div wire:loading.class="opacity-50 pointer-events-none" class="transition-opacity">
......
......@@ -3,7 +3,7 @@
<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>
<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">
......@@ -23,15 +23,16 @@ class="inline-flex items-center gap-2 px-4 py-2 text-gray-600 bg-gray-100 rounde
@endif
{{-- Step Indicator --}}
@if($currentStep < 5)
@if($currentStep < 6)
<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 => 'الدفع والتأكيد',
1 => 'بيانات اللاعب',
2 => 'ولي الأمر',
3 => 'اختيار البرنامج',
4 => 'الشهور المستحقة',
5 => 'الدفع والتأكيد',
];
@endphp
@foreach($steps as $num => $label)
......@@ -69,64 +70,144 @@ class="flex items-center gap-2 {{ $num < $currentStep ? 'cursor-pointer' : 'curs
{{-- Step Content --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6">
{{-- Step 1: Select Participant --}}
{{-- Step 1: Participant Info --}}
@if($currentStep === 1)
<div>
<h2 class="text-lg font-semibold text-gray-800 mb-6">{{ __('اختيار المشترك') }}</h2>
<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>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
{{-- Name --}}
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('اسم اللاعب بالكامل') }} *</label>
<input type="text" wire:model="participant_name_ar"
class="w-full px-4 py-3 min-h-14 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-base @error('participant_name_ar') border-red-500 @enderror"
placeholder="{{ __('الاسم رباعي') }}">
@error('participant_name_ar') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</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>
{{-- National ID --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('الرقم القومي') }}</label>
<input type="text" wire:model.live.debounce.500ms="participant_national_id" dir="ltr" maxlength="14"
class="w-full px-4 py-3 min-h-14 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 text-base font-mono @error('participant_national_id') border-red-500 @enderror"
placeholder="29901011234567">
@if($participant_nid_decoded)
<p class="mt-1 text-xs text-green-600">{{ __('تم استخراج تاريخ الميلاد والنوع تلقائياً') }}</p>
@endif
@error('participant_national_id') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
{{-- Date of Birth --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('تاريخ الميلاد') }}</label>
<input type="date" wire:model="participant_date_of_birth" dir="ltr"
class="w-full px-4 py-3 min-h-14 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 text-base @error('participant_date_of_birth') border-red-500 @enderror"
{{ $participant_nid_decoded ? 'disabled' : '' }}>
@error('participant_date_of_birth') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
{{-- Gender --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __('النوع') }} *</label>
<div class="flex gap-4">
<label class="relative cursor-pointer flex-1">
<input type="radio" wire:model="participant_gender" value="male" class="peer sr-only" {{ $participant_nid_decoded ? 'disabled' : '' }}>
<div class="p-3 min-h-14 flex items-center justify-center border border-gray-300 rounded-xl transition-all text-sm font-medium
peer-checked:border-indigo-500 peer-checked:bg-indigo-50 peer-checked:text-indigo-700 hover:border-gray-400">
{{ __('ذكر') }}
</div>
</label>
<label class="relative cursor-pointer flex-1">
<input type="radio" wire:model="participant_gender" value="female" class="peer sr-only" {{ $participant_nid_decoded ? 'disabled' : '' }}>
<div class="p-3 min-h-14 flex items-center justify-center border border-gray-300 rounded-xl transition-all text-sm font-medium
peer-checked:border-indigo-500 peer-checked:bg-indigo-50 peer-checked:text-indigo-700 hover:border-gray-400">
{{ __('أنثى') }}
</div>
</label>
</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">
{{ __('تغيير') }}
{{-- Phone --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('هاتف اللاعب') }}</label>
<input type="tel" wire:model="participant_phone" dir="ltr"
class="w-full px-4 py-3 min-h-14 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 text-base"
placeholder="01012345678">
</div>
{{-- Medical Notes --}}
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('ملاحظات طبية') }}</label>
<textarea wire:model="participant_medical_notes" rows="2"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 text-sm"
placeholder="{{ __('حساسية، إصابات سابقة، أمراض مزمنة...') }}"></textarea>
</div>
</div>
<div class="flex justify-end mt-8">
<button wire:click="nextStep" @if(!$participant_name_ar) disabled @endif
class="inline-flex items-center gap-2 px-6 py-3 min-h-14 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>
@endif
</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>
{{-- Step 2: Guardian Info --}}
@if($currentStep === 2)
<div>
<h2 class="text-lg font-semibold text-gray-800 mb-6">{{ __('بيانات ولي الأمر') }}</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
{{-- Guardian Name --}}
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('اسم ولي الأمر') }} *</label>
<input type="text" wire:model="guardian_name_ar"
class="w-full px-4 py-3 min-h-14 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-base @error('guardian_name_ar') border-red-500 @enderror"
placeholder="{{ __('الاسم بالكامل') }}">
@error('guardian_name_ar') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
{{-- Guardian Phone --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('هاتف ولي الأمر') }} *</label>
<input type="tel" wire:model="guardian_phone" dir="ltr"
class="w-full px-4 py-3 min-h-14 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 text-base @error('guardian_phone') border-red-500 @enderror"
placeholder="01012345678">
@error('guardian_phone') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
{{-- Relation --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __('صلة القرابة') }} *</label>
<div class="grid grid-cols-3 gap-2">
@foreach(['father' => 'أب', 'mother' => 'أم', 'brother' => 'أخ', 'sister' => 'أخت', 'uncle' => 'عم/خال', 'other' => 'أخرى'] as $key => $label)
<label class="relative cursor-pointer">
<input type="radio" wire:model="guardian_relation" value="{{ $key }}" class="peer sr-only">
<div class="p-2.5 flex items-center justify-center border border-gray-300 rounded-lg 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>
<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>
@error('guardian_relation') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</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">
<div class="flex justify-between mt-8">
<button wire:click="previousStep"
class="inline-flex items-center gap-2 px-6 py-3 min-h-14 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(!$guardian_name_ar || !$guardian_phone) disabled @endif
class="inline-flex items-center gap-2 px-6 py-3 min-h-14 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"/>
......@@ -136,8 +217,8 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-indigo-600 text-whit
</div>
@endif
{{-- Step 2: Program Selection --}}
@if($currentStep === 2)
{{-- Step 3: Program Selection --}}
@if($currentStep === 3)
<div>
<h2 class="text-lg font-semibold text-gray-800 mb-6">{{ __('اختيار البرنامج') }}</h2>
......@@ -148,7 +229,7 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-indigo-600 text-whit
@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
<div class="p-4 min-h-14 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>
......@@ -169,7 +250,7 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-indigo-600 text-whit
@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
<div class="p-4 min-h-14 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)
......@@ -186,14 +267,14 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-indigo-600 text-whit
<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">
class="inline-flex items-center gap-2 px-6 py-3 min-h-14 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">
class="inline-flex items-center gap-2 px-6 py-3 min-h-14 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"/>
......@@ -203,8 +284,8 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-indigo-600 text-whit
</div>
@endif
{{-- Step 3: Months Owed --}}
@if($currentStep === 3)
{{-- Step 4: Months Owed --}}
@if($currentStep === 4)
<div>
<h2 class="text-lg font-semibold text-gray-800 mb-6">{{ __('الشهور المستحقة') }}</h2>
......@@ -311,14 +392,14 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:rin
<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">
class="inline-flex items-center gap-2 px-6 py-3 min-h-14 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">
class="inline-flex items-center gap-2 px-6 py-3 min-h-14 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"/>
......@@ -328,16 +409,20 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-indigo-600 text-whit
</div>
@endif
{{-- Step 4: Payment & Confirm --}}
@if($currentStep === 4)
{{-- Step 5: Payment & Confirm --}}
@if($currentStep === 5)
<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>
<span class="text-gray-600">{{ __('اللاعب') }}</span>
<span class="font-medium">{{ $participant_name_ar }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600">{{ __('ولي الأمر') }}</span>
<span class="font-medium">{{ $guardian_name_ar }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600">{{ __('تاريخ البدء') }}</span>
......@@ -359,7 +444,7 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-indigo-600 text-whit
@if($platformFee->isActive())
<div class="border-t border-gray-200 pt-3 flex items-center justify-between text-sm">
<span class="text-gray-500">{{ __('رسوم خدمة المنصة') }} ({{ $platformFee->getPercentage() }}%)</span>
<span class="font-medium text-gray-600" dir="ltr">{{ number_format($platformFee->calculate(count($monthStatuses) * $monthlyPrice) / 100, 2) }} {{ __('ج.م') }}</span>
<span class="font-medium text-gray-600" dir="ltr">{{ number_format($platformFee->calculate(count($monthStatuses) * $this->monthlyPrice) / 100, 2) }} {{ __('ج.م') }}</span>
</div>
<p class="text-xs text-gray-400 mt-1">{{ __('تُحسب على كل الشهور وتُسجَّل في الشهر الحالي') }}</p>
@endif
......@@ -391,14 +476,14 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-indigo-600 text-whit
<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">
class="inline-flex items-center gap-2 px-6 py-3 min-h-14 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">
class="inline-flex items-center gap-2 px-8 py-3 min-h-14 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">
......@@ -412,8 +497,8 @@ class="inline-flex items-center gap-2 px-8 py-3 min-h-16 bg-green-600 text-white
</div>
@endif
{{-- Step 5: Success --}}
@if($currentStep === 5)
{{-- Step 6: Success --}}
@if($currentStep === 6)
<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">
......@@ -422,14 +507,20 @@ class="inline-flex items-center gap-2 px-8 py-3 min-h-16 bg-green-600 text-white
</div>
<h2 class="text-2xl font-bold text-green-700 mb-2">{{ __('تم التسجيل بنجاح!') }}</h2>
<p class="text-gray-600 mb-6">{{ __('تم تسجيل اللاعب بأثر رجعي وإنشاء الفواتير') }}</p>
<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>
<p class="text-xs text-gray-500">{{ __('اللاعب') }}</p>
<p class="font-medium text-gray-800">{{ $participant_name_ar }}</p>
</div>
@if($participant_number)
<div>
<p class="text-xs text-gray-500">{{ __('رقم المشترك') }}</p>
<p class="font-medium text-gray-800" dir="ltr">{{ $participant_number }}</p>
</div>
@endif
<div>
<p class="text-xs text-gray-500">{{ __('البرنامج') }}</p>
<p class="font-medium text-gray-800">{{ $enrollment_summary }}</p>
......@@ -452,12 +543,12 @@ class="inline-flex items-center gap-2 px-8 py-3 min-h-16 bg-green-600 text-white
<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">
class="inline-flex items-center gap-2 px-6 py-3 min-h-14 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">
class="inline-flex items-center gap-2 px-6 py-3 min-h-14 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 text-base font-medium">
{{ __('تسجيل آخر') }}
</a>
</div>
......
......@@ -253,6 +253,8 @@
->middleware('permission:facilities.update');
Route::get('/expenses/rent/create', \App\Livewire\Financial\FacilityRentForm::class)->name('expenses.rent.create')
->middleware('permission:facilities.update');
Route::get('/revenue/external', \App\Livewire\Financial\ExternalRevenueForm::class)->name('revenue.external.create')
->middleware('permission:expenses.create');
// Invoices
Route::get('/invoices', InvoiceList::class)->name('invoices.list')
......
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