Commit 9454aa03 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Replace emoji with icons; scope schedule builder by branch and sport

Icons
- No emoji anywhere in the UI. Extracted the sidebar's inline SVG map into
  a single <x-ui.icon name="..."> component and added the icons the pricing
  work needed, so there is one source instead of a per-view copy. Discount
  recipes now carry icon NAMES, not glyphs.

Schedule builder
- Facilities are scoped to the selected branch. The screen listed every
  branch's facilities, which is how someone books the wrong building. A
  ?facility_id= carried over from another branch (bookmark, back button) is
  now dropped instead of silently overriding the branch scope.
- Groups are scoped to the facility's branch AND to the sports that facility
  hosts, so a football court no longer offers swimming groups. That link did
  not exist, so this adds a facility_activities pivot. A facility that
  declares no activities still hosts anything, so nothing breaks for academies
  that have not filled it in.

Facility grid
- Removed the arbitrary ceilings (rows/columns capped at 10, lanes at 20).
  Physical space is not limited to a number we picked.
- New facilities never got a layout, which is why the grid silently failed to
  appear on them. FacilityService::create now seeds one, the migration
  backfills every existing facility that has none, and the default is a 1x1
  grid — "one whole space, not subdivided yet" — rather than inventing a
  subdivision nobody asked for.
- Grid size is editable straight from facility settings, with a live preview
  of the cells being described. Shrinking onto a segment that holds a
  confirmed future reservation is refused rather than silently dropping
  someone's booking.
- Sports and starting grid are both settable at creation time too.
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
parent b3f127c8
......@@ -98,6 +98,26 @@ public function reservations(): HasMany
return $this->hasMany(SpaceReservation::class);
}
/**
* Sports this facility can host. Empty = hosts anything (the default for
* facilities created before the link existed).
*/
public function activities(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
{
return $this->belongsToMany(
\App\Domain\Training\Models\Activity::class,
'facility_activities',
'facility_id',
'activity_id'
)->withTimestamps();
}
/** Facilities belonging to one branch. */
public function scopeForBranch($query, ?int $branchId)
{
return $branchId ? $query->where('branch_id', $branchId) : $query;
}
public function scopeActive($query)
{
return $query->where('status', FacilityStatus::Active);
......
......@@ -19,13 +19,36 @@ class FacilityService
'unavailable' => ['active'],
];
public function create(array $data, User $creator): Facility
public function __construct(private SpaceLayoutService $layouts) {}
/**
* @param array $data Facility attributes
* @param User $creator
* @param array $activityIds Sports this facility can host (empty = any)
* @param array $grid ['rows' => n, 'columns' => n] for the starting layout
*/
public function create(array $data, User $creator, array $activityIds = [], array $grid = []): Facility
{
return DB::transaction(function () use ($data, $creator) {
return Facility::create([
return DB::transaction(function () use ($data, $creator, $activityIds, $grid) {
$facility = Facility::create([
...$data,
'created_by' => $creator->id,
]);
if ($activityIds) {
$facility->activities()->sync($activityIds);
}
// Every facility gets a layout on day one. Without one the schedule
// grid renders empty and the facility looks broken rather than new.
$this->layouts->createDefaultLayout(
$facility,
$creator,
max(1, (int) ($grid['rows'] ?? 1)),
max(1, (int) ($grid['columns'] ?? 1)),
);
return $facility->fresh();
});
}
......
......@@ -2,7 +2,9 @@
namespace App\Domain\Facility\Services;
use App\Domain\Facility\Models\Facility;
use App\Domain\Facility\Models\SpaceLayout;
use App\Domain\Facility\Models\SpaceReservation;
use App\Domain\Facility\Models\SpaceSegment;
use App\Domain\Shared\Exceptions\DomainException;
use App\Models\User;
......@@ -32,6 +34,82 @@ public function create(array $data, User $creator): SpaceLayout
/**
* Update a layout. Regenerates segments if type/config changed.
*/
/**
* The starting layout for a new facility.
*
* Defaults to a single 1x1 cell — "the whole facility, not subdivided yet" —
* which is honest and keeps the schedule grid from rendering empty. The
* academy resizes it in facility settings whenever they are ready.
*/
public function createDefaultLayout(
Facility $facility,
User $creator,
int $rows = 1,
int $columns = 1
): SpaceLayout {
return $this->create([
'academy_id' => $facility->academy_id,
'facility_id' => $facility->id,
'name' => 'Default layout',
'name_ar' => 'التقسيم الافتراضي',
'layout_type' => 'grid',
'layout_config' => ['rows' => max(1, $rows), 'columns' => max(1, $columns)],
'is_recurring' => true,
'start_time' => $facility->operating_start ?: '00:00',
'end_time' => $facility->operating_end ?: '23:59',
'metadata' => ['auto_created' => true],
], $creator);
}
/**
* Resize a grid layout in place, regenerating its segments.
*
* Segments currently held by a confirmed reservation are protected — a
* shrink that would orphan a booking is refused rather than silently
* dropping someone's slot.
*/
public function resizeGrid(SpaceLayout $layout, int $rows, int $columns): SpaceLayout
{
$rows = max(1, $rows);
$columns = max(1, $columns);
$keptCodes = [];
for ($r = 1; $r <= $rows; $r++) {
for ($c = 1; $c <= $columns; $c++) {
$keptCodes[] = "R{$r}C{$c}";
}
}
$droppedIds = $layout->segments()
->whereNotIn('code', $keptCodes)
->pluck('id');
if ($droppedIds->isNotEmpty()) {
$inUse = SpaceReservation::where('status', 'confirmed')
->where('facility_id', $layout->facility_id)
->whereDate('reservation_date', '>=', now()->toDateString())
->get()
->filter(function ($reservation) use ($droppedIds) {
$segments = is_array($reservation->segment_ids)
? $reservation->segment_ids
: json_decode($reservation->segment_ids ?? '[]', true);
return ! empty(array_intersect($droppedIds->all(), $segments ?? []));
});
if ($inUse->isNotEmpty()) {
throw new DomainException(
'لا يمكن تصغير التقسيم: توجد حجوزات مؤكدة على الأجزاء التي ستُحذف'
);
}
}
return $this->update($layout, [
'layout_type' => 'grid',
'layout_config' => ['rows' => $rows, 'columns' => $columns],
]);
}
public function update(SpaceLayout $layout, array $data): SpaceLayout
{
return DB::transaction(function () use ($layout, $data) {
......
......@@ -5,6 +5,7 @@
/**
* The twelve discounts a sports academy actually gives.
*
* Icons are icon NAMES resolved by <x-ui.icon>, never emoji.
* A recipe pre-fills the builder — rule type, conditions, adjustment, stacking,
* pin — so authoring a sibling discount is "change two numbers", not five steps
* of database columns. Recipes are templates, not rows: once created the rule is
......@@ -14,7 +15,7 @@
{
public const RECIPES = [
'sibling' => [
'icon' => '👨‍👩‍👧',
'icon' => 'user-group',
'name_ar' => 'خصم الإخوة',
'hint_ar' => 'الابن الثاني فأكثر',
'rule_type' => 'sibling_order',
......@@ -25,7 +26,7 @@
'is_pinned' => true,
],
'early_bird' => [
'icon' => '🐦',
'icon' => 'clock',
'name_ar' => 'الحجز المبكر',
'hint_ar' => 'قبل بداية الموسم',
'rule_type' => 'enrollment_timing',
......@@ -36,7 +37,7 @@
'is_pinned' => true,
],
'scholarship' => [
'icon' => '🎓',
'icon' => 'academic-cap',
'name_ar' => 'منحة دراسية',
'hint_ar' => 'حسب التصنيف',
'rule_type' => 'classification',
......@@ -47,7 +48,7 @@
'is_pinned' => false,
],
'extra_program' => [
'icon' => '🏃',
'icon' => 'squares-plus',
'name_ar' => 'برنامج إضافي',
'hint_ar' => 'من البرنامج الثاني',
'rule_type' => 'enrollment_volume',
......@@ -58,7 +59,7 @@
'is_pinned' => true,
],
'morning_slot' => [
'icon' => '🌅',
'icon' => 'sun',
'name_ar' => 'مواعيد الصباح',
'hint_ar' => 'قبل الساعة ٣',
'rule_type' => 'day_time',
......@@ -69,7 +70,7 @@
'is_pinned' => false,
],
'annual' => [
'icon' => '📅',
'icon' => 'calendar-days',
'name_ar' => 'اشتراك سنوي',
'hint_ar' => 'دفعة واحدة',
'rule_type' => 'membership_duration',
......@@ -80,7 +81,7 @@
'is_pinned' => false,
],
'staff_child' => [
'icon' => '👔',
'icon' => 'briefcase',
'name_ar' => 'أبناء العاملين',
'hint_ar' => 'يُجمع مع غيره',
'rule_type' => 'classification',
......@@ -91,7 +92,7 @@
'is_pinned' => false,
],
'loyalty' => [
'icon' => '',
'icon' => 'shield-check',
'name_ar' => 'عضو قديم',
'hint_ar' => 'بعد ١٢ شهر',
'rule_type' => 'loyalty',
......@@ -102,7 +103,7 @@
'is_pinned' => false,
],
'season_end' => [
'icon' => '🏁',
'icon' => 'flag',
'name_ar' => 'نهاية الموسم',
'hint_ar' => 'شهور محددة',
'rule_type' => 'seasonal',
......@@ -113,7 +114,7 @@
'is_pinned' => false,
],
'birthday' => [
'icon' => '🎂',
'icon' => 'cake',
'name_ar' => 'عرض عيد الميلاد',
'hint_ar' => 'شهر الميلاد',
'rule_type' => 'seasonal',
......@@ -124,7 +125,7 @@
'is_pinned' => false,
],
'juniors' => [
'icon' => '🧒',
'icon' => 'user',
'name_ar' => 'خصم البراعم',
'hint_ar' => 'أقل من ٧ سنوات',
'rule_type' => 'age',
......@@ -135,7 +136,7 @@
'is_pinned' => false,
],
'manual' => [
'icon' => '',
'icon' => 'hand-raised',
'name_ar' => 'خصم استثنائي',
'hint_ar' => 'يدوي باعتماد',
'rule_type' => 'custom',
......
......@@ -50,6 +50,13 @@ public function mount(): void
$this->branches = Branch::orderBy('name_ar')->get(['id', 'name_ar'])->toArray();
}
/** Sports this facility can host (empty = any). */
public array $activityIds = [];
/** Starting grid. 1x1 means "one whole space, not subdivided yet". */
public int $gridRows = 1;
public int $gridColumns = 1;
public function getStepLabels(): array
{
return [
......@@ -150,7 +157,12 @@ public function confirm(): void
'status' => 'active',
];
app(FacilityService::class)->create($data, auth()->user());
app(FacilityService::class)->create(
$data,
auth()->user(),
$this->activityIds,
['rows' => $this->gridRows, 'columns' => $this->gridColumns],
);
$this->completed = true;
session()->flash('success', 'تم إنشاء المنشأة بنجاح');
......@@ -161,6 +173,10 @@ public function confirm(): void
public function render()
{
return view('livewire.facilities.create-facility-wizard');
return view('livewire.facilities.create-facility-wizard', [
'activities' => \App\Domain\Training\Models\Activity::where('is_active', true)
->orderBy('name_ar')
->get(['id', 'name_ar']),
]);
}
}
......@@ -42,6 +42,14 @@ class FacilityForm extends Component
public ?string $latitude = null;
public ?string $longitude = null;
public int $sort_order = 0;
/** Sports this facility hosts. Empty = any sport (no filtering). */
public array $activityIds = [];
/** Live grid size, edited straight from facility settings. */
public int $gridRows = 1;
public int $gridColumns = 1;
public ?string $gridError = null;
public string $branch_id = '';
public function mount(?Facility $facility = null): void
......@@ -75,6 +83,13 @@ public function mount(?Facility $facility = null): void
$this->longitude = $facility->longitude;
$this->sort_order = $facility->sort_order;
$this->branch_id = (string) $facility->branch_id;
$this->activityIds = $facility->activities()->pluck('activities.id')->all();
$layout = $facility->activeLayout;
$config = $layout?->layout_config ?? [];
$this->gridRows = (int) ($config['rows'] ?? 1);
$this->gridColumns = (int) ($config['columns'] ?? 1);
}
}
......@@ -173,9 +188,16 @@ public function save(FacilityService $service): void
if ($this->editing) {
$service->update($this->facility, $data);
$this->facility->activities()->sync($this->activityIds);
$this->syncGrid();
session()->flash('success', __('تم تحديث المنشأة بنجاح'));
} else {
$service->create($data, auth()->user());
$service->create(
$data,
auth()->user(),
$this->activityIds,
['rows' => $this->gridRows, 'columns' => $this->gridColumns],
);
session()->flash('success', __('تم إنشاء المنشأة بنجاح'));
}
......@@ -185,10 +207,49 @@ public function save(FacilityService $service): void
}
}
/**
* Apply the grid size typed in facility settings.
*
* A facility with no layout gets one rather than staying grid-less — that
* silent gap is why new facilities showed an empty schedule.
*/
private function syncGrid(): void
{
$this->gridError = null;
$layouts = app(\App\Domain\Facility\Services\SpaceLayoutService::class);
$rows = max(1, $this->gridRows);
$columns = max(1, $this->gridColumns);
try {
$layout = $this->facility->activeLayout;
if (! $layout) {
$layouts->createDefaultLayout($this->facility, auth()->user(), $rows, $columns);
return;
}
$config = $layout->layout_config ?? [];
if ((int) ($config['rows'] ?? 0) === $rows && (int) ($config['columns'] ?? 0) === $columns) {
return;
}
$layouts->resizeGrid($layout, $rows, $columns);
} catch (DomainException $e) {
// Shrinking onto a booked segment is refused; keep the rest of the save.
$this->gridError = $e->getMessage();
session()->flash('warning', $e->getMessage());
}
}
public function render()
{
return view('livewire.facilities.facility-form', [
'branches' => Branch::orderBy('name_ar')->get(['id', 'name_ar']),
'activities' => \App\Domain\Training\Models\Activity::where('is_active', true)
->orderBy('name_ar')
->get(['id', 'name_ar']),
'typeOptions' => collect(FacilityType::cases())->mapWithKeys(fn ($t) => [$t->value => $t->label()]),
'statusOptions' => collect(FacilityStatus::cases())->mapWithKeys(fn ($s) => [$s->value => $s->label()]),
]);
......
......@@ -9,6 +9,7 @@
use App\Domain\HR\Services\TrainerWorkloadService;
use App\Domain\Scheduling\Services\ScheduleConflictService;
use App\Domain\Training\Models\TrainingGroup;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Models\TrainingSchedule;
use App\Models\User;
use Illuminate\Support\Carbon;
......@@ -22,6 +23,8 @@
#[Title('جدولة المنشآت')]
class VisualScheduleBuilder extends Component
{
use UsesBranchScope;
#[Url]
public string $facility_id = '';
......@@ -58,11 +61,30 @@ public function mount(): void
if (!$this->selectedDay) {
$this->selectedDay = now()->toDateString();
}
// A ?facility_id= carried over from another branch (bookmark, back
// button, shared link) must not survive the branch scope.
if ($this->facility_id) {
$branchId = $this->getActiveBranchId();
$belongs = Facility::where('id', $this->facility_id)
->forBranch($branchId)
->exists();
if (! $belongs) {
$this->facility_id = '';
}
}
}
public function render()
{
$facilities = Facility::active()->with('branch')->orderBy('name_ar')->get();
// Only this branch's facilities. Showing every branch's courts on a
// branch-scoped screen is how a receptionist books the wrong building.
$facilities = Facility::active()
->forBranch($this->getActiveBranchId())
->with('branch')
->orderBy('name_ar')
->get();
$facility = $this->facility_id ? Facility::with('activeLayout')->find($this->facility_id) : null;
......@@ -930,9 +952,29 @@ private function resolveLayout(?Facility $facility): ?SpaceLayout
->first();
}
/**
* Groups offerable in the selected facility.
*
* Scoped two ways: the facility's branch, and the sports that facility can
* host. A football court must not list swimming groups. A facility that
* declares no activities hosts anything, so nothing breaks for academies
* that have not filled the link in yet.
*/
private function getAvailableGroups(): array
{
$facility = $this->facility_id
? Facility::with('activities:id')->find($this->facility_id)
: null;
$branchId = $facility?->branch_id ?? $this->getActiveBranchId();
$activityIds = $facility ? $facility->activities->pluck('id')->all() : [];
return TrainingGroup::whereIn('status', ['active', 'forming', 'full'])
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->when($activityIds, fn ($q) => $q->whereHas(
'program',
fn ($p) => $p->whereIn('activity_id', $activityIds)
))
->with(['program', 'headTrainer'])
->withCount(['schedules as active_schedules_count' => fn ($q) => $q->where('is_active', true)])
->orderBy('name_ar')
......
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
/**
* 1. facility_activities — which sports a facility can host.
*
* Without this the schedule builder offered every group in the academy for
* every facility, so a football court listed swimming groups.
* A facility with NO rows declared hosts anything (existing behaviour), so
* this is additive and nothing breaks until an academy opts in.
*
* 2. A default layout for every facility that has none — the reason the grid
* silently failed to appear on newly created facilities.
*/
return new class extends Migration
{
public function up(): void
{
Schema::create('facility_activities', function (Blueprint $table) {
$table->id();
$table->foreignId('academy_id')->constrained('academies');
$table->foreignId('facility_id')->constrained('facilities')->cascadeOnDelete();
$table->foreignId('activity_id')->constrained('activities')->cascadeOnDelete();
$table->timestamps();
$table->unique(['facility_id', 'activity_id'], 'facility_activities_unique');
$table->index(['academy_id', 'activity_id']);
});
$this->backfillDefaultLayouts();
}
/**
* Every facility needs at least one layout or the schedule grid renders empty.
* A 1x1 grid ("the whole facility") is the honest default — it says the space
* is not subdivided yet, rather than inventing a subdivision nobody asked for.
*/
private function backfillDefaultLayouts(): void
{
$facilities = DB::table('facilities')
->whereNull('deleted_at')
->whereNotExists(function ($q) {
$q->selectRaw('1')->from('space_layouts')
->whereColumn('space_layouts.facility_id', 'facilities.id');
})
->get(['id', 'academy_id', 'created_by', 'operating_start', 'operating_end']);
foreach ($facilities as $facility) {
$layoutId = DB::table('space_layouts')->insertGetId([
'uuid' => (string) Str::uuid(),
'academy_id' => $facility->academy_id,
'facility_id' => $facility->id,
'name' => 'Default layout',
'name_ar' => 'التقسيم الافتراضي',
'layout_type' => 'grid',
'layout_config' => json_encode(['rows' => 1, 'columns' => 1]),
'is_recurring' => true,
'effective_day_of_week' => null,
'effective_date' => null,
'start_time' => $facility->operating_start ?: '00:00:00',
'end_time' => $facility->operating_end ?: '23:59:00',
'is_active' => true,
'sort_order' => 0,
'metadata' => json_encode(['auto_created' => true]),
'created_by' => $facility->created_by,
'created_at' => now(),
'updated_at' => now(),
]);
DB::table('space_segments')->insert([
'uuid' => (string) Str::uuid(),
'space_layout_id' => $layoutId,
'code' => 'R1C1',
'name' => 'Whole facility',
'name_ar' => 'المنشأة كاملة',
'row_index' => 1,
'col_index' => 1,
'sort_order' => 0,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
public function down(): void
{
Schema::dropIfExists('facility_activities');
// Only remove layouts this migration created.
$ids = DB::table('space_layouts')->where('metadata->auto_created', true)->pluck('id');
DB::table('space_segments')->whereIn('space_layout_id', $ids)->delete();
DB::table('space_layouts')->whereIn('id', $ids)->delete();
}
};
......@@ -37,12 +37,12 @@
class="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg border-2 text-sm font-bold transition-colors
{{ $isOn ? 'bg-emerald-100 border-emerald-600 text-emerald-800'
: 'bg-white border-gray-300 text-gray-700 hover:border-emerald-400' }}">
@if($pin['icon'])<span>{{ $pin['icon'] }}</span>@endif
@if($pin['icon'])<x-ui.icon :name="$pin['icon']" class="w-4 h-4" />@endif
<span>{{ $pin['name'] }}</span>
@if($pin['value_label'])
<span dir="ltr" class="tabular-nums">{{ $pin['value_label'] }}</span>
@endif
@if($isOn)<span class="opacity-60">✕</span>@endif
@if($isOn)<x-ui.icon name="x-mark" class="w-3.5 h-3.5 opacity-60" />@endif
</button>
@endforeach
</div>
......@@ -53,8 +53,12 @@ class="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg border-2 text-sm fo
class="w-full flex items-center justify-between gap-2 px-3 py-2.5 bg-white
border-2 border-emerald-500 rounded-lg text-sm font-semibold text-gray-700
focus:outline-none focus:ring-2 focus:ring-emerald-500">
<span>🔍 {{ __('اختر خصم...') }}</span>
<span class="text-emerald-600 text-xs" x-text="open ? '▴' : '▾'"></span>
<span class="inline-flex items-center gap-1.5">
<x-ui.icon name="magnifying-glass" class="w-4 h-4 text-gray-400" />
{{ __('اختر خصم...') }}
</span>
<x-ui.icon name="chevron-down" class="w-4 h-4 text-emerald-600 transition-transform"
x-bind:class="open && 'rotate-180'" />
</button>
<div x-show="open" x-cloak @click.outside="open = false" x-transition
......@@ -91,8 +95,14 @@ class="w-full flex items-center justify-between gap-3 px-3 py-2 text-sm text-sta
: ($isOn ? 'bg-emerald-50 text-emerald-800 font-bold'
: ($approval ? 'text-amber-700 hover:bg-amber-50' : 'text-gray-700 hover:bg-gray-50')) }}">
<span class="flex items-center gap-1.5">
@if($isOn)<span>✓</span>@elseif($approval)<span>⚠</span>@elseif($blocked)<span>✕</span>@endif
@if($row['icon'])<span>{{ $row['icon'] }}</span>@endif
@if($isOn)
<x-ui.icon name="check" class="w-4 h-4" />
@elseif($approval)
<x-ui.icon name="exclamation-triangle" class="w-4 h-4" />
@elseif($blocked)
<x-ui.icon name="no-symbol" class="w-4 h-4" />
@endif
@if($row['icon'])<x-ui.icon :name="$row['icon']" class="w-4 h-4" />@endif
<span>{{ $row['name'] }}</span>
</span>
<span dir="ltr" class="tabular-nums text-xs whitespace-nowrap">
......@@ -114,7 +124,8 @@ class="w-full flex items-center justify-between gap-3 px-3 py-2 text-sm text-sta
<div class="border-t border-gray-100 bg-gray-50 p-3">
<button type="button" @click="manual = !manual"
class="text-sm font-bold text-amber-700 hover:text-amber-800">
✋ {{ __('خصم استثنائي') }}
<x-ui.icon name="hand-raised" class="w-4 h-4 inline-block align-text-bottom" />
{{ __('خصم استثنائي') }}
<span class="text-xs font-normal text-gray-500">
({{ __('حتى') }} {{ $manualCap }}%)
</span>
......@@ -159,7 +170,9 @@ class="w-full px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2
@endif
@if($canRemove)
<button type="button" wire:click="removeDiscount({{ $row['rule_id'] }})"
class="opacity-60 hover:opacity-100" title="{{ __('إلغاء') }}">✕</button>
class="opacity-60 hover:opacity-100" title="{{ __('إلغاء') }}">
<x-ui.icon name="x-mark" class="w-3.5 h-3.5" />
</button>
@endif
</span>
@endforeach
......
This diff is collapsed.
......@@ -197,6 +197,55 @@ class="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
</div>
</div>
</div>
{{-- Sports hosted + starting grid --}}
<div class="mt-6 pt-6 border-t border-gray-200 space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('الرياضات في هذه المنشأة') }}</label>
<p class="text-xs text-gray-500 mb-3">
{{ __('تُصفّي المجموعات المعروضة عند الجدولة — اتركها فارغة لعرض كل الرياضات') }}
</p>
<div class="flex flex-wrap gap-2">
@foreach($activities as $activity)
<label class="cursor-pointer">
<input type="checkbox" value="{{ $activity->id }}" wire:model="activityIds" class="peer sr-only">
<span class="inline-block px-3 py-1.5 rounded-full border-2 text-sm font-medium
border-gray-200 text-gray-600
peer-checked:border-emerald-500 peer-checked:bg-emerald-50 peer-checked:text-emerald-700">
{{ $activity->name_ar }}
</span>
</label>
@endforeach
@if($activities->isEmpty())
<p class="text-sm text-gray-400">{{ __('لا توجد أنشطة معرّفة بعد') }}</p>
@endif
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('تقسيم المساحة') }}</label>
<p class="text-xs text-gray-500 mb-3">
{{ __('اتركها 1×1 إذا كانت المنشأة مساحة واحدة — يمكن تغييرها لاحقاً من إعدادات المنشأة') }}
</p>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
<div>
<label class="block text-xs text-gray-500 mb-1">{{ __('صفوف') }}</label>
<input type="number" wire:model.live="gridRows" min="1" dir="ltr"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500">
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">{{ __('أعمدة') }}</label>
<input type="number" wire:model.live="gridColumns" min="1" dir="ltr"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500">
</div>
<div class="flex items-end">
<p class="text-sm text-gray-600 pb-2">
{{ max(1, (int) $gridRows) * max(1, (int) $gridColumns) }} {{ __('قطعة') }}
</p>
</div>
</div>
</div>
</div>
@endif
{{-- Step 3: Operating Hours --}}
......
......@@ -232,6 +232,96 @@ class="w-full text-sm px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2
</div>
</div>
{{-- Sports hosted here --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6">
<div class="flex items-start gap-2 mb-1">
<x-ui.icon name="bolt" class="w-5 h-5 text-blue-600 shrink-0 mt-0.5" />
<div>
<h2 class="text-base font-bold text-gray-800">{{ __('الرياضات في هذه المنشأة') }}</h2>
<p class="text-xs text-gray-500">
{{ __('تُستخدم لتصفية المجموعات في جدولة المنشآت — اتركها فارغة لعرض كل الرياضات') }}
</p>
</div>
</div>
<div class="flex flex-wrap gap-2 mt-4">
@foreach($activities as $activity)
<label class="cursor-pointer">
<input type="checkbox" value="{{ $activity->id }}" wire:model="activityIds" class="peer sr-only">
<span class="inline-block px-3 py-1.5 rounded-full border-2 text-sm font-medium
border-gray-200 text-gray-600
peer-checked:border-blue-500 peer-checked:bg-blue-50 peer-checked:text-blue-700">
{{ $activity->name_ar }}
</span>
</label>
@endforeach
@if($activities->isEmpty())
<p class="text-sm text-gray-400">{{ __('لا توجد أنشطة معرّفة بعد') }}</p>
@endif
</div>
</div>
{{-- Grid size --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6">
<div class="flex items-start gap-2 mb-1">
<x-ui.icon name="grid" class="w-5 h-5 text-blue-600 shrink-0 mt-0.5" />
<div>
<h2 class="text-base font-bold text-gray-800">{{ __('تقسيم المساحة') }}</h2>
<p class="text-xs text-gray-500">
{{ __('عدد الصفوف والأعمدة التي تُقسم بها المنشأة فعلياً — بدون حد أقصى') }}
</p>
</div>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4 mt-4">
<div>
<label for="gridRows" class="block text-sm font-medium text-gray-700 mb-1">{{ __('عدد الصفوف') }}</label>
<input type="number" id="gridRows" wire:model.live="gridRows" min="1" dir="ltr"
class="w-full text-sm px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label for="gridColumns" class="block text-sm font-medium text-gray-700 mb-1">{{ __('عدد الأعمدة') }}</label>
<input type="number" id="gridColumns" wire:model.live="gridColumns" min="1" dir="ltr"
class="w-full text-sm px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div class="flex items-end">
<p class="text-sm text-gray-600 pb-2.5">
{{ __('الناتج') }}:
<span class="font-bold text-gray-900">{{ max(1, (int) $gridRows) * max(1, (int) $gridColumns) }}</span>
{{ __('قطعة') }}
</p>
</div>
</div>
{{-- Live preview of the grid being described --}}
@php
$pr = min(max(1, (int) $gridRows), 12);
$pc = min(max(1, (int) $gridColumns), 12);
@endphp
<div class="mt-4">
<div class="inline-grid gap-1 p-2 bg-gray-50 border border-gray-200 rounded-lg"
style="grid-template-columns: repeat({{ $pc }}, minmax(0, 1fr));">
@for($r = 1; $r <= $pr; $r++)
@for($c = 1; $c <= $pc; $c++)
<div class="w-9 h-9 rounded bg-white border border-blue-200 flex items-center justify-center
text-[10px] text-blue-700 font-medium" dir="ltr">R{{ $r }}C{{ $c }}</div>
@endfor
@endfor
</div>
@if($pr < (int) $gridRows || $pc < (int) $gridColumns)
<p class="text-xs text-gray-500 mt-2">
{{ __('المعاينة تعرض أول 12×12 فقط — سيتم إنشاء كل القطع عند الحفظ') }}
</p>
@endif
</div>
@if($gridError)
<div class="mt-3 p-3 bg-amber-50 border border-amber-200 rounded-lg text-sm text-amber-800">
{{ $gridError }}
</div>
@endif
</div>
{{-- Actions --}}
<div class="flex flex-col-reverse sm:flex-row items-stretch sm:items-center justify-end gap-3">
<a href="{{ route('facilities.list') }}" wire:navigate
......
......@@ -64,18 +64,18 @@ class="w-full sm:w-auto px-4 py-2.5 bg-blue-600 text-white font-medium rounded-x
<div class="grid grid-cols-2 gap-3 sm:gap-4 p-4 bg-gray-50 rounded-xl">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('عدد الصفوف') }}</label>
<input type="number" wire:model="grid_rows" min="1" max="10" dir="ltr" class="w-full border-gray-300 rounded-lg py-2.5">
<input type="number" wire:model.live="grid_rows" min="1" dir="ltr" class="w-full border-gray-300 rounded-lg py-2.5">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('عدد الأعمدة') }}</label>
<input type="number" wire:model="grid_columns" min="1" max="10" dir="ltr" class="w-full border-gray-300 rounded-lg py-2.5">
<input type="number" wire:model.live="grid_columns" min="1" dir="ltr" class="w-full border-gray-300 rounded-lg py-2.5">
</div>
<p class="col-span-2 text-xs text-gray-500">{{ __('سيتم إنشاء') }} {{ $grid_rows * $grid_columns }} {{ __('قطعة') }}</p>
</div>
@elseif($layout_type === 'lanes')
<div class="p-4 bg-gray-50 rounded-xl">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('عدد الحارات') }}</label>
<input type="number" wire:model="lane_count" min="1" max="20" dir="ltr" class="w-32 border-gray-300 rounded-lg py-2.5">
<input type="number" wire:model.live="lane_count" min="1" dir="ltr" class="w-32 border-gray-300 rounded-lg py-2.5">
</div>
@elseif($layout_type === 'zones' || $layout_type === 'custom')
<div class="p-4 bg-gray-50 rounded-xl space-y-3">
......
......@@ -47,7 +47,7 @@ class="px-6 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-5
<button type="button" wire:click="chooseRecipe('{{ $key }}')"
class="p-4 rounded-xl border-2 border-gray-200 hover:border-emerald-500 hover:bg-emerald-50
text-center transition-colors focus:outline-none focus:ring-2 focus:ring-emerald-500">
<div class="text-2xl leading-9">{{ $recipe['icon'] }}</div>
<x-ui.icon :name="$recipe['icon']" class="w-7 h-7 mx-auto mb-1.5 text-emerald-600" />
<div class="text-sm font-bold text-gray-800">{{ $recipe['name_ar'] }}</div>
<div class="text-xs text-gray-400 mt-0.5">{{ $recipe['hint_ar'] }}</div>
</button>
......@@ -228,11 +228,13 @@ class="px-6 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-5
@if($audience)
<div class="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg text-sm text-blue-800 font-semibold">
👥 {{ __('تنطبق على') }} {{ $audience['matched'] }}
<x-ui.icon name="users" class="w-4 h-4 inline-block align-text-bottom" />
{{ __('تنطبق على') }} {{ $audience['matched'] }}
{{ __('مشترك من') }} {{ $audience['total'] }}
@if($audience['total'] > 0 && $audience['matched'] === $audience['total'])
<div class="text-xs font-normal text-blue-700 mt-1">
⚠ {{ __('تنطبق على الجميع — تأكد أن هذا مقصود') }}
<x-ui.icon name="exclamation-triangle" class="w-3.5 h-3.5 inline-block align-text-bottom" />
{{ __('تنطبق على الجميع — تأكد أن هذا مقصود') }}
</div>
@endif
</div>
......
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