Commit 44c8d8aa authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add Events/Tournaments system — full vertical slice

Complete events module with admin CRUD, public registration, and management:
- Migrations: events, event_registrations, media collections, permissions
- Domain: Event/Enums (5), Models (2), Services (EventService, EventRegistrationService)
- Admin: EventList, CreateEventWizard (4-step with form builder), EventShow, EventRegistrationList
- Public: event listing + detail pages under /site/{slug}/events with Livewire registration form
- Form builder: button-based add/remove/reorder fields stored as JSONB
- Registration: dynamic validation from form_fields, person matching, registration numbers
- Admin registrations: dynamic columns, bulk confirm/cancel, CSV export
- Livewire scripts added to website layout for public forms
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 72872427
<?php
namespace App\Domain\Event\Enums;
enum EventStatus: string
{
case Draft = 'draft';
case Published = 'published';
case RegistrationClosed = 'registration_closed';
case InProgress = 'in_progress';
case Completed = 'completed';
case Cancelled = 'cancelled';
public function label(): string
{
return match ($this) {
self::Draft => 'مسودة',
self::Published => 'منشور',
self::RegistrationClosed => 'التسجيل مغلق',
self::InProgress => 'جارٍ',
self::Completed => 'مكتمل',
self::Cancelled => 'ملغي',
};
}
public function color(): string
{
return match ($this) {
self::Draft => 'gray',
self::Published => 'green',
self::RegistrationClosed => 'yellow',
self::InProgress => 'blue',
self::Completed => 'purple',
self::Cancelled => 'red',
};
}
public function canTransitionTo(self $target): bool
{
return match ($this) {
self::Draft => in_array($target, [self::Published, self::Cancelled]),
self::Published => in_array($target, [self::RegistrationClosed, self::InProgress, self::Cancelled]),
self::RegistrationClosed => in_array($target, [self::InProgress, self::Published, self::Cancelled]),
self::InProgress => in_array($target, [self::Completed, self::Cancelled]),
self::Completed, self::Cancelled => false,
};
}
}
<?php
namespace App\Domain\Event\Enums;
enum EventType: string
{
case Tournament = 'tournament';
case Camp = 'camp';
case Competition = 'competition';
case OpenDay = 'open_day';
case Workshop = 'workshop';
case Friendly = 'friendly';
case Exhibition = 'exhibition';
case Other = 'other';
public function label(): string
{
return match ($this) {
self::Tournament => 'بطولة',
self::Camp => 'معسكر',
self::Competition => 'مسابقة',
self::OpenDay => 'يوم مفتوح',
self::Workshop => 'ورشة عمل',
self::Friendly => 'ودية',
self::Exhibition => 'عرض',
self::Other => 'أخرى',
};
}
public function icon(): string
{
return match ($this) {
self::Tournament => 'trophy',
self::Camp => 'fire',
self::Competition => 'bolt',
self::OpenDay => 'sun',
self::Workshop => 'academic-cap',
self::Friendly => 'hand-raised',
self::Exhibition => 'eye',
self::Other => 'calendar',
};
}
}
<?php
namespace App\Domain\Event\Enums;
enum FormFieldType: string
{
case Text = 'text';
case Textarea = 'textarea';
case Number = 'number';
case Email = 'email';
case Phone = 'phone';
case Date = 'date';
case Select = 'select';
case MultiSelect = 'multi_select';
case Checkbox = 'checkbox';
case FileUpload = 'file_upload';
public function label(): string
{
return match ($this) {
self::Text => 'نص قصير',
self::Textarea => 'نص طويل',
self::Number => 'رقم',
self::Email => 'بريد إلكتروني',
self::Phone => 'هاتف',
self::Date => 'تاريخ',
self::Select => 'قائمة اختيار',
self::MultiSelect => 'اختيار متعدد',
self::Checkbox => 'خانة اختيار',
self::FileUpload => 'رفع ملف',
};
}
public function icon(): string
{
return match ($this) {
self::Text => 'bars-3-bottom-left',
self::Textarea => 'document-text',
self::Number => 'hashtag',
self::Email => 'at-symbol',
self::Phone => 'phone',
self::Date => 'calendar',
self::Select => 'chevron-down',
self::MultiSelect => 'list-bullet',
self::Checkbox => 'check-circle',
self::FileUpload => 'arrow-up-tray',
};
}
public function baseValidation(): string
{
return match ($this) {
self::Text => 'string|max:500',
self::Textarea => 'string|max:5000',
self::Number => 'numeric',
self::Email => 'email|max:255',
self::Phone => 'string|max:20',
self::Date => 'date',
self::Select => 'string',
self::MultiSelect => 'array',
self::Checkbox => 'boolean',
self::FileUpload => 'file|max:5120',
};
}
}
<?php
namespace App\Domain\Event\Enums;
enum LocationType: string
{
case Facility = 'facility';
case External = 'external';
public function label(): string
{
return match ($this) {
self::Facility => 'منشأة داخلية',
self::External => 'موقع خارجي',
};
}
}
<?php
namespace App\Domain\Event\Enums;
enum RegistrationStatus: string
{
case Pending = 'pending';
case Confirmed = 'confirmed';
case Cancelled = 'cancelled';
case Attended = 'attended';
case NoShow = 'no_show';
public function label(): string
{
return match ($this) {
self::Pending => 'قيد المراجعة',
self::Confirmed => 'مؤكد',
self::Cancelled => 'ملغي',
self::Attended => 'حضر',
self::NoShow => 'لم يحضر',
};
}
public function color(): string
{
return match ($this) {
self::Pending => 'yellow',
self::Confirmed => 'green',
self::Cancelled => 'red',
self::Attended => 'blue',
self::NoShow => 'gray',
};
}
}
<?php
namespace App\Domain\Event\Models;
use App\Domain\Event\Enums\EventStatus;
use App\Domain\Event\Enums\EventType;
use App\Domain\Event\Enums\LocationType;
use App\Domain\Facility\Models\Facility;
use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\Shared\Traits\HasUuid;
use App\Domain\Website\Models\Media;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\SoftDeletes;
class Event extends Model
{
use BelongsToAcademy, HasUuid, SoftDeletes;
protected $fillable = [
'academy_id',
'title',
'title_en',
'slug',
'description',
'description_en',
'type',
'status',
'location_type',
'facility_id',
'location_name',
'location_address',
'starts_at',
'ends_at',
'registration_opens_at',
'registration_closes_at',
'max_capacity',
'form_fields',
'settings',
'registrations_count',
'published_at',
'created_by',
];
protected $casts = [
'type' => EventType::class,
'status' => EventStatus::class,
'location_type' => LocationType::class,
'starts_at' => 'datetime',
'ends_at' => 'datetime',
'registration_opens_at' => 'datetime',
'registration_closes_at' => 'datetime',
'published_at' => 'datetime',
'max_capacity' => 'integer',
'registrations_count' => 'integer',
'form_fields' => 'array',
'settings' => 'array',
];
public function facility(): BelongsTo
{
return $this->belongsTo(Facility::class);
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function registrations(): HasMany
{
return $this->hasMany(EventRegistration::class);
}
public function cover(): MorphOne
{
return $this->morphOne(Media::class, 'mediable')->where('collection', 'event_cover');
}
public function gallery(): MorphMany
{
return $this->morphMany(Media::class, 'mediable')->where('collection', 'event_gallery')->orderBy('sort_order');
}
public function scopePublished($query)
{
return $query->whereNotIn('status', ['draft', 'cancelled']);
}
public function scopeUpcoming($query)
{
return $query->where('starts_at', '>', now());
}
public function scopeRegistrationOpen($query)
{
return $query->where('status', 'published')
->where(function ($q) {
$q->whereNull('registration_opens_at')->orWhere('registration_opens_at', '<=', now());
})
->where(function ($q) {
$q->whereNull('registration_closes_at')->orWhere('registration_closes_at', '>=', now());
});
}
public function isRegistrationOpen(): bool
{
if ($this->status !== EventStatus::Published) {
return false;
}
if ($this->registration_opens_at && now()->lt($this->registration_opens_at)) {
return false;
}
if ($this->registration_closes_at && now()->gt($this->registration_closes_at)) {
return false;
}
return $this->hasCapacity();
}
public function hasCapacity(): bool
{
if ($this->max_capacity === null) {
return true;
}
return $this->registrations_count < $this->max_capacity;
}
public function spotsRemaining(): ?int
{
if ($this->max_capacity === null) {
return null;
}
return max(0, $this->max_capacity - $this->registrations_count);
}
public function getPublicUrl(): string
{
$academy = $this->academy ?? app('current_academy');
return url("/site/{$academy->slug}/events/{$this->slug}");
}
}
<?php
namespace App\Domain\Event\Models;
use App\Domain\Event\Enums\RegistrationStatus;
use App\Domain\Identity\Models\Person;
use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\Shared\Traits\HasUuid;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class EventRegistration extends Model
{
use BelongsToAcademy, HasUuid, SoftDeletes;
protected $fillable = [
'academy_id',
'event_id',
'person_id',
'registration_number',
'status',
'form_data',
'registrant_name',
'registrant_phone',
'registrant_email',
'admin_notes',
'confirmed_at',
'cancelled_at',
'attended_at',
'ip_address',
];
protected $casts = [
'status' => RegistrationStatus::class,
'form_data' => 'array',
'confirmed_at' => 'datetime',
'cancelled_at' => 'datetime',
'attended_at' => 'datetime',
];
public function event(): BelongsTo
{
return $this->belongsTo(Event::class);
}
public function person(): BelongsTo
{
return $this->belongsTo(Person::class);
}
}
<?php
namespace App\Domain\Event\Services;
use App\Domain\Event\Enums\FormFieldType;
use App\Domain\Event\Enums\RegistrationStatus;
use App\Domain\Event\Models\Event;
use App\Domain\Event\Models\EventRegistration;
use App\Domain\Identity\Models\Person;
use App\Domain\Shared\Exceptions\DomainException;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class EventRegistrationService
{
public function register(Event $event, array $formData, array $meta): EventRegistration
{
if (! $event->isRegistrationOpen()) {
throw new DomainException('التسجيل مغلق لهذا الحدث');
}
if (! $event->hasCapacity()) {
throw new DomainException('تم اكتمال العدد المسموح به');
}
return DB::transaction(function () use ($event, $formData, $meta) {
$personId = $this->matchPerson($meta['phone'], $meta['email'] ?? null, $event->academy_id);
$registration = EventRegistration::create([
'academy_id' => $event->academy_id,
'event_id' => $event->id,
'person_id' => $personId,
'registration_number' => $this->generateRegistrationNumber($event),
'status' => 'pending',
'form_data' => $formData,
'registrant_name' => $meta['name'],
'registrant_phone' => $meta['phone'],
'registrant_email' => $meta['email'] ?? null,
'ip_address' => $meta['ip'] ?? null,
]);
$event->increment('registrations_count');
return $registration;
});
}
public function confirm(EventRegistration $registration, User $admin): EventRegistration
{
if ($registration->status !== RegistrationStatus::Pending) {
throw new DomainException('لا يمكن تأكيد هذا التسجيل');
}
$registration->update([
'status' => 'confirmed',
'confirmed_at' => now(),
]);
return $registration->fresh();
}
public function cancel(EventRegistration $registration, User $admin, ?string $reason = null): EventRegistration
{
if (in_array($registration->status, [RegistrationStatus::Cancelled, RegistrationStatus::Attended])) {
throw new DomainException('لا يمكن إلغاء هذا التسجيل');
}
DB::transaction(function () use ($registration, $reason) {
$registration->update([
'status' => 'cancelled',
'cancelled_at' => now(),
'admin_notes' => $reason ? ($registration->admin_notes ? $registration->admin_notes . "\n" : '') . "سبب الإلغاء: {$reason}" : $registration->admin_notes,
]);
$registration->event->decrement('registrations_count');
});
return $registration->fresh();
}
public function markAttended(EventRegistration $registration, User $admin): EventRegistration
{
if ($registration->status === RegistrationStatus::Cancelled) {
throw new DomainException('لا يمكن تسجيل حضور تسجيل ملغي');
}
$registration->update([
'status' => 'attended',
'attended_at' => now(),
]);
return $registration->fresh();
}
public function bulkConfirm(array $ids, User $admin): int
{
$count = 0;
$registrations = EventRegistration::whereIn('id', $ids)->where('status', 'pending')->get();
foreach ($registrations as $reg) {
$this->confirm($reg, $admin);
$count++;
}
return $count;
}
public function bulkCancel(array $ids, User $admin): int
{
$count = 0;
$registrations = EventRegistration::whereIn('id', $ids)
->whereNotIn('status', ['cancelled', 'attended'])
->get();
foreach ($registrations as $reg) {
$this->cancel($reg, $admin);
$count++;
}
return $count;
}
public function validateFormData(Event $event, array $submitted): array
{
$rules = [];
$messages = [];
foreach ($event->form_fields as $field) {
$key = "fields.{$field['key']}";
$fieldRules = [];
if ($field['is_required']) {
$fieldRules[] = 'required';
$messages["{$key}.required"] = "حقل {$field['label']} مطلوب";
} else {
$fieldRules[] = 'nullable';
}
$type = FormFieldType::tryFrom($field['type']);
if ($type) {
foreach (explode('|', $type->baseValidation()) as $rule) {
$fieldRules[] = $rule;
}
}
if (in_array($field['type'], ['select']) && ! empty($field['options'])) {
$values = array_column($field['options'], 'value');
$fieldRules[] = 'in:' . implode(',', $values);
}
$rules[$key] = implode('|', $fieldRules);
}
return ['rules' => $rules, 'messages' => $messages];
}
public function matchPerson(string $phone, ?string $email, int $academyId): ?int
{
$person = Person::withoutGlobalScopes()
->where('academy_id', $academyId)
->where(function ($q) use ($phone, $email) {
$q->where('phone', $phone);
if ($email) {
$q->orWhere('email', $email);
}
})
->first();
return $person?->id;
}
public function generateRegistrationNumber(Event $event): string
{
$prefix = 'EVT-' . now()->format('ym');
$lastNumber = EventRegistration::withoutGlobalScopes()
->where('academy_id', $event->academy_id)
->where('registration_number', 'like', $prefix . '%')
->max('registration_number');
if ($lastNumber) {
$seq = (int) substr($lastNumber, -4) + 1;
} else {
$seq = 1;
}
return $prefix . '-' . str_pad($seq, 4, '0', STR_PAD_LEFT);
}
}
<?php
namespace App\Domain\Event\Services;
use App\Domain\Event\Enums\EventStatus;
use App\Domain\Event\Models\Event;
use App\Domain\Shared\Exceptions\DomainException;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class EventService
{
public function create(array $data, User $actor): Event
{
return DB::transaction(function () use ($data, $actor) {
$data['created_by'] = $actor->id;
$data['status'] = 'draft';
$data['slug'] = $this->generateSlug($data['title'], $data['academy_id']);
return Event::create($data);
});
}
public function update(Event $event, array $data, User $actor): Event
{
return DB::transaction(function () use ($event, $data) {
if (isset($data['title']) && $data['title'] !== $event->title) {
$data['slug'] = $this->generateSlug($data['title'], $event->academy_id, $event->id);
}
$event->update($data);
return $event->fresh();
});
}
public function publish(Event $event, User $actor): Event
{
if ($event->status !== EventStatus::Draft) {
throw new DomainException('لا يمكن نشر حدث غير مسودة');
}
return DB::transaction(function () use ($event) {
$event->update([
'status' => 'published',
'published_at' => now(),
]);
return $event->fresh();
});
}
public function changeStatus(Event $event, EventStatus $newStatus, User $actor): Event
{
if (! $event->status->canTransitionTo($newStatus)) {
throw new DomainException("لا يمكن تغيير الحالة من '{$event->status->label()}' إلى '{$newStatus->label()}'");
}
return DB::transaction(function () use ($event, $newStatus) {
$updates = ['status' => $newStatus->value];
if ($newStatus === EventStatus::Published && ! $event->published_at) {
$updates['published_at'] = now();
}
$event->update($updates);
return $event->fresh();
});
}
public function delete(Event $event): void
{
if ($event->registrations_count > 0 && $event->status !== EventStatus::Draft) {
throw new DomainException('لا يمكن حذف حدث به تسجيلات. قم بإلغائه بدلاً من ذلك.');
}
$event->delete();
}
public function duplicate(Event $event, User $actor): Event
{
$data = $event->only([
'academy_id', 'title', 'title_en', 'description', 'description_en',
'type', 'location_type', 'facility_id', 'location_name', 'location_address',
'max_capacity', 'form_fields', 'settings',
]);
$data['title'] = $data['title'] . ' (نسخة)';
return $this->create($data, $actor);
}
public function generateSlug(string $title, int $academyId, ?int $excludeId = null): string
{
$base = Str::slug($title, '-', null) ?: Str::random(8);
$slug = $base;
$counter = 1;
while (
Event::withoutGlobalScopes()
->where('academy_id', $academyId)
->where('slug', $slug)
->when($excludeId, fn ($q) => $q->where('id', '!=', $excludeId))
->exists()
) {
$slug = $base . '-' . $counter;
$counter++;
}
return $slug;
}
}
......@@ -15,6 +15,8 @@
case NewsImage = 'news_image';
case SectionImage = 'section_image';
case General = 'general';
case EventCover = 'event_cover';
case EventGallery = 'event_gallery';
public function dimensions(): array
{
......@@ -30,6 +32,8 @@ public function dimensions(): array
self::NewsImage => ['width' => 1200, 'height' => 630],
self::SectionImage => ['width' => 800, 'height' => 600],
self::General => ['width' => 1200, 'height' => 800],
self::EventCover => ['width' => 1920, 'height' => 600],
self::EventGallery => ['width' => 1200, 'height' => 800],
};
}
......@@ -47,6 +51,8 @@ public function aspectRatio(): string
self::NewsImage => '1.91:1',
self::SectionImage => '4:3',
self::General => '3:2',
self::EventCover => '16:5',
self::EventGallery => '3:2',
};
}
......@@ -64,6 +70,8 @@ public function maxSizeKb(): int
self::NewsImage => 1536,
self::SectionImage => 1024,
self::General => 2048,
self::EventCover => 2048,
self::EventGallery => 1536,
};
}
......@@ -81,6 +89,8 @@ public function label(): string
self::NewsImage => 'صورة الخبر',
self::SectionImage => 'صورة القسم',
self::General => 'صورة عامة',
self::EventCover => 'غلاف الحدث',
self::EventGallery => 'معرض صور الحدث',
};
}
}
<?php
namespace App\Http\Controllers;
use App\Domain\Event\Models\Event;
class PublicEventController extends Controller
{
public function index(string $slug)
{
$academy = app('current_academy');
$events = Event::withoutGlobalScopes()
->where('academy_id', $academy->id)
->whereNotIn('status', ['draft', 'cancelled'])
->with('cover')
->orderByDesc('starts_at')
->paginate(12);
$settings = app('website_settings');
return view('website.events.index', compact('academy', 'events', 'settings'));
}
public function show(string $slug, string $eventSlug)
{
$academy = app('current_academy');
$event = Event::withoutGlobalScopes()
->where('academy_id', $academy->id)
->where('slug', $eventSlug)
->where('status', '!=', 'draft')
->with(['cover', 'gallery', 'facility'])
->firstOrFail();
$settings = app('website_settings');
return view('website.events.show', compact('academy', 'event', 'settings'));
}
}
<?php
namespace App\Livewire\Events;
use App\Domain\Event\Enums\EventType;
use App\Domain\Event\Enums\FormFieldType;
use App\Domain\Event\Enums\LocationType;
use App\Domain\Event\Models\Event;
use App\Domain\Event\Services\EventService;
use App\Domain\Facility\Models\Facility;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Website\Enums\MediaCollection;
use App\Domain\Website\Services\MediaService;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
use Livewire\WithFileUploads;
#[Layout('layouts.app')]
#[Title('إنشاء حدث')]
class CreateEventWizard extends Component
{
use WithFileUploads;
public ?Event $event = null;
public bool $editing = false;
public int $currentStep = 1;
public int $totalSteps = 4;
public bool $completed = false;
// Step 1: Details
public string $title = '';
public string $titleEn = '';
public string $description = '';
public string $descriptionEn = '';
public string $type = '';
public string $locationType = 'facility';
public ?int $facilityId = null;
public string $locationName = '';
public string $locationAddress = '';
public string $startsAt = '';
public string $endsAt = '';
public string $registrationOpensAt = '';
public string $registrationClosesAt = '';
public ?int $maxCapacity = null;
// Step 2: Form Fields
public array $formFields = [];
// Step 3: Media
public $coverPhoto = null;
public array $galleryPhotos = [];
public function mount(?Event $event = null): void
{
if ($event && $event->exists) {
$this->authorize('events.update');
$this->event = $event;
$this->editing = true;
$this->fillFromEvent($event);
} else {
$this->authorize('events.create');
$this->formFields = $this->getDefaultFields();
}
}
private function fillFromEvent(Event $event): void
{
$this->title = $event->title;
$this->titleEn = $event->title_en ?? '';
$this->description = $event->description ?? '';
$this->descriptionEn = $event->description_en ?? '';
$this->type = $event->type->value;
$this->locationType = $event->location_type->value;
$this->facilityId = $event->facility_id;
$this->locationName = $event->location_name ?? '';
$this->locationAddress = $event->location_address ?? '';
$this->startsAt = $event->starts_at?->format('Y-m-d\TH:i');
$this->endsAt = $event->ends_at?->format('Y-m-d\TH:i');
$this->registrationOpensAt = $event->registration_opens_at?->format('Y-m-d\TH:i') ?? '';
$this->registrationClosesAt = $event->registration_closes_at?->format('Y-m-d\TH:i') ?? '';
$this->maxCapacity = $event->max_capacity;
$this->formFields = $event->form_fields ?: $this->getDefaultFields();
}
private function getDefaultFields(): array
{
return [
[
'key' => 'name',
'type' => 'text',
'label' => 'الاسم الكامل',
'label_en' => 'Full Name',
'placeholder' => '',
'is_required' => true,
'sort_order' => 1,
'options' => [],
'width' => 'full',
],
[
'key' => 'phone',
'type' => 'phone',
'label' => 'رقم الهاتف',
'label_en' => 'Phone',
'placeholder' => '01XXXXXXXXX',
'is_required' => true,
'sort_order' => 2,
'options' => [],
'width' => 'half',
],
[
'key' => 'email',
'type' => 'email',
'label' => 'البريد الإلكتروني',
'label_en' => 'Email',
'placeholder' => '',
'is_required' => false,
'sort_order' => 3,
'options' => [],
'width' => 'half',
],
];
}
public function nextStep(): void
{
$this->validateStep();
$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;
}
}
private function validateStep(): void
{
$rules = match ($this->currentStep) {
1 => [
'title' => 'required|string|max:255',
'type' => 'required|in:' . implode(',', array_column(EventType::cases(), 'value')),
'locationType' => 'required|in:facility,external',
'facilityId' => $this->locationType === 'facility' ? 'required|exists:facilities,id' : 'nullable',
'locationName' => $this->locationType === 'external' ? 'required|string|max:255' : 'nullable',
'startsAt' => 'required|date',
'endsAt' => 'required|date|after:startsAt',
],
2 => [],
3 => [
'coverPhoto' => 'nullable|image|max:2048',
],
default => [],
};
if (! empty($rules)) {
$this->validate($rules, [
'title.required' => 'عنوان الحدث مطلوب',
'type.required' => 'نوع الحدث مطلوب',
'locationType.required' => 'نوع الموقع مطلوب',
'facilityId.required' => 'يرجى اختيار المنشأة',
'locationName.required' => 'اسم الموقع مطلوب',
'startsAt.required' => 'تاريخ البداية مطلوب',
'endsAt.required' => 'تاريخ النهاية مطلوب',
'endsAt.after' => 'تاريخ النهاية يجب أن يكون بعد تاريخ البداية',
]);
}
}
// Form Builder Actions
public function addField(): void
{
$order = count($this->formFields) + 1;
$this->formFields[] = [
'key' => 'field_' . $order,
'type' => 'text',
'label' => '',
'label_en' => '',
'placeholder' => '',
'is_required' => false,
'sort_order' => $order,
'options' => [],
'width' => 'full',
];
}
public function removeField(int $index): void
{
unset($this->formFields[$index]);
$this->formFields = array_values($this->formFields);
$this->reorderFields();
}
public function moveFieldUp(int $index): void
{
if ($index <= 0) return;
$temp = $this->formFields[$index];
$this->formFields[$index] = $this->formFields[$index - 1];
$this->formFields[$index - 1] = $temp;
$this->reorderFields();
}
public function moveFieldDown(int $index): void
{
if ($index >= count($this->formFields) - 1) return;
$temp = $this->formFields[$index];
$this->formFields[$index] = $this->formFields[$index + 1];
$this->formFields[$index + 1] = $temp;
$this->reorderFields();
}
public function addOption(int $fieldIndex): void
{
$this->formFields[$fieldIndex]['options'][] = ['value' => '', 'label' => ''];
}
public function removeOption(int $fieldIndex, int $optionIndex): void
{
unset($this->formFields[$fieldIndex]['options'][$optionIndex]);
$this->formFields[$fieldIndex]['options'] = array_values($this->formFields[$fieldIndex]['options']);
}
private function reorderFields(): void
{
foreach ($this->formFields as $i => &$field) {
$field['sort_order'] = $i + 1;
$field['key'] = $field['key'] ?: 'field_' . ($i + 1);
}
}
public function save(EventService $eventService, MediaService $mediaService): void
{
$this->validateStep();
$academyId = app('current_academy')->id;
$data = [
'academy_id' => $academyId,
'title' => $this->title,
'title_en' => $this->titleEn ?: null,
'description' => $this->description ?: null,
'description_en' => $this->descriptionEn ?: null,
'type' => $this->type,
'location_type' => $this->locationType,
'facility_id' => $this->locationType === 'facility' ? $this->facilityId : null,
'location_name' => $this->locationType === 'external' ? $this->locationName : null,
'location_address' => $this->locationType === 'external' ? $this->locationAddress : null,
'starts_at' => $this->startsAt,
'ends_at' => $this->endsAt,
'registration_opens_at' => $this->registrationOpensAt ?: null,
'registration_closes_at' => $this->registrationClosesAt ?: null,
'max_capacity' => $this->maxCapacity ?: null,
'form_fields' => $this->formFields,
];
try {
if ($this->editing) {
$event = $eventService->update($this->event, $data, auth()->user());
} else {
$event = $eventService->create($data, auth()->user());
}
// Upload cover
if ($this->coverPhoto) {
if ($event->cover) {
$mediaService->replace($event->cover, $this->coverPhoto);
} else {
$mediaService->upload(
$this->coverPhoto,
MediaCollection::EventCover,
Event::class,
$event->id
);
}
}
// Upload gallery
foreach ($this->galleryPhotos as $photo) {
$mediaService->upload(
$photo,
MediaCollection::EventGallery,
Event::class,
$event->id
);
}
$this->event = $event;
$this->completed = true;
session()->flash('success', $this->editing ? 'تم تحديث الحدث بنجاح' : 'تم إنشاء الحدث بنجاح');
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
} catch (\Exception $e) {
session()->flash('error', 'حدث خطأ أثناء الحفظ: ' . $e->getMessage());
}
}
public function render()
{
return view('livewire.events.create-event-wizard', [
'eventTypes' => EventType::cases(),
'locationTypes' => LocationType::cases(),
'fieldTypes' => FormFieldType::cases(),
'facilities' => Facility::where('status', 'active')->orderBy('name_ar')->get(['id', 'name_ar']),
]);
}
}
<?php
namespace App\Livewire\Events;
use App\Domain\Event\Enums\EventStatus;
use App\Domain\Event\Enums\EventType;
use App\Domain\Event\Models\Event;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
#[Layout('layouts.app')]
#[Title('الأحداث')]
class EventList extends Component
{
use WithPagination;
#[Url]
public string $search = '';
#[Url]
public string $status = '';
#[Url]
public string $type = '';
public function mount(): void
{
$this->authorize('events.list');
}
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedStatus(): void
{
$this->resetPage();
}
public function updatedType(): void
{
$this->resetPage();
}
public function render()
{
$events = Event::query()
->when($this->search, fn ($q) => $q->where('title', 'ilike', "%{$this->search}%"))
->when($this->status, fn ($q) => $q->where('status', $this->status))
->when($this->type, fn ($q) => $q->where('type', $this->type))
->withCount('registrations')
->orderByDesc('starts_at')
->paginate(15);
return view('livewire.events.event-list', [
'events' => $events,
'statuses' => EventStatus::cases(),
'types' => EventType::cases(),
]);
}
}
<?php
namespace App\Livewire\Events;
use App\Domain\Event\Models\Event;
use App\Domain\Event\Models\EventRegistration;
use App\Domain\Event\Services\EventRegistrationService;
use App\Domain\Shared\Exceptions\DomainException;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
use Symfony\Component\HttpFoundation\StreamedResponse;
#[Layout('layouts.app')]
#[Title('تسجيلات الحدث')]
class EventRegistrationList extends Component
{
use WithPagination;
public Event $event;
#[Url]
public string $search = '';
#[Url]
public string $status = '';
public array $selected = [];
public bool $selectAll = false;
public function mount(Event $event): void
{
$this->authorize('events.view');
$this->event = $event->load('cover');
}
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedStatus(): void
{
$this->resetPage();
}
public function updatedSelectAll(bool $value): void
{
if ($value) {
$this->selected = $this->event->registrations()
->when($this->status, fn ($q) => $q->where('status', $this->status))
->pluck('id')
->map(fn ($id) => (string) $id)
->toArray();
} else {
$this->selected = [];
}
}
public function bulkConfirm(EventRegistrationService $service): void
{
$this->authorize('events.manage');
if (empty($this->selected)) {
return;
}
$count = $service->bulkConfirm($this->selected, auth()->user());
$this->selected = [];
$this->selectAll = false;
session()->flash('success', __("تم تأكيد :count تسجيل", ['count' => $count]));
}
public function bulkCancel(EventRegistrationService $service): void
{
$this->authorize('events.manage');
if (empty($this->selected)) {
return;
}
$count = $service->bulkCancel($this->selected, auth()->user());
$this->selected = [];
$this->selectAll = false;
session()->flash('success', __("تم إلغاء :count تسجيل", ['count' => $count]));
}
public function confirm(int $id, EventRegistrationService $service): void
{
$this->authorize('events.manage');
try {
$registration = EventRegistration::findOrFail($id);
$service->confirm($registration, auth()->user());
session()->flash('success', __('تم تأكيد التسجيل'));
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
}
}
public function cancel(int $id, EventRegistrationService $service): void
{
$this->authorize('events.manage');
try {
$registration = EventRegistration::findOrFail($id);
$service->cancel($registration, auth()->user());
session()->flash('success', __('تم إلغاء التسجيل'));
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
}
}
public function markAttended(int $id, EventRegistrationService $service): void
{
$this->authorize('events.manage');
try {
$registration = EventRegistration::findOrFail($id);
$service->markAttended($registration, auth()->user());
session()->flash('success', __('تم تسجيل الحضور'));
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
}
}
public function export(): StreamedResponse
{
$this->authorize('events.export');
$registrations = $this->event->registrations()
->when($this->status, fn ($q) => $q->where('status', $this->status))
->orderByDesc('created_at')
->get();
$fields = $this->event->form_fields;
return response()->streamDownload(function () use ($registrations, $fields) {
$handle = fopen('php://output', 'w');
fprintf($handle, chr(0xEF) . chr(0xBB) . chr(0xBF));
$headers = ['#', 'رقم التسجيل', 'الاسم', 'الهاتف', 'البريد', 'الحالة', 'تاريخ التسجيل'];
foreach ($fields as $field) {
$headers[] = $field['label'];
}
fputcsv($handle, $headers);
foreach ($registrations as $i => $reg) {
$statusLabels = [
'pending' => 'معلق',
'confirmed' => 'مؤكد',
'cancelled' => 'ملغي',
'attended' => 'حضر',
'no_show' => 'لم يحضر',
];
$row = [
$i + 1,
$reg->registration_number,
$reg->registrant_name,
$reg->registrant_phone,
$reg->registrant_email ?? '-',
$statusLabels[$reg->status] ?? $reg->status,
$reg->created_at->format('Y-m-d H:i'),
];
foreach ($fields as $field) {
$value = $reg->form_data[$field['key']] ?? '-';
if (is_array($value)) {
$value = implode(', ', $value);
}
if (is_bool($value)) {
$value = $value ? 'نعم' : 'لا';
}
$row[] = $value;
}
fputcsv($handle, $row);
}
fclose($handle);
}, "registrations-{$this->event->slug}.csv", [
'Content-Type' => 'text/csv; charset=UTF-8',
]);
}
public function render()
{
$query = $this->event->registrations()
->when($this->search, function ($q) {
$q->where(function ($q) {
$q->where('registrant_name', 'ilike', "%{$this->search}%")
->orWhere('registrant_phone', 'ilike', "%{$this->search}%")
->orWhere('registrant_email', 'ilike', "%{$this->search}%")
->orWhere('registration_number', 'ilike', "%{$this->search}%");
});
})
->when($this->status, fn ($q) => $q->where('status', $this->status))
->orderByDesc('created_at');
return view('livewire.events.event-registration-list', [
'registrations' => $query->paginate(25),
'formFields' => $this->event->form_fields,
'statusCounts' => [
'all' => $this->event->registrations()->count(),
'pending' => $this->event->registrations()->where('status', 'pending')->count(),
'confirmed' => $this->event->registrations()->where('status', 'confirmed')->count(),
'cancelled' => $this->event->registrations()->where('status', 'cancelled')->count(),
'attended' => $this->event->registrations()->where('status', 'attended')->count(),
],
]);
}
}
<?php
namespace App\Livewire\Events;
use App\Domain\Event\Enums\EventStatus;
use App\Domain\Event\Models\Event;
use App\Domain\Event\Services\EventService;
use App\Domain\Shared\Exceptions\DomainException;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('تفاصيل الحدث')]
class EventShow extends Component
{
public Event $event;
public function mount(Event $event): void
{
$this->authorize('events.view');
$this->event = $event->load(['facility', 'creator', 'cover', 'gallery']);
}
public function changeStatus(string $status, EventService $service): void
{
$this->authorize('events.manage');
try {
$newStatus = EventStatus::from($status);
$service->changeStatus($this->event, $newStatus, auth()->user());
$this->event->refresh();
session()->flash('success', __('تم تغيير حالة الحدث'));
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
}
}
public function publish(EventService $service): void
{
$this->authorize('events.manage');
try {
$service->publish($this->event, auth()->user());
$this->event->refresh();
session()->flash('success', __('تم نشر الحدث بنجاح'));
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
}
}
public function deleteEvent(EventService $service): void
{
$this->authorize('events.manage');
try {
$service->delete($this->event);
session()->flash('success', __('تم حذف الحدث'));
$this->redirect(route('events.list'), navigate: true);
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
}
}
public function render()
{
return view('livewire.events.event-show', [
'confirmedCount' => $this->event->registrations()->where('status', 'confirmed')->count(),
'pendingCount' => $this->event->registrations()->where('status', 'pending')->count(),
]);
}
}
<?php
namespace App\Livewire\Public;
use App\Domain\Event\Models\Event;
use App\Domain\Event\Services\EventRegistrationService;
use App\Domain\Shared\Exceptions\DomainException;
use Livewire\Component;
class EventRegistrationForm extends Component
{
public Event $event;
public array $fields = [];
public bool $submitted = false;
public ?string $registrationNumber = null;
public function mount(Event $event): void
{
$this->event = $event;
$this->initFields();
}
private function initFields(): void
{
foreach ($this->event->form_fields as $field) {
$default = match ($field['type']) {
'checkbox' => false,
'multi_select' => [],
default => '',
};
$this->fields[$field['key']] = $default;
}
}
public function submit(EventRegistrationService $service): void
{
$validation = $service->validateFormData($this->event, $this->fields);
$this->validate($validation['rules'], $validation['messages']);
$name = $this->fields['name'] ?? $this->fields['player_name'] ?? $this->fields['participant_name'] ?? ($this->fields['first_name'] ?? '') . ' ' . ($this->fields['last_name'] ?? '');
$phone = $this->fields['phone'] ?? $this->fields['mobile'] ?? '';
$email = $this->fields['email'] ?? null;
if (empty(trim($name))) {
$name = 'مسجل';
}
try {
$registration = $service->register($this->event, $this->fields, [
'name' => trim($name),
'phone' => $phone,
'email' => $email,
'ip' => request()->ip(),
]);
$this->submitted = true;
$this->registrationNumber = $registration->registration_number;
} catch (DomainException $e) {
$this->addError('form', $e->getMessage());
}
}
public function render()
{
return view('livewire.public.event-registration-form');
}
}
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('events', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('academy_id')->constrained('academies');
$table->string('title');
$table->string('title_en')->nullable();
$table->string('slug');
$table->text('description')->nullable();
$table->text('description_en')->nullable();
$table->string('type', 30);
$table->string('status', 30)->default('draft');
$table->string('location_type', 20);
$table->foreignId('facility_id')->nullable()->constrained('facilities')->nullOnDelete();
$table->string('location_name')->nullable();
$table->text('location_address')->nullable();
$table->timestamp('starts_at');
$table->timestamp('ends_at');
$table->timestamp('registration_opens_at')->nullable();
$table->timestamp('registration_closes_at')->nullable();
$table->unsignedInteger('max_capacity')->nullable();
$table->jsonb('form_fields')->default('[]');
$table->jsonb('settings')->default('{}');
$table->unsignedInteger('registrations_count')->default(0);
$table->timestamp('published_at')->nullable();
$table->foreignId('created_by')->constrained('users');
$table->timestamps();
$table->softDeletes();
$table->unique(['academy_id', 'slug']);
$table->index(['academy_id', 'status']);
$table->index(['academy_id', 'starts_at']);
$table->index(['academy_id', 'type']);
});
DB::statement("ALTER TABLE events ADD CONSTRAINT events_type_check CHECK (type IN ('tournament', 'camp', 'competition', 'open_day', 'workshop', 'friendly', 'exhibition', 'other'))");
DB::statement("ALTER TABLE events ADD CONSTRAINT events_status_check CHECK (status IN ('draft', 'published', 'registration_closed', 'in_progress', 'completed', 'cancelled'))");
DB::statement("ALTER TABLE events ADD CONSTRAINT events_location_type_check CHECK (location_type IN ('facility', 'external'))");
}
public function down(): void
{
Schema::dropIfExists('events');
}
};
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('event_registrations', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('academy_id')->constrained('academies');
$table->foreignId('event_id')->constrained('events')->cascadeOnDelete();
$table->foreignId('person_id')->nullable()->constrained('people')->nullOnDelete();
$table->string('registration_number', 30);
$table->string('status', 20)->default('pending');
$table->jsonb('form_data')->default('{}');
$table->string('registrant_name');
$table->string('registrant_phone', 20);
$table->string('registrant_email')->nullable();
$table->text('admin_notes')->nullable();
$table->timestamp('confirmed_at')->nullable();
$table->timestamp('cancelled_at')->nullable();
$table->timestamp('attended_at')->nullable();
$table->string('ip_address', 45)->nullable();
$table->timestamps();
$table->softDeletes();
$table->unique(['academy_id', 'registration_number']);
$table->index(['event_id', 'status']);
$table->index(['event_id', 'created_at']);
$table->index(['academy_id', 'registrant_phone']);
});
DB::statement("ALTER TABLE event_registrations ADD CONSTRAINT event_registrations_status_check CHECK (status IN ('pending', 'confirmed', 'cancelled', 'attended', 'no_show'))");
}
public function down(): void
{
Schema::dropIfExists('event_registrations');
}
};
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
DB::statement("ALTER TABLE media DROP CONSTRAINT IF EXISTS media_collection_check");
DB::statement("ALTER TABLE media ADD CONSTRAINT media_collection_check CHECK (collection IN (
'academy_logo', 'academy_cover', 'activity_photo', 'branch_photo',
'gallery', 'team_photo', 'testimonial_avatar', 'partner_logo',
'news_image', 'section_image', 'general',
'event_cover', 'event_gallery'
))");
}
public function down(): void
{
DB::statement("ALTER TABLE media DROP CONSTRAINT IF EXISTS media_collection_check");
DB::statement("ALTER TABLE media ADD CONSTRAINT media_collection_check CHECK (collection IN (
'academy_logo', 'academy_cover', 'activity_photo', 'branch_photo',
'gallery', 'team_photo', 'testimonial_avatar', 'partner_logo',
'news_image', 'section_image', 'general'
))");
}
};
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
$permissions = [
['name' => 'events.list', 'module' => 'events', 'action' => 'list'],
['name' => 'events.create', 'module' => 'events', 'action' => 'create'],
['name' => 'events.view', 'module' => 'events', 'action' => 'view'],
['name' => 'events.update', 'module' => 'events', 'action' => 'update'],
['name' => 'events.manage', 'module' => 'events', 'action' => 'manage'],
['name' => 'events.export', 'module' => 'events', 'action' => 'export'],
];
foreach ($permissions as $perm) {
DB::table('permissions')->insertOrIgnore([
'name' => $perm['name'],
'module' => $perm['module'],
'action' => $perm['action'],
'created_at' => now(),
]);
}
}
public function down(): void
{
DB::table('permissions')->whereIn('name', [
'events.list', 'events.create', 'events.view',
'events.update', 'events.manage', 'events.export',
])->delete();
}
};
......@@ -28,6 +28,11 @@
['label' => 'لوحة المدرب', 'route' => 'trainer.dashboard', 'icon' => 'user', 'permission' => 'attendance.mark'],
]],
['section' => 'الأحداث', 'items' => [
['label' => 'الأحداث', 'route' => 'events.list', 'icon' => 'calendar', 'permission' => 'events.list'],
['label' => 'إنشاء حدث', 'route' => 'events.create', 'icon' => 'plus-circle', 'permission' => 'events.create'],
]],
['section' => 'الموارد البشرية', 'items' => [
['label' => 'الموظفين', 'route' => 'employees.list', 'icon' => 'briefcase', 'permission' => 'employees.list'],
['label' => 'المدربين', 'route' => 'trainers.list', 'icon' => 'academic-cap', 'permission' => 'trainers.list'],
......
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{{-- Flash Messages --}}
@if(session('error'))
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-800 text-sm">{{ session('error') }}</div>
@endif
@if($completed)
{{-- Success State --}}
<div class="bg-white rounded-xl border border-gray-200 p-12 text-center">
<div class="w-16 h-16 mx-auto bg-green-100 rounded-full flex items-center justify-center mb-4">
<svg class="w-8 h-8 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-xl font-bold text-gray-800 mb-2">{{ $editing ? __('تم تحديث الحدث') : __('تم إنشاء الحدث بنجاح') }}</h2>
<p class="text-sm text-gray-500 mb-6">{{ __('يمكنك نشر الحدث أو تعديله من صفحة التفاصيل') }}</p>
<div class="flex items-center justify-center gap-3">
<a href="{{ route('events.show', $event) }}" wire:navigate class="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors">
{{ __('عرض الحدث') }}
</a>
<a href="{{ route('events.list') }}" wire:navigate class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-200 transition-colors">
{{ __('قائمة الأحداث') }}
</a>
</div>
</div>
@else
{{-- Wizard Header --}}
<div class="mb-8">
<h1 class="text-2xl font-bold text-gray-800 mb-4">{{ $editing ? __('تعديل الحدث') : __('إنشاء حدث جديد') }}</h1>
{{-- Steps Indicator --}}
<div class="flex items-center gap-2">
@foreach([1 => 'التفاصيل', 2 => 'نموذج التسجيل', 3 => 'الصور', 4 => 'المراجعة'] as $step => $label)
<button wire:click="goToStep({{ $step }})" @if($step > $currentStep) disabled @endif
class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors
{{ $currentStep === $step ? 'bg-blue-600 text-white' : ($step < $currentStep ? 'bg-green-100 text-green-700 hover:bg-green-200' : 'bg-gray-100 text-gray-400') }}">
<span class="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold
{{ $currentStep === $step ? 'bg-white/20' : ($step < $currentStep ? 'bg-green-200' : 'bg-gray-200') }}">
@if($step < $currentStep)
<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="3" d="M5 13l4 4L19 7"/></svg>
@else
{{ $step }}
@endif
</span>
<span class="hidden sm:inline">{{ __($label) }}</span>
</button>
@if($step < $totalSteps)
<div class="flex-1 h-px bg-gray-200"></div>
@endif
@endforeach
</div>
</div>
{{-- Step 1: Details --}}
@if($currentStep === 1)
<div class="bg-white rounded-xl border border-gray-200 p-6 space-y-6">
<h2 class="text-lg font-semibold text-gray-800">{{ __('تفاصيل الحدث') }}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('عنوان الحدث') }} <span class="text-red-500">*</span></label>
<input type="text" wire:model="title" class="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('title') border-red-500 @enderror">
@error('title') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('العنوان بالإنجليزية') }}</label>
<input type="text" wire:model="titleEn" dir="ltr" class="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('نوع الحدث') }} <span class="text-red-500">*</span></label>
<select wire:model="type" class="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('type') border-red-500 @enderror">
<option value="">{{ __('اختر النوع') }}</option>
@foreach($eventTypes as $t)
<option value="{{ $t->value }}">{{ $t->label() }}</option>
@endforeach
</select>
@error('type') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('السعة القصوى') }}</label>
<input type="number" wire:model="maxCapacity" dir="ltr" min="1" placeholder="{{ __('غير محدود') }}"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('تاريخ البداية') }} <span class="text-red-500">*</span></label>
<input type="datetime-local" wire:model="startsAt" dir="ltr"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('startsAt') border-red-500 @enderror">
@error('startsAt') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('تاريخ النهاية') }} <span class="text-red-500">*</span></label>
<input type="datetime-local" wire:model="endsAt" dir="ltr"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('endsAt') border-red-500 @enderror">
@error('endsAt') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('فتح التسجيل') }}</label>
<input type="datetime-local" wire:model="registrationOpensAt" dir="ltr"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('إغلاق التسجيل') }}</label>
<input type="datetime-local" wire:model="registrationClosesAt" dir="ltr"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
</div>
{{-- Location --}}
<div class="border-t pt-4">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __('الموقع') }} <span class="text-red-500">*</span></label>
<div class="flex gap-4 mb-4">
@foreach($locationTypes as $lt)
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" wire:model.live="locationType" value="{{ $lt->value }}" class="text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-700">{{ $lt->label() }}</span>
</label>
@endforeach
</div>
@if($locationType === 'facility')
<select wire:model="facilityId" class="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('facilityId') border-red-500 @enderror">
<option value="">{{ __('اختر المنشأة') }}</option>
@foreach($facilities as $f)
<option value="{{ $f->id }}">{{ $f->name_ar }}</option>
@endforeach
</select>
@error('facilityId') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
@else
<div class="space-y-3">
<input type="text" wire:model="locationName" placeholder="{{ __('اسم الموقع') }}"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('locationName') border-red-500 @enderror">
@error('locationName') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
<textarea wire:model="locationAddress" rows="2" placeholder="{{ __('العنوان التفصيلي') }}"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"></textarea>
</div>
@endif
</div>
{{-- Description --}}
<div class="border-t pt-4">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('وصف الحدث') }}</label>
<textarea wire:model="description" rows="4" class="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"></textarea>
</div>
</div>
@endif
{{-- Step 2: Form Builder --}}
@if($currentStep === 2)
<div class="bg-white rounded-xl border border-gray-200 p-6">
<div class="flex items-center justify-between mb-4">
<div>
<h2 class="text-lg font-semibold text-gray-800">{{ __('نموذج التسجيل') }}</h2>
<p class="text-sm text-gray-500">{{ __('حدد الحقول التي سيملأها المسجلون') }}</p>
</div>
<button wire:click="addField" class="inline-flex items-center gap-1.5 px-3 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 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 4v16m8-8H4"/></svg>
{{ __('إضافة حقل') }}
</button>
</div>
@if(empty($formFields))
<div class="text-center py-8 text-gray-400">
<p>{{ __('لا توجد حقول. أضف حقولاً ليملأها المسجلون.') }}</p>
</div>
@else
<div class="space-y-4">
@foreach($formFields as $index => $field)
<div class="border border-gray-200 rounded-lg p-4 bg-gray-50" wire:key="field-{{ $index }}">
<div class="flex items-start justify-between gap-4">
{{-- Field Config --}}
<div class="flex-1 grid grid-cols-1 md:grid-cols-3 gap-3">
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">{{ __('العنوان') }} <span class="text-red-500">*</span></label>
<input type="text" wire:model="formFields.{{ $index }}.label"
placeholder="{{ __('مثال: الاسم الكامل') }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">{{ __('النوع') }}</label>
<select wire:model.live="formFields.{{ $index }}.type"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
@foreach($fieldTypes as $ft)
<option value="{{ $ft->value }}">{{ $ft->label() }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">{{ __('العرض') }}</label>
<select wire:model="formFields.{{ $index }}.width"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="full">{{ __('كامل') }}</option>
<option value="half">{{ __('نصف') }}</option>
</select>
</div>
</div>
{{-- Actions --}}
<div class="flex items-center gap-1 pt-5">
<label class="flex items-center gap-1 text-xs text-gray-500 me-2">
<input type="checkbox" wire:model="formFields.{{ $index }}.is_required" class="rounded text-blue-600 focus:ring-blue-500">
{{ __('مطلوب') }}
</label>
<button wire:click="moveFieldUp({{ $index }})" @if($index === 0) disabled @endif
class="p-1.5 text-gray-400 hover:text-gray-600 disabled:opacity-30">
<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 15l7-7 7 7"/></svg>
</button>
<button wire:click="moveFieldDown({{ $index }})" @if($index === count($formFields) - 1) disabled @endif
class="p-1.5 text-gray-400 hover:text-gray-600 disabled:opacity-30">
<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="M19 9l-7 7-7-7"/></svg>
</button>
<button wire:click="removeField({{ $index }})" class="p-1.5 text-red-400 hover:text-red-600">
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
</button>
</div>
</div>
{{-- Options for select/multi_select --}}
@if(in_array($field['type'], ['select', 'multi_select']))
<div class="mt-3 pt-3 border-t border-gray-200">
<label class="block text-xs font-medium text-gray-500 mb-2">{{ __('الخيارات') }}</label>
<div class="space-y-2">
@foreach($field['options'] ?? [] as $optIndex => $option)
<div class="flex items-center gap-2">
<input type="text" wire:model="formFields.{{ $index }}.options.{{ $optIndex }}.label"
placeholder="{{ __('نص الخيار') }}"
class="flex-1 px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<input type="text" wire:model="formFields.{{ $index }}.options.{{ $optIndex }}.value"
placeholder="{{ __('القيمة') }}" dir="ltr"
class="w-32 px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<button wire:click="removeOption({{ $index }}, {{ $optIndex }})" class="p-1 text-red-400 hover:text-red-600">
<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>
</button>
</div>
@endforeach
<button wire:click="addOption({{ $index }})" class="text-xs text-blue-600 hover:text-blue-800 font-medium">
+ {{ __('إضافة خيار') }}
</button>
</div>
</div>
@endif
</div>
@endforeach
</div>
@endif
</div>
@endif
{{-- Step 3: Media --}}
@if($currentStep === 3)
<div class="bg-white rounded-xl border border-gray-200 p-6 space-y-6">
<h2 class="text-lg font-semibold text-gray-800">{{ __('صور الحدث') }}</h2>
{{-- Cover Photo --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __('صورة الغلاف') }}</label>
<div class="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:border-blue-400 transition-colors">
@if($coverPhoto)
<img src="{{ $coverPhoto->temporaryUrl() }}" class="max-h-48 mx-auto rounded-lg mb-3">
@elseif($editing && $event->cover)
<img src="{{ $event->cover->url }}" class="max-h-48 mx-auto rounded-lg mb-3">
@endif
<input type="file" wire:model="coverPhoto" accept="image/*" class="block w-full text-sm text-gray-500 file:me-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
<p class="text-xs text-gray-400 mt-2">{{ __('يفضل 1920×600 بكسل') }}</p>
@error('coverPhoto') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
</div>
{{-- Gallery --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __('معرض الصور') }}</label>
<div class="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:border-blue-400 transition-colors">
@if(!empty($galleryPhotos))
<div class="grid grid-cols-3 gap-3 mb-3">
@foreach($galleryPhotos as $photo)
<img src="{{ $photo->temporaryUrl() }}" class="w-full h-24 object-cover rounded-lg">
@endforeach
</div>
@endif
<input type="file" wire:model="galleryPhotos" accept="image/*" multiple class="block w-full text-sm text-gray-500 file:me-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
<p class="text-xs text-gray-400 mt-2">{{ __('يمكنك رفع عدة صور') }}</p>
</div>
</div>
</div>
@endif
{{-- Step 4: Review --}}
@if($currentStep === 4)
<div class="bg-white rounded-xl border border-gray-200 p-6 space-y-6">
<h2 class="text-lg font-semibold text-gray-800">{{ __('مراجعة البيانات') }}</h2>
<div class="grid grid-cols-2 gap-4 text-sm">
<div><span class="text-gray-500">{{ __('العنوان') }}:</span> <span class="font-medium">{{ $title }}</span></div>
<div><span class="text-gray-500">{{ __('النوع') }}:</span> <span class="font-medium">{{ \App\Domain\Event\Enums\EventType::tryFrom($type)?->label() ?? '—' }}</span></div>
<div><span class="text-gray-500">{{ __('البداية') }}:</span> <span class="font-medium" dir="ltr">{{ $startsAt }}</span></div>
<div><span class="text-gray-500">{{ __('النهاية') }}:</span> <span class="font-medium" dir="ltr">{{ $endsAt }}</span></div>
<div><span class="text-gray-500">{{ __('السعة') }}:</span> <span class="font-medium">{{ $maxCapacity ?? __('غير محدود') }}</span></div>
<div><span class="text-gray-500">{{ __('حقول النموذج') }}:</span> <span class="font-medium">{{ count($formFields) }} {{ __('حقل') }}</span></div>
</div>
@if(!empty($formFields))
<div class="border-t pt-4">
<h3 class="text-sm font-medium text-gray-700 mb-2">{{ __('حقول التسجيل') }}</h3>
<div class="flex flex-wrap gap-2">
@foreach($formFields as $field)
<span class="px-2.5 py-1 bg-gray-100 text-gray-700 rounded-full text-xs">
{{ $field['label'] ?: __('بدون عنوان') }}
@if($field['is_required']) <span class="text-red-500">*</span> @endif
</span>
@endforeach
</div>
</div>
@endif
</div>
@endif
{{-- Navigation --}}
<div class="flex items-center justify-between mt-6">
@if($currentStep > 1)
<button wire:click="previousStep" class="px-4 py-2.5 bg-gray-100 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-200 transition-colors">
{{ __('السابق') }}
</button>
@else
<div></div>
@endif
@if($currentStep < $totalSteps)
<button wire:click="nextStep" class="px-6 py-2.5 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors">
{{ __('التالي') }}
</button>
@else
<button wire:click="save" wire:loading.attr="disabled" wire:target="save"
class="px-6 py-2.5 bg-green-600 text-white rounded-lg text-sm font-medium hover:bg-green-700 transition-colors">
<span wire:loading.remove wire:target="save">{{ $editing ? __('حفظ التعديلات') : __('إنشاء الحدث') }}</span>
<span wire:loading wire:target="save">{{ __('جارٍ الحفظ...') }}</span>
</button>
@endif
</div>
@endif
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{{-- Header --}}
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-800">{{ __('الأحداث') }}</h1>
<p class="text-sm text-gray-500 mt-1">{{ __('إدارة البطولات والمعسكرات والأحداث') }}</p>
</div>
@can('events.create')
<a href="{{ route('events.create') }}" wire:navigate
class="inline-flex items-center gap-2 px-4 py-2.5 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
{{ __('إنشاء حدث') }}
</a>
@endcan
</div>
{{-- Filters --}}
<div class="bg-white rounded-xl border border-gray-200 p-4 mb-6">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<input type="text" wire:model.live.debounce.300ms="search"
placeholder="{{ __('بحث بالعنوان...') }}"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<select wire:model.live="status" class="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="">{{ __('كل الحالات') }}</option>
@foreach($statuses as $s)
<option value="{{ $s->value }}">{{ $s->label() }}</option>
@endforeach
</select>
</div>
<div>
<select wire:model.live="type" class="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="">{{ __('كل الأنواع') }}</option>
@foreach($types as $t)
<option value="{{ $t->value }}">{{ $t->label() }}</option>
@endforeach
</select>
</div>
</div>
</div>
{{-- Events Grid --}}
<div wire:loading.class="opacity-50 pointer-events-none">
@if($events->isEmpty())
<div class="bg-white rounded-xl border border-gray-200 p-12 text-center">
<svg class="w-16 h-16 mx-auto text-gray-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"/></svg>
<h3 class="text-lg font-medium text-gray-600 mb-1">{{ __('لا توجد أحداث') }}</h3>
<p class="text-sm text-gray-400">{{ __('ابدأ بإنشاء حدث جديد') }}</p>
</div>
@else
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach($events as $event)
<a href="{{ route('events.show', $event) }}" wire:navigate
class="bg-white rounded-xl border border-gray-200 overflow-hidden hover:shadow-md transition-shadow group">
{{-- Cover --}}
<div class="h-40 bg-gradient-to-br from-blue-500 to-purple-600 relative">
@if($event->cover)
<img src="{{ $event->cover->url }}" alt="{{ $event->title }}" class="w-full h-full object-cover">
@endif
<div class="absolute top-3 start-3">
<span class="px-2.5 py-1 rounded-full text-xs font-medium
{{ match($event->status->color()) {
'green' => 'bg-green-100 text-green-800',
'yellow' => 'bg-yellow-100 text-yellow-800',
'blue' => 'bg-blue-100 text-blue-800',
'purple' => 'bg-purple-100 text-purple-800',
'red' => 'bg-red-100 text-red-800',
default => 'bg-gray-100 text-gray-800',
} }}">
{{ $event->status->label() }}
</span>
</div>
<div class="absolute top-3 end-3">
<span class="px-2.5 py-1 rounded-full text-xs font-medium bg-white/90 text-gray-700">
{{ $event->type->label() }}
</span>
</div>
</div>
{{-- Content --}}
<div class="p-4">
<h3 class="font-semibold text-gray-800 group-hover:text-blue-600 transition-colors mb-2 line-clamp-1">
{{ $event->title }}
</h3>
<div class="space-y-1.5 text-sm text-gray-500">
<div class="flex items-center gap-2">
<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="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
{{ $event->starts_at->translatedFormat('d M Y - H:i') }}
</div>
<div class="flex items-center gap-2">
<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="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
{{ $event->registrations_count }} {{ __('مسجل') }}
@if($event->max_capacity)
/ {{ $event->max_capacity }}
@endif
</div>
</div>
</div>
</a>
@endforeach
</div>
<div class="mt-6">
{{ $events->links() }}
</div>
@endif
</div>
</div>
<div>
{{-- Header --}}
<div class="mb-6">
<div class="flex items-center gap-2 text-sm text-gray-500 mb-2">
<a href="{{ route('events.list') }}" class="hover:text-gray-700" wire:navigate>{{ __('الأحداث') }}</a>
<svg class="w-4 h-4 rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
<a href="{{ route('events.show', $event) }}" class="hover:text-gray-700" wire:navigate>{{ $event->title }}</a>
<svg class="w-4 h-4 rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
<span class="text-gray-900">{{ __('التسجيلات') }}</span>
</div>
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<h1 class="text-2xl font-bold text-gray-900">{{ __('تسجيلات') }} — {{ $event->title }}</h1>
<button wire:click="export" class="inline-flex items-center gap-2 px-4 py-2 bg-green-600 text-white text-sm font-medium rounded-lg hover:bg-green-700 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 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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>
{{ __('تصدير CSV') }}
</button>
</div>
</div>
{{-- Flash Messages --}}
@if(session('success'))
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-xl text-green-800 text-sm">{{ session('success') }}</div>
@endif
@if(session('error'))
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-xl text-red-800 text-sm">{{ session('error') }}</div>
@endif
{{-- Status Tabs --}}
<div class="flex flex-wrap gap-2 mb-6">
<button wire:click="$set('status', '')"
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {{ $status === '' ? 'bg-gray-900 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200' }}">
{{ __('الكل') }} ({{ $statusCounts['all'] }})
</button>
<button wire:click="$set('status', 'pending')"
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {{ $status === 'pending' ? 'bg-yellow-600 text-white' : 'bg-yellow-50 text-yellow-700 hover:bg-yellow-100' }}">
{{ __('معلق') }} ({{ $statusCounts['pending'] }})
</button>
<button wire:click="$set('status', 'confirmed')"
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {{ $status === 'confirmed' ? 'bg-green-600 text-white' : 'bg-green-50 text-green-700 hover:bg-green-100' }}">
{{ __('مؤكد') }} ({{ $statusCounts['confirmed'] }})
</button>
<button wire:click="$set('status', 'attended')"
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {{ $status === 'attended' ? 'bg-blue-600 text-white' : 'bg-blue-50 text-blue-700 hover:bg-blue-100' }}">
{{ __('حضر') }} ({{ $statusCounts['attended'] }})
</button>
<button wire:click="$set('status', 'cancelled')"
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {{ $status === 'cancelled' ? 'bg-red-600 text-white' : 'bg-red-50 text-red-700 hover:bg-red-100' }}">
{{ __('ملغي') }} ({{ $statusCounts['cancelled'] }})
</button>
</div>
{{-- Search + Bulk Actions --}}
<div class="flex flex-col sm:flex-row gap-3 mb-4">
<div class="flex-1">
<input type="text"
wire:model.live.debounce.300ms="search"
placeholder="{{ __('بحث بالاسم أو الهاتف أو رقم التسجيل...') }}"
class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
@if(count($selected) > 0)
<div class="flex items-center gap-2">
<span class="text-sm text-gray-500">{{ count($selected) }} {{ __('محدد') }}</span>
@can('events.manage')
<button wire:click="bulkConfirm" wire:confirm="{{ __('تأكيد التسجيلات المحددة؟') }}"
class="px-3 py-2 bg-green-600 text-white text-xs font-medium rounded-lg hover:bg-green-700">
{{ __('تأكيد الكل') }}
</button>
<button wire:click="bulkCancel" wire:confirm="{{ __('إلغاء التسجيلات المحددة؟') }}"
class="px-3 py-2 bg-red-600 text-white text-xs font-medium rounded-lg hover:bg-red-700">
{{ __('إلغاء الكل') }}
</button>
@endcan
</div>
@endif
</div>
{{-- Table --}}
<div class="bg-white border border-gray-200 rounded-xl overflow-hidden shadow-sm">
<div class="overflow-x-auto" wire:loading.class="opacity-50 pointer-events-none">
<table class="w-full text-sm">
<thead>
<tr class="bg-gray-50 border-b border-gray-200">
<th class="px-4 py-3 text-start">
<input type="checkbox" wire:model.live="selectAll" class="rounded text-blue-600 focus:ring-blue-500">
</th>
<th class="px-4 py-3 text-start font-semibold text-gray-700">{{ __('رقم التسجيل') }}</th>
<th class="px-4 py-3 text-start font-semibold text-gray-700">{{ __('الاسم') }}</th>
<th class="px-4 py-3 text-start font-semibold text-gray-700">{{ __('الهاتف') }}</th>
@foreach($formFields as $field)
@if(!in_array($field['key'], ['name', 'player_name', 'participant_name', 'phone', 'mobile', 'email']))
<th class="px-4 py-3 text-start font-semibold text-gray-700 whitespace-nowrap">{{ $field['label'] }}</th>
@endif
@endforeach
<th class="px-4 py-3 text-start font-semibold text-gray-700">{{ __('الحالة') }}</th>
<th class="px-4 py-3 text-start font-semibold text-gray-700">{{ __('التاريخ') }}</th>
<th class="px-4 py-3 text-start font-semibold text-gray-700">{{ __('إجراءات') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@forelse($registrations as $reg)
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-4 py-3">
<input type="checkbox" wire:model.live="selected" value="{{ $reg->id }}" class="rounded text-blue-600 focus:ring-blue-500">
</td>
<td class="px-4 py-3">
<span class="font-mono text-xs bg-gray-100 px-2 py-1 rounded" dir="ltr">{{ $reg->registration_number }}</span>
</td>
<td class="px-4 py-3 font-medium text-gray-900">{{ $reg->registrant_name }}</td>
<td class="px-4 py-3 text-gray-600" dir="ltr">{{ $reg->registrant_phone }}</td>
@foreach($formFields as $field)
@if(!in_array($field['key'], ['name', 'player_name', 'participant_name', 'phone', 'mobile', 'email']))
<td class="px-4 py-3 text-gray-600">
@php
$val = $reg->form_data[$field['key']] ?? '-';
if (is_array($val)) $val = implode(', ', $val);
if (is_bool($val)) $val = $val ? 'نعم' : 'لا';
@endphp
{{ $val }}
</td>
@endif
@endforeach
<td class="px-4 py-3">
@php
$statusStyles = [
'pending' => 'bg-yellow-100 text-yellow-800',
'confirmed' => 'bg-green-100 text-green-800',
'cancelled' => 'bg-red-100 text-red-800',
'attended' => 'bg-blue-100 text-blue-800',
'no_show' => 'bg-gray-100 text-gray-800',
];
$statusNames = [
'pending' => 'معلق',
'confirmed' => 'مؤكد',
'cancelled' => 'ملغي',
'attended' => 'حضر',
'no_show' => 'لم يحضر',
];
@endphp
<span class="inline-block px-2 py-0.5 text-xs font-medium rounded-full {{ $statusStyles[$reg->status] ?? 'bg-gray-100 text-gray-800' }}">
{{ $statusNames[$reg->status] ?? $reg->status }}
</span>
</td>
<td class="px-4 py-3 text-gray-500 text-xs whitespace-nowrap" dir="ltr">
{{ $reg->created_at->format('Y-m-d H:i') }}
</td>
<td class="px-4 py-3">
@can('events.manage')
<div class="flex items-center gap-1">
@if($reg->status === 'pending')
<button wire:click="confirm({{ $reg->id }})" title="{{ __('تأكيد') }}"
class="p-1.5 text-green-600 hover:bg-green-50 rounded-lg 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="M5 13l4 4L19 7"/></svg>
</button>
@endif
@if(in_array($reg->status, ['pending', 'confirmed']))
<button wire:click="markAttended({{ $reg->id }})" title="{{ __('حضر') }}"
class="p-1.5 text-blue-600 hover:bg-blue-50 rounded-lg 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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
</button>
@endif
@if(!in_array($reg->status, ['cancelled', 'attended']))
<button wire:click="cancel({{ $reg->id }})" wire:confirm="{{ __('إلغاء هذا التسجيل؟') }}" title="{{ __('إلغاء') }}"
class="p-1.5 text-red-600 hover:bg-red-50 rounded-lg 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>
</button>
@endif
</div>
@endcan
</td>
</tr>
@empty
<tr>
<td colspan="100" class="px-4 py-12 text-center text-gray-500">
<svg class="w-12 h-12 mx-auto text-gray-300 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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>
<p>{{ __('لا توجد تسجيلات بعد') }}</p>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
{{-- Pagination --}}
<div class="mt-4">
{{ $registrations->links() }}
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{{-- Flash Messages --}}
@if(session('success'))
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg text-green-800 text-sm">{{ session('success') }}</div>
@endif
@if(session('error'))
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-800 text-sm">{{ session('error') }}</div>
@endif
{{-- Header --}}
<div class="flex items-start justify-between mb-6">
<div>
<div class="flex items-center gap-3 mb-2">
<a href="{{ route('events.list') }}" wire:navigate class="text-gray-400 hover:text-gray-600">
<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="M10 19l-7-7m0 0l7-7m-7 7h18"/></svg>
</a>
<h1 class="text-2xl font-bold text-gray-800">{{ $event->title }}</h1>
<span class="px-2.5 py-1 rounded-full text-xs font-medium
{{ match($event->status->color()) {
'green' => 'bg-green-100 text-green-800',
'yellow' => 'bg-yellow-100 text-yellow-800',
'blue' => 'bg-blue-100 text-blue-800',
'purple' => 'bg-purple-100 text-purple-800',
'red' => 'bg-red-100 text-red-800',
default => 'bg-gray-100 text-gray-800',
} }}">
{{ $event->status->label() }}
</span>
</div>
<p class="text-sm text-gray-500">{{ $event->type->label() }} &middot; {{ __('أنشئ بواسطة') }} {{ $event->creator?->name ?? '—' }}</p>
</div>
<div class="flex items-center gap-2">
@if($event->status === \App\Domain\Event\Enums\EventStatus::Draft)
<button wire:click="publish" wire:loading.attr="disabled"
class="px-4 py-2 bg-green-600 text-white rounded-lg text-sm font-medium hover:bg-green-700 transition-colors">
{{ __('نشر الحدث') }}
</button>
@endif
@can('events.update')
<a href="{{ route('events.edit', $event) }}" wire:navigate
class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-200 transition-colors">
{{ __('تعديل') }}
</a>
@endcan
</div>
</div>
{{-- Stats Cards --}}
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="bg-white rounded-xl border border-gray-200 p-4">
<div class="text-2xl font-bold text-blue-600">{{ $event->registrations_count }}</div>
<div class="text-sm text-gray-500">{{ __('إجمالي التسجيلات') }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-200 p-4">
<div class="text-2xl font-bold text-green-600">{{ $confirmedCount }}</div>
<div class="text-sm text-gray-500">{{ __('مؤكد') }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-200 p-4">
<div class="text-2xl font-bold text-yellow-600">{{ $pendingCount }}</div>
<div class="text-sm text-gray-500">{{ __('قيد المراجعة') }}</div>
</div>
<div class="bg-white rounded-xl border border-gray-200 p-4">
<div class="text-2xl font-bold text-gray-600">
@if($event->max_capacity)
{{ $event->spotsRemaining() }} / {{ $event->max_capacity }}
@else
&infin;
@endif
</div>
<div class="text-sm text-gray-500">{{ __('أماكن متاحة') }}</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
{{-- Event Details --}}
<div class="lg:col-span-2 space-y-6">
<div class="bg-white rounded-xl border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">{{ __('تفاصيل الحدث') }}</h2>
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="text-gray-500">{{ __('تاريخ البداية') }}</span>
<p class="font-medium text-gray-800">{{ $event->starts_at->translatedFormat('d M Y - H:i') }}</p>
</div>
<div>
<span class="text-gray-500">{{ __('تاريخ النهاية') }}</span>
<p class="font-medium text-gray-800">{{ $event->ends_at->translatedFormat('d M Y - H:i') }}</p>
</div>
<div>
<span class="text-gray-500">{{ __('فتح التسجيل') }}</span>
<p class="font-medium text-gray-800">{{ $event->registration_opens_at?->translatedFormat('d M Y - H:i') ?? __('فوري') }}</p>
</div>
<div>
<span class="text-gray-500">{{ __('إغلاق التسجيل') }}</span>
<p class="font-medium text-gray-800">{{ $event->registration_closes_at?->translatedFormat('d M Y - H:i') ?? __('حتى بداية الحدث') }}</p>
</div>
</div>
@if($event->description)
<div class="pt-4 border-t">
<span class="text-sm text-gray-500">{{ __('الوصف') }}</span>
<p class="text-gray-700 mt-1 whitespace-pre-line">{{ $event->description }}</p>
</div>
@endif
<div class="pt-4 border-t">
<span class="text-sm text-gray-500">{{ __('الموقع') }}</span>
<p class="font-medium text-gray-800 mt-1">
@if($event->location_type === \App\Domain\Event\Enums\LocationType::Facility && $event->facility)
{{ $event->facility->name_ar }}
@else
{{ $event->location_name }}
@if($event->location_address)
<span class="text-sm text-gray-500 block">{{ $event->location_address }}</span>
@endif
@endif
</p>
</div>
</div>
</div>
{{-- Registrations Quick View --}}
@can('events.view')
<div class="bg-white rounded-xl border border-gray-200 p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-gray-800">{{ __('آخر التسجيلات') }}</h2>
<a href="{{ route('events.registrations', $event) }}" wire:navigate class="text-sm text-blue-600 hover:text-blue-800">
{{ __('عرض الكل') }} &larr;
</a>
</div>
@php $latestRegs = $event->registrations()->latest()->limit(5)->get(); @endphp
@if($latestRegs->isEmpty())
<p class="text-sm text-gray-400">{{ __('لا توجد تسجيلات بعد') }}</p>
@else
<div class="space-y-3">
@foreach($latestRegs as $reg)
<div class="flex items-center justify-between py-2 border-b last:border-0">
<div>
<span class="font-medium text-gray-800 text-sm">{{ $reg->registrant_name }}</span>
<span class="text-xs text-gray-400 ms-2">{{ $reg->registrant_phone }}</span>
</div>
<span class="px-2 py-0.5 rounded-full text-xs font-medium
{{ match($reg->status->color()) {
'green' => 'bg-green-100 text-green-700',
'yellow' => 'bg-yellow-100 text-yellow-700',
'red' => 'bg-red-100 text-red-700',
default => 'bg-gray-100 text-gray-700',
} }}">
{{ $reg->status->label() }}
</span>
</div>
@endforeach
</div>
@endif
</div>
@endcan
</div>
{{-- Sidebar --}}
<div class="space-y-6">
{{-- QR Code --}}
@if($event->status !== \App\Domain\Event\Enums\EventStatus::Draft)
<div class="bg-white rounded-xl border border-gray-200 p-6 text-center">
<h3 class="text-sm font-semibold text-gray-700 mb-3">{{ __('رمز QR للتسجيل') }}</h3>
<div class="inline-block p-3 bg-white border rounded-lg">
<img src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data={{ urlencode($event->getPublicUrl()) }}"
alt="QR Code" class="w-48 h-48">
</div>
<p class="text-xs text-gray-400 mt-2 break-all">{{ $event->getPublicUrl() }}</p>
<button onclick="navigator.clipboard.writeText('{{ $event->getPublicUrl() }}')"
class="mt-3 px-3 py-1.5 text-xs bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors">
{{ __('نسخ الرابط') }}
</button>
</div>
@endif
{{-- Status Actions --}}
@can('events.manage')
<div class="bg-white rounded-xl border border-gray-200 p-6">
<h3 class="text-sm font-semibold text-gray-700 mb-3">{{ __('تغيير الحالة') }}</h3>
<div class="space-y-2">
@foreach(\App\Domain\Event\Enums\EventStatus::cases() as $s)
@if($event->status->canTransitionTo($s))
<button wire:click="changeStatus('{{ $s->value }}')"
wire:loading.attr="disabled"
class="w-full px-3 py-2 text-start text-sm rounded-lg border hover:bg-gray-50 transition-colors">
{{ $s->label() }}
</button>
@endif
@endforeach
</div>
</div>
@endcan
{{-- Form Fields Summary --}}
<div class="bg-white rounded-xl border border-gray-200 p-6">
<h3 class="text-sm font-semibold text-gray-700 mb-3">{{ __('حقول النموذج') }}</h3>
@if(count($event->form_fields ?? []) > 0)
<ul class="space-y-1.5 text-sm text-gray-600">
@foreach($event->form_fields as $field)
<li class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full {{ $field['is_required'] ? 'bg-red-400' : 'bg-gray-300' }}"></span>
{{ $field['label'] }}
<span class="text-xs text-gray-400">({{ $field['type'] }})</span>
</li>
@endforeach
</ul>
@else
<p class="text-sm text-gray-400">{{ __('لم يتم إضافة حقول') }}</p>
@endif
</div>
{{-- Danger Zone --}}
@can('events.manage')
@if($event->status === \App\Domain\Event\Enums\EventStatus::Draft)
<div class="bg-red-50 rounded-xl border border-red-200 p-6">
<h3 class="text-sm font-semibold text-red-700 mb-3">{{ __('منطقة الخطر') }}</h3>
<button wire:click="deleteEvent"
wire:confirm="{{ __('هل أنت متأكد من حذف هذا الحدث؟') }}"
class="w-full px-3 py-2 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 transition-colors">
{{ __('حذف الحدث') }}
</button>
</div>
@endif
@endcan
</div>
</div>
</div>
<div>
@if($submitted)
{{-- Success --}}
<div class="bg-green-50 border border-green-200 rounded-2xl p-8 text-center">
<div class="w-16 h-16 mx-auto bg-green-100 rounded-full flex items-center justify-center mb-4">
<svg class="w-8 h-8 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>
<h3 class="text-xl font-bold text-green-800 mb-2">{{ __('تم التسجيل بنجاح!') }}</h3>
<p class="text-green-700 mb-4">{{ __('رقم التسجيل الخاص بك:') }}</p>
<div class="inline-block bg-white border-2 border-green-300 rounded-xl px-6 py-3">
<span class="text-2xl font-mono font-bold text-green-800" dir="ltr">{{ $registrationNumber }}</span>
</div>
<p class="text-sm text-green-600 mt-4">{{ __('يرجى الاحتفاظ بهذا الرقم للمتابعة') }}</p>
</div>
@else
{{-- Registration Form --}}
<form wire:submit="submit" class="space-y-5">
@error('form')
<div class="p-4 bg-red-50 border border-red-200 rounded-xl text-red-700 text-sm">{{ $message }}</div>
@enderror
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@foreach($event->form_fields as $field)
<div class="{{ $field['width'] === 'full' ? 'md:col-span-2' : '' }}">
<label class="block text-sm font-medium text-gray-700 mb-1">
{{ $field['label'] }}
@if($field['is_required']) <span class="text-red-500">*</span> @endif
</label>
@switch($field['type'])
@case('text')
@case('email')
@case('phone')
@case('number')
<input type="{{ $field['type'] === 'phone' ? 'tel' : $field['type'] }}"
wire:model="fields.{{ $field['key'] }}"
placeholder="{{ $field['placeholder'] ?? '' }}"
@if(in_array($field['type'], ['email', 'phone', 'number'])) dir="ltr" @endif
class="w-full px-4 py-3 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('fields.'.$field['key']) border-red-500 @enderror">
@break
@case('textarea')
<textarea wire:model="fields.{{ $field['key'] }}" rows="3"
placeholder="{{ $field['placeholder'] ?? '' }}"
class="w-full px-4 py-3 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('fields.'.$field['key']) border-red-500 @enderror"></textarea>
@break
@case('date')
<input type="date" wire:model="fields.{{ $field['key'] }}" dir="ltr"
class="w-full px-4 py-3 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('fields.'.$field['key']) border-red-500 @enderror">
@break
@case('select')
<select wire:model="fields.{{ $field['key'] }}"
class="w-full px-4 py-3 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('fields.'.$field['key']) border-red-500 @enderror">
<option value="">{{ $field['placeholder'] ?? __('اختر...') }}</option>
@foreach($field['options'] ?? [] as $option)
<option value="{{ $option['value'] }}">{{ $option['label'] }}</option>
@endforeach
</select>
@break
@case('multi_select')
<div class="space-y-2 border border-gray-300 rounded-xl p-3 @error('fields.'.$field['key']) border-red-500 @enderror">
@foreach($field['options'] ?? [] as $option)
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" wire:model="fields.{{ $field['key'] }}" value="{{ $option['value'] }}" class="rounded text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-700">{{ $option['label'] }}</span>
</label>
@endforeach
</div>
@break
@case('checkbox')
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" wire:model="fields.{{ $field['key'] }}" class="rounded text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-700">{{ $field['placeholder'] ?? $field['label'] }}</span>
</label>
@break
@endswitch
@error('fields.'.$field['key']) <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
@endforeach
</div>
<button type="submit" wire:loading.attr="disabled" wire:target="submit"
class="w-full py-3.5 bg-blue-600 text-white rounded-xl font-medium hover:bg-blue-700 transition-colors disabled:opacity-50">
<span wire:loading.remove wire:target="submit">{{ __('تسجيل') }}</span>
<span wire:loading wire:target="submit">{{ __('جارٍ التسجيل...') }}</span>
</button>
</form>
@endif
</div>
@extends('website.layout')
@section('content')
<div class="site-section">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="text-center mb-12">
<h1 class="section-title mx-auto">{{ __('الأحداث والبطولات') }}</h1>
<p class="mt-4 text-gray-500 text-lg">{{ __('تابع أحدث الفعاليات والبطولات وسجّل الآن') }}</p>
</div>
@if($events->count())
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach($events as $event)
@php
$cover = $event->cover;
$statusColors = [
'published' => 'bg-green-100 text-green-800',
'registration_closed' => 'bg-yellow-100 text-yellow-800',
'in_progress' => 'bg-blue-100 text-blue-800',
'completed' => 'bg-gray-100 text-gray-800',
];
$statusLabels = [
'published' => 'التسجيل مفتوح',
'registration_closed' => 'التسجيل مغلق',
'in_progress' => 'جارٍ الآن',
'completed' => 'انتهى',
];
@endphp
<a href="{{ route('website.events.show', ['slug' => $academy->slug, 'eventSlug' => $event->slug]) }}"
class="site-card group block transition-transform hover:-translate-y-1">
<div class="relative overflow-hidden aspect-[16/9] rounded-t-xl">
@if($cover)
<img src="{{ $cover->url }}"
alt="{{ $event->title }}"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300">
@else
<div class="w-full h-full bg-gradient-to-br from-[var(--site-accent)]/20 to-[var(--site-primary)]/10 flex items-center justify-center">
<svg class="w-14 h-14 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
</div>
@endif
{{-- Status Badge --}}
<div class="absolute top-3 start-3">
<span class="inline-block px-2.5 py-1 text-xs font-bold rounded-full {{ $statusColors[$event->status] ?? 'bg-gray-100 text-gray-800' }}">
{{ $statusLabels[$event->status] ?? $event->status }}
</span>
</div>
</div>
<div class="p-5">
<h3 class="font-bold text-lg text-gray-900 group-hover:text-[var(--site-accent)] transition-colors line-clamp-1">
{{ $event->title }}
</h3>
<div class="mt-3 space-y-2 text-sm text-gray-500">
{{-- Date --}}
<div class="flex items-center gap-2">
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<span dir="ltr">{{ $event->starts_at->translatedFormat('d M Y') }}</span>
</div>
{{-- Location --}}
@if($event->location_name || $event->facility)
<div class="flex items-center gap-2">
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
<span class="line-clamp-1">{{ $event->location_name ?? $event->facility?->name_ar }}</span>
</div>
@endif
{{-- Capacity --}}
@if($event->max_capacity)
<div class="flex items-center gap-2">
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
<span>{{ $event->registrations_count }}/{{ $event->max_capacity }}</span>
</div>
@endif
</div>
</div>
</a>
@endforeach
</div>
<div class="mt-10">
{{ $events->links() }}
</div>
@else
<div class="text-center py-16">
<svg class="w-16 h-16 mx-auto text-gray-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<p class="text-gray-500 text-lg">{{ __('لا توجد أحداث حالياً') }}</p>
</div>
@endif
</div>
</div>
@endsection
@extends('website.layout')
@section('content')
<div class="site-section">
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{{-- Back --}}
<a href="{{ route('website.events.index', $academy->slug) }}"
class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-[var(--site-accent)] mb-6 transition-colors">
<svg class="w-4 h-4 rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
{{ __('العودة للأحداث') }}
</a>
{{-- Cover Image --}}
@if($event->cover)
<div class="relative rounded-2xl overflow-hidden aspect-[21/9] mb-8">
<img src="{{ $event->cover->url }}"
alt="{{ $event->title }}"
class="w-full h-full object-cover">
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
</div>
@endif
{{-- Header --}}
<div class="mb-8">
@php
$statusColors = [
'published' => 'bg-green-100 text-green-800',
'registration_closed' => 'bg-yellow-100 text-yellow-800',
'in_progress' => 'bg-blue-100 text-blue-800',
'completed' => 'bg-gray-100 text-gray-800',
];
$statusLabels = [
'published' => 'التسجيل مفتوح',
'registration_closed' => 'التسجيل مغلق',
'in_progress' => 'جارٍ الآن',
'completed' => 'انتهى',
];
$typeLabels = [
'tournament' => 'بطولة',
'camp' => 'معسكر',
'competition' => 'مسابقة',
'open_day' => 'يوم مفتوح',
'workshop' => 'ورشة عمل',
'friendly' => 'ودية',
'exhibition' => 'معرض',
'other' => 'أخرى',
];
@endphp
<div class="flex flex-wrap items-center gap-3 mb-4">
<span class="inline-block px-3 py-1 text-xs font-bold rounded-full {{ $statusColors[$event->status] ?? 'bg-gray-100 text-gray-800' }}">
{{ $statusLabels[$event->status] ?? $event->status }}
</span>
<span class="inline-block px-3 py-1 text-xs font-medium rounded-full bg-[var(--site-accent)]/10 text-[var(--site-accent)]">
{{ $typeLabels[$event->type] ?? $event->type }}
</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-gray-900" style="font-family: var(--site-heading-font);">
{{ $event->title }}
</h1>
</div>
{{-- Info Cards --}}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-10">
{{-- Date --}}
<div class="bg-white border border-gray-100 rounded-xl p-4 shadow-sm">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-blue-50 flex items-center justify-center">
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
</div>
<div>
<p class="text-xs text-gray-500">{{ __('التاريخ') }}</p>
<p class="text-sm font-semibold text-gray-900" dir="ltr">{{ $event->starts_at->translatedFormat('d M Y') }}</p>
</div>
</div>
</div>
{{-- Time --}}
<div class="bg-white border border-gray-100 rounded-xl p-4 shadow-sm">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-purple-50 flex items-center justify-center">
<svg class="w-5 h-5 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>
<div>
<p class="text-xs text-gray-500">{{ __('الوقت') }}</p>
<p class="text-sm font-semibold text-gray-900" dir="ltr">{{ $event->starts_at->format('h:i A') }}</p>
</div>
</div>
</div>
{{-- Location --}}
<div class="bg-white border border-gray-100 rounded-xl p-4 shadow-sm">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-green-50 flex items-center justify-center">
<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
</div>
<div>
<p class="text-xs text-gray-500">{{ __('المكان') }}</p>
<p class="text-sm font-semibold text-gray-900 line-clamp-1">{{ $event->location_name ?? $event->facility?->name_ar ?? '-' }}</p>
</div>
</div>
</div>
{{-- Capacity --}}
<div class="bg-white border border-gray-100 rounded-xl p-4 shadow-sm">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-orange-50 flex items-center justify-center">
<svg class="w-5 h-5 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
</div>
<div>
<p class="text-xs text-gray-500">{{ __('المقاعد') }}</p>
<p class="text-sm font-semibold text-gray-900">
@if($event->max_capacity)
{{ $event->registrations_count }}/{{ $event->max_capacity }}
@else
{{ __('مفتوح') }}
@endif
</p>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
{{-- Main Content --}}
<div class="lg:col-span-2 space-y-8">
{{-- Description --}}
@if($event->description)
<div class="prose prose-lg max-w-none text-gray-700 leading-relaxed">
{!! nl2br(e($event->description)) !!}
</div>
@endif
{{-- Gallery --}}
@if($event->gallery && $event->gallery->count())
<div>
<h3 class="text-lg font-bold text-gray-900 mb-4">{{ __('معرض الصور') }}</h3>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
@foreach($event->gallery as $media)
<div class="aspect-square rounded-xl overflow-hidden">
<img src="{{ $media->url }}" alt="" class="w-full h-full object-cover hover:scale-105 transition-transform duration-300">
</div>
@endforeach
</div>
</div>
@endif
</div>
{{-- Sidebar: Registration Form --}}
<div class="lg:col-span-1">
<div class="sticky top-6">
<div class="bg-white border border-gray-200 rounded-2xl p-6 shadow-sm">
<h3 class="text-lg font-bold text-gray-900 mb-4">{{ __('التسجيل في الحدث') }}</h3>
@if($event->isRegistrationOpen())
@if($event->max_capacity)
<div class="mb-4">
@php $remaining = $event->spotsRemaining(); @endphp
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-500">{{ __('المقاعد المتبقية') }}</span>
<span class="font-semibold {{ $remaining <= 5 ? 'text-red-600' : 'text-green-600' }}">{{ $remaining }}</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="h-2 rounded-full {{ $remaining <= 5 ? 'bg-red-500' : 'bg-green-500' }}" style="width: {{ min(100, ($event->registrations_count / $event->max_capacity) * 100) }}%"></div>
</div>
</div>
@endif
@livewire('public.event-registration-form', ['event' => $event])
@elseif(!$event->hasCapacity())
<div class="text-center py-6">
<div class="w-12 h-12 mx-auto bg-red-100 rounded-full flex items-center justify-center mb-3">
<svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/>
</svg>
</div>
<p class="text-red-700 font-semibold">{{ __('تم اكتمال العدد') }}</p>
</div>
@else
<div class="text-center py-6">
<div class="w-12 h-12 mx-auto bg-yellow-100 rounded-full flex items-center justify-center mb-3">
<svg class="w-6 h-6 text-yellow-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>
<p class="text-yellow-700 font-semibold">{{ __('التسجيل مغلق حالياً') }}</p>
@if($event->registration_opens_at && $event->registration_opens_at->isFuture())
<p class="text-sm text-gray-500 mt-2">
{{ __('يفتح التسجيل:') }}
<span dir="ltr">{{ $event->registration_opens_at->translatedFormat('d M Y - h:i A') }}</span>
</p>
@endif
</div>
@endif
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
......@@ -23,6 +23,9 @@
{{-- Vite Assets --}}
@vite(['resources/css/website.css', 'resources/js/website.js'])
{{-- Livewire --}}
@livewireStyles
{{-- Dynamic CSS Variables from Academy Branding --}}
<style>
:root {
......@@ -99,5 +102,7 @@ class="whatsapp-float"
وضع المعاينة — غير منشور
</div>
@endisset
@livewireScripts
</body>
</html>
......@@ -96,6 +96,8 @@
->group(function () {
Route::get('/', [PublicWebsiteController::class, 'show'])->name('show');
Route::post('/contact', [ContactFormController::class, 'submit'])->name('contact.submit');
Route::get('/events', [\App\Http\Controllers\PublicEventController::class, 'index'])->name('events.index');
Route::get('/events/{eventSlug}', [\App\Http\Controllers\PublicEventController::class, 'show'])->name('events.show');
});
/*
......@@ -310,6 +312,18 @@
Route::get('/hr/trainers/{trainer}/edit', \App\Livewire\HR\TrainerForm::class)->name('trainers.edit')
->middleware('permission:trainers.update');
// Events
Route::get('/events', \App\Livewire\Events\EventList::class)->name('events.list')
->middleware('permission:events.list');
Route::get('/events/create', \App\Livewire\Events\CreateEventWizard::class)->name('events.create')
->middleware('permission:events.create');
Route::get('/events/{event}', \App\Livewire\Events\EventShow::class)->name('events.show')
->middleware('permission:events.view');
Route::get('/events/{event}/edit', \App\Livewire\Events\CreateEventWizard::class)->name('events.edit')
->middleware('permission:events.update');
Route::get('/events/{event}/registrations', \App\Livewire\Events\EventRegistrationList::class)->name('events.registrations')
->middleware('permission:events.view');
// HR - Payroll
Route::get('/hr/payroll', \App\Livewire\HR\PayrollDashboard::class)->name('payroll.dashboard')
->middleware('permission:payroll.manage');
......
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