diff --git a/app/Http/Controllers/Admin/AdminOperatorController.php b/app/Http/Controllers/Admin/AdminOperatorController.php new file mode 100644 index 0000000..fb0a2b0 --- /dev/null +++ b/app/Http/Controllers/Admin/AdminOperatorController.php @@ -0,0 +1,92 @@ +with('assignedLot') + ->orderBy('name') + ->get(); + + $lots = ParkingLot::orderBy('name')->get(); + + return view('admin.operators.index', compact('operators', 'lots')); + } + + public function store(Request $request) + { + $data = $request->validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|email|unique:users,email', + 'phone' => 'nullable|string|max:20', + 'password' => ['required', Password::min(8)], + 'parking_lot_id' => 'nullable|exists:parking_lots,id', + ], [ + 'email.unique' => 'هذا البريد الإلكتروني مستخدم بالفعل.', + ]); + + User::create([ + 'name' => $data['name'], + 'email' => $data['email'], + 'phone' => $data['phone'] ?? null, + 'password' => Hash::make($data['password']), + 'role' => 'operator', + 'parking_lot_id' => $data['parking_lot_id'] ?? null, + ]); + + return response()->json(['success' => true, 'message' => 'تم إنشاء حساب المشغّل بنجاح.']); + } + + public function update(Request $request, User $operator) + { + if ($operator->role !== 'operator') { + return response()->json(['success' => false, 'message' => 'المستخدم ليس مشغّلاً.'], 403); + } + + $data = $request->validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|email|unique:users,email,' . $operator->id, + 'phone' => 'nullable|string|max:20', + 'password' => ['nullable', Password::min(8)], + 'parking_lot_id' => 'nullable|exists:parking_lots,id', + ], [ + 'email.unique' => 'هذا البريد الإلكتروني مستخدم بالفعل.', + ]); + + $updates = [ + 'name' => $data['name'], + 'email' => $data['email'], + 'phone' => $data['phone'] ?? null, + 'parking_lot_id' => $data['parking_lot_id'] ?? null, + ]; + + if (!empty($data['password'])) { + $updates['password'] = Hash::make($data['password']); + } + + $operator->update($updates); + + return response()->json(['success' => true, 'message' => 'تم تحديث بيانات المشغّل.']); + } + + public function destroy(User $operator) + { + if ($operator->role !== 'operator') { + return response()->json(['success' => false, 'message' => 'المستخدم ليس مشغّلاً.'], 403); + } + + $operator->delete(); + + return response()->json(['success' => true, 'message' => 'تم حذف المشغّل.']); + } +} diff --git a/app/Http/Controllers/Operator/OperatorController.php b/app/Http/Controllers/Operator/OperatorController.php index 71f6b49..2dabdaa 100644 --- a/app/Http/Controllers/Operator/OperatorController.php +++ b/app/Http/Controllers/Operator/OperatorController.php @@ -7,6 +7,7 @@ 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; @@ -16,23 +17,39 @@ 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) + $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, - 'image' => $lot->image ? Storage::url($lot->image) : null, + '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' => $assignedLotId !== null && $lot->id !== $assignedLotId, ])->values(); - $selectedLotId = $request->get('lot_id'); + // 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; + } + } + $selectedLot = null; $activeCars = collect(); $reservations = collect(); @@ -56,7 +73,7 @@ class OperatorController extends Controller } return view('operator.dashboard', compact( - 'parkingLots', 'selectedLot', 'activeCars', 'reservations', 'selectedLotId' + 'parkingLots', 'selectedLot', 'activeCars', 'reservations', 'selectedLotId', 'assignedLotId' )); } diff --git a/app/Models/User.php b/app/Models/User.php index 977decd..b05eed8 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -24,6 +24,7 @@ class User extends Authenticatable 'phone', 'password', 'role', + 'parking_lot_id', ]; protected $casts = [ @@ -54,4 +55,9 @@ class User extends Authenticatable 'password' => 'hashed', ]; } + + public function assignedLot(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(ParkingLot::class, 'parking_lot_id'); + } } diff --git a/database/migrations/2026_04_16_140529_add_parking_lot_id_to_users_table.php b/database/migrations/2026_04_16_140529_add_parking_lot_id_to_users_table.php new file mode 100644 index 0000000..acfeea0 --- /dev/null +++ b/database/migrations/2026_04_16_140529_add_parking_lot_id_to_users_table.php @@ -0,0 +1,27 @@ +foreignId('parking_lot_id') + ->nullable() + ->after('role') + ->constrained('parking_lots') + ->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropForeignIdFor(\App\Models\ParkingLot::class); + $table->dropColumn('parking_lot_id'); + }); + } +}; diff --git a/resources/views/admin/operators/index.blade.php b/resources/views/admin/operators/index.blade.php new file mode 100644 index 0000000..358b6fa --- /dev/null +++ b/resources/views/admin/operators/index.blade.php @@ -0,0 +1,392 @@ +@extends('layouts.admin') +@section('title', 'إدارة المشغّلين — دمشق باركينغ') +@section('page-title', 'إدارة المشغّلين') + +@section('styles') + +@endsection + +@section('content') + +
+
+

إنشاء حسابات المشغّلين وتخصيص مواقف لهم

+
+ +
+ +{{-- Operators table --}} +
+
+ + + + + + + + + + + + @forelse($operators as $op) + + + + + + + + @empty + + + + @endforelse + +
المشغّلالبريد الإلكترونيالهاتفالموقف المخصصإجراءات
+
+
{{ mb_substr($op->name, 0, 1) }}
+ {{ $op->name }} +
+
{{ $op->email }}{{ $op->phone ?? '—' }} + @if($op->assignedLot) + + + {{ $op->assignedLot->name }} + + @else + + + غير مخصص + + @endif + +
+ + +
+
+ + لا يوجد مشغّلون بعد. أضف أول مشغّل الآن. +
+
+
+ +{{-- ══ CREATE MODAL ══════════════════════════════════════════════════════════ --}} + + +{{-- ══ EDIT MODAL ════════════════════════════════════════════════════════════ --}} + + +{{-- ══ DELETE MODAL ══════════════════════════════════════════════════════════ --}} + + +{{-- Toast --}} + + +@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/layouts/admin.blade.php b/resources/views/layouts/admin.blade.php index e07109a..c09d61f 100644 --- a/resources/views/layouts/admin.blade.php +++ b/resources/views/layouts/admin.blade.php @@ -60,6 +60,12 @@ + + + المشغّلون + + @@ -180,6 +186,11 @@ الحجوزات + + + المشغّلون + diff --git a/resources/views/operator/dashboard.blade.php b/resources/views/operator/dashboard.blade.php index a5d7d16..26f78c3 100644 --- a/resources/views/operator/dashboard.blade.php +++ b/resources/views/operator/dashboard.blade.php @@ -20,6 +20,13 @@ transform:translateY(-8px); box-shadow:0 18px 40px rgba(0,0,0,.13); } + .lot-portrait-card.lot-locked { + cursor:not-allowed; + } + .lot-portrait-card.lot-locked:hover { + transform:none; + box-shadow:0 6px 20px rgba(0,0,0,.07); + } /* Image area */ .lot-card-img-wrap { @@ -185,6 +192,7 @@ $gradients = [ $pct = $lot['total'] > 0 ? round($lot['occupied'] / $lot['total'] * 100) : 0; $avail = $lot['avail']; $gradient = $gradients[$lot['id'] % 6]; + $locked = $lot['locked'] ?? false; if ($avail === 0) { $badgeColor='rgba(239,68,68,.82)'; $badgeTxt='ممتلئ'; $barCol='#ef4444'; } elseif ($avail < $lot['total'] * 0.2) { $badgeColor='rgba(245,158,11,.82)'; $badgeTxt=$avail.' مكان'; $barCol='#f59e0b'; } else { $badgeColor='rgba(16,185,129,.82)'; $badgeTxt=$avail.' متاح'; $barCol='#10b981'; } @@ -193,25 +201,34 @@ $gradients = [ data-name="{{ mb_strtolower($lot['name']) }}" data-address="{{ mb_strtolower($lot['address']) }}"> -
+
{{-- Image or gradient placeholder --}} diff --git a/routes/web.php b/routes/web.php index e4dcb2e..85b2699 100644 --- a/routes/web.php +++ b/routes/web.php @@ -38,6 +38,12 @@ Route::prefix('admin')->middleware('admin')->name('admin.')->group(function () { Route::post('/parking-lots/{parkingLot}/toggle', [\App\Http\Controllers\Admin\ParkingLotController::class, 'toggleStatus'])->name('parking-lots.toggle'); Route::delete('/parking-lots/{parkingLot}', [\App\Http\Controllers\Admin\ParkingLotController::class, 'destroy'])->name('parking-lots.destroy'); + // Operators management + Route::get('/operators', [\App\Http\Controllers\Admin\AdminOperatorController::class, 'index'])->name('operators.index'); + Route::post('/operators', [\App\Http\Controllers\Admin\AdminOperatorController::class, 'store'])->name('operators.store'); + Route::put('/operators/{operator}', [\App\Http\Controllers\Admin\AdminOperatorController::class, 'update'])->name('operators.update'); + Route::delete('/operators/{operator}', [\App\Http\Controllers\Admin\AdminOperatorController::class, 'destroy'])->name('operators.destroy'); + // Active Bookings Route::get('/bookings/active', [\App\Http\Controllers\Admin\BookingController::class, 'activeIndex'])->name('bookings.active'); Route::get('/bookings/{booking}/checkout-preview', [\App\Http\Controllers\Admin\BookingController::class, 'checkoutPreview'])->name('bookings.checkout-preview');