backup: before removing all alert/confirm dialogs

This commit is contained in:
Ghassan Yusuf 2026-04-16 16:02:45 +03:00
parent 900eb24b37
commit 51bdf50292
2 changed files with 214 additions and 17 deletions

View File

@ -87,35 +87,89 @@
</div> </div>
{{-- ── Charts ──────────────────────────────────────────────────────────────── --}} {{-- ── Charts ──────────────────────────────────────────────────────────────── --}}
<div class="row g-3 mb-4"> <div class="row g-3 mb-4" style="align-items:stretch;">
<div class="col-lg-7"> <div class="col-lg-7 d-flex flex-column">
<div class="card h-100"> <div class="card h-100 d-flex flex-column">
<div class="card-header d-flex align-items-center justify-content-between"> <div class="card-header d-flex align-items-center justify-content-between flex-shrink-0">
<span class="fw-700" style="font-size:.9rem;"> <span class="fw-700" style="font-size:.9rem;">
<i class="bi bi-bar-chart-line me-2" style="color:#6366f1;"></i> <i class="bi bi-bar-chart-line me-2" style="color:#6366f1;"></i>
الحجوزات اليومية آخر 7 أيام الحجوزات اليومية آخر 7 أيام
</span> </span>
<span class="text-xs" style="color:#94a3b8;">اضغط على أي عمود للتفاصيل</span>
</div> </div>
<div class="card-body p-3"> <div class="card-body p-3 flex-grow-1 d-flex flex-column" style="min-height:0;">
<canvas id="dailyChart" height="140"></canvas> <div style="position:relative;flex:1;min-height:0;">
<canvas id="dailyChart"></canvas>
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-5"> </div>
<div class="card h-100"> <div class="col-lg-5 d-flex flex-column">
<div class="card-header d-flex align-items-center justify-content-between"> <div class="card h-100 d-flex flex-column">
<div class="card-header d-flex align-items-center justify-content-between flex-shrink-0">
<span class="fw-700" style="font-size:.9rem;"> <span class="fw-700" style="font-size:.9rem;">
<i class="bi bi-trophy me-2" style="color:#f59e0b;"></i> <i class="bi bi-trophy me-2" style="color:#f59e0b;"></i>
أفضل 5 مواقف أفضل 5 مواقف
</span> </span>
<span class="text-xs" style="color:#94a3b8;">اضغط على أي شريحة للتفاصيل</span>
</div> </div>
<div class="card-body p-3 d-flex align-items-center justify-content-center"> <div class="card-body p-3 d-flex align-items-center justify-content-center flex-grow-1">
<canvas id="topChart" height="140"></canvas> <canvas id="topChart" height="140"></canvas>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{{-- ── Chart Segment Modal ──────────────────────────────────────────────────── --}}
<div class="modal fade" id="segmentModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" style="max-width:640px;">
<div class="modal-content" style="border:none;border-radius:1rem;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.15);">
{{-- Header --}}
<div id="segmentModalTop" style="padding:1.25rem 1.5rem;background:#f8fafc;border-bottom:1px solid #f1f5f9;">
<div class="d-flex align-items-center gap-3">
<div id="segmentModalIcon" style="width:44px;height:44px;border-radius:.75rem;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i id="segmentModalIconEl" style="font-size:1.3rem;"></i>
</div>
<div class="flex-grow-1 min-width-0">
<div id="segmentModalTitle" class="fw-800" style="font-size:1rem;color:#0f172a;line-height:1.3;"></div>
<div id="segmentModalSub" class="text-xs mt-1" style="color:#64748b;"></div>
</div>
<button type="button" class="btn-close flex-shrink-0" data-bs-dismiss="modal"></button>
</div>
</div>
{{-- Summary stats --}}
<div id="segmentModalStats" class="px-4 pt-3 pb-2 d-flex gap-3 flex-wrap"></div>
{{-- Scrollable booking table --}}
<div class="px-4 pb-3">
<div id="segmentModalTableWrap" style="max-height:320px;overflow-y:auto;border-radius:.5rem;border:1px solid #f1f5f9;">
<table class="app-table w-100 mb-0" id="segmentModalTable">
<thead style="position:sticky;top:0;z-index:1;">
<tr>
<th>#</th>
<th>العميل</th>
<th>الهاتف</th>
<th>الحالة</th>
<th>البدء</th>
<th>الانتهاء</th>
</tr>
</thead>
<tbody id="segmentModalTbody">
<tr><td colspan="6" class="text-center py-4" style="color:#94a3b8;">
<div class="spinner-border spinner-border-sm me-2"></div>جاري التحميل...
</td></tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer" style="border-top:1px solid #f1f5f9;padding:.75rem 1.25rem;">
<button type="button" class="btn btn-sm fw-600 w-100"
style="background:#f1f5f9;color:#475569;border:none;border-radius:.5rem;font-family:'Cairo',sans-serif;"
data-bs-dismiss="modal">إغلاق</button>
</div>
</div>
</div>
</div>
{{-- ── Recent Bookings ─────────────────────────────────────────────────────── --}} {{-- ── Recent Bookings ─────────────────────────────────────────────────────── --}}
<div class="card"> <div class="card">
<div class="card-header d-flex align-items-center justify-content-between"> <div class="card-header d-flex align-items-center justify-content-between">
@ -153,7 +207,65 @@
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<script> <script>
let dailyChart, topChart; let dailyChart, topChart;
let chartData = {};
let segmentModal = null;
function getSegmentModal() {
if (!segmentModal) segmentModal = new bootstrap.Modal(document.getElementById('segmentModal'));
return segmentModal;
}
// ── Segment modal ─────────────────────────────────────────────────────────────
function openSegmentModal({ title, sub, icon, iconBg, iconColor, statChips }) {
document.getElementById('segmentModalTitle').textContent = title;
document.getElementById('segmentModalSub').textContent = sub;
document.getElementById('segmentModalIconEl').className = `bi ${icon}`;
document.getElementById('segmentModalIconEl').style.color = iconColor;
document.getElementById('segmentModalIcon').style.background = iconBg;
// Stat chips row
const chips = (statChips || []).map(c =>
`<div class="px-3 py-2 rounded-3 text-center" style="background:${c.bg};flex:1;min-width:100px;">
<div class="fw-800" style="font-size:1.1rem;color:${c.color};">${c.value}</div>
<div class="text-xs" style="color:#64748b;">${c.label}</div>
</div>`
).join('');
document.getElementById('segmentModalStats').innerHTML = chips;
// Reset table to loading
document.getElementById('segmentModalTbody').innerHTML =
`<tr><td colspan="6" class="text-center py-4" style="color:#94a3b8;">
<div class="spinner-border spinner-border-sm me-2"></div>جاري التحميل...
</td></tr>`;
getSegmentModal().show();
}
function renderBookingTable(bookings) {
const tbody = document.getElementById('segmentModalTbody');
if (!bookings.length) {
tbody.innerHTML = `<tr><td colspan="6" class="text-center py-4" style="color:#94a3b8;">لا توجد حجوزات</td></tr>`;
return;
}
const statusBadge = s => s === 'active'
? '<span class="badge badge-soft-warning">نشط</span>'
: s === 'completed'
? '<span class="badge badge-soft-success">مكتمل</span>'
: '<span class="badge badge-soft-danger">ملغي</span>';
const fmt = iso => new Date(iso).toLocaleString('ar-SA', {month:'short', day:'numeric', hour:'2-digit', minute:'2-digit'});
tbody.innerHTML = bookings.map((b, i) => `
<tr>
<td class="text-xs" style="color:#94a3b8;">${i + 1}</td>
<td><span class="fw-600" style="font-size:.85rem;">${b.customer_name ?? '--'}</span></td>
<td style="direction:ltr;text-align:right;font-size:.82rem;color:#475569;">${b.phone ?? '--'}</td>
<td>${statusBadge(b.status)}</td>
<td class="text-xs" style="color:#64748b;">${fmt(b.start_time)}</td>
<td class="text-xs" style="color:#64748b;">${fmt(b.end_time)}</td>
</tr>`).join('');
}
// ── Stats ─────────────────────────────────────────────────────────────────────
async function loadStats() { async function loadStats() {
try { try {
const { success, data } = await fetch('/admin/stats').then(r => r.json()); const { success, data } = await fetch('/admin/stats').then(r => r.json());
@ -167,22 +279,30 @@ async function loadStats() {
} catch {} } catch {}
} }
// ── Charts ────────────────────────────────────────────────────────────────────
async function loadCharts() { async function loadCharts() {
try { try {
const { success, data } = await fetch('/admin/charts').then(r => r.json()); const { success, data } = await fetch('/admin/charts').then(r => r.json());
if (!success) return; if (!success) return;
chartData = data;
const labels = Array.from({length: 7}, (_, i) => { const dayLabels = Array.from({length: 7}, (_, i) => {
const d = new Date();
d.setDate(d.getDate() - (6 - i));
return d.toLocaleDateString('ar-SA', {weekday: 'short', month: 'short', day: 'numeric'});
});
const dayLabelsShort = Array.from({length: 7}, (_, i) => {
const d = new Date(); const d = new Date();
d.setDate(d.getDate() - (6 - i)); d.setDate(d.getDate() - (6 - i));
return d.toLocaleDateString('ar-SA', {weekday: 'short'}); return d.toLocaleDateString('ar-SA', {weekday: 'short'});
}); });
// ── Bar chart ──────────────────────────────────────────────────────────
if (dailyChart) dailyChart.destroy(); if (dailyChart) dailyChart.destroy();
dailyChart = new Chart(document.getElementById('dailyChart'), { dailyChart = new Chart(document.getElementById('dailyChart'), {
type: 'bar', type: 'bar',
data: { data: {
labels, labels: dayLabelsShort,
datasets: [{ datasets: [{
label: 'حجوزات', label: 'حجوزات',
data: data.daily_bookings ?? [], data: data.daily_bookings ?? [],
@ -190,44 +310,113 @@ async function loadCharts() {
borderColor: '#6366f1', borderColor: '#6366f1',
borderWidth: 2, borderWidth: 2,
borderRadius: 6, borderRadius: 6,
hoverBackgroundColor: 'rgba(99,102,241,.45)',
}] }]
}, },
options: { options: {
responsive: true, responsive: true,
maintainAspectRatio: false,
plugins: { plugins: {
legend: { display: false } legend: { display: false },
tooltip: { callbacks: {
title: ctx => dayLabels[ctx[0].dataIndex],
label: ctx => ` ${ctx.parsed.y} حجز`,
}}
}, },
scales: { scales: {
y: { beginAtZero: true, ticks: { precision: 0 }, grid: { color: '#f1f5f9' } }, y: { beginAtZero: true, ticks: { precision: 0 }, grid: { color: '#f1f5f9' } },
x: { grid: { display: false } } x: { grid: { display: false } }
},
onClick(e, elements) {
if (!elements.length) return;
const i = elements[0].index;
const count = (data.daily_bookings ?? [])[i] ?? 0;
const label = dayLabels[i];
const date = (data.daily_dates ?? [])[i];
const weekTotal = (data.daily_bookings ?? []).reduce((a, b) => a + b, 0);
const pct = weekTotal ? Math.round(count / weekTotal * 100) : 0;
openSegmentModal({
title: label,
sub: 'قائمة الحجوزات المسجّلة في هذا اليوم',
icon: 'bi-calendar3',
iconBg: 'rgba(99,102,241,.12)',
iconColor: '#6366f1',
statChips: [
{ label: 'الحجوزات', value: count, color: '#6366f1', bg: 'rgba(99,102,241,.08)' },
{ label: 'من الأسبوع', value: pct + '%', color: '#0ea5e9', bg: 'rgba(14,165,233,.08)' },
],
});
if (date) {
fetch(`/api/v1/bookings?date=${date}&per_page=100`)
.then(r => r.json())
.then(({ data: d }) => renderBookingTable(d?.data ?? []))
.catch(() => renderBookingTable([]));
}
} }
} }
}); });
// ── Doughnut chart ─────────────────────────────────────────────────────
if (topChart) topChart.destroy(); if (topChart) topChart.destroy();
const lots = data.top_parking_lots ?? []; const lots = data.top_parking_lots ?? [];
const colors = ['#6366f1','#10b981','#f59e0b','#ef4444','#0ea5e9'];
const total = lots.reduce((s, l) => s + l.value, 0);
topChart = new Chart(document.getElementById('topChart'), { topChart = new Chart(document.getElementById('topChart'), {
type: 'doughnut', type: 'doughnut',
data: { data: {
labels: lots.map(l => l.name), labels: lots.map(l => l.name),
datasets: [{ datasets: [{
data: lots.map(l => l.value), data: lots.map(l => l.value),
backgroundColor: ['#6366f1','#10b981','#f59e0b','#ef4444','#0ea5e9'], backgroundColor: colors,
borderWidth: 3, borderWidth: 3,
borderColor: '#fff', borderColor: '#fff',
hoverBorderColor: '#fff',
hoverOffset: 8,
}] }]
}, },
options: { options: {
responsive: true, responsive: true,
plugins: { plugins: {
legend: { position: 'bottom', labels: { usePointStyle: true, padding: 12, font: { family: 'Cairo', size: 12 } } } legend: { position: 'bottom', labels: { usePointStyle: true, padding: 12, font: { family: 'Cairo', size: 12 } } },
tooltip: { callbacks: {
label: ctx => ` ${ctx.label}: ${ctx.parsed} حجز (${total ? Math.round(ctx.parsed/total*100) : 0}%)`,
}}
}, },
cutout: '65%', cutout: '65%',
onClick(e, elements) {
if (!elements.length) return;
const i = elements[0].index;
const lot = lots[i];
if (!lot) return;
const pct = total ? Math.round(lot.value / total * 100) : 0;
openSegmentModal({
title: lot.name,
sub: 'قائمة جميع حجوزات هذا الموقف',
icon: 'bi-buildings',
iconBg: colors[i] + '22',
iconColor: colors[i],
statChips: [
{ label: 'الحجوزات', value: lot.value, color: colors[i], bg: colors[i] + '14' },
{ label: 'من الكل', value: pct + '%', color: '#0ea5e9', bg: 'rgba(14,165,233,.08)' },
{ label: 'الترتيب', value: '#' + (i + 1), color: '#f59e0b', bg: 'rgba(245,158,11,.08)' },
],
});
fetch(`/api/v1/bookings?parking_lot_id=${lot.id}&per_page=100`)
.then(r => r.json())
.then(({ data: d }) => renderBookingTable(d?.data ?? []))
.catch(() => renderBookingTable([]));
}
} }
}); });
} catch {} } catch {}
} }
// ── Bookings ──────────────────────────────────────────────────────────────────
async function loadBookings() { async function loadBookings() {
const tbody = document.getElementById('bookings-body'); const tbody = document.getElementById('bookings-body');
try { try {

View File

@ -818,10 +818,18 @@ document.getElementById('receiptModal').addEventListener('hidden.bs.modal', () =
document.getElementById('paymentProofFile').value = ''; document.getElementById('paymentProofFile').value = '';
}); });
// ── Auto-refresh ─────────────────────────────────────────────────────── // ── Auto-refresh (pauses while any modal is open) ─────────────────────
let t = 30; let t = 30;
let refreshPaused = false;
const badge = document.getElementById('refresh-badge'); 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(() => { setInterval(() => {
if (refreshPaused) return;
t--; t--;
if (badge) badge.textContent = `تحديث بعد ${t}ث`; if (badge) badge.textContent = `تحديث بعد ${t}ث`;
if (t <= 0) location.reload(); if (t <= 0) location.reload();