backup: before operator assignment feature

This commit is contained in:
Ghassan Yusuf 2026-04-16 17:03:17 +03:00
parent c481135280
commit ba453d7b82
12 changed files with 1126 additions and 101 deletions

View File

@ -19,7 +19,8 @@
"Bash(php -r ':*)", "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(php -r \"echo file_put_contents\\('C:/xampp/htdocs/scp-syria/storage/app/public/test.txt', 'ok'\\) ? 'write OK' : 'write FAIL';\")",
"Bash(curl:*)", "Bash(curl:*)",
"Bash(php:*)" "Bash(php:*)",
"Bash(grep -v \"^$\")"
] ]
} }
} }

View File

@ -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') { if ($booking->status !== 'active') {
return response()->json([ return response()->json(['success' => false, 'message' => 'الحجز غير نشط'], 400);
'success' => false,
'message' => 'الحجز غير نشط'
], 400);
} }
$parkingLot = $booking->parkingLot; $lot = $booking->parkingLot;
$actualDurationHours = $booking->start_time->diffInHours(now()); $start = $booking->start_time;
$actualFee = ceil($actualDurationHours) * $parkingLot->price_per_hour; $end = now();
$duration = $start->diffInMinutes($end);
$booking->update([ $calc = $lot->calculateFee($start, $end, $booking->pricing_snapshot);
'status' => 'completed',
'end_time' => now()
]);
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'message' => 'تم إنهاء الحجز بنجاح',
'data' => [ 'data' => [
'actual_duration' => $actualDurationHours . ' ساعات', 'plate' => $booking->vehicle_plate,
'actual_fee' => number_format($actualFee, 2) . ' ' . config('app.currency', 'ريال'), 'customer_name' => $booking->customer_name,
'vehicle_plate' => $booking->vehicle_plate ?? $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' => 'تم إنهاء الحجز بنجاح']);
}
} }
?> ?>

View File

@ -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) ─────────────────────── // ── Legacy checkout (kept for backward compatibility) ───────────────────────
public function checkOut(Request $request, Booking $booking): JsonResponse public function checkOut(Request $request, Booking $booking): JsonResponse

View File

@ -81,6 +81,7 @@ class ProfileController extends Controller
'customer_name' => $user->name, 'customer_name' => $user->name,
'phone' => $user->phone ?? '', 'phone' => $user->phone ?? '',
'vehicle_plate' => $request->vehicle_plate, 'vehicle_plate' => $request->vehicle_plate,
'source' => 'reservation',
'start_time' => $request->start_time, 'start_time' => $request->start_time,
'end_time' => $request->end_time, 'end_time' => $request->end_time,
'status' => 'active', 'status' => 'active',
@ -101,6 +102,12 @@ class ProfileController extends Controller
->latest() ->latest()
->get(); ->get();
$pendingDebt = $bookings
->where('status', 'cancelled')
->where('total_fee', '>', 0)
->filter(fn($b) => is_null($b->paid_at))
->sum('total_fee');
$stats = [ $stats = [
'total' => $bookings->count(), 'total' => $bookings->count(),
'active' => $bookings->where('status', 'active')->count(), 'active' => $bookings->where('status', 'active')->count(),
@ -108,6 +115,110 @@ class ProfileController extends Controller
'cancelled' => $bookings->where('status', 'cancelled')->count(), '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]]);
} }
} }

View File

@ -26,6 +26,7 @@ class Booking extends Model
'payment_proof', 'payment_proof',
'paid_at', 'paid_at',
'pricing_snapshot', 'pricing_snapshot',
'notes',
]; ];
protected $casts = [ protected $casts = [

View File

@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('bookings', function (Blueprint $table) {
$table->text('notes')->nullable()->after('pricing_snapshot');
});
}
public function down(): void
{
Schema::table('bookings', function (Blueprint $table) {
$table->dropColumn('notes');
});
}
};

View File

@ -338,47 +338,185 @@
</div> </div>
{{-- ── Confirm complete modal ───────────────────────────────────────────────── --}} {{-- ── End booking modal (multi-step) ──────────────────────────────────────── --}}
<div class="modal fade" id="confirmCompleteModal" tabindex="-1" aria-hidden="true"> <div class="modal fade" id="endBookingModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" style="max-width:380px;"> <div class="modal-dialog modal-dialog-centered" style="max-width:500px;">
<div class="modal-content" style="border:none;border-radius:1rem;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.15);"> <div class="modal-content border-0 shadow-lg" style="border-radius:1rem;overflow:hidden;">
<div style="background:linear-gradient(135deg,#dc2626,#ef4444);padding:1.25rem 1.5rem;">
<div class="d-flex align-items-center gap-3"> {{-- Header --}}
<div style="width:44px;height:44px;background:rgba(255,255,255,.15);border-radius:.75rem;display:flex;align-items:center;justify-content:center;flex-shrink:0;"> <div class="modal-header border-0 text-white"
<i class="bi bi-stop-circle-fill" style="font-size:1.3rem;color:#fff;"></i> style="background:linear-gradient(135deg,#1e293b,#334155);padding:1.1rem 1.4rem;">
<div>
<div class="fw-800" style="font-size:.95rem;">
<i class="bi bi-stop-circle me-2"></i>إنهاء الحجز
</div> </div>
<div class="flex-grow-1"> <div class="text-xs mt-1" style="color:rgba(255,255,255,.6);">
<div class="fw-800" style="color:#fff;font-size:1rem;">إنهاء الحجز</div> <span id="endBkPlate" style="font-family:monospace;letter-spacing:.05em;"></span>
<div class="text-xs mt-1" style="color:rgba(255,255,255,.75);">تأكيد تسجيل خروج السيارة</div> &nbsp;·&nbsp;
</div> <span id="endBkLot"></span>
<button type="button" class="btn-close btn-close-white flex-shrink-0" data-bs-dismiss="modal"></button>
</div> </div>
</div> </div>
<div class="p-4"> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
<div class="p-3 rounded-3 mb-4" style="background:#f8fafc;border:1px solid #f1f5f9;">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-xs fw-600" style="color:#64748b;">رقم اللوحة</span>
<span class="fw-800" id="confirmPlate" style="font-family:monospace;font-size:1rem;color:#0f172a;letter-spacing:.05em;"></span>
</div> </div>
<div class="d-flex justify-content-between align-items-center">
<span class="text-xs fw-600" style="color:#64748b;">الموقف</span> <div class="modal-body p-0">
<span class="fw-600 text-sm" id="confirmLot" style="color:#475569;"></span>
</div> {{-- ── STEP 1 : choose action ─────────────────────────────── --}}
</div> <div id="endStep1" class="p-4">
<p class="text-sm mb-0" style="color:#64748b;text-align:center;"> <p class="text-sm text-center mb-3 fw-600" style="color:#475569;">اختر طريقة الإنهاء:</p>
هل تريد إنهاء هذا الحجز وتسجيل خروج السيارة؟ <div class="row g-3">
</p>
</div> <div class="col-6">
<div class="px-4 pb-4 d-flex gap-2"> <button class="w-100 h-100 border-0 rounded-3 p-3 text-center"
<button type="button" class="btn btn-sm fw-600" style="background:#f0fdf4;cursor:pointer;transition:box-shadow .15s;"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;padding:.5rem 1rem;" onmouseover="this.style.boxShadow='0 0 0 2px #10b981'"
data-bs-dismiss="modal">إلغاء</button> onmouseout="this.style.boxShadow=''"
<button type="button" id="confirmCompleteBtn" onclick="endChoose('payment')">
class="btn btn-sm fw-700 flex-grow-1" <div style="font-size:2rem;margin-bottom:.5rem;">💳</div>
style="background:#ef4444;color:#fff;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;padding:.5rem;"> <div class="fw-800 text-sm" style="color:#065f46;font-family:'Cairo',sans-serif;">إتمام الدفع</div>
<i class="bi bi-stop-circle me-1"></i>تأكيد الإنهاء <div class="text-xs mt-1" style="color:#6b7280;">احتساب الرسوم وتسجيل الدفع</div>
</button> </button>
</div> </div>
<div class="col-6">
<button class="w-100 h-100 border-0 rounded-3 p-3 text-center"
style="background:#fff7ed;cursor:pointer;transition:box-shadow .15s;"
onmouseover="this.style.boxShadow='0 0 0 2px #f59e0b'"
onmouseout="this.style.boxShadow=''"
onclick="endChoose('force')">
<div style="font-size:2rem;margin-bottom:.5rem;">⚠️</div>
<div class="fw-800 text-sm" style="color:#92400e;font-family:'Cairo',sans-serif;">إغلاق طارئ</div>
<div class="text-xs mt-1" style="color:#6b7280;">السيارة غادرت بدون دفع</div>
</button>
</div>
</div>
</div>
{{-- ── STEP 2A : receipt + payment ────────────────────────── --}}
<div id="endStep2Pay" style="display:none;" class="p-4">
<button class="btn btn-sm mb-3 fw-600" onclick="endBack()"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
<i class="bi bi-arrow-right me-1"></i>رجوع
</button>
{{-- Times --}}
<div class="row g-2 mb-3">
<div class="col-6">
<div class="p-2 rounded-3 text-center" style="background:#f0fdf4;">
<div class="text-xs fw-600 mb-1" style="color:#64748b;">وقت الدخول</div>
<div class="fw-700 small" id="payRcptEntry" style="color:#059669;"></div>
</div>
</div>
<div class="col-6">
<div class="p-2 rounded-3 text-center" style="background:#fef2f2;">
<div class="text-xs fw-600 mb-1" style="color:#64748b;">وقت الخروج</div>
<div class="fw-700 small" id="payRcptExit" style="color:#dc2626;"></div>
</div>
</div>
</div>
{{-- Fee breakdown --}}
<div class="mb-3">
<div class="fw-bold small mb-2" style="color:#0f172a;font-family:'Cairo',sans-serif;">تفصيل الأجرة</div>
<div id="payRcptBreakdown" style="background:#f8fafc;border-radius:10px;padding:10px 14px;">
<div class="text-center py-2" style="color:#94a3b8;">
<span class="spinner-border spinner-border-sm"></span>
</div>
</div>
<div style="display:flex;justify-content:space-between;padding:.625rem 0;border-top:2px solid #e2e8f0;font-weight:800;font-size:1.05rem;color:#0f172a;margin-top:.25rem;">
<span style="font-family:'Cairo',sans-serif;">الإجمالي</span>
<span id="payRcptTotal" style="color:#27ae60;"></span>
</div>
</div>
{{-- Payment method --}}
<div class="mb-1">
<div class="fw-bold small mb-2" style="color:#0f172a;font-family:'Cairo',sans-serif;">طريقة الدفع</div>
<div class="row g-2">
<div class="col-6">
<label style="cursor:pointer;display:block;">
<input type="radio" name="adminPayType" value="cash" class="d-none" checked onchange="adminSelectPayment('cash')">
<div id="adminPayOptCash"
class="text-center p-3 rounded-3"
style="border:2px solid #10b981;background:#f0fdf4;">
<i class="bi bi-cash-coin d-block mb-1" style="font-size:1.4rem;color:#10b981;"></i>
<span class="text-xs fw-600" style="color:#065f46;">نقداً</span>
</div>
</label>
</div>
<div class="col-6">
<label style="cursor:pointer;display:block;">
<input type="radio" name="adminPayType" value="upload" class="d-none" onchange="adminSelectPayment('upload')">
<div id="adminPayOptUpload"
class="text-center p-3 rounded-3"
style="border:2px solid #e2e8f0;background:#fff;">
<i class="bi bi-cloud-upload d-block mb-1" style="font-size:1.4rem;color:#3b82f6;"></i>
<span class="text-xs fw-600" style="color:#1e40af;">إيصال إلكتروني</span>
</div>
</label>
</div>
</div>
<div id="adminUploadArea" class="mt-2 p-3 border rounded-3 bg-light" style="display:none;">
<input type="file" id="adminPaymentProof" class="form-control form-control-sm" accept="image/*,.pdf">
<div class="text-xs mt-1 text-muted">JPG / PNG / PDF حد أقصى 4MB</div>
</div>
</div>
</div>
{{-- ── STEP 2B : force close ───────────────────────────────── --}}
<div id="endStep2Force" style="display:none;" class="p-4">
<button class="btn btn-sm mb-3 fw-600" onclick="endBack()"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;">
<i class="bi bi-arrow-right me-1"></i>رجوع
</button>
<div class="text-center py-2 mb-3">
<div style="font-size:2.8rem;margin-bottom:.75rem;">⚠️</div>
<p class="fw-700 mb-1" style="color:#0f172a;font-family:'Cairo',sans-serif;">إغلاق طارئ بدون دفع</p>
<p class="text-sm mb-0" style="color:#64748b;">سيتم إنهاء الحجز فوراً دون تسجيل أي رسوم.<br>استخدم هذا الخيار فقط إذا غادرت السيارة بدون دفع.</p>
</div>
<div class="mb-3">
<label class="form-label text-sm fw-600 mb-1" style="color:#475569;font-family:'Cairo',sans-serif;">
سبب الإغلاق <span style="color:#94a3b8;font-weight:400;">(اختياري)</span>
</label>
<textarea id="endForceNotes" rows="2"
class="form-control"
style="border-color:#e2e8f0;border-radius:.625rem;font-family:'Cairo',sans-serif;resize:none;"
placeholder="مثال: السيارة غادرت دون إذن، خلل في النظام..."></textarea>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-light fw-600 px-4"
style="font-family:'Cairo',sans-serif;border-radius:.5rem;"
data-bs-dismiss="modal">إلغاء</button>
<button type="button" id="endConfirmForceBtn"
class="btn btn-warning fw-bold flex-grow-1 text-dark"
style="font-family:'Cairo',sans-serif;border-radius:.5rem;">
<i class="bi bi-x-circle me-1"></i>تأكيد الإغلاق الطارئ
</button>
</div>
</div>
</div>
{{-- Payment step footer (inside modal-content, outside modal-body) --}}
<div id="endPayFooter" class="modal-footer border-0 px-4 pb-4 pt-2 gap-2" style="display:none;">
<button type="button" class="btn btn-light fw-600 px-4"
style="font-family:'Cairo',sans-serif;border-radius:.5rem;"
data-bs-dismiss="modal">إلغاء</button>
<button type="button" id="endConfirmPayBtn"
class="btn btn-success fw-bold flex-grow-1"
style="font-family:'Cairo',sans-serif;border-radius:.5rem;">
<i class="bi bi-check2-circle me-1"></i>تأكيد الدفع والخروج
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -414,59 +552,182 @@ function showToast(msg, type = 'success') {
setTimeout(() => { el.style.transition='opacity .4s'; el.style.opacity='0'; setTimeout(()=>el.remove(),400); }, 3500); setTimeout(() => { el.style.transition='opacity .4s'; el.style.opacity='0'; setTimeout(()=>el.remove(),400); }, 3500);
} }
// ── Confirm complete modal ──────────────────────────────────────────────────── // ── End booking modal ─────────────────────────────────────────────────────────
let pendingId = null; const csrf = document.querySelector('meta[name="csrf-token"]').content;
let confirmModal = null; let endBookingId = null;
let endChoiceType = null;
let adminPayMethod = 'cash';
let endModal = null;
function getConfirmModal() { function getEndModal() {
if (!confirmModal) confirmModal = new bootstrap.Modal(document.getElementById('confirmCompleteModal')); if (!endModal) endModal = new bootstrap.Modal(document.getElementById('endBookingModal'));
return confirmModal; return endModal;
} }
function askComplete(id, plate, lot) { function askComplete(id, plate, lot) {
pendingId = id; endBookingId = id;
document.getElementById('confirmPlate').textContent = plate; endChoiceType = null;
document.getElementById('confirmLot').textContent = lot; adminPayMethod = 'cash';
getConfirmModal().show(); document.getElementById('endBkPlate').textContent = plate;
document.getElementById('endBkLot').textContent = lot;
showEndStep('step1');
getEndModal().show();
} }
document.getElementById('confirmCompleteBtn').addEventListener('click', async () => { function showEndStep(step) {
if (!pendingId) return; document.getElementById('endStep1').style.display = step === 'step1' ? '' : 'none';
const btn = document.getElementById('confirmCompleteBtn'); document.getElementById('endStep2Pay').style.display = step === 'step2Pay' ? '' : 'none';
btn.disabled = true; document.getElementById('endStep2Force').style.display = step === 'step2Force'? '' : 'none';
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>جاري الإنهاء...'; 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 = '<i class="bi bi-check2-circle me-1"></i>تأكيد الدفع والخروج';
document.getElementById('payRcptBreakdown').innerHTML =
'<div class="text-center py-2" style="color:#94a3b8;"><span class="spinner-border spinner-border-sm"></span></div>';
document.getElementById('payRcptEntry').textContent = '—';
document.getElementById('payRcptExit').textContent = '—';
document.getElementById('payRcptTotal').textContent = '—';
try { try {
const res = await fetch(`/admin/bookings/${pendingId}/complete`, { const res = await fetch(`/admin/bookings/${endBookingId}/checkout-preview`);
method: 'POST', const data = await res.json();
headers: { if (!data.success) { showToast(data.message || 'حدث خطأ', 'error'); endBack(); return; }
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, const d = data.data;
'Content-Type': 'application/json', 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 => `
<div style="display:flex;justify-content:space-between;padding:.3rem 0;border-bottom:1px dashed #f1f5f9;font-size:.82rem;">
<span>${r.day} <small style="color:#94a3b8;">${r.date}</small></span>
<span style="color:#64748b;">${r.hours}س × ${Number(r.rate).toLocaleString('ar-SA')}</span>
<span class="fw-600" style="color:#0f172a;">${Number(r.subtotal).toLocaleString('ar-SA')} ل.س</span>
</div>`).join('');
document.getElementById('payRcptBreakdown').innerHTML =
rows || '<p class="text-xs text-center" style="color:#94a3b8;">لا تفاصيل</p>';
} 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 = '<span class="spinner-border spinner-border-sm me-1"></span>جاري المعالجة...';
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/${endBookingId}/complete`, {
method: 'POST', headers: { 'X-CSRF-TOKEN': csrf }, body: fd
}); });
const data = await res.json(); const data = await res.json();
getConfirmModal().hide();
if (data.success) { if (data.success) {
const row = document.getElementById('row-' + pendingId); getEndModal().hide();
removeRow(endBookingId);
showToast('تم تسجيل الدفع والخروج بنجاح');
} else {
showToast(data.message || 'حدث خطأ', 'error');
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-check2-circle me-1"></i>تأكيد الدفع والخروج';
}
} catch {
showToast('تعذّر الاتصال بالخادم', 'error');
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-check2-circle me-1"></i>تأكيد الدفع والخروج';
}
});
// Force-close confirm
document.getElementById('endConfirmForceBtn').addEventListener('click', async () => {
if (!endBookingId) return;
const btn = document.getElementById('endConfirmForceBtn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>جاري الإغلاق...';
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 = '<i class="bi bi-x-circle me-1"></i>تأكيد الإغلاق الطارئ';
}
} catch {
showToast('تعذّر الاتصال بالخادم', 'error');
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-x-circle me-1"></i>تأكيد الإغلاق الطارئ';
}
});
function removeRow(id) {
const row = document.getElementById('row-' + id);
if (row) { if (row) {
row.style.transition = 'opacity .4s, transform .4s'; row.style.transition = 'opacity .4s, transform .4s';
row.style.opacity = '0'; row.style.opacity = '0';
row.style.transform = 'translateX(20px)'; row.style.transform = 'translateX(20px)';
setTimeout(() => row.remove(), 420); setTimeout(() => row.remove(), 420);
} }
showToast('تم إنهاء الحجز بنجاح.');
} else {
showToast(data.message || 'حدث خطأ أثناء الإنهاء.', 'error');
} }
} catch {
getConfirmModal().hide();
showToast('تعذّر الاتصال بالخادم. حاول مجدداً.', 'error');
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-stop-circle me-1"></i>تأكيد الإنهاء';
pendingId = null;
}
});
// ── Auto-refresh (pauses while modal is open) ───────────────────────────────── // ── Auto-refresh (pauses while modal is open) ─────────────────────────────────
let t = 30; let t = 30;

View File

@ -290,9 +290,24 @@
</div> </div>
<div style="border-top:1px dashed #bbf7d0;margin:.5rem 0;"></div> <div style="border-top:1px dashed #bbf7d0;margin:.5rem 0;"></div>
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<span class="fw-700" style="color:#065f46;font-size:.85rem;">المبلغ الإجمالي</span> <span class="fw-700" style="color:#065f46;font-size:.85rem;">رسوم الحجز</span>
<span class="fw-800" id="bkTotal" style="color:#059669;font-size:1.15rem;">--</span> <span class="fw-800" id="bkTotal" style="color:#059669;font-size:1.15rem;">--</span>
</div> </div>
{{-- Pending debt row (hidden when 0) --}}
<div id="bkDebtRow" style="display:none;">
<div style="border-top:1px dashed #fbbf24;margin:.5rem 0;"></div>
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="fw-600 text-xs" style="color:#92400e;">
<i class="bi bi-exclamation-triangle me-1"></i>رصيد مستحق سابق
</span>
<span class="fw-700 text-xs" id="bkDebtAmt" style="color:#ef4444;">--</span>
</div>
<div style="border-top:1.5px solid #e2e8f0;margin:.5rem 0;"></div>
<div class="d-flex justify-content-between align-items-center">
<span class="fw-700" style="color:#0f172a;font-size:.85rem;">الإجمالي مع المستحق</span>
<span class="fw-800" id="bkGrandTotal" style="color:#ef4444;font-size:1.15rem;">--</span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -530,6 +545,8 @@
return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
} }
let userPendingDebt = 0;
function calcTotal() { function calcTotal() {
const start = document.getElementById('bkStart').value; const start = document.getElementById('bkStart').value;
const end = document.getElementById('bkEnd').value; const end = document.getElementById('bkEnd').value;
@ -541,6 +558,16 @@
document.getElementById('bkDuration').textContent = hours + (hours === 1 ? ' ساعة' : ' ساعات'); document.getElementById('bkDuration').textContent = hours + (hours === 1 ? ' ساعة' : ' ساعات');
document.getElementById('bkHourlyRate').textContent = fmtPrice(current.price); document.getElementById('bkHourlyRate').textContent = fmtPrice(current.price);
document.getElementById('bkTotal').textContent = fmtPrice(total); 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() { function resetPrice() {
@ -552,7 +579,7 @@
document.getElementById('bkStart').addEventListener('change', calcTotal); document.getElementById('bkStart').addEventListener('change', calcTotal);
document.getElementById('bkEnd').addEventListener('change', calcTotal); document.getElementById('bkEnd').addEventListener('change', calcTotal);
function openBookingModal(lot) { async function openBookingModal(lot) {
document.getElementById('bookingLotName').textContent = lot.name; document.getElementById('bookingLotName').textContent = lot.name;
document.getElementById('bookingLotMeta').textContent = lot.address + ' · ' + fmtPrice(lot.price) + ' / ساعة'; document.getElementById('bookingLotMeta').textContent = lot.address + ' · ' + fmtPrice(lot.price) + ' / ساعة';
document.getElementById('bookingUserInfo').textContent = document.getElementById('bookingUserInfo').textContent =
@ -568,6 +595,16 @@
document.getElementById('bookingAlert').className = 'd-none mb-3'; document.getElementById('bookingAlert').className = 'd-none mb-3';
document.getElementById('bookingAlert').innerHTML = ''; 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(); getBookingModal().show();
} }

View File

@ -58,5 +58,6 @@
@yield('content') @yield('content')
</main> </main>
@stack('scripts')
</body> </body>
</html> </html>

View File

@ -336,11 +336,15 @@ $gradients = [
onclick="activateRes({{ $res->id }}, this)"> onclick="activateRes({{ $res->id }}, this)">
<i class="bi bi-box-arrow-in-right me-1"></i>تسجيل دخول <i class="bi bi-box-arrow-in-right me-1"></i>تسجيل دخول
</button> </button>
<button class="btn btn-outline-danger btn-sm w-100" <button class="btn btn-outline-danger btn-sm w-100 mb-2"
id="checkout-{{ $res->id }}" disabled id="checkout-{{ $res->id }}" disabled
onclick="openReceipt({{ $res->id }})"> onclick="openReceipt({{ $res->id }})">
<i class="bi bi-receipt me-1"></i>خروج وفاتورة <i class="bi bi-receipt me-1"></i>خروج وفاتورة
</button> </button>
<button class="btn btn-outline-secondary btn-sm w-100"
onclick="openCancelModal({{ $res->id }}, {{ json_encode($res->customer_name ?? '') }}, {{ json_encode($res->phone ?? '') }})">
<i class="bi bi-x-circle me-1"></i>إلغاء الحجز
</button>
</div> </div>
</div> </div>
</div> </div>
@ -590,6 +594,48 @@ $gradients = [
</div> </div>
</div> </div>
{{-- ══════════════════════════════════════════════════════════════════════
CANCEL RESERVATION MODAL
══════════════════════════════════════════════════════════════════════ --}}
<div class="modal fade" id="cancelResModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" style="max-width:420px;">
<div class="modal-content border-0 shadow-lg" style="border-radius:16px;overflow:hidden;">
<div class="modal-header text-white border-0"
style="background:linear-gradient(135deg,#7f1d1d,#dc2626);">
<h6 class="modal-title fw-bold mb-0" style="font-family:'Cairo',sans-serif;">
<i class="bi bi-x-circle me-2"></i>إلغاء الحجز
</h6>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<p class="text-muted small mb-3" style="font-family:'Cairo',sans-serif;">
للتحقق من هويتك، أدخل <strong>اسم</strong> صاحب الحجز أو <strong>رقم هاتفه</strong> بالضبط:
</p>
<input type="text" id="cancelVerifyInput"
class="form-control form-control-lg text-center"
style="border-radius:10px;letter-spacing:1px;"
placeholder="الاسم أو رقم الهاتف"
autocomplete="off">
<div id="cancelVerifyHint" class="text-xs mt-2 text-center" style="color:#94a3b8;min-height:1.2em;"></div>
</div>
<div class="modal-footer border-0 pt-0 pb-4 px-4 gap-2">
<button type="button" class="btn btn-light flex-fill fw-600"
style="font-family:'Cairo',sans-serif;border-radius:10px;"
data-bs-dismiss="modal">تراجع</button>
<button type="button" id="cancelResConfirmBtn" disabled
class="btn btn-danger flex-fill fw-bold"
style="font-family:'Cairo',sans-serif;border-radius:10px;">
<i class="bi bi-x-circle me-1"></i>تأكيد الإلغاء
</button>
</div>
</div>
</div>
</div>
{{-- ══════════════════════════════════════════════════════════════════════ {{-- ══════════════════════════════════════════════════════════════════════
ACTIVATE RESERVATION CONFIRM MODAL ACTIVATE RESERVATION CONFIRM MODAL
══════════════════════════════════════════════════════════════════════ --}} ══════════════════════════════════════════════════════════════════════ --}}
@ -783,6 +829,81 @@ document.getElementById('activateConfirmBtn').addEventListener('click', async ()
} }
}); });
// ── Cancel reservation ────────────────────────────────────────────────
let cancelBookingId = null;
let cancelName = '';
let cancelPhone = '';
let cancelModal = null;
function getCancelModal() {
if (!cancelModal) cancelModal = new bootstrap.Modal(document.getElementById('cancelResModal'));
return cancelModal;
}
function openCancelModal(id, name, phone) {
cancelBookingId = id;
cancelName = (name || '').trim().toLowerCase();
cancelPhone = (phone || '').trim();
const input = document.getElementById('cancelVerifyInput');
const hint = document.getElementById('cancelVerifyHint');
const btn = document.getElementById('cancelResConfirmBtn');
input.value = '';
hint.textContent = '';
btn.disabled = true;
getCancelModal().show();
setTimeout(() => input.focus(), 400);
}
document.getElementById('cancelVerifyInput').addEventListener('input', function () {
const val = this.value.trim();
const btn = document.getElementById('cancelResConfirmBtn');
const hint = document.getElementById('cancelVerifyHint');
const nameOk = cancelName && val.toLowerCase() === cancelName;
const phoneOk = cancelPhone && val === cancelPhone;
const ok = nameOk || phoneOk;
btn.disabled = !ok;
hint.textContent = ok ? '✓ تم التحقق' : (val.length > 0 ? 'لا يطابق الاسم أو الهاتف' : '');
hint.style.color = ok ? '#10b981' : '#ef4444';
});
document.getElementById('cancelResConfirmBtn').addEventListener('click', async () => {
if (!cancelBookingId) return;
const btn = document.getElementById('cancelResConfirmBtn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>جاري الإلغاء...';
try {
const fd = new FormData();
fd.append('verification', document.getElementById('cancelVerifyInput').value.trim());
const res = await fetch(`/operator/${cancelBookingId}/cancel`, {
method: 'POST', headers: { 'X-CSRF-TOKEN': csrf }, body: fd
});
const data = await res.json();
if (data.success) {
getCancelModal().hide();
const card = document.getElementById('card-res-' + cancelBookingId);
if (card) { card.style.transition = 'opacity .4s'; card.style.opacity = '0'; setTimeout(() => card.remove(), 400); }
showToast('تم إلغاء الحجز بنجاح', 'success');
} else {
showToast(data.message || 'حدث خطأ', 'danger');
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-x-circle me-1"></i>تأكيد الإلغاء';
}
} catch {
showToast('خطأ في الاتصال', 'danger');
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-x-circle me-1"></i>تأكيد الإلغاء';
}
});
document.getElementById('cancelResModal').addEventListener('hidden.bs.modal', () => {
document.getElementById('cancelVerifyInput').value = '';
document.getElementById('cancelVerifyHint').textContent = '';
document.getElementById('cancelResConfirmBtn').disabled = true;
document.getElementById('cancelResConfirmBtn').innerHTML = '<i class="bi bi-x-circle me-1"></i>تأكيد الإلغاء';
});
// ── Receipt modal ────────────────────────────────────────────────────── // ── Receipt modal ──────────────────────────────────────────────────────
let receiptModal = null; let receiptModal = null;
let currentBookingId = null; let currentBookingId = null;

View File

@ -16,6 +16,27 @@
</div> </div>
</div> </div>
{{-- ── Debt banner ─────────────────────────────────────────────────────────── --}}
@if($pendingDebt > 0)
<div class="mb-4 p-3 rounded-3 d-flex align-items-center gap-3 flex-wrap"
style="background:linear-gradient(135deg,#fff7ed,#fef3c7);border:1.5px solid #fbbf24;">
<div style="width:42px;height:42px;background:#f59e0b;border-radius:.625rem;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i class="bi bi-exclamation-triangle-fill" style="color:#fff;font-size:1.1rem;"></i>
</div>
<div class="flex-grow-1">
<div class="fw-700" style="color:#92400e;font-size:.9rem;">رصيد مستحق غير مدفوع</div>
<div class="text-sm" style="color:#b45309;">
لديك رسوم إلغاء متراكمة بقيمة
<strong>{{ number_format($pendingDebt) }} ل.س</strong>
ستُضاف تلقائياً إلى حجزك القادم.
</div>
</div>
<div class="fw-800" style="color:#92400e;font-size:1.25rem;white-space:nowrap;">
{{ number_format($pendingDebt) }} ل.س
</div>
</div>
@endif
{{-- ── Stats row ──────────────────────────────────────────────────────────── --}} {{-- ── Stats row ──────────────────────────────────────────────────────────── --}}
<div class="row g-3 mb-4"> <div class="row g-3 mb-4">
@php @php
@ -73,37 +94,86 @@
<tr> <tr>
<th>#</th> <th>#</th>
<th>الموقف</th> <th>الموقف</th>
<th>تاريخ البدء</th> <th>بدء</th>
<th>تاريخ الانتهاء</th> <th>انتهاء</th>
<th>الرسوم</th>
<th>الحالة</th> <th>الحالة</th>
<th class="text-center">إجراء</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach($bookings as $booking) @foreach($bookings as $booking)
<tr> @php
$canCancel = $booking->status === 'active' && $booking->source === 'reservation';
$hasDebt = $booking->status === 'cancelled' && $booking->total_fee > 0 && is_null($booking->paid_at);
$isFree = $booking->status === 'cancelled' && ($booking->total_fee == 0 || is_null($booking->total_fee));
$isPaid = $booking->status === 'completed' || ($booking->status === 'cancelled' && !is_null($booking->paid_at) && $booking->total_fee > 0);
@endphp
<tr id="bk-row-{{ $booking->id }}">
<td class="text-sm" style="color:#94a3b8;">{{ $booking->id }}</td> <td class="text-sm" style="color:#94a3b8;">{{ $booking->id }}</td>
<td> <td>
<span class="fw-600" style="color:#0f172a;"> <span class="fw-600" style="color:#0f172a;">{{ $booking->parkingLot?->name ?? '—' }}</span>
{{ $booking->parkingLot?->name ?? '—' }}
</span>
<div class="text-xs" style="color:#94a3b8;">{{ $booking->parkingLot?->address }}</div> <div class="text-xs" style="color:#94a3b8;">{{ $booking->parkingLot?->address }}</div>
</td> </td>
<td class="text-sm" style="color:#475569;"> <td class="text-sm" style="color:#475569;">
{{ $booking->start_time->format('Y/m/d') }} {{ $booking->start_time->format('Y/m/d') }}
<div class="text-xs" style="color:#94a3b8;">{{ $booking->start_time->format('H:i') }}</div> <div class="text-xs" style="color:#94a3b8;">{{ $booking->start_time->format('H:i') }}</div>
</td> </td>
<td class="text-sm" style="color:#475569;"> <td class="text-sm" style="color:#475569;">
{{ $booking->end_time->format('Y/m/d') }} {{ $booking->end_time->format('Y/m/d') }}
<div class="text-xs" style="color:#94a3b8;">{{ $booking->end_time->format('H:i') }}</div> <div class="text-xs" style="color:#94a3b8;">{{ $booking->end_time->format('H:i') }}</div>
</td> </td>
{{-- Fee column --}}
<td>
@if($hasDebt)
<span class="fw-700" style="color:#ef4444;">{{ number_format($booking->total_fee) }} ل.س</span>
<div class="text-xs fw-600" style="color:#ef4444;">مستحق</div>
@elseif($booking->total_fee > 0 && !is_null($booking->paid_at))
<span class="fw-600 text-sm" style="color:#10b981;">{{ number_format($booking->total_fee) }} ل.س</span>
<div class="text-xs" style="color:#10b981;">مدفوع</div>
@elseif($booking->total_fee > 0)
<span class="fw-600 text-sm" style="color:#475569;">{{ number_format($booking->total_fee) }} ل.س</span>
@elseif($isFree && $booking->status === 'cancelled')
<span class="text-xs" style="color:#94a3b8;">مجاني</span>
@else
<span class="text-xs" style="color:#94a3b8;"></span>
@endif
</td>
{{-- Status column --}}
<td> <td>
@if($booking->status === 'active') @if($booking->status === 'active')
<span class="badge badge-soft-warning">نشط</span> @if($booking->source === 'reservation')
<span class="badge badge-soft-primary">حجز مسبق</span>
@else
<span class="badge badge-soft-warning">داخل الموقف</span>
@endif
@elseif($booking->status === 'completed') @elseif($booking->status === 'completed')
<span class="badge badge-soft-success">مكتمل</span> <span class="badge badge-soft-success">مكتمل</span>
@else
@if($hasDebt)
<span class="badge" style="background:rgba(239,68,68,.1);color:#dc2626;">ملغي رسوم معلقة</span>
@else @else
<span class="badge badge-soft-danger">ملغي</span> <span class="badge badge-soft-danger">ملغي</span>
@endif @endif
@endif
</td>
{{-- Action column --}}
<td class="text-center">
@if($canCancel)
<button class="btn btn-sm fw-600"
style="background:rgba(239,68,68,.1);color:#dc2626;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;padding:.3rem .75rem;"
onclick="openCancelModal({{ $booking->id }}, '{{ addslashes($booking->parkingLot?->name ?? '') }}', '{{ $booking->start_time->toISOString() }}')">
<i class="bi bi-x-circle me-1"></i>إلغاء
</button>
@else
<span class="text-xs" style="color:#94a3b8;"></span>
@endif
</td> </td>
</tr> </tr>
@endforeach @endforeach
@ -113,4 +183,333 @@
@endif @endif
</div> </div>
{{-- ══════════════════════════════════════════════════════════════════════════
CANCEL RESERVATION MODAL
══════════════════════════════════════════════════════════════════════════ --}}
<div class="modal fade" id="cancelBookingModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" style="max-width:480px;">
<div class="modal-content border-0 shadow-lg" style="border-radius:1rem;overflow:hidden;">
<div class="modal-header border-0 text-white"
style="background:linear-gradient(135deg,#1e293b,#334155);padding:1.1rem 1.4rem;">
<div>
<div class="fw-800" style="font-size:.95rem;">
<i class="bi bi-x-circle me-2"></i>إلغاء الحجز
</div>
<div class="text-xs mt-1" style="color:rgba(255,255,255,.6);" id="cancelBkLot"></div>
</div>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
{{-- Loading --}}
<div id="cancelStep0" class="text-center p-5">
<span class="spinner-border" style="color:#6366f1;"></span>
</div>
{{-- Free cancellation --}}
<div id="cancelStepFree" style="display:none;" class="p-4 text-center">
<div style="font-size:3rem;margin-bottom:.75rem;"></div>
<p class="fw-700 mb-1" style="color:#0f172a;font-family:'Cairo',sans-serif;">إلغاء مجاني</p>
<p class="text-sm" style="color:#64748b;">لم يبدأ وقت الحجز بعد يمكنك الإلغاء مجاناً.</p>
<div class="d-flex gap-2 mt-4">
<button type="button" class="btn btn-light fw-600 flex-fill"
style="font-family:'Cairo',sans-serif;border-radius:.5rem;"
data-bs-dismiss="modal">تراجع</button>
<button type="button" id="cancelFreeConfirmBtn"
class="btn btn-danger fw-bold flex-fill"
style="font-family:'Cairo',sans-serif;border-radius:.5rem;">
<i class="bi bi-x-circle me-1"></i>تأكيد الإلغاء
</button>
</div>
</div>
{{-- Paid cancellation (after start time) --}}
<div id="cancelStepPaid" style="display:none;" class="p-4">
<div class="p-3 rounded-3 mb-3" style="background:#fef2f2;border:1px solid #fecaca;">
<div class="fw-700 text-sm mb-1" style="color:#991b1b;font-family:'Cairo',sans-serif;">
<i class="bi bi-exclamation-triangle me-1"></i>بدأ وقت حجزك
</div>
<div class="text-xs" style="color:#b91c1c;">
الإلغاء بعد وقت البدء يستوجب دفع رسوم المدة المنقضية.
</div>
</div>
{{-- Fee breakdown --}}
<div class="mb-3">
<div class="fw-700 small mb-2" style="color:#0f172a;font-family:'Cairo',sans-serif;">تفصيل الرسوم المستحقة</div>
<div id="cancelFeeBreakdown"
style="background:#f8fafc;border-radius:.625rem;padding:.75rem 1rem;font-size:.82rem;">
</div>
<div style="display:flex;justify-content:space-between;padding:.5rem 0;border-top:2px solid #e2e8f0;font-weight:800;font-size:1rem;color:#0f172a;margin-top:.25rem;">
<span style="font-family:'Cairo',sans-serif;">المستحق عليك</span>
<span id="cancelFeeTotal" style="color:#ef4444;"></span>
</div>
</div>
{{-- Payment method (shown only for pay-now) --}}
<div id="cancelPayMethodWrap" style="display:none;" class="mb-3">
<div class="fw-700 small mb-2" style="color:#0f172a;font-family:'Cairo',sans-serif;">طريقة الدفع</div>
<div class="row g-2">
<div class="col-6">
<label style="cursor:pointer;display:block;">
<input type="radio" name="cancelPayType" value="cash" class="d-none" checked
onchange="cancelSelectPayment('cash')">
<div id="cancelPayOptCash"
class="text-center p-3 rounded-3"
style="border:2px solid #10b981;background:#f0fdf4;">
<i class="bi bi-cash-coin d-block mb-1" style="font-size:1.3rem;color:#10b981;"></i>
<span class="text-xs fw-600" style="color:#065f46;">نقداً</span>
</div>
</label>
</div>
<div class="col-6">
<label style="cursor:pointer;display:block;">
<input type="radio" name="cancelPayType" value="upload" class="d-none"
onchange="cancelSelectPayment('upload')">
<div id="cancelPayOptUpload"
class="text-center p-3 rounded-3"
style="border:2px solid #e2e8f0;background:#fff;">
<i class="bi bi-cloud-upload d-block mb-1" style="font-size:1.3rem;color:#3b82f6;"></i>
<span class="text-xs fw-600" style="color:#1e40af;">إيصال إلكتروني</span>
</div>
</label>
</div>
</div>
<div id="cancelUploadArea" class="mt-2 p-3 border rounded-3 bg-light" style="display:none;">
<input type="file" id="cancelPayProof" class="form-control form-control-sm" accept="image/*,.pdf">
<div class="text-xs mt-1 text-muted">JPG / PNG / PDF حد أقصى 4MB</div>
</div>
</div>
{{-- Action buttons --}}
<div id="cancelPaidActions" class="d-flex gap-2">
<button type="button" class="btn btn-light fw-600"
style="font-family:'Cairo',sans-serif;border-radius:.5rem;padding:.5rem 1rem;"
data-bs-dismiss="modal">تراجع</button>
<button type="button" id="cancelPayNowBtn"
class="btn btn-success fw-bold flex-grow-1"
style="font-family:'Cairo',sans-serif;border-radius:.5rem;">
<i class="bi bi-credit-card me-1"></i>دفع الآن وإلغاء
</button>
<button type="button" id="cancelPayLaterBtn"
class="btn btn-outline-warning fw-bold flex-grow-1"
style="font-family:'Cairo',sans-serif;border-radius:.5rem;color:#b45309;">
<i class="bi bi-clock me-1"></i>سأدفع لاحقاً
</button>
</div>
{{-- After pay-now chosen: back + confirm --}}
<div id="cancelPayNowActions" style="display:none;" class="d-flex gap-2">
<button type="button" class="btn btn-light fw-600"
style="font-family:'Cairo',sans-serif;border-radius:.5rem;"
onclick="cancelBackToActions()">
<i class="bi bi-arrow-right me-1"></i>رجوع
</button>
<button type="button" id="cancelConfirmPayBtn"
class="btn btn-success fw-bold flex-grow-1"
style="font-family:'Cairo',sans-serif;border-radius:.5rem;">
<i class="bi bi-check2-circle me-1"></i>تأكيد الدفع والإلغاء
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{{-- Toast container --}}
<div id="dashToastWrap"
style="position:fixed;bottom:1.5rem;inset-inline-end:1.25rem;z-index:9999;display:flex;flex-direction:column;gap:.5rem;"></div>
@push('scripts')
<script>
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
// ── Toast ─────────────────────────────────────────────────────────────────
function showToast(msg, type = 'success') {
const colors = { success:'#10b981', danger:'#ef4444', warning:'#f59e0b' };
const t = document.createElement('div');
t.style.cssText = `background:${colors[type]||colors.success};color:#fff;padding:.7rem 1.2rem;
border-radius:.625rem;font-family:'Cairo',sans-serif;font-size:.875rem;font-weight:600;
box-shadow:0 8px 24px rgba(0,0,0,.18);opacity:0;transition:opacity .25s;max-width:320px;`;
t.textContent = msg;
document.getElementById('dashToastWrap').appendChild(t);
requestAnimationFrame(() => t.style.opacity = '1');
setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 300); }, 4000);
}
// ── Cancel modal state ────────────────────────────────────────────────────
let cancelBkId = null;
let cancelPayMode = 'cash';
let cancelModal = null;
function getCancelModal() {
if (!cancelModal) cancelModal = new bootstrap.Modal(document.getElementById('cancelBookingModal'));
return cancelModal;
}
function showCancelStep(step) {
['cancelStep0','cancelStepFree','cancelStepPaid'].forEach(id => {
document.getElementById(id).style.display = 'none';
});
document.getElementById(step).style.display = '';
}
async function openCancelModal(id, lotName, startIso) {
cancelBkId = id;
cancelPayMode = 'cash';
document.getElementById('cancelBkLot').textContent = lotName;
showCancelStep('cancelStep0');
getCancelModal().show();
try {
const res = await fetch(`/reservations/${id}/cancel-preview`);
const data = await res.json();
if (!data.success) { showToast(data.message || 'حدث خطأ', 'danger'); getCancelModal().hide(); return; }
const d = data.data;
if (d.is_free) {
showCancelStep('cancelStepFree');
} else {
// Build breakdown
const rows = d.fee_details.map(r => `
<div style="display:flex;justify-content:space-between;padding:.25rem 0;border-bottom:1px dashed #f1f5f9;">
<span>${r.day} <small style="color:#94a3b8;">${r.date}</small></span>
<span style="color:#64748b;">${r.hours}س × ${Number(r.rate).toLocaleString('ar-SA')}</span>
<span class="fw-600" style="color:#0f172a;">${Number(r.subtotal).toLocaleString('ar-SA')} ل.س</span>
</div>`).join('');
document.getElementById('cancelFeeBreakdown').innerHTML =
rows || '<span style="color:#94a3b8;font-size:.8rem;">— مدة قصيرة جداً —</span>';
document.getElementById('cancelFeeTotal').textContent =
Number(d.fee).toLocaleString('ar-SA') + ' ل.س';
// Reset UI
document.getElementById('cancelPayMethodWrap').style.display = 'none';
document.getElementById('cancelPaidActions').style.display = 'flex';
document.getElementById('cancelPayNowActions').style.display = 'none';
cancelSelectPayment('cash');
document.querySelector('input[name="cancelPayType"][value="cash"]').checked = true;
showCancelStep('cancelStepPaid');
}
} catch {
showToast('تعذّر تحميل بيانات الإلغاء', 'danger');
getCancelModal().hide();
}
}
// Free cancel confirm
document.getElementById('cancelFreeConfirmBtn').addEventListener('click', async () => {
const btn = document.getElementById('cancelFreeConfirmBtn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>';
try {
const fd = new FormData();
fd.append('type', 'free');
const res = await fetch(`/reservations/${cancelBkId}/cancel`, {
method: 'POST', headers: { 'X-CSRF-TOKEN': csrfToken }, body: fd
});
const data = await res.json();
getCancelModal().hide();
if (data.success) {
showToast(data.message, 'success');
const row = document.getElementById('bk-row-' + cancelBkId);
if (row) { row.style.opacity = '0'; row.style.transition = 'opacity .4s'; setTimeout(() => location.reload(), 600); }
} else { showToast(data.message || 'حدث خطأ', 'danger'); }
} catch {
showToast('خطأ في الاتصال', 'danger');
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-x-circle me-1"></i>تأكيد الإلغاء';
}
});
// Pay-now button (show payment method)
document.getElementById('cancelPayNowBtn').addEventListener('click', () => {
document.getElementById('cancelPaidActions').style.display = 'none';
document.getElementById('cancelPayNowActions').style.display = 'flex';
document.getElementById('cancelPayMethodWrap').style.display = 'block';
});
function cancelBackToActions() {
document.getElementById('cancelPayNowActions').style.display = 'none';
document.getElementById('cancelPaidActions').style.display = 'flex';
document.getElementById('cancelPayMethodWrap').style.display = 'none';
}
// Pay later
document.getElementById('cancelPayLaterBtn').addEventListener('click', async () => {
const btn = document.getElementById('cancelPayLaterBtn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>';
try {
const fd = new FormData();
fd.append('type', 'pay_later');
const res = await fetch(`/reservations/${cancelBkId}/cancel`, {
method: 'POST', headers: { 'X-CSRF-TOKEN': csrfToken }, body: fd
});
const data = await res.json();
getCancelModal().hide();
if (data.success) { showToast(data.message, 'warning'); setTimeout(() => location.reload(), 1200); }
else { showToast(data.message || 'حدث خطأ', 'danger'); }
} catch {
showToast('خطأ في الاتصال', 'danger');
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-clock me-1"></i>سأدفع لاحقاً';
}
});
// Confirm pay now
document.getElementById('cancelConfirmPayBtn').addEventListener('click', async () => {
if (cancelPayMode === 'upload' && !document.getElementById('cancelPayProof').files.length) {
showToast('يرجى رفع إيصال الدفع', 'danger'); return;
}
const btn = document.getElementById('cancelConfirmPayBtn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>جاري الدفع...';
try {
const fd = new FormData();
fd.append('type', 'pay_now');
fd.append('payment_method', cancelPayMode);
if (cancelPayMode === 'upload') fd.append('payment_proof', document.getElementById('cancelPayProof').files[0]);
const res = await fetch(`/reservations/${cancelBkId}/cancel`, {
method: 'POST', headers: { 'X-CSRF-TOKEN': csrfToken }, body: fd
});
const data = await res.json();
getCancelModal().hide();
if (data.success) { showToast(data.message, 'success'); setTimeout(() => location.reload(), 800); }
else { showToast(data.message || 'حدث خطأ', 'danger'); }
} catch {
showToast('خطأ في الاتصال', 'danger');
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-check2-circle me-1"></i>تأكيد الدفع والإلغاء';
}
});
function cancelSelectPayment(method) {
cancelPayMode = method;
const cashOpt = document.getElementById('cancelPayOptCash');
const uploadOpt = document.getElementById('cancelPayOptUpload');
const area = document.getElementById('cancelUploadArea');
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;';
area.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;';
area.style.display = 'block';
}
}
</script>
@endpush
@endsection @endsection

View File

@ -17,6 +17,9 @@ Route::middleware('auth')->group(function () {
// Booking from the public landing page (web session auth) // Booking from the public landing page (web session auth)
Route::post('/reserve', [ProfileController::class, 'reserve'])->name('reserve'); Route::post('/reserve', [ProfileController::class, 'reserve'])->name('reserve');
Route::get('/reservations/{booking}/cancel-preview', [ProfileController::class, 'cancelPreview'])->name('reservations.cancel-preview');
Route::post('/reservations/{booking}/cancel', [ProfileController::class, 'cancelBooking'])->name('reservations.cancel');
Route::get('/user/pending-debt', [ProfileController::class, 'pendingDebt'])->name('user.pending-debt');
}); });
// Admin Dashboard routes (protected - uncomment auth middleware for production) // Admin Dashboard routes (protected - uncomment auth middleware for production)
@ -37,6 +40,7 @@ Route::prefix('admin')->middleware('admin')->name('admin.')->group(function () {
// Active Bookings // Active Bookings
Route::get('/bookings/active', [\App\Http\Controllers\Admin\BookingController::class, 'activeIndex'])->name('bookings.active'); 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');
Route::post('/bookings/{booking}/complete', [\App\Http\Controllers\Admin\BookingController::class, 'completeBooking'])->name('bookings.complete'); Route::post('/bookings/{booking}/complete', [\App\Http\Controllers\Admin\BookingController::class, 'completeBooking'])->name('bookings.complete');
}); });
// }); // });
@ -49,6 +53,7 @@ Route::prefix('operator')->middleware('operator')->name('operator.')->group(func
Route::get('/{booking}/checkout-preview', [\App\Http\Controllers\Operator\OperatorController::class, 'checkoutPreview'])->name('checkoutPreview'); Route::get('/{booking}/checkout-preview', [\App\Http\Controllers\Operator\OperatorController::class, 'checkoutPreview'])->name('checkoutPreview');
Route::post('/{booking}/payment', [\App\Http\Controllers\Operator\OperatorController::class, 'processPayment'])->name('payment'); Route::post('/{booking}/payment', [\App\Http\Controllers\Operator\OperatorController::class, 'processPayment'])->name('payment');
Route::post('/{booking}/checkout', [\App\Http\Controllers\Operator\OperatorController::class, 'checkOut'])->name('checkOut'); Route::post('/{booking}/checkout', [\App\Http\Controllers\Operator\OperatorController::class, 'checkOut'])->name('checkOut');
Route::post('/{booking}/cancel', [\App\Http\Controllers\Operator\OperatorController::class, 'cancelReservation'])->name('cancel');
}); });
// }); // });