diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f225c26..b11ae4a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -15,7 +15,11 @@ "Bash(git reset:*)", "Bash(php -l app/Http/Controllers/Admin/ParkingLotController.php)", "Bash(php -l app/Http/Requests/StoreParkingLotRequest.php)", - "Bash(php -l app/Http/Controllers/Admin/BookingController.php)" + "Bash(php -l app/Http/Controllers/Admin/BookingController.php)", + "Bash(php -r ':*)", + "Bash(php -r \"echo file_put_contents\\('C:/xampp/htdocs/scp-syria/storage/app/public/test.txt', 'ok'\\) ? 'write OK' : 'write FAIL';\")", + "Bash(curl:*)", + "Bash(php:*)" ] } } diff --git a/app/Http/Controllers/Admin/ParkingLotController.php b/app/Http/Controllers/Admin/ParkingLotController.php index 6fb4a75..8afbcd8 100644 --- a/app/Http/Controllers/Admin/ParkingLotController.php +++ b/app/Http/Controllers/Admin/ParkingLotController.php @@ -52,6 +52,10 @@ class ParkingLotController extends Controller { $data = $request->validated(); + // Never let validated() overwrite the stored image path with null. + // Only update the image column when a new file is actually uploaded. + unset($data['image']); + if ($request->hasFile('image')) { if ($parkingLot->image) { Storage::disk('public')->delete($parkingLot->image); @@ -91,6 +95,38 @@ class ParkingLotController extends Controller ]); } + public function updatePricing(Request $request, ParkingLot $parkingLot): JsonResponse + { + $data = $request->validate([ + 'price_per_hour' => 'required|numeric|min:0|max:100000', + 'pricing_rules' => 'nullable|array', + 'pricing_rules.*'=> 'nullable|numeric|min:0|max:100000', + ]); + + // Build clean rules array: only include days that differ from base price. + // Keys 1-7 (Mon-Sun). Null or empty string means "use base price". + $rules = []; + foreach ($data['pricing_rules'] ?? [] as $day => $rate) { + if ($rate !== null && $rate !== '' && (float) $rate !== (float) $data['price_per_hour']) { + $rules[(int) $day] = (float) $rate; + } + } + + $parkingLot->update([ + 'price_per_hour' => $data['price_per_hour'], + 'pricing_rules' => empty($rules) ? null : $rules, + ]); + + return response()->json([ + 'success' => true, + 'message' => 'تم حفظ أسعار الموقف بنجاح', + 'data' => [ + 'price_per_hour' => (float) $parkingLot->price_per_hour, + 'pricing_rules' => $parkingLot->pricing_rules, + ], + ]); + } + public function toggleStatus(ParkingLot $parkingLot): JsonResponse { $parkingLot->update(['is_active' => !$parkingLot->is_active]); diff --git a/app/Http/Controllers/Api/BookingController.php b/app/Http/Controllers/Api/BookingController.php index 2edc65f..8651659 100644 --- a/app/Http/Controllers/Api/BookingController.php +++ b/app/Http/Controllers/Api/BookingController.php @@ -18,6 +18,9 @@ class BookingController extends Controller { $validated = $request->validated(); + $lot = \App\Models\ParkingLot::find($validated['parking_lot_id']); + $validated['pricing_snapshot'] = $lot?->pricingSnapshot(); + $booking = \App\Models\Booking::create($validated); // Note: In production, create CarRegistry here for the booking car diff --git a/app/Http/Controllers/Operator/OperatorController.php b/app/Http/Controllers/Operator/OperatorController.php index 1813092..2b90403 100644 --- a/app/Http/Controllers/Operator/OperatorController.php +++ b/app/Http/Controllers/Operator/OperatorController.php @@ -80,14 +80,15 @@ class OperatorController extends Controller } $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', + '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', + 'pricing_snapshot' => $lot->pricingSnapshot(), ]); return response()->json([ @@ -128,7 +129,7 @@ class OperatorController extends Controller $start = $booking->start_time; $end = now(); $duration = $start->diffInMinutes($end); - $calc = $lot->calculateFee($start, $end); + $calc = $lot->calculateFee($start, $end, $booking->pricing_snapshot); return response()->json([ 'success' => true, @@ -163,7 +164,7 @@ class OperatorController extends Controller $lot = $booking->parkingLot; $start = $booking->start_time; $end = now(); - $calc = $lot->calculateFee($start, $end); + $calc = $lot->calculateFee($start, $end, $booking->pricing_snapshot); $proofPath = null; if ($request->hasFile('payment_proof')) { diff --git a/app/Models/Booking.php b/app/Models/Booking.php index 7b08695..dcdb75f 100644 --- a/app/Models/Booking.php +++ b/app/Models/Booking.php @@ -25,13 +25,15 @@ class Booking extends Model 'payment_method', 'payment_proof', 'paid_at', + 'pricing_snapshot', ]; protected $casts = [ 'start_time' => 'datetime', 'end_time' => 'datetime', 'paid_at' => 'datetime', - 'total_fee' => 'decimal:2', + 'total_fee' => 'decimal:2', + 'pricing_snapshot' => 'array', ]; public function parkingLot(): BelongsTo diff --git a/app/Models/ParkingLot.php b/app/Models/ParkingLot.php index 10572ab..1e682ca 100644 --- a/app/Models/ParkingLot.php +++ b/app/Models/ParkingLot.php @@ -81,15 +81,33 @@ class ParkingLot extends Model return $activeBookings + $activeRegistries; } + /** + * Build the pricing snapshot array to store on a booking at creation time. + * Format: ['base' => float, 'rules' => [1 => float, ..., 7 => float]] + */ + public function pricingSnapshot(): array + { + return [ + 'base' => (float) $this->price_per_hour, + 'rules' => $this->pricing_rules ?? [], + ]; + } + /** * Calculate fee and per-day breakdown between two timestamps. - * Uses pricing_rules (ISO weekday keys 1–7) if set, otherwise price_per_hour. + * + * Pass a $snapshot (from booking->pricing_snapshot) to use the rates that were + * in effect when the booking was created, guaranteeing price changes never + * retroactively affect historical or in-progress bookings. * * Returns ['total' => float, 'details' => [['day','date','hours','rate','subtotal'], ...]] */ - public function calculateFee(Carbon $start, Carbon $end): array + public function calculateFee(Carbon $start, Carbon $end, ?array $snapshot = null): array { - $rules = $this->pricing_rules ?? []; + $base = $snapshot ? (float) ($snapshot['base'] ?? $this->price_per_hour) + : (float) $this->price_per_hour; + $rules = $snapshot ? ($snapshot['rules'] ?? []) + : ($this->pricing_rules ?? []); $details = []; $total = 0.0; $cursor = $start->copy()->seconds(0); @@ -99,7 +117,7 @@ class ParkingLot extends Model $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; + $rate = isset($rules[$dow]) ? (float) $rules[$dow] : $base; $hours = round($cursor->diffInMinutes($segEnd) / 60, 4); $subtotal = $hours * $rate; diff --git a/database/migrations/2026_04_15_132115_add_pricing_snapshot_to_bookings_table.php b/database/migrations/2026_04_15_132115_add_pricing_snapshot_to_bookings_table.php new file mode 100644 index 0000000..ed0b5d8 --- /dev/null +++ b/database/migrations/2026_04_15_132115_add_pricing_snapshot_to_bookings_table.php @@ -0,0 +1,31 @@ +json('pricing_snapshot')->nullable()->after('paid_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('bookings', function (Blueprint $table) { + $table->dropColumn('pricing_snapshot'); + }); + } +}; diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js index d5c8135..acd7ae8 100644 --- a/resources/js/bootstrap.js +++ b/resources/js/bootstrap.js @@ -1,4 +1,5 @@ -import 'bootstrap'; +import * as bootstrap from 'bootstrap'; +window.bootstrap = bootstrap; import axios from 'axios'; window.axios = axios; diff --git a/resources/views/admin/parking-lots/index.blade.php b/resources/views/admin/parking-lots/index.blade.php index f5d4ef3..a897058 100644 --- a/resources/views/admin/parking-lots/index.blade.php +++ b/resources/views/admin/parking-lots/index.blade.php @@ -5,11 +5,91 @@ @section('styles') @endsection @@ -31,174 +111,168 @@ -{{-- ── Overview Map ─────────────────────────────────────────────────────────── --}} -
-
- - - خريطة المواقف - -
-
-
+{{-- ── Search bar ───────────────────────────────────────────────────────────── --}} +
+ + @if(request('search')) + نتائج البحث عن «{{ request('search') }}» + @else + عرض جميع المواقف + @endif + +
+ + + @if(request('search')) + + + + @endif
-{{-- ── Table Card ───────────────────────────────────────────────────────────── --}} -
+{{-- ── Cards grid ───────────────────────────────────────────────────────────── --}} +@php +$gradients = [ + 'linear-gradient(135deg,#667eea,#764ba2)', + 'linear-gradient(135deg,#f093fb,#f5576c)', + 'linear-gradient(135deg,#4facfe,#00f2fe)', + 'linear-gradient(135deg,#43e97b,#38f9d7)', + 'linear-gradient(135deg,#fa709a,#fee140)', + 'linear-gradient(135deg,#a18cd1,#fbc2eb)', +]; +@endphp + +@if($parkingLots->isEmpty()) +
+ +

لا توجد مواقف بعد

+

ابدأ بإضافة أول موقف سيارات

+ +
+@else +
+ @foreach($parkingLots as $lot) + @php $grad = $gradients[$lot->id % count($gradients)]; @endphp +
+
+ + {{-- Image / Placeholder --}} +
+ @if($lot->image) + {{ $lot->name }} + @else +
+ +
+ @endif + + {{-- Status dot --}} + + + + {{-- Active bookings badge --}} + @if(($lot->active_bookings_count ?? 0) > 0) + + {{ $lot->active_bookings_count }} نشط + + @endif +
+ + {{-- Card body --}} +
+
{{ $lot->name }}
+

+ {{ $lot->address }} +

+ +
+ + + {{ $lot->total_capacity }} مركبة + + + + {{ $lot->working_hours }} + + + + {{ number_format($lot->price_per_hour, 0) }} ر.س/س + @if(!empty($lot->pricing_rules)) + (مخصص) + @endif + +
+
+ + {{-- Actions --}} + - {{-- Toolbar --}} -
- - - قائمة المواقف - -
- -
- -
- - - - - - - - - - - - - - - @forelse($parkingLots as $lot) - - - - - - - - - - - @empty - - - - @endforelse - -
الموقفالعنوانالسعةالسعر / ساعةساعات العملالحالةنشط حالياًإجراءات
-
- @if($lot->image) - - @else -
- -
- @endif -
-
{{ $lot->name }}
-
- {{ number_format($lot->latitude, 5) }}, {{ number_format($lot->longitude, 5) }} -
-
-
-
- - {{ Str::limit($lot->address, 40) }} - - - {{ $lot->total_capacity }} - - - {{ number_format($lot->price_per_hour, 0) }} ر.س - - - {{ $lot->working_hours }} - - @if($lot->is_active) - - نشط - - @else - - معطل - - @endif - - {{ $lot->active_bookings_count ?? 0 }} - -
- - - -
-
- -

لا توجد مواقف بعد

-

ابدأ بإضافة أول موقف سيارات

- -
-
- - @if($parkingLots->hasPages()) -
- - عرض {{ $parkingLots->firstItem() ?? 0 }}–{{ $parkingLots->lastItem() ?? 0 }} - من {{ $parkingLots->total() }} - - {{ $parkingLots->appends(request()->query())->links('pagination::bootstrap-5') }} -
- @endif - + @endforeach
+{{-- Pagination --}} +@if($parkingLots->hasPages()) +
+ + عرض {{ $parkingLots->firstItem() }}–{{ $parkingLots->lastItem() }} + من {{ $parkingLots->total() }} + + {{ $parkingLots->appends(request()->query())->links('pagination::bootstrap-5') }} +
+@endif +@endif + {{-- ── Add / Edit Modal ────────────────────────────────────────────────────── --}}