From 3643fdb5d4f7195fe7c3ad9e566f8b2a0bfa190e Mon Sep 17 00:00:00 2001 From: Ghassan Yusuf Date: Wed, 15 Apr 2026 12:21:40 +0300 Subject: [PATCH] feature: portrait lot picker cards with images, admin image upload support --- .claude/settings.local.json | 6 +- .../Admin/ParkingLotController.php | 20 +- .../Operator/OperatorController.php | 1 + app/Http/Requests/StoreParkingLotRequest.php | 1 + app/Models/ParkingLot.php | 1 + ...091842_add_image_to_parking_lots_table.php | 28 ++ .../views/admin/parking-lots/index.blade.php | 59 +++- resources/views/operator/dashboard.blade.php | 319 +++++++++--------- 8 files changed, 270 insertions(+), 165 deletions(-) create mode 100644 database/migrations/2026_04_15_091842_add_image_to_parking_lots_table.php diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b423537..e200da5 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -10,7 +10,11 @@ "Bash(php -l app/Models/ParkingLot.php)", "Bash(git init:*)", "Bash(git add:*)", - "Bash(git commit:*)" + "Bash(git commit:*)", + "Bash(git checkout:*)", + "Bash(git reset:*)", + "Bash(php -l app/Http/Controllers/Admin/ParkingLotController.php)", + "Bash(php -l app/Http/Requests/StoreParkingLotRequest.php)" ] } } diff --git a/app/Http/Controllers/Admin/ParkingLotController.php b/app/Http/Controllers/Admin/ParkingLotController.php index 3555dd3..9723dab 100644 --- a/app/Http/Controllers/Admin/ParkingLotController.php +++ b/app/Http/Controllers/Admin/ParkingLotController.php @@ -7,6 +7,7 @@ use App\Http\Requests\StoreParkingLotRequest; use App\Models\ParkingLot; use Illuminate\Http\Request; use Illuminate\Http\JsonResponse; +use Illuminate\Support\Facades\Storage; class ParkingLotController extends Controller { @@ -32,7 +33,13 @@ class ParkingLotController extends Controller public function store(StoreParkingLotRequest $request): JsonResponse { - $parkingLot = ParkingLot::create($request->validated()); + $data = $request->validated(); + + if ($request->hasFile('image')) { + $data['image'] = $request->file('image')->store('parking-lots', 'public'); + } + + $parkingLot = ParkingLot::create($data); return response()->json([ 'success' => true, @@ -43,7 +50,16 @@ class ParkingLotController extends Controller public function update(StoreParkingLotRequest $request, ParkingLot $parkingLot): JsonResponse { - $parkingLot->update($request->validated()); + $data = $request->validated(); + + if ($request->hasFile('image')) { + if ($parkingLot->image) { + Storage::disk('public')->delete($parkingLot->image); + } + $data['image'] = $request->file('image')->store('parking-lots', 'public'); + } + + $parkingLot->update($data); return response()->json([ 'success' => true, diff --git a/app/Http/Controllers/Operator/OperatorController.php b/app/Http/Controllers/Operator/OperatorController.php index b4de978..1813092 100644 --- a/app/Http/Controllers/Operator/OperatorController.php +++ b/app/Http/Controllers/Operator/OperatorController.php @@ -29,6 +29,7 @@ class OperatorController extends Controller 'hours' => $lot->working_hours, 'lat' => (float) $lot->latitude, 'lng' => (float) $lot->longitude, + 'image' => $lot->image ? Storage::url($lot->image) : null, ])->values(); $selectedLotId = $request->get('lot_id'); diff --git a/app/Http/Requests/StoreParkingLotRequest.php b/app/Http/Requests/StoreParkingLotRequest.php index 162dcb5..40b07f4 100644 --- a/app/Http/Requests/StoreParkingLotRequest.php +++ b/app/Http/Requests/StoreParkingLotRequest.php @@ -30,6 +30,7 @@ class StoreParkingLotRequest extends FormRequest 'longitude' => 'required|numeric|between:-180,180', 'working_hours' => 'required|string|max:100', 'is_active' => 'boolean', + 'image' => 'nullable|image|mimes:jpg,jpeg,png,webp|max:3072', ]; } diff --git a/app/Models/ParkingLot.php b/app/Models/ParkingLot.php index e2e6931..10572ab 100644 --- a/app/Models/ParkingLot.php +++ b/app/Models/ParkingLot.php @@ -21,6 +21,7 @@ class ParkingLot extends Model 'longitude', 'working_hours', 'is_active', + 'image', ]; protected $casts = [ diff --git a/database/migrations/2026_04_15_091842_add_image_to_parking_lots_table.php b/database/migrations/2026_04_15_091842_add_image_to_parking_lots_table.php new file mode 100644 index 0000000..827c35a --- /dev/null +++ b/database/migrations/2026_04_15_091842_add_image_to_parking_lots_table.php @@ -0,0 +1,28 @@ +string('image')->nullable()->after('working_hours'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('parking_lots', function (Blueprint $table) { + $table->dropColumn('image'); + }); + } +}; diff --git a/resources/views/admin/parking-lots/index.blade.php b/resources/views/admin/parking-lots/index.blade.php index e58998b..f1f1270 100644 --- a/resources/views/admin/parking-lots/index.blade.php +++ b/resources/views/admin/parking-lots/index.blade.php @@ -82,9 +82,21 @@ @forelse($parkingLots as $lot) -
{{ $lot->name }}
-
- {{ number_format($lot->latitude, 5) }}, {{ number_format($lot->longitude, 5) }} +
+ @if($lot->image) + + @else +
+ +
+ @endif +
+
{{ $lot->name }}
+
+ {{ number_format($lot->latitude, 5) }}, {{ number_format($lot->longitude, 5) }} +
+
@@ -206,6 +218,19 @@
+ {{-- Image --}} +
+ + +
JPG / PNG / WebP — حد أقصى 3MB. اتركه فارغاً للإبقاء على الصورة الحالية عند التعديل.
+ +
+ {{-- Location --}}

@@ -385,9 +410,21 @@ document.getElementById('addLotBtn').onclick = () => { document.getElementById('lotForm').reset(); document.getElementById('modalLabel').textContent = 'إضافة موقف جديد'; document.getElementById('submitText').textContent = 'حفظ الموقف'; + document.getElementById('imagePreviewWrap').style.display = 'none'; modal.show(); }; +function previewImage(input) { + const wrap = document.getElementById('imagePreviewWrap'); + const img = document.getElementById('imagePreview'); + if (input.files && input.files[0]) { + img.src = URL.createObjectURL(input.files[0]); + wrap.style.display = 'block'; + } else { + wrap.style.display = 'none'; + } +} + async function editLot(id) { editingId = id; setBtnLoading(true); @@ -402,6 +439,16 @@ async function editLot(id) { document.getElementById('f_lat').value = data.latitude; document.getElementById('f_lng').value = data.longitude; document.getElementById('f_address').value = data.address; + document.getElementById('f_image').value = ''; + // Show existing image preview + const wrap = document.getElementById('imagePreviewWrap'); + const img = document.getElementById('imagePreview'); + if (data.image) { + img.src = '/storage/' + data.image; + wrap.style.display = 'block'; + } else { + wrap.style.display = 'none'; + } modal.show(); } catch { alert('خطأ في تحميل البيانات'); } finally { setBtnLoading(false); } @@ -411,9 +458,11 @@ document.getElementById('lotForm').onsubmit = async (e) => { e.preventDefault(); setBtnLoading(true); try { + const fd = new FormData(e.target); + if (editingId) fd.append('_method', 'PUT'); const res = await fetch(editingId ? `/admin/parking-lots/${editingId}` : '/admin/parking-lots', { - method: editingId ? 'PUT' : 'POST', - body: new FormData(e.target) + method: 'POST', + body: fd }); const result = await res.json(); result.success ? location.reload() : alert(result.message || 'خطأ في العملية'); diff --git a/resources/views/operator/dashboard.blade.php b/resources/views/operator/dashboard.blade.php index 6e91c96..2745f79 100644 --- a/resources/views/operator/dashboard.blade.php +++ b/resources/views/operator/dashboard.blade.php @@ -3,38 +3,89 @@ @section('page-title', 'لوحة المشغّل') @push('styles') -