diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index b11ae4a..9521e60 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -19,7 +19,8 @@
"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:*)"
+ "Bash(php:*)",
+ "Bash(grep -v \"^$\")"
]
}
}
diff --git a/app/Http/Controllers/Admin/BookingController.php b/app/Http/Controllers/Admin/BookingController.php
index 23029fa..c5f078c 100644
--- a/app/Http/Controllers/Admin/BookingController.php
+++ b/app/Http/Controllers/Admin/BookingController.php
@@ -47,34 +47,75 @@ class BookingController extends Controller
));
}
- public function completeBooking(Request $request, Booking $booking): JsonResponse
+ public function checkoutPreview(Booking $booking): JsonResponse
{
if ($booking->status !== 'active') {
- return response()->json([
- 'success' => false,
- 'message' => 'الحجز غير نشط'
- ], 400);
+ 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()
- ]);
+ $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,
- 'message' => 'تم إنهاء الحجز بنجاح',
- 'data' => [
- 'actual_duration' => $actualDurationHours . ' ساعات',
- 'actual_fee' => number_format($actualFee, 2) . ' ' . config('app.currency', 'ريال'),
- 'vehicle_plate' => $booking->vehicle_plate ?? $booking->customer_name
- ]
+ 'data' => [
+ '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_label' => floor($duration / 60) . 'س ' . ($duration % 60) . 'د',
+ 'fee_details' => $calc['details'],
+ 'total_fee' => $calc['total'],
+ ],
]);
}
+
+ public function completeBooking(Request $request, Booking $booking): JsonResponse
+ {
+ if ($booking->status !== 'active') {
+ return response()->json(['success' => false, 'message' => 'الحجز غير نشط'], 400);
+ }
+
+ $end = now();
+ $type = $request->input('type', 'force'); // 'payment' | 'force'
+
+ if ($type === 'payment') {
+ $request->validate([
+ 'payment_method' => 'required|in:cash,upload',
+ 'payment_proof' => 'nullable|file|mimes:jpg,jpeg,png,pdf|max:4096',
+ ]);
+
+ $lot = $booking->parkingLot;
+ $calc = $lot->calculateFee($booking->start_time, $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' => $request->payment_method,
+ 'payment_proof' => $proofPath,
+ 'paid_at' => $end,
+ ]);
+ } else {
+ // Force close — car left without paying
+ $booking->update([
+ 'status' => 'completed',
+ 'end_time' => $end,
+ 'notes' => $request->input('notes') ?: null,
+ ]);
+ }
+
+ return response()->json(['success' => true, 'message' => 'تم إنهاء الحجز بنجاح']);
+ }
}
?>
diff --git a/app/Http/Controllers/Operator/OperatorController.php b/app/Http/Controllers/Operator/OperatorController.php
index d7c64d0..71f6b49 100644
--- a/app/Http/Controllers/Operator/OperatorController.php
+++ b/app/Http/Controllers/Operator/OperatorController.php
@@ -191,6 +191,28 @@ class OperatorController extends Controller
]);
}
+ // ── 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
diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php
index 86ba32d..84253fb 100644
--- a/app/Http/Controllers/ProfileController.php
+++ b/app/Http/Controllers/ProfileController.php
@@ -81,6 +81,7 @@ class ProfileController extends Controller
'customer_name' => $user->name,
'phone' => $user->phone ?? '',
'vehicle_plate' => $request->vehicle_plate,
+ 'source' => 'reservation',
'start_time' => $request->start_time,
'end_time' => $request->end_time,
'status' => 'active',
@@ -101,6 +102,12 @@ class ProfileController extends Controller
->latest()
->get();
+ $pendingDebt = $bookings
+ ->where('status', 'cancelled')
+ ->where('total_fee', '>', 0)
+ ->filter(fn($b) => is_null($b->paid_at))
+ ->sum('total_fee');
+
$stats = [
'total' => $bookings->count(),
'active' => $bookings->where('status', 'active')->count(),
@@ -108,6 +115,110 @@ class ProfileController extends Controller
'cancelled' => $bookings->where('status', 'cancelled')->count(),
];
- return view('user.dashboard', compact('bookings', 'stats'));
+ return view('user.dashboard', compact('bookings', 'stats', 'pendingDebt'));
+ }
+
+ // ── Cancel preview — returns fee (or free) ────────────────────────────────
+
+ public function cancelPreview(Booking $booking): \Illuminate\Http\JsonResponse
+ {
+ if ($booking->user_id !== Auth::id()) {
+ return response()->json(['success' => false, 'message' => 'غير مصرح'], 403);
+ }
+ if ($booking->status !== 'active' || $booking->source !== 'reservation') {
+ return response()->json(['success' => false, 'message' => 'لا يمكن إلغاء هذا الحجز'], 400);
+ }
+
+ $isFree = now()->lt($booking->start_time);
+
+ if ($isFree) {
+ return response()->json([
+ 'success' => true,
+ 'data' => ['is_free' => true, 'fee' => 0, 'fee_details' => []],
+ ]);
+ }
+
+ $lot = $booking->parkingLot;
+ $calcEnd = now()->lt($booking->end_time) ? now() : $booking->end_time;
+ $calc = $lot->calculateFee($booking->start_time, $calcEnd, $booking->pricing_snapshot);
+
+ return response()->json([
+ 'success' => true,
+ 'data' => [
+ 'is_free' => false,
+ 'fee' => $calc['total'],
+ 'fee_details' => $calc['details'],
+ 'entry_time' => $booking->start_time->format('Y/m/d H:i'),
+ 'cancel_time' => now()->format('Y/m/d H:i'),
+ ],
+ ]);
+ }
+
+ // ── Process cancellation ──────────────────────────────────────────────────
+
+ public function cancelBooking(Request $request, Booking $booking): \Illuminate\Http\JsonResponse
+ {
+ if ($booking->user_id !== Auth::id()) {
+ return response()->json(['success' => false, 'message' => 'غير مصرح'], 403);
+ }
+ if ($booking->status !== 'active' || $booking->source !== 'reservation') {
+ return response()->json(['success' => false, 'message' => 'لا يمكن إلغاء هذا الحجز'], 400);
+ }
+
+ $isFree = now()->lt($booking->start_time);
+ $end = now();
+
+ if ($isFree) {
+ $booking->update(['status' => 'cancelled', 'total_fee' => 0, 'end_time' => $end]);
+ return response()->json(['success' => true, 'message' => 'تم إلغاء الحجز مجاناً']);
+ }
+
+ // After start time — fee applies
+ $lot = $booking->parkingLot;
+ $calcEnd = $end->lt($booking->end_time) ? $end : $booking->end_time;
+ $calc = $lot->calculateFee($booking->start_time, $calcEnd, $booking->pricing_snapshot);
+ $type = $request->input('type'); // 'pay_now' | 'pay_later'
+
+ if ($type === 'pay_now') {
+ $request->validate(['payment_method' => 'required|in:cash,upload']);
+ $proofPath = null;
+ if ($request->hasFile('payment_proof')) {
+ $proofPath = $request->file('payment_proof')->store('payment_proofs', 'public');
+ }
+ $booking->update([
+ 'status' => 'cancelled',
+ 'end_time' => $end,
+ 'total_fee' => $calc['total'],
+ 'payment_method' => $request->payment_method,
+ 'payment_proof' => $proofPath,
+ 'paid_at' => $end,
+ ]);
+ return response()->json(['success' => true, 'message' => 'تم إلغاء الحجز وتسجيل الدفع']);
+ }
+
+ // Pay later — record fee as debt
+ $booking->update([
+ 'status' => 'cancelled',
+ 'end_time' => $end,
+ 'total_fee' => $calc['total'],
+ ]);
+ return response()->json([
+ 'success' => true,
+ 'message' => 'تم الإلغاء. الرسوم المستحقة ' . number_format($calc['total']) . ' ل.س ستُضاف لحجزك القادم.',
+ 'data' => ['pending_fee' => $calc['total']],
+ ]);
+ }
+
+ // ── Pending debt (AJAX for booking modal) ─────────────────────────────────
+
+ public function pendingDebt(): \Illuminate\Http\JsonResponse
+ {
+ $debt = Booking::where('user_id', Auth::id())
+ ->where('status', 'cancelled')
+ ->where('total_fee', '>', 0)
+ ->whereNull('paid_at')
+ ->sum('total_fee');
+
+ return response()->json(['success' => true, 'data' => ['debt' => (float) $debt]]);
}
}
diff --git a/app/Models/Booking.php b/app/Models/Booking.php
index dcdb75f..1e0bae5 100644
--- a/app/Models/Booking.php
+++ b/app/Models/Booking.php
@@ -26,6 +26,7 @@ class Booking extends Model
'payment_proof',
'paid_at',
'pricing_snapshot',
+ 'notes',
];
protected $casts = [
diff --git a/database/migrations/2026_04_16_133434_add_notes_to_bookings_table.php b/database/migrations/2026_04_16_133434_add_notes_to_bookings_table.php
new file mode 100644
index 0000000..d0184ea
--- /dev/null
+++ b/database/migrations/2026_04_16_133434_add_notes_to_bookings_table.php
@@ -0,0 +1,25 @@
+text('notes')->nullable()->after('pricing_snapshot');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('bookings', function (Blueprint $table) {
+ $table->dropColumn('notes');
+ });
+ }
+};
diff --git a/resources/views/admin/bookings/active.blade.php b/resources/views/admin/bookings/active.blade.php
index d82a2d6..5117b35 100644
--- a/resources/views/admin/bookings/active.blade.php
+++ b/resources/views/admin/bookings/active.blade.php
@@ -338,47 +338,185 @@
-{{-- ── Confirm complete modal ───────────────────────────────────────────────── --}}
-
-
-
-
-
-
-
+{{-- ── End booking modal (multi-step) ──────────────────────────────────────── --}}
+
+
+
+
+ {{-- Header --}}
+
-
-
-
- رقم اللوحة
-
-
-
-
- هل تريد إنهاء هذا الحجز وتسجيل خروج السيارة؟
-
+
-
-
+
+ {{-- Payment step footer (inside modal-content, outside modal-body) --}}
+
+
@@ -414,60 +552,183 @@ function showToast(msg, type = 'success') {
setTimeout(() => { el.style.transition='opacity .4s'; el.style.opacity='0'; setTimeout(()=>el.remove(),400); }, 3500);
}
-// ── Confirm complete modal ────────────────────────────────────────────────────
-let pendingId = null;
-let confirmModal = null;
+// ── End booking modal ─────────────────────────────────────────────────────────
+const csrf = document.querySelector('meta[name="csrf-token"]').content;
+let endBookingId = null;
+let endChoiceType = null;
+let adminPayMethod = 'cash';
+let endModal = null;
-function getConfirmModal() {
- if (!confirmModal) confirmModal = new bootstrap.Modal(document.getElementById('confirmCompleteModal'));
- return confirmModal;
+function getEndModal() {
+ if (!endModal) endModal = new bootstrap.Modal(document.getElementById('endBookingModal'));
+ return endModal;
}
function askComplete(id, plate, lot) {
- pendingId = id;
- document.getElementById('confirmPlate').textContent = plate;
- document.getElementById('confirmLot').textContent = lot;
- getConfirmModal().show();
+ endBookingId = id;
+ endChoiceType = null;
+ adminPayMethod = 'cash';
+ document.getElementById('endBkPlate').textContent = plate;
+ document.getElementById('endBkLot').textContent = lot;
+ showEndStep('step1');
+ getEndModal().show();
}
-document.getElementById('confirmCompleteBtn').addEventListener('click', async () => {
- if (!pendingId) return;
- const btn = document.getElementById('confirmCompleteBtn');
+function showEndStep(step) {
+ document.getElementById('endStep1').style.display = step === 'step1' ? '' : 'none';
+ document.getElementById('endStep2Pay').style.display = step === 'step2Pay' ? '' : 'none';
+ document.getElementById('endStep2Force').style.display = step === 'step2Force'? '' : 'none';
+ document.getElementById('endPayFooter').style.display = step === 'step2Pay' ? 'flex' : 'none';
+}
+
+function endBack() { showEndStep('step1'); }
+
+async function endChoose(type) {
+ endChoiceType = type;
+
+ if (type === 'payment') {
+ showEndStep('step2Pay');
+ document.querySelector('input[name="adminPayType"][value="cash"]').checked = true;
+ adminSelectPayment('cash');
+
+ // Reset pay button state
+ const payBtn = document.getElementById('endConfirmPayBtn');
+ payBtn.disabled = false;
+ payBtn.innerHTML = '
تأكيد الدفع والخروج';
+
+ document.getElementById('payRcptBreakdown').innerHTML =
+ '
';
+ document.getElementById('payRcptEntry').textContent = '—';
+ document.getElementById('payRcptExit').textContent = '—';
+ document.getElementById('payRcptTotal').textContent = '—';
+
+ try {
+ const res = await fetch(`/admin/bookings/${endBookingId}/checkout-preview`);
+ const data = await res.json();
+ if (!data.success) { showToast(data.message || 'حدث خطأ', 'error'); endBack(); return; }
+ const d = data.data;
+ document.getElementById('payRcptEntry').textContent = d.entry_time;
+ document.getElementById('payRcptExit').textContent = d.exit_time;
+ document.getElementById('payRcptTotal').textContent = Number(d.total_fee).toLocaleString('ar-SA') + ' ل.س';
+ const rows = d.fee_details.map(r => `
+
+ ${r.day} ${r.date}
+ ${r.hours}س × ${Number(r.rate).toLocaleString('ar-SA')}
+ ${Number(r.subtotal).toLocaleString('ar-SA')} ل.س
+
`).join('');
+ document.getElementById('payRcptBreakdown').innerHTML =
+ rows || '
لا تفاصيل
';
+ } catch {
+ showToast('تعذّر تحميل بيانات الفاتورة', 'error');
+ endBack();
+ }
+
+ } else {
+ showEndStep('step2Force');
+ document.getElementById('endForceNotes').value = '';
+ }
+}
+
+function adminSelectPayment(method) {
+ adminPayMethod = method;
+ const cashOpt = document.getElementById('adminPayOptCash');
+ const uploadOpt = document.getElementById('adminPayOptUpload');
+ const uploadArea = document.getElementById('adminUploadArea');
+ if (method === 'cash') {
+ cashOpt.style.cssText = 'border:2px solid #10b981;background:#f0fdf4;border-radius:.75rem;text-align:center;padding:.75rem;';
+ uploadOpt.style.cssText = 'border:2px solid #e2e8f0;background:#fff;border-radius:.75rem;text-align:center;padding:.75rem;';
+ uploadArea.style.display = 'none';
+ } else {
+ cashOpt.style.cssText = 'border:2px solid #e2e8f0;background:#fff;border-radius:.75rem;text-align:center;padding:.75rem;';
+ uploadOpt.style.cssText = 'border:2px solid #3b82f6;background:#eff6ff;border-radius:.75rem;text-align:center;padding:.75rem;';
+ uploadArea.style.display = 'block';
+ }
+}
+
+// Payment confirm
+document.getElementById('endConfirmPayBtn').addEventListener('click', async () => {
+ if (!endBookingId) return;
+ const btn = document.getElementById('endConfirmPayBtn');
+
+ if (adminPayMethod === 'upload' && !document.getElementById('adminPaymentProof').files.length) {
+ showToast('يرجى رفع إيصال الدفع', 'error');
+ return;
+ }
+
btn.disabled = true;
- btn.innerHTML = '
جاري الإنهاء...';
+ btn.innerHTML = '
جاري المعالجة...';
+
+ const fd = new FormData();
+ fd.append('type', 'payment');
+ fd.append('payment_method', adminPayMethod);
+ if (adminPayMethod === 'upload') {
+ fd.append('payment_proof', document.getElementById('adminPaymentProof').files[0]);
+ }
try {
- const res = await fetch(`/admin/bookings/${pendingId}/complete`, {
- method: 'POST',
- headers: {
- 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
- 'Content-Type': 'application/json',
- }
+ const res = await fetch(`/admin/bookings/${endBookingId}/complete`, {
+ method: 'POST', headers: { 'X-CSRF-TOKEN': csrf }, body: fd
});
const data = await res.json();
- getConfirmModal().hide();
if (data.success) {
- const row = document.getElementById('row-' + pendingId);
- if (row) {
- row.style.transition = 'opacity .4s, transform .4s';
- row.style.opacity = '0';
- row.style.transform = 'translateX(20px)';
- setTimeout(() => row.remove(), 420);
- }
- showToast('تم إنهاء الحجز بنجاح.');
+ getEndModal().hide();
+ removeRow(endBookingId);
+ showToast('تم تسجيل الدفع والخروج بنجاح');
} else {
- showToast(data.message || 'حدث خطأ أثناء الإنهاء.', 'error');
+ showToast(data.message || 'حدث خطأ', 'error');
+ btn.disabled = false;
+ btn.innerHTML = '
تأكيد الدفع والخروج';
}
} catch {
- getConfirmModal().hide();
- showToast('تعذّر الاتصال بالخادم. حاول مجدداً.', 'error');
- } finally {
+ showToast('تعذّر الاتصال بالخادم', 'error');
btn.disabled = false;
- btn.innerHTML = '
تأكيد الإنهاء';
- pendingId = null;
+ btn.innerHTML = '
تأكيد الدفع والخروج';
}
});
+// Force-close confirm
+document.getElementById('endConfirmForceBtn').addEventListener('click', async () => {
+ if (!endBookingId) return;
+ const btn = document.getElementById('endConfirmForceBtn');
+ btn.disabled = true;
+ btn.innerHTML = '
جاري الإغلاق...';
+
+ const fd = new FormData();
+ fd.append('type', 'force');
+ const notes = document.getElementById('endForceNotes').value.trim();
+ if (notes) fd.append('notes', notes);
+
+ try {
+ const res = await fetch(`/admin/bookings/${endBookingId}/complete`, {
+ method: 'POST', headers: { 'X-CSRF-TOKEN': csrf }, body: fd
+ });
+ const data = await res.json();
+ if (data.success) {
+ getEndModal().hide();
+ removeRow(endBookingId);
+ showToast('تم الإغلاق الطارئ للحجز');
+ } else {
+ showToast(data.message || 'حدث خطأ', 'error');
+ btn.disabled = false;
+ btn.innerHTML = '
تأكيد الإغلاق الطارئ';
+ }
+ } catch {
+ showToast('تعذّر الاتصال بالخادم', 'error');
+ btn.disabled = false;
+ btn.innerHTML = '
تأكيد الإغلاق الطارئ';
+ }
+});
+
+function removeRow(id) {
+ const row = document.getElementById('row-' + id);
+ if (row) {
+ row.style.transition = 'opacity .4s, transform .4s';
+ row.style.opacity = '0';
+ row.style.transform = 'translateX(20px)';
+ setTimeout(() => row.remove(), 420);
+ }
+}
+
// ── Auto-refresh (pauses while modal is open) ─────────────────────────────────
let t = 30;
let refreshPaused = false;
diff --git a/resources/views/index.blade.php b/resources/views/index.blade.php
index 0ecc78f..6fff46a 100644
--- a/resources/views/index.blade.php
+++ b/resources/views/index.blade.php
@@ -290,9 +290,24 @@
- المبلغ الإجمالي
+ رسوم الحجز
--
+ {{-- Pending debt row (hidden when 0) --}}
+
+
+
+
+ رصيد مستحق سابق
+
+ --
+
+
+
+ الإجمالي مع المستحق
+ --
+
+
@@ -530,6 +545,8 @@
return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
}
+ let userPendingDebt = 0;
+
function calcTotal() {
const start = document.getElementById('bkStart').value;
const end = document.getElementById('bkEnd').value;
@@ -541,6 +558,16 @@
document.getElementById('bkDuration').textContent = hours + (hours === 1 ? ' ساعة' : ' ساعات');
document.getElementById('bkHourlyRate').textContent = fmtPrice(current.price);
document.getElementById('bkTotal').textContent = fmtPrice(total);
+
+ // Show debt row if user has pending fees
+ const debtRow = document.getElementById('bkDebtRow');
+ if (userPendingDebt > 0) {
+ document.getElementById('bkDebtAmt').textContent = fmtPrice(userPendingDebt);
+ document.getElementById('bkGrandTotal').textContent = fmtPrice(total + userPendingDebt);
+ debtRow.style.display = '';
+ } else {
+ debtRow.style.display = 'none';
+ }
}
function resetPrice() {
@@ -552,7 +579,7 @@
document.getElementById('bkStart').addEventListener('change', calcTotal);
document.getElementById('bkEnd').addEventListener('change', calcTotal);
- function openBookingModal(lot) {
+ async function openBookingModal(lot) {
document.getElementById('bookingLotName').textContent = lot.name;
document.getElementById('bookingLotMeta').textContent = lot.address + ' · ' + fmtPrice(lot.price) + ' / ساعة';
document.getElementById('bookingUserInfo').textContent =
@@ -568,6 +595,16 @@
document.getElementById('bookingAlert').className = 'd-none mb-3';
document.getElementById('bookingAlert').innerHTML = '';
+ document.getElementById('bkDebtRow').style.display = 'none';
+
+ // Fetch pending debt (if logged in)
+ userPendingDebt = 0;
+ try {
+ const dr = await fetch('/user/pending-debt');
+ const dd = await dr.json();
+ if (dd.success) userPendingDebt = dd.data.debt || 0;
+ } catch {}
+ calcTotal();
getBookingModal().show();
}
diff --git a/resources/views/layouts/user.blade.php b/resources/views/layouts/user.blade.php
index e8d0760..7e1ff47 100644
--- a/resources/views/layouts/user.blade.php
+++ b/resources/views/layouts/user.blade.php
@@ -58,5 +58,6 @@
@yield('content')
+ @stack('scripts')