initial: project state before operator dashboard card redesign
This commit is contained in:
commit
e0aed8eed7
113 files changed
+20074
No files matched your search
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Booking;
|
||||
use App\Models\ParkingLot;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class BookingController extends Controller
|
||||
{
|
||||
public function activeIndex(Request $request): \Illuminate\View\View
|
||||
{
|
||||
$parkingLotId = $request->get('parking_lot_id');
|
||||
$parkingLots = ParkingLot::active()->get(['id', 'name']);
|
||||
|
||||
$query = Booking::with('parkingLot')
|
||||
->where('status', 'active')
|
||||
->orderBy('start_time', 'desc');
|
||||
|
||||
if ($parkingLotId) {
|
||||
$query->where('parking_lot_id', $parkingLotId);
|
||||
}
|
||||
|
||||
$activeBookings = $query->paginate(50);
|
||||
|
||||
return view('admin.bookings.active', compact('activeBookings', 'parkingLots', 'parkingLotId'));
|
||||
}
|
||||
|
||||
public function completeBooking(Request $request, Booking $booking): JsonResponse
|
||||
{
|
||||
if ($booking->status !== 'active') {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'الحجز غير نشط'
|
||||
], 400);
|
||||
}
|
||||
|
||||
$parkingLot = $booking->parkingLot;
|
||||
$actualDurationHours = $booking->start_time->diffInHours(now());
|
||||
$actualFee = ceil($actualDurationHours) * $parkingLot->price_per_hour;
|
||||
|
||||
$booking->update([
|
||||
'status' => 'completed',
|
||||
'end_time' => now()
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'تم إنهاء الحجز بنجاح',
|
||||
'data' => [
|
||||
'actual_duration' => $actualDurationHours . ' ساعات',
|
||||
'actual_fee' => number_format($actualFee, 2) . ' ' . config('app.currency', 'ريال'),
|
||||
'vehicle_plate' => $booking->vehicle_plate ?? $booking->customer_name
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Booking;
|
||||
use App\Models\ParkingLot;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return view('admin.dashboard');
|
||||
}
|
||||
|
||||
public function statsJson(Request $request)
|
||||
{
|
||||
// Total metrics
|
||||
$totalParkingLots = ParkingLot::count();
|
||||
$totalBookings = Booking::count();
|
||||
|
||||
// Active bookings (assume status = 'active'; adjust if using time overlap)
|
||||
$activeBookings = Booking::where('status', 'active')->count();
|
||||
|
||||
// Occupancy rate: (active bookings / total capacity) * 100
|
||||
$totalCapacity = ParkingLot::sum('total_capacity');
|
||||
$occupancyRate = $totalCapacity > 0 ? round(($activeBookings / $totalCapacity) * 100, 1) : 0;
|
||||
|
||||
// Estimated revenue: sum price_per_hour for all active bookings
|
||||
$revenue = Booking::where('status', 'active')
|
||||
->join('parking_lots', 'bookings.parking_lot_id', '=', 'parking_lots.id')
|
||||
->sum('parking_lots.price_per_hour');
|
||||
|
||||
// Available spots (total capacity - active)
|
||||
$availableSpots = $totalCapacity - $activeBookings;
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'total_parking_lots' => $totalParkingLots,
|
||||
'total_bookings' => $totalBookings,
|
||||
'active_bookings' => $activeBookings,
|
||||
'occupancy_rate' => $occupancyRate,
|
||||
'estimated_revenue' => round($revenue, 2),
|
||||
'available_spots' => $availableSpots,
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function chartsJson(Request $request)
|
||||
{
|
||||
$now = Carbon::now();
|
||||
|
||||
// Daily bookings last 7 days
|
||||
$dailyBookings = Booking::selectRaw('DATE(created_at) as date, COUNT(*) as count')
|
||||
->whereBetween('created_at', [$now->clone()->subDays(7), $now])
|
||||
->groupBy('date')
|
||||
->orderBy('date')
|
||||
->pluck('count', 'date')
|
||||
->toArray();
|
||||
|
||||
// Fill missing days
|
||||
$dates = [];
|
||||
for ($i = 6; $i >= 0; $i--) {
|
||||
$date = $now->clone()->subDays($i)->format('Y-m-d');
|
||||
$dates[$date] = $dailyBookings[$date] ?? 0;
|
||||
}
|
||||
ksort($dates);
|
||||
|
||||
// Top 5 parking lots by total bookings
|
||||
$topLots = ParkingLot::withCount('bookings')
|
||||
->orderBy('bookings_count', 'desc')
|
||||
->take(5)
|
||||
->get(['id', 'name', 'bookings_count']);
|
||||
|
||||
// Occupancy trend: current vs previous day (simplified)
|
||||
$todayActive = Booking::where('status', 'active')->whereDate('created_at', $now->format('Y-m-d'))->count();
|
||||
$yesterdayActive = Booking::where('status', 'active')->whereDate('created_at', $now->clone()->subDay()->format('Y-m-d'))->count();
|
||||
$trend = $todayActive - $yesterdayActive; // positive growth
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'daily_bookings' => array_values($dates),
|
||||
'top_parking_lots' => $topLots->map(fn($lot) => ['name' => $lot->name, 'value' => $lot->bookings_count])->toArray(),
|
||||
'occupancy_trend' => $trend,
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreParkingLotRequest;
|
||||
use App\Models\ParkingLot;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class ParkingLotController extends Controller
|
||||
{
|
||||
public function index(Request $request): \Illuminate\View\View
|
||||
{
|
||||
$parkingLots = ParkingLot::withCount(['bookings as active_bookings_count' => fn($q) => $q->where('status', 'active')])
|
||||
->when($request->search, fn($q) => $q->search($request->search))
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(20);
|
||||
|
||||
return view('admin.parking-lots.index', compact('parkingLots'));
|
||||
}
|
||||
|
||||
public function show(ParkingLot $parkingLot): JsonResponse
|
||||
{
|
||||
$parkingLot->loadCount(['bookings as active_bookings_count' => fn($q) => $q->where('status', 'active')]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $parkingLot
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StoreParkingLotRequest $request): JsonResponse
|
||||
{
|
||||
$parkingLot = ParkingLot::create($request->validated());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'تم إضافة موقف السيارات بنجاح',
|
||||
'data' => $parkingLot->loadCount(['bookings as active_bookings_count' => fn($q) => $q->where('status', 'active')])
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(StoreParkingLotRequest $request, ParkingLot $parkingLot): JsonResponse
|
||||
{
|
||||
$parkingLot->update($request->validated());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'تم تحديث موقف السيارات بنجاح',
|
||||
'data' => $parkingLot->loadCount(['bookings as active_bookings_count' => fn($q) => $q->where('status', 'active')])
|
||||
]);
|
||||
}
|
||||
|
||||
public function toggleStatus(ParkingLot $parkingLot): JsonResponse
|
||||
{
|
||||
$parkingLot->update(['is_active' => !$parkingLot->is_active]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $parkingLot->is_active ? 'تم تفعيل الموقف' : 'تم إلغاء تفعيل الموقف',
|
||||
'data' => ['is_active' => $parkingLot->is_active]
|
||||
]);
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreBookingRequest;
|
||||
use App\Http\Resources\BookingResource;
|
||||
use App\Models\Booking;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
|
||||
class BookingController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handle the incoming request.
|
||||
*/
|
||||
public function __invoke(StoreBookingRequest $request)
|
||||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
$booking = \App\Models\Booking::create($validated);
|
||||
|
||||
// Note: In production, create CarRegistry here for the booking car
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => new BookingResource($booking),
|
||||
'message' => 'Booking created successfully',
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$bookings = Booking::with('parkingLot')
|
||||
->latest()
|
||||
->paginate(10);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'data' => BookingResource::collection($bookings),
|
||||
'current_page' => $bookings->currentPage(),
|
||||
'last_page' => $bookings->lastPage(),
|
||||
'per_page' => $bookings->perPage(),
|
||||
'total' => $bookings->total(),
|
||||
],
|
||||
'message' => 'Recent bookings retrieved successfully',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreOperatorCheckInRequest;
|
||||
use App\Models\CarRegistry;
|
||||
use App\Models\ParkingLot;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class CarRegistryController extends Controller
|
||||
{
|
||||
public function checkIn(StoreOperatorCheckInRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$parkingLot = ParkingLot::findOrFail($validated['parking_lot_id']);
|
||||
|
||||
// Check capacity vs active registries
|
||||
$activeCount = $parkingLot->carRegistries()->active()->count();
|
||||
if ($activeCount >= $parkingLot->total_capacity) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Parking lot is full',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$registry = CarRegistry::create([
|
||||
'parking_lot_id' => $validated['parking_lot_id'],
|
||||
'plate_number' => $validated['vehicle_plate'],
|
||||
'entry_time' => now(),
|
||||
// Optional: predicted exit based on duration
|
||||
'exit_time' => isset($validated['duration_hours']) ? now()->addHours($validated['duration_hours']) : null,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Car entry registered successfully',
|
||||
'data' => $registry->load('parkingLot'),
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function checkOut(Request $request, CarRegistry $carRegistry): JsonResponse
|
||||
{
|
||||
if (! $carRegistry->exit_time) {
|
||||
$carRegistry->update([
|
||||
'exit_time' => now(),
|
||||
]);
|
||||
|
||||
$durationHours = $carRegistry->entry_time->diffInHours(now());
|
||||
$parkingLot = $carRegistry->parkingLot;
|
||||
$fee = ceil($durationHours) * $parkingLot->price_per_hour;
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Car exit registered successfully',
|
||||
'data' => [
|
||||
'duration_hours' => round($durationHours, 2),
|
||||
'fee' => $fee,
|
||||
'plate_number' => $carRegistry->plate_number,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Car already checked out',
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ParkingLot;
|
||||
use App\Http\Resources\ParkingLotResource;
|
||||
use App\Models\Booking;
|
||||
use App\Models\CarRegistry;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ParkingLotController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = ParkingLot::query()
|
||||
->withCount([
|
||||
'carRegistries as active_registries_count' => function($q) {
|
||||
$q->active();
|
||||
},
|
||||
'bookings as active_bookings_count' => function($q) {
|
||||
$q->where('status', 'active');
|
||||
}
|
||||
]);
|
||||
|
||||
$lots = $query->paginate(10);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'data' => ParkingLotResource::collection($lots),
|
||||
'current_page' => $lots->currentPage(),
|
||||
'last_page' => $lots->lastPage(),
|
||||
'per_page' => $lots->perPage(),
|
||||
'total' => $lots->total()
|
||||
],
|
||||
'message' => 'Parking lots retrieved successfully',
|
||||
]);
|
||||
}
|
||||
|
||||
public function search(Request $request)
|
||||
{
|
||||
$q = $request->get('q', '');
|
||||
|
||||
$query = ParkingLot::query()
|
||||
->withCount([
|
||||
'carRegistries as active_registries_count' => function($q) {
|
||||
$q->active();
|
||||
},
|
||||
'bookings as active_bookings_count' => function($q) {
|
||||
$q->where('status', 'active');
|
||||
}
|
||||
])
|
||||
->search($q);
|
||||
|
||||
$lots = $query->paginate(10);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'data' => ParkingLotResource::collection($lots),
|
||||
'current_page' => $lots->currentPage(),
|
||||
'last_page' => $lots->lastPage(),
|
||||
'per_page' => $lots->perPage(),
|
||||
'total' => $lots->total()
|
||||
],
|
||||
'message' => 'Parking lots search results',
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(ParkingLot $parkingLot)
|
||||
{
|
||||
$parkingLot->loadCount([
|
||||
'carRegistries as active_registries_count' => function($q) {
|
||||
$q->active();
|
||||
},
|
||||
'bookings as active_bookings_count' => function($q) {
|
||||
$q->where('status', 'active');
|
||||
}
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => new ParkingLotResource($parkingLot),
|
||||
'message' => 'Parking lot details retrieved',
|
||||
]);
|
||||
}
|
||||
|
||||
public function status(ParkingLot $parkingLot)
|
||||
{
|
||||
$activeRegistries = $parkingLot->carRegistries()->active()->count();
|
||||
$activeBookings = $parkingLot->bookings()->where('status', 'active')->count();
|
||||
$occupied = $activeRegistries + $activeBookings;
|
||||
$available = max(0, $parkingLot->total_capacity - $occupied);
|
||||
$usagePercentage = $parkingLot->total_capacity > 0 ? round(($occupied / $parkingLot->total_capacity) * 100, 2) : 0;
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'id' => $parkingLot->id,
|
||||
'name' => $parkingLot->name,
|
||||
'total_capacity' => $parkingLot->total_capacity,
|
||||
'occupied_spaces' => $occupied,
|
||||
'available_spaces' => $available,
|
||||
'usage_percentage' => $usagePercentage,
|
||||
'active_registries_count' => $activeRegistries,
|
||||
'active_bookings_count' => $activeBookings,
|
||||
'status' => $available === 0 ? 'full' : ($available < $parkingLot->total_capacity * 0.2 ? 'limited' : 'available'),
|
||||
'active_cars' => $parkingLot->carRegistries()->active()->pluck('plate_number')->toArray(),
|
||||
'active_bookings' => $parkingLot->bookings()->where('status', 'active')->get(['id', 'customer_name', 'phone', 'start_time', 'end_time'])->toArray(),
|
||||
],
|
||||
'message' => 'Parking lot status retrieved successfully',
|
||||
]);
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
<?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\Storage;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class OperatorController extends Controller
|
||||
{
|
||||
// ── Lot picker + panel ──────────────────────────────────────────────────────
|
||||
|
||||
public function dashboard(Request $request): \Illuminate\View\View
|
||||
{
|
||||
$rawLots = ParkingLot::active()->withStatus()->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,
|
||||
])->values();
|
||||
|
||||
$selectedLotId = $request->get('lot_id');
|
||||
$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'
|
||||
));
|
||||
}
|
||||
|
||||
// ── 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($data['duration_hours']),
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
$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'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// ── Legacy checkout (kept for backward compatibility) ───────────────────────
|
||||
|
||||
public function checkOut(Request $request, Booking $booking): JsonResponse
|
||||
{
|
||||
return $this->processPayment($request->merge(['payment_method' => 'cash']), $booking);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\ParkingLot;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ParkingController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$lots = ParkingLot::active()
|
||||
->withStatus()
|
||||
->get()
|
||||
->map(fn($lot) => [
|
||||
'id' => $lot->id,
|
||||
'name' => $lot->name,
|
||||
'address' => $lot->address,
|
||||
'total' => $lot->total_capacity,
|
||||
'avail' => max(0, $lot->total_capacity - $lot->active_registries_count - $lot->active_bookings_count),
|
||||
'price' => (float) $lot->price_per_hour,
|
||||
'hours' => $lot->working_hours ?? '24/7',
|
||||
'lat' => (float) $lot->latitude,
|
||||
'lng' => (float) $lot->longitude,
|
||||
]);
|
||||
|
||||
return view('index', compact('lots'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use App\Models\Booking;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
public function show()
|
||||
{
|
||||
return view('user.profile', ['user' => Auth::user()]);
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
$validated = $request->validateWithBag('updateName', [
|
||||
'name' => 'required|string|max:255',
|
||||
]);
|
||||
|
||||
Auth::user()->update(['name' => $validated['name']]);
|
||||
|
||||
return back()->with('success', 'تم تحديث الاسم بنجاح.');
|
||||
}
|
||||
|
||||
public function updatePassword(Request $request)
|
||||
{
|
||||
$request->validateWithBag('updatePassword', [
|
||||
'current_password' => 'required',
|
||||
'password' => 'required|min:8|confirmed',
|
||||
]);
|
||||
|
||||
if (!Hash::check($request->current_password, Auth::user()->password)) {
|
||||
return back()->withErrors(
|
||||
['current_password' => 'كلمة السر الحالية غير صحيحة.'],
|
||||
'updatePassword'
|
||||
);
|
||||
}
|
||||
|
||||
Auth::user()->update(['password' => Hash::make($request->password)]);
|
||||
|
||||
return back()->with('success', 'تم تغيير كلمة السر بنجاح.');
|
||||
}
|
||||
|
||||
public function dashboard()
|
||||
{
|
||||
$bookings = Booking::with('parkingLot')
|
||||
->where('user_id', Auth::id())
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
$stats = [
|
||||
'total' => $bookings->count(),
|
||||
'active' => $bookings->where('status', 'active')->count(),
|
||||
'completed' => $bookings->where('status', 'completed')->count(),
|
||||
'cancelled' => $bookings->where('status', 'cancelled')->count(),
|
||||
];
|
||||
|
||||
return view('user.dashboard', compact('bookings', 'stats'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureAdmin
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (!auth()->check()) {
|
||||
return redirect()->route('login');
|
||||
}
|
||||
if (auth()->user()->role !== 'admin') {
|
||||
abort(403, 'الوصول للإدارة محظور');
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureOperator
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (!auth()->check() || !in_array(auth()->user()->role, ['operator', 'admin'])) {
|
||||
abort(403, 'الوصول للمشغل محظور');
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use App\Models\ParkingLot;
|
||||
|
||||
class StoreBookingRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to this request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'parking_lot_id' => 'required|exists:parking_lots,id',
|
||||
'customer_name' => 'required|string|max:255',
|
||||
'phone' => 'required|string|max:20|regex:/^09[0-9]{8}$/',
|
||||
'start_time' => 'required|date|after:now',
|
||||
'end_time' => 'required|date|after:start_time',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'phone.regex' => 'رقم الهاتف يجب أن يكون رقم سوري صالح (09xxxxxxxx).',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the validator after the validation rules have run.
|
||||
*/
|
||||
protected function passedValidation(): void
|
||||
{
|
||||
$lot = ParkingLot::findOrFail($this->parking_lot_id);
|
||||
$activeCount = $lot->carRegistries()->active()->count();
|
||||
|
||||
if ($lot->total_capacity <= $activeCount) {
|
||||
throw \Illuminate\Validation\ValidationException::withMessages([
|
||||
'parking_lot_id' => ['الموقف ممتلئ حالياً. يرجى اختيار موقف آخر أو المحاولة لاحقاً.'],
|
||||
])->status(409);
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreOperatorCheckInRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true; // Enforce via operator middleware
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'parking_lot_id' => 'required|exists:parking_lots,id',
|
||||
'vehicle_plate' => 'required|string|max:50',
|
||||
'user_name' => 'nullable|string|max:255',
|
||||
'user_phone' => 'nullable|string|max:20',
|
||||
'duration_hours' => 'required|numeric|min:0.25|max:24',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'vehicle_plate.required' => 'رقم اللوحة مطلوب.',
|
||||
'parking_lot_id.exists' => 'موقف السيارات غير موجود.',
|
||||
'duration_hours.required' => 'مدة الإقامة مطلوبة.',
|
||||
];
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreParkingLotRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true; // Enforce via middleware
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string|max:255',
|
||||
'address' => 'required|string|max:500',
|
||||
'total_capacity' => 'required|integer|min:1|max:10000',
|
||||
'price_per_hour' => 'required|numeric|min:0.01|max:1000',
|
||||
'latitude' => 'required|numeric|between:-90,90',
|
||||
'longitude' => 'required|numeric|between:-180,180',
|
||||
'working_hours' => 'required|string|max:100',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'اسم الموقف مطلوب.',
|
||||
'address.required' => 'العنوان مطلوب.',
|
||||
'total_capacity.required' => 'السعة مطلوبة.',
|
||||
'price_per_hour.required' => 'سعر الساعة مطلوب.',
|
||||
'latitude.required' => 'خط العرض مطلوب.',
|
||||
'longitude.required' => 'خط الطول مطلوب.',
|
||||
];
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class BookingResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'parking_lot_id' => $this->parking_lot_id,
|
||||
'parking_lot' => $this->whenLoaded('parkingLot', fn() => [
|
||||
'id' => $this->parkingLot->id,
|
||||
'name' => $this->parkingLot->name,
|
||||
]),
|
||||
'customer_name' => $this->customer_name,
|
||||
'phone' => $this->phone,
|
||||
'start_time' => $this->start_time->toIso8601String(),
|
||||
'end_time' => $this->end_time->toIso8601String(),
|
||||
'status' => $this->status,
|
||||
];
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class CarRegistryResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'plate_number' => $this->plate_number,
|
||||
'entry_time' => $this->entry_time,
|
||||
'exit_time' => $this->exit_time,
|
||||
'is_active' => $this->active,
|
||||
'duration_hours' => $this->entry_time ? $this->entry_time->diffInHours(now()) : null,
|
||||
'parking_lot' => new ParkingLotResource($this->whenLoaded('parkingLot')),
|
||||
];
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ParkingLotResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'address' => $this->address,
|
||||
'total_capacity' => $this->total_capacity,
|
||||
'occupied_spaces' => $this->occupied_spaces,
|
||||
'available_spaces' => $this->available_spaces,
|
||||
'usage_percentage' => $this->usage_percentage,
|
||||
'active_registries_count' => $this->active_registries_count ?? 0,
|
||||
'active_bookings_count' => $this->active_bookings_count ?? 0,
|
||||
'price_per_hour' => $this->price_per_hour,
|
||||
'working_hours' => $this->working_hours,
|
||||
'lat' => $this->latitude,
|
||||
'lng' => $this->longitude,
|
||||
'status' => $this->getAvailabilityStatus(),
|
||||
];
|
||||
}
|
||||
|
||||
private function getAvailabilityStatus()
|
||||
{
|
||||
$available = $this->available_spaces;
|
||||
$total = $this->total_capacity;
|
||||
|
||||
if ($available === 0) return 'full';
|
||||
if ($available < $total * 0.2) return 'limited';
|
||||
return 'available';
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use App\Models\User;
|
||||
|
||||
class Booking extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'parking_lot_id',
|
||||
'customer_name',
|
||||
'phone',
|
||||
'vehicle_plate',
|
||||
'source',
|
||||
'start_time',
|
||||
'end_time',
|
||||
'status',
|
||||
'total_fee',
|
||||
'payment_method',
|
||||
'payment_proof',
|
||||
'paid_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'start_time' => 'datetime',
|
||||
'end_time' => 'datetime',
|
||||
'paid_at' => 'datetime',
|
||||
'total_fee' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function parkingLot(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ParkingLot::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class CarRegistry extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'parking_lot_id',
|
||||
'plate_number',
|
||||
'entry_time',
|
||||
'exit_time',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'entry_time' => 'datetime',
|
||||
'exit_time' => 'datetime',
|
||||
];
|
||||
|
||||
public function parkingLot(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ParkingLot::class);
|
||||
}
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->whereNull('exit_time')
|
||||
->orWhere('exit_time', '>', Carbon::now());
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class ParkingLot extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'address',
|
||||
'total_capacity',
|
||||
'price_per_hour',
|
||||
'pricing_rules',
|
||||
'latitude',
|
||||
'longitude',
|
||||
'working_hours',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'latitude' => 'decimal:8',
|
||||
'longitude' => 'decimal:8',
|
||||
'price_per_hour' => 'decimal:2',
|
||||
'total_capacity' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
'pricing_rules' => 'array',
|
||||
];
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function scopeWithStatus($query)
|
||||
{
|
||||
return $query->withCount([
|
||||
'carRegistries as active_registries_count' => function ($q) {
|
||||
$q->active();
|
||||
},
|
||||
'bookings as active_bookings_count' => function ($q) {
|
||||
$q->where('status', 'active');
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
public function bookings(): HasMany
|
||||
{
|
||||
return $this->hasMany(Booking::class);
|
||||
}
|
||||
|
||||
public function carRegistries(): HasMany
|
||||
{
|
||||
return $this->hasMany(CarRegistry::class);
|
||||
}
|
||||
|
||||
public function scopeSearch($query, $search)
|
||||
{
|
||||
return $query->where('name', 'like', "%{$search}%")
|
||||
->orWhere('address', 'like', "%{$search}%");
|
||||
}
|
||||
|
||||
public function getAvailableSpacesAttribute()
|
||||
{
|
||||
$activeBookings = $this->bookings()->where('status', 'active')->count();
|
||||
$activeRegistries = $this->carRegistries()->active()->count();
|
||||
$occupied = $activeBookings + $activeRegistries;
|
||||
return max(0, $this->total_capacity - $occupied);
|
||||
}
|
||||
|
||||
public function getOccupiedSpacesAttribute()
|
||||
{
|
||||
$activeBookings = $this->bookings()->where('status', 'active')->count();
|
||||
$activeRegistries = $this->carRegistries()->active()->count();
|
||||
return $activeBookings + $activeRegistries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate fee and per-day breakdown between two timestamps.
|
||||
* Uses pricing_rules (ISO weekday keys 1–7) if set, otherwise price_per_hour.
|
||||
*
|
||||
* Returns ['total' => float, 'details' => [['day','date','hours','rate','subtotal'], ...]]
|
||||
*/
|
||||
public function calculateFee(Carbon $start, Carbon $end): array
|
||||
{
|
||||
$rules = $this->pricing_rules ?? [];
|
||||
$details = [];
|
||||
$total = 0.0;
|
||||
$cursor = $start->copy()->seconds(0);
|
||||
|
||||
while ($cursor < $end) {
|
||||
$dayEnd = $cursor->copy()->endOfDay()->addSecond();
|
||||
$segEnd = ($dayEnd < $end) ? $dayEnd : $end;
|
||||
|
||||
$dow = (int) $cursor->format('N'); // 1=Mon … 7=Sun
|
||||
$rate = isset($rules[$dow]) ? (float) $rules[$dow] : (float) $this->price_per_hour;
|
||||
$hours = round($cursor->diffInMinutes($segEnd) / 60, 4);
|
||||
$subtotal = $hours * $rate;
|
||||
|
||||
$details[] = [
|
||||
'day' => $this->arabicDayName($dow),
|
||||
'date' => $cursor->format('Y/m/d'),
|
||||
'hours' => round($hours, 2),
|
||||
'rate' => $rate,
|
||||
'subtotal' => round($subtotal, 2),
|
||||
];
|
||||
|
||||
$total += $subtotal;
|
||||
$cursor = $segEnd;
|
||||
}
|
||||
|
||||
return ['total' => (float) number_format(ceil($total), 2, '.', ''), 'details' => $details];
|
||||
}
|
||||
|
||||
private function arabicDayName(int $iso): string
|
||||
{
|
||||
return ['الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت', 'الأحد'][$iso - 1] ?? '';
|
||||
}
|
||||
|
||||
public function getUsagePercentageAttribute()
|
||||
{
|
||||
$occupied = $this->occupied_spaces;
|
||||
return $this->total_capacity > 0 ? round(($occupied / $this->total_capacity) * 100, 2) : 0;
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'role',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'role' => 'string',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user