assignedLots->pluck('id')->toArray(); // Operators only see their assigned lots; no assignment = no lots visible $query = ParkingLot::active()->withStatus(); if (!empty($assignedLotIds)) { $query->whereIn('id', $assignedLotIds); } else { $query->whereRaw('1 = 0'); // no lots assigned → show nothing } $rawLots = $query->get(); $parkingLots = $rawLots->map(fn($lot) => [ 'id' => $lot->id, 'name' => $lot->name, 'address' => $lot->address, 'total' => $lot->total_capacity, 'avail' => max(0, $lot->total_capacity - ($lot->active_bookings_count + $lot->active_registries_count)), 'occupied' => $lot->active_bookings_count + $lot->active_registries_count, 'price' => (float) $lot->price_per_hour, 'hours' => $lot->working_hours, 'lat' => (float) $lot->latitude, 'lng' => (float) $lot->longitude, 'image' => $lot->image ? Storage::url($lot->image) : null, 'locked' => false, ])->values(); $selectedLotId = $request->get('lot_id'); // Reject attempts to access a lot not in the operator's assigned list if ($selectedLotId && !in_array((int) $selectedLotId, $assignedLotIds)) { if (!empty($assignedLotIds)) { return redirect()->route('operator.dashboard', ['lot_id' => $assignedLotIds[0]]); } return redirect()->route('operator.dashboard'); } // Auto-select when only one lot is assigned if (!$selectedLotId && count($assignedLotIds) === 1) { $selectedLotId = $assignedLotIds[0]; } $selectedLot = null; $activeCars = collect(); $reservations = collect(); if ($selectedLotId) { $selectedLot = ParkingLot::active()->findOrFail($selectedLotId); // Walk-in cars currently inside (source=walk_in, status=active) $activeCars = Booking::where('parking_lot_id', $selectedLotId) ->where('source', 'walk_in') ->where('status', 'active') ->latest('start_time') ->get(); // Pre-reservations not yet activated (source=reservation, status=active) $reservations = Booking::where('parking_lot_id', $selectedLotId) ->where('source', 'reservation') ->where('status', 'active') ->orderBy('start_time') ->get(); } return view('operator.dashboard', compact( 'parkingLots', 'selectedLot', 'activeCars', 'reservations', 'selectedLotId', 'assignedLotIds' )); } // ── Operator stats page ───────────────────────────────────────────────────── public function statsPage(): \Illuminate\View\View { $user = Auth::user(); $assignedLotIds = $user->assignedLots->pluck('id')->toArray(); $lots = ParkingLot::whereIn('id', $assignedLotIds)->orderBy('name')->get(); return view('operator.stats', compact('lots', 'assignedLotIds')); } public function statsJson(): JsonResponse { $user = Auth::user(); $assignedLotIds = $user->assignedLots->pluck('id')->toArray(); // Active vehicles right now $activeCars = Booking::whereIn('parking_lot_id', $assignedLotIds) ->where('source', 'walk_in') ->where('status', 'active') ->count(); // Walk-in check-ins started today $checkInsToday = Booking::whereIn('parking_lot_id', $assignedLotIds) ->where('source', 'walk_in') ->whereDate('start_time', today()) ->count(); // Revenue collected today (completed + paid today) $revenueToday = (float) Booking::whereIn('parking_lot_id', $assignedLotIds) ->where('status', 'completed') ->whereDate('paid_at', today()) ->sum('total_fee'); // Total capacity & occupied across assigned lots $lots = ParkingLot::whereIn('id', $assignedLotIds)->get(); $totalCapacity = $lots->sum('total_capacity'); $totalOccupied = Booking::whereIn('parking_lot_id', $assignedLotIds) ->where('status', 'active') ->count(); $availableSpaces = max(0, $totalCapacity - $totalOccupied); // Pending reservations (not yet activated) $pendingReservations = Booking::whereIn('parking_lot_id', $assignedLotIds) ->where('source', 'reservation') ->where('status', 'active') ->count(); // 7-day daily check-ins and revenue $dailyCheckIns = []; $dailyRevenue = []; $dailyDates = []; for ($i = 6; $i >= 0; $i--) { $date = now()->subDays($i)->toDateString(); $dailyDates[] = $date; $dailyCheckIns[] = Booking::whereIn('parking_lot_id', $assignedLotIds) ->where('source', 'walk_in') ->whereDate('start_time', $date) ->count(); $dailyRevenue[] = (float) Booking::whereIn('parking_lot_id', $assignedLotIds) ->where('status', 'completed') ->whereDate('paid_at', $date) ->sum('total_fee'); } // Per-lot breakdown $lotStats = $lots->map(function ($lot) { $active = Booking::where('parking_lot_id', $lot->id)->where('status', 'active')->count(); $todayIns = Booking::where('parking_lot_id', $lot->id) ->where('source', 'walk_in') ->whereDate('start_time', today()) ->count(); $todayRev = (float) Booking::where('parking_lot_id', $lot->id) ->where('status', 'completed') ->whereDate('paid_at', today()) ->sum('total_fee'); return [ 'id' => $lot->id, 'name' => $lot->name, 'address' => $lot->address, 'total' => $lot->total_capacity, 'active' => $active, 'available' => max(0, $lot->total_capacity - $active), 'pct' => $lot->total_capacity > 0 ? round($active / $lot->total_capacity * 100) : 0, 'today_ins' => $todayIns, 'today_rev' => $todayRev, ]; })->values(); // Last 8 completed bookings $recentCompletions = Booking::whereIn('parking_lot_id', $assignedLotIds) ->where('status', 'completed') ->with('parkingLot:id,name') ->latest('paid_at') ->limit(8) ->get(['id', 'parking_lot_id', 'vehicle_plate', 'customer_name', 'start_time', 'paid_at', 'total_fee', 'payment_method']); // ── Work progress ────────────────────────────────────────────────────── // Checkouts completed today $checkoutsToday = Booking::whereIn('parking_lot_id', $assignedLotIds) ->where('status', 'completed') ->whereDate('paid_at', today()) ->count(); // Cancellations today $cancelledToday = Booking::whereIn('parking_lot_id', $assignedLotIds) ->where('status', 'cancelled') ->whereDate('updated_at', today()) ->count(); // Yesterday check-ins (comparison) $checkInsYesterday = Booking::whereIn('parking_lot_id', $assignedLotIds) ->where('source', 'walk_in') ->whereDate('start_time', today()->subDay()) ->count(); // Average stay duration in minutes (completed bookings today, start→paid_at) $completedToday = Booking::whereIn('parking_lot_id', $assignedLotIds) ->where('status', 'completed') ->whereDate('paid_at', today()) ->get(['start_time', 'paid_at']); $avgDurationMin = $completedToday->isNotEmpty() ? round($completedToday->avg(fn($b) => $b->start_time->diffInMinutes($b->paid_at))) : 0; // Completion rate today: completed / (completed + cancelled) $totalClosed = $checkoutsToday + $cancelledToday; $completionRate = $totalClosed > 0 ? round($checkoutsToday / $totalClosed * 100) : 100; // Hourly activity today (check-ins per hour 0–23) $hourlyActivity = array_fill(0, 24, 0); Booking::whereIn('parking_lot_id', $assignedLotIds) ->where('source', 'walk_in') ->whereDate('start_time', today()) ->get(['start_time']) ->each(function ($b) use (&$hourlyActivity) { $hourlyActivity[(int) $b->start_time->format('H')]++; }); $peakHour = (int) array_search(max($hourlyActivity), $hourlyActivity); return response()->json([ 'success' => true, 'data' => [ 'active_cars' => $activeCars, 'checkins_today' => $checkInsToday, 'revenue_today' => $revenueToday, 'available_spaces' => $availableSpaces, 'pending_reservations' => $pendingReservations, 'daily_checkins' => $dailyCheckIns, 'daily_revenue' => $dailyRevenue, 'daily_dates' => $dailyDates, 'lot_stats' => $lotStats, 'recent_completions' => $recentCompletions, // work progress 'checkouts_today' => $checkoutsToday, 'cancelled_today' => $cancelledToday, 'checkins_yesterday' => $checkInsYesterday, 'avg_duration_min' => $avgDurationMin, 'completion_rate' => $completionRate, 'hourly_activity' => array_values($hourlyActivity), 'peak_hour' => $peakHour, ], ]); } // ── Walk-in check-in ──────────────────────────────────────────────────────── public function checkIn(Request $request): JsonResponse { $data = $request->validate([ 'parking_lot_id' => 'required|exists:parking_lots,id', 'vehicle_plate' => 'required|string|max:50', 'customer_name' => 'nullable|string|max:255', 'phone' => 'nullable|string|max:20', 'duration_hours' => 'required|numeric|min:0.25|max:72', ]); $lot = ParkingLot::findOrFail($data['parking_lot_id']); $activeCount = $lot->bookings()->where('status', 'active')->count(); if ($activeCount >= $lot->total_capacity) { return response()->json(['success' => false, 'message' => 'الموقف ممتلئ حالياً'], 422); } $booking = Booking::create([ 'parking_lot_id' => $lot->id, 'vehicle_plate' => $data['vehicle_plate'], 'customer_name' => $data['customer_name'] ?? null, 'phone' => $data['phone'] ?? null, 'source' => 'walk_in', 'start_time' => now(), 'end_time' => now()->addHours((float) $data['duration_hours']), 'status' => 'active', 'pricing_snapshot' => $lot->pricingSnapshot(), ]); return response()->json([ 'success' => true, 'message' => 'تم تسجيل دخول السيارة بنجاح', 'data' => ['id' => $booking->id, 'plate' => $booking->vehicle_plate], ]); } // ── Activate a reservation (open parking for reserved car) ───────────────── public function activateReservation(Booking $booking): JsonResponse { if ($booking->source !== 'reservation' || $booking->status !== 'active') { return response()->json(['success' => false, 'message' => 'الحجز غير صالح للتفعيل'], 400); } $booking->update([ 'source' => 'walk_in', // now treated as an active walk-in 'start_time' => now(), 'end_time' => now()->addHours( max(1, now()->diffInHours($booking->end_time, false)) ), ]); return response()->json(['success' => true, 'message' => 'تم تفعيل الحجز وفتح بوابة الدخول']); } // ── Checkout: calculate fee and return receipt data ───────────────────────── public function checkoutPreview(Booking $booking): JsonResponse { if ($booking->status !== 'active') { return response()->json(['success' => false, 'message' => 'الحجز غير نشط'], 400); } $lot = $booking->parkingLot; $start = $booking->start_time; $end = now(); $duration = $start->diffInMinutes($end); $calc = $lot->calculateFee($start, $end, $booking->pricing_snapshot); return response()->json([ 'success' => true, 'data' => [ 'booking_id' => $booking->id, 'plate' => $booking->vehicle_plate, 'customer_name' => $booking->customer_name, 'lot_name' => $lot->name, 'entry_time' => $start->format('Y/m/d H:i'), 'exit_time' => $end->format('Y/m/d H:i'), 'duration_min' => $duration, 'duration_label'=> floor($duration / 60) . 'س ' . ($duration % 60) . 'د', 'fee_details' => $calc['details'], 'total_fee' => $calc['total'], ], ]); } // ── Process payment and complete booking ───────────────────────────────────── public function processPayment(Request $request, Booking $booking): JsonResponse { $data = $request->validate([ 'payment_method' => 'required|in:cash,upload', 'payment_proof' => 'nullable|file|mimes:jpg,jpeg,png,pdf|max:4096', ]); if ($booking->status !== 'active') { return response()->json(['success' => false, 'message' => 'الحجز غير نشط'], 400); } $lot = $booking->parkingLot; $start = $booking->start_time; $end = now(); $calc = $lot->calculateFee($start, $end, $booking->pricing_snapshot); $proofPath = null; if ($request->hasFile('payment_proof')) { $proofPath = $request->file('payment_proof') ->store('payment_proofs', 'public'); } $booking->update([ 'status' => 'completed', 'end_time' => $end, 'total_fee' => $calc['total'], 'payment_method' => $data['payment_method'], 'payment_proof' => $proofPath, 'paid_at' => now(), ]); return response()->json([ 'success' => true, 'message' => 'تم تسجيل الخروج وإتمام الدفع بنجاح', 'data' => [ 'total_fee' => $calc['total'], 'payment_method' => $data['payment_method'], ], ]); } // ── Cancel a pre-reservation (operator verifies name or phone) ────────────── public function cancelReservation(Request $request, Booking $booking): JsonResponse { if ($booking->source !== 'reservation' || $booking->status !== 'active') { return response()->json(['success' => false, 'message' => 'هذا الحجز لا يمكن إلغاؤه'], 400); } $input = trim($request->input('verification', '')); $nameMatch = $booking->customer_name && mb_strtolower($input) === mb_strtolower($booking->customer_name); $phoneMatch = $booking->phone && $input === $booking->phone; if (!$nameMatch && !$phoneMatch) { return response()->json(['success' => false, 'message' => 'الاسم أو رقم الهاتف غير مطابق'], 422); } $booking->update(['status' => 'cancelled']); return response()->json(['success' => true, 'message' => 'تم إلغاء الحجز بنجاح']); } // ── Legacy checkout (kept for backward compatibility) ─────────────────────── public function checkOut(Request $request, Booking $booking): JsonResponse { return $this->processPayment($request->merge(['payment_method' => 'cash']), $booking); } }