This commit is contained in:
root committed 2026-05-11 17:58:21 +00:00
1 parent b03c1d4619
commit b07423aef3
42 files changed
+1431 -394

No files matched your search

@@ -17,10 +17,18 @@ class OperatorController extends Controller
public function dashboard(Request $request): \Illuminate\View\View
{
$user = Auth::user();
$assignedLotId = $user->parking_lot_id; // null = no restriction (e.g. admin)
$user = Auth::user();
$assignedLotIds = $user->assignedLots->pluck('id')->toArray();
$rawLots = ParkingLot::active()->withStatus()->get();
// 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,
@@ -34,20 +42,22 @@ class OperatorController extends Controller
'lat' => (float) $lot->latitude,
'lng' => (float) $lot->longitude,
'image' => $lot->image ? Storage::url($lot->image) : null,
'locked' => $assignedLotId !== null && $lot->id !== $assignedLotId,
'locked' => false,
])->values();
// If operator has an assigned lot, force that lot
$selectedLotId = $request->get('lot_id');
if ($assignedLotId !== null) {
// Reject any attempt to view a different lot
if ($selectedLotId && (int) $selectedLotId !== $assignedLotId) {
return redirect()->route('operator.dashboard', ['lot_id' => $assignedLotId]);
}
// Auto-select assigned lot if nothing selected
if (!$selectedLotId) {
$selectedLotId = $assignedLotId;
// 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;
@@ -73,10 +83,177 @@ class OperatorController extends Controller
}
return view('operator.dashboard', compact(
'parkingLots', 'selectedLot', 'activeCars', 'reservations', 'selectedLotId', 'assignedLotId'
'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