backup: before cancel reservation feature on operator dashboard

This commit is contained in:
ghassan committed 2026-04-16 16:19:33 +03:00
1 parent 51bdf50292
commit c481135280
21 files changed
+793 -91

No files matched your search

+112 -23
View File
@@ -307,7 +307,7 @@
<td class="text-center">
<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:.35rem .9rem;"
onclick="completeBooking({{ $booking->id }}, this)">
onclick="askComplete({{ $booking->id }}, '{{ addslashes($booking->vehicle_plate ?? '—') }}', '{{ addslashes($booking->parkingLot->name) }}')">
<i class="bi bi-stop-circle me-1"></i>إنهاء
</button>
</td>
@@ -338,59 +338,148 @@
</div>
{{-- ── Confirm complete modal ───────────────────────────────────────────────── --}}
<div class="modal fade" id="confirmCompleteModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" style="max-width:380px;">
<div class="modal-content" style="border:none;border-radius:1rem;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.15);">
<div style="background:linear-gradient(135deg,#dc2626,#ef4444);padding:1.25rem 1.5rem;">
<div class="d-flex align-items-center gap-3">
<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;">
<i class="bi bi-stop-circle-fill" style="font-size:1.3rem;color:#fff;"></i>
</div>
<div class="flex-grow-1">
<div class="fw-800" style="color:#fff;font-size:1rem;">إنهاء الحجز</div>
<div class="text-xs mt-1" style="color:rgba(255,255,255,.75);">تأكيد تسجيل خروج السيارة</div>
</div>
<button type="button" class="btn-close btn-close-white flex-shrink-0" data-bs-dismiss="modal"></button>
</div>
</div>
<div class="p-4">
<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 class="d-flex justify-content-between align-items-center">
<span class="text-xs fw-600" style="color:#64748b;">الموقف</span>
<span class="fw-600 text-sm" id="confirmLot" style="color:#475569;"></span>
</div>
</div>
<p class="text-sm mb-0" style="color:#64748b;text-align:center;">
هل تريد إنهاء هذا الحجز وتسجيل خروج السيارة؟
</p>
</div>
<div class="px-4 pb-4 d-flex gap-2">
<button type="button" class="btn btn-sm fw-600"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;padding:.5rem 1rem;"
data-bs-dismiss="modal">إلغاء</button>
<button type="button" id="confirmCompleteBtn"
class="btn btn-sm fw-700 flex-grow-1"
style="background:#ef4444;color:#fff;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;padding:.5rem;">
<i class="bi bi-stop-circle me-1"></i>تأكيد الإنهاء
</button>
</div>
</div>
</div>
</div>
{{-- ── Toast ────────────────────────────────────────────────────────────────── --}}
<div id="toastWrap" style="position:fixed;bottom:1.5rem;inset-inline-start:50%;transform:translateX(-50%);z-index:9999;pointer-events:none;"></div>
@push('scripts')
<script>
// ── Filters ───────────────────────────────────────────────────────────────────
function applyFilters() {
const url = new URL(window.location);
const lot = document.getElementById('lotFilter').value;
const search = document.getElementById('searchInput').value.trim();
lot ? url.searchParams.set('parking_lot_id', lot) : url.searchParams.delete('parking_lot_id');
search ? url.searchParams.set('search', search) : url.searchParams.delete('search');
lot ? url.searchParams.set('parking_lot_id', lot) : url.searchParams.delete('parking_lot_id');
search ? url.searchParams.set('search', search) : url.searchParams.delete('search');
url.searchParams.delete('page');
window.location.href = url.toString();
}
document.getElementById('lotFilter').addEventListener('change', applyFilters);
document.getElementById('searchInput').addEventListener('keydown', e => {
if (e.key === 'Enter') applyFilters();
});
async function completeBooking(id, btn) {
if (!confirm('إنهاء هذا الحجز؟')) return;
const orig = btn.innerHTML;
// ── Toast ─────────────────────────────────────────────────────────────────────
function showToast(msg, type = 'success') {
const color = type === 'success' ? '#10b981' : '#ef4444';
const icon = type === 'success' ? 'bi-check-circle-fill' : 'bi-exclamation-triangle-fill';
const el = document.createElement('div');
el.style.cssText = `background:#0f172a;color:#f8fafc;padding:.65rem 1.25rem;border-radius:.625rem;font-size:.85rem;font-weight:600;box-shadow:0 4px 20px rgba(0,0,0,.25);display:flex;align-items:center;gap:.6rem;pointer-events:auto;margin-top:.5rem;`;
el.innerHTML = `<i class="bi ${icon}" style="color:${color};font-size:1rem;flex-shrink:0;"></i>${msg}`;
document.getElementById('toastWrap').appendChild(el);
setTimeout(() => { el.style.transition='opacity .4s'; el.style.opacity='0'; setTimeout(()=>el.remove(),400); }, 3500);
}
// ── Confirm complete modal ────────────────────────────────────────────────────
let pendingId = null;
let confirmModal = null;
function getConfirmModal() {
if (!confirmModal) confirmModal = new bootstrap.Modal(document.getElementById('confirmCompleteModal'));
return confirmModal;
}
function askComplete(id, plate, lot) {
pendingId = id;
document.getElementById('confirmPlate').textContent = plate;
document.getElementById('confirmLot').textContent = lot;
getConfirmModal().show();
}
document.getElementById('confirmCompleteBtn').addEventListener('click', async () => {
if (!pendingId) return;
const btn = document.getElementById('confirmCompleteBtn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>جاري الإنهاء...';
try {
const res = await fetch(`/admin/bookings/${id}/complete`, {
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'
'Content-Type': 'application/json',
}
});
const data = await res.json();
getConfirmModal().hide();
if (data.success) {
const row = document.getElementById('row-' + id);
row.style.transition = 'opacity .4s, transform .4s';
row.style.opacity = '0';
row.style.transform = 'translateX(20px)';
setTimeout(() => row.remove(), 420);
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('تم إنهاء الحجز بنجاح.');
} else {
alert(data.message || 'خطأ');
btn.innerHTML = orig;
btn.disabled = false;
showToast(data.message || 'حدث خطأ أثناء الإنهاء.', 'error');
}
} catch {
alert('خطأ في الاتصال');
btn.innerHTML = orig;
btn.disabled = false;
getConfirmModal().hide();
showToast('تعذّر الاتصال بالخادم. حاول مجدداً.', 'error');
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-stop-circle me-1"></i>تأكيد الإنهاء';
pendingId = null;
}
}
});
// Countdown refresh
// ── Auto-refresh (pauses while modal is open) ─────────────────────────────────
let t = 30;
let refreshPaused = false;
const badge = document.getElementById('refresh-badge');
document.querySelectorAll('.modal').forEach(m => {
m.addEventListener('show.bs.modal', () => { refreshPaused = true; });
m.addEventListener('hidden.bs.modal', () => { refreshPaused = false; t = 30; });
});
setInterval(() => {
if (refreshPaused) return;
t--;
badge.textContent = `تحديث بعد ${t}ث`;
if (t <= 0) location.reload();
+1 -1
View File
@@ -74,7 +74,7 @@
</a>
</div>
<div class="col-lg-3 col-sm-6">
<div class="quick-card card h-100 p-4" onclick="alert('قريباً...')">
<div class="quick-card card h-100 p-4" style="cursor:default;">
<div class="quick-icon" style="background:rgba(100,116,139,.1);">
<i class="bi bi-gear" style="color:#64748b;"></i>
</div>
@@ -485,10 +485,81 @@ $gradients = [
</div>
</div>
{{-- Toggle Status Confirmation Modal --}}
<div class="modal fade" id="toggleStatusModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" style="max-width:380px;">
<div class="modal-content border-0 shadow-lg" style="border-radius:16px;overflow:hidden;">
<div class="modal-header border-0 text-white" style="background:linear-gradient(135deg,#f59e0b,#d97706);">
<h6 class="modal-title fw-bold mb-0" style="font-family:'Cairo',sans-serif;">
<i class="bi bi-toggle-on me-2"></i>تغيير حالة الموقف
</h6>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center p-4">
<p class="mb-0 fw-600" style="color:#0f172a;font-family:'Cairo',sans-serif;">
هل تريد تغيير حالة هذا الموقف؟
</p>
</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="toggleStatusConfirmBtn"
class="btn btn-warning flex-fill fw-bold text-white"
style="font-family:'Cairo',sans-serif;border-radius:10px;">
<i class="bi bi-check2 me-1"></i>تأكيد
</button>
</div>
</div>
</div>
</div>
{{-- Delete Lot Confirmation Modal --}}
<div class="modal fade" id="deleteLotModal" 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 border-0 text-white" style="background:linear-gradient(135deg,#dc2626,#991b1b);">
<h6 class="modal-title fw-bold mb-0" style="font-family:'Cairo',sans-serif;">
<i class="bi bi-trash3 me-2"></i>حذف الموقف
</h6>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center p-4">
<div class="mb-3" style="font-size:2.5rem;">⚠️</div>
<p class="fw-700 mb-1" style="color:#0f172a;font-family:'Cairo',sans-serif;">
حذف "<span id="deleteLotName"></span>"
</p>
<p class="text-muted small mb-0">سيتم حذف جميع بيانات الموقف نهائياً ولا يمكن التراجع.</p>
</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="deleteLotConfirmBtn"
class="btn btn-danger flex-fill fw-bold"
style="font-family:'Cairo',sans-serif;border-radius:10px;">
<i class="bi bi-trash3 me-1"></i>حذف نهائياً
</button>
</div>
</div>
</div>
</div>
@push('scripts')
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<script>
window.onerror = (msg, src, line) => { alert('JS خطأ في السطر ' + line + ':\n' + msg); };
// ── Toast helper ─────────────────────────────────────────────────────────
function showToast(msg, type = 'success') {
const colors = { success:'#10b981', danger:'#ef4444', warning:'#f59e0b', info:'#3b82f6' };
const t = document.createElement('div');
t.style.cssText = `position:fixed;bottom:1.25rem;inset-inline-end:1.25rem;z-index:9999;
background:${colors[type]||colors.success};color:#fff;padding:.75rem 1.25rem;border-radius:10px;
font-family:'Cairo',sans-serif;font-size:.9rem;font-weight:600;
box-shadow:0 8px 24px rgba(0,0,0,.18);opacity:0;transition:opacity .25s;max-width:360px;`;
t.textContent = msg;
document.body.appendChild(t);
requestAnimationFrame(() => { t.style.opacity = '1'; });
setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 300); }, 3500);
}
// ── Parking lots data from server ─────────────────────────────────────────
const lotsData = @json($parkingLots->items());
@@ -591,7 +662,7 @@ function previewImage(input) {
function editLot(id) {
const lot = lotsData.find(l => l.id === id);
if (!lot) { alert('لم يتم العثور على بيانات الموقف'); return; }
if (!lot) { showToast('لم يتم العثور على بيانات الموقف', 'danger'); return; }
editingId = id;
document.getElementById('modalLabel').textContent = 'تعديل: ' + lot.name;
@@ -632,12 +703,12 @@ async function saveLot() {
if (result.success) {
location.reload();
} else if (result.errors) {
const msgs = Object.values(result.errors).flat().join('\n');
alert(msgs);
const msgs = Object.values(result.errors).flat().join(' — ');
showToast(msgs, 'danger');
} else {
alert(result.message || 'خطأ في العملية');
showToast(result.message || 'خطأ في العملية', 'danger');
}
} catch(err) { alert('خطأ في الاتصال: ' + err.message); }
} catch(err) { showToast('خطأ في الاتصال', 'danger'); }
finally { setBtnLoading(false); }
}
@@ -646,14 +717,27 @@ function setBtnLoading(on) {
document.getElementById('submitSpinner').classList.toggle('d-none', !on);
}
let pendingToggleId = null;
const toggleModal = new bootstrap.Modal(document.getElementById('toggleStatusModal'));
function toggleStatus(id) {
if (!confirm('تغيير حالة الموقف؟')) return;
fetch(`/admin/parking-lots/${id}/toggle`, {
method: 'POST',
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content }
}).then(r => r.json()).then(d => d.success ? location.reload() : alert(d.message));
pendingToggleId = id;
toggleModal.show();
}
document.getElementById('toggleStatusConfirmBtn').addEventListener('click', async () => {
if (!pendingToggleId) return;
toggleModal.hide();
try {
const res = await fetch(`/admin/parking-lots/${pendingToggleId}/toggle`, {
method: 'POST',
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content }
});
const d = await res.json();
d.success ? location.reload() : showToast(d.message || 'حدث خطأ', 'danger');
} catch { showToast('خطأ في الاتصال', 'danger'); }
});
document.getElementById('searchInput').addEventListener('keypress', e => {
if (e.key === 'Enter') doSearch();
});
@@ -685,7 +769,7 @@ function openPricingModal(id, name) {
updatePricingPreview();
bootstrap.Modal.getOrCreateInstance(document.getElementById('pricingModal')).show();
} catch(e) {
alert('خطأ في فتح نافذة التسعير:\n' + e.message);
showToast('خطأ في فتح نافذة التسعير', 'danger');
}
}
@@ -726,7 +810,7 @@ async function savePricing() {
const base = parseFloat(document.getElementById('p_base').value);
if (isNaN(base) || base < 0) {
alert('يرجى إدخال سعر أساسي صحيح');
showToast('يرجى إدخال سعر أساسي صحيح', 'warning');
return;
}
@@ -755,21 +839,33 @@ async function savePricing() {
bootstrap.Modal.getInstance(document.getElementById('pricingModal'))?.hide();
location.reload();
} else {
alert(data.message || 'خطأ في الحفظ');
showToast(data.message || 'خطأ في الحفظ', 'danger');
}
} catch {
alert('خطأ في الاتصال');
showToast('خطأ في الاتصال', 'danger');
} finally {
document.getElementById('savePricingBtn').disabled = false;
document.getElementById('pricingSpinner').classList.add('d-none');
}
}
async function deleteLot(id, name) {
if (!confirm(`حذف الموقف "${name}"؟\nسيتم حذف جميع بيانات الموقف نهائياً.`)) return;
let pendingDeleteId = null;
const deleteModal = new bootstrap.Modal(document.getElementById('deleteLotModal'));
function deleteLot(id, name) {
pendingDeleteId = id;
document.getElementById('deleteLotName').textContent = name;
deleteModal.show();
}
document.getElementById('deleteLotConfirmBtn').addEventListener('click', async () => {
if (!pendingDeleteId) return;
const btn = document.getElementById('deleteLotConfirmBtn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>جاري الحذف...';
deleteModal.hide();
try {
const res = await fetch(`/admin/parking-lots/${id}`, {
const res = await fetch(`/admin/parking-lots/${pendingDeleteId}`, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
@@ -780,12 +876,16 @@ async function deleteLot(id, name) {
if (data.success) {
location.reload();
} else {
alert(data.message || 'تعذّر الحذف');
showToast(data.message || 'تعذّر الحذف', 'danger');
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-trash3 me-1"></i>حذف نهائياً';
}
} catch {
alert('خطأ في الاتصال');
showToast('خطأ في الاتصال', 'danger');
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-trash3 me-1"></i>حذف نهائياً';
}
}
});
</script>
@endpush