scp-syria/app/Http/Controllers/Operator/OperatorController.php
2026-05-11 17:58:21 +00:00

417 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Http\Controllers\Operator;
use App\Http\Controllers\Controller;
use App\Models\Booking;
use App\Models\ParkingLot;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Carbon\Carbon;
class OperatorController extends Controller
{
// ── Lot picker + panel ──────────────────────────────────────────────────────
public function dashboard(Request $request): \Illuminate\View\View
{
$user = Auth::user();
$assignedLotIds = $user->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 023)
$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);
}
}