Commit 62b40017 authored by Mahmoud Aglan's avatar Mahmoud Aglan

upload

parent 81622d8b
...@@ -3,116 +3,1151 @@ ...@@ -3,116 +3,1151 @@
<?php $__template->section('page_actions'); ?> <?php $__template->section('page_actions'); ?>
<a href="/sa/mirror" class="btn btn-outline"><i data-lucide="arrow-right" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> كل المرافق</a> <a href="/sa/mirror" class="btn btn-outline"><i data-lucide="arrow-right" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> كل المرافق</a>
<button type="button" class="btn btn-outline" id="btn-fullscreen" title="وضع ملء الشاشة"><i data-lucide="maximize-2" style="width:16px;height:16px;vertical-align:middle;"></i></button>
<button type="button" class="btn btn-outline" onclick="window.print();" title="طباعة"><i data-lucide="printer" style="width:16px;height:16px;vertical-align:middle;"></i></button>
<?php $__template->endSection(); ?> <?php $__template->endSection(); ?>
<?php $__template->section('content'); ?> <?php $__template->section('content'); ?>
<meta http-equiv="refresh" content="30">
<div id="mirror-app" data-facility-id="<?= (int) $facility['id'] ?>" data-date="<?= e($date) ?>" data-csrf="<?= e(csrf_token()) ?>">
<!-- Date Picker -->
<div class="card" style="margin-bottom:20px;padding:15px;display:flex;align-items:center;gap:15px;"> <!-- Stats Bar -->
<label style="font-weight:600;font-size:14px;">التاريخ:</label> <div class="card" id="stats-bar" style="margin-bottom:12px;padding:12px 20px;display:flex;align-items:center;gap:25px;flex-wrap:wrap;">
<input type="date" id="mirror-date" value="<?= e($date) ?>" class="form-input" style="width:180px;" <div style="display:flex;align-items:center;gap:6px;">
onchange="window.location.href='/sa/mirror/<?= (int) $facility['id'] ?>/' + this.value;"> <i data-lucide="bar-chart-3" style="width:18px;height:18px;color:#6366F1;"></i>
<?php if ($date === date('Y-m-d')): ?> <span style="font-size:13px;color:#6B7280;">نسبة الإشغال:</span>
<span style="background:#ECFDF5;color:#059669;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;">اليوم</span> <strong id="stat-utilization" style="font-size:15px;color:#111;"></strong>
<?php endif; ?> </div>
<span style="font-size:11px;color:#9CA3AF;margin-right:auto;">يتم التحديث تلقائيا كل 30 ثانية</span> <div style="display:flex;align-items:center;gap:6px;">
<i data-lucide="banknote" style="width:18px;height:18px;color:#059669;"></i>
<span style="font-size:13px;color:#6B7280;">إيرادات اليوم:</span>
<strong id="stat-revenue" style="font-size:15px;color:#111;"></strong>
</div>
<div style="display:flex;align-items:center;gap:6px;">
<i data-lucide="calendar-check" style="width:18px;height:18px;color:#D97706;"></i>
<span style="font-size:13px;color:#6B7280;">حجوزات:</span>
<strong id="stat-bookings" style="font-size:15px;color:#111;"></strong>
</div>
<div style="margin-right:auto;display:flex;align-items:center;gap:8px;">
<span id="live-indicator" style="width:8px;height:8px;background:#10B981;border-radius:50%;display:inline-block;animation:pulse 2s infinite;"></span>
<span style="font-size:11px;color:#9CA3AF;">تحديث مباشر</span>
</div>
</div> </div>
<!-- Legend --> <!-- Navigation + Controls -->
<div class="card" style="margin-bottom:20px;padding:12px;display:flex;gap:15px;flex-wrap:wrap;font-size:12px;"> <div class="card" style="margin-bottom:12px;padding:12px 15px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<span><span style="display:inline-block;width:14px;height:14px;background:#D1FAE5;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> متاح</span> <button type="button" class="btn btn-outline btn-sm" id="btn-prev-day"><i data-lucide="chevron-right" style="width:14px;height:14px;"></i></button>
<span><span style="display:inline-block;width:14px;height:14px;background:#FEF3C7;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> جزئي</span> <input type="date" id="mirror-date" value="<?= e($date) ?>" class="form-input" style="width:160px;font-size:13px;">
<span><span style="display:inline-block;width:14px;height:14px;background:#FEE2E2;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> ممتلئ</span> <button type="button" class="btn btn-outline btn-sm" id="btn-next-day"><i data-lucide="chevron-left" style="width:14px;height:14px;"></i></button>
<span><span style="display:inline-block;width:14px;height:14px;background:#DBEAFE;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> تدريب</span> <button type="button" class="btn btn-outline btn-sm" id="btn-today">اليوم</button>
<span><span style="display:inline-block;width:14px;height:14px;background:#D1FAE5;border:2px solid #059669;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> حجز عضو</span>
<span><span style="display:inline-block;width:14px;height:14px;background:#FEF9C3;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> حجز زائر</span> <div style="border-right:1px solid #E5E7EB;height:24px;margin:0 8px;"></div>
<span><span style="display:inline-block;width:14px;height:14px;background:#FFEDD5;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> حجز مؤسسة</span>
<span><span style="display:inline-block;width:14px;height:14px;background:#CFFAFE;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> دخول حر</span> <div style="display:flex;gap:4px;">
<span><span style="display:inline-block;width:14px;height:14px;background:#E5E7EB;border-radius:3px;vertical-align:middle;margin-left:4px;"></span> مغلق</span> <button type="button" class="btn btn-sm view-toggle active" data-view="day">يوم</button>
<button type="button" class="btn btn-sm btn-outline view-toggle" data-view="week">أسبوع</button>
</div>
<div style="border-right:1px solid #E5E7EB;height:24px;margin:0 8px;"></div>
<label style="font-size:12px;display:flex;align-items:center;gap:4px;cursor:pointer;">
<input type="checkbox" id="toggle-heatmap"> خريطة حرارية
</label>
<div style="margin-right:auto;"></div>
<button type="button" class="btn btn-sm btn-outline" id="btn-bulk-block" title="حجب متعدد"><i data-lucide="shield-off" style="width:14px;height:14px;"></i> حجب</button>
<button type="button" class="btn btn-sm btn-outline" id="btn-copy-day" title="نسخ اليوم"><i data-lucide="copy" style="width:14px;height:14px;"></i> نسخ</button>
<button type="button" class="btn btn-sm btn-outline" id="btn-templates" title="القوالب"><i data-lucide="layout-template" style="width:14px;height:14px;"></i> قوالب</button>
</div> </div>
<!-- Mirror Grid --> <!-- Main Layout: Sidebar + Grid -->
<div class="card" style="overflow-x:auto;"> <div style="display:flex;gap:12px;align-items:flex-start;">
<table style="width:100%;border-collapse:collapse;font-size:12px;min-width:800px;">
<thead> <!-- Groups Sidebar -->
<tr> <div id="groups-sidebar" class="card" style="width:260px;min-width:260px;max-height:calc(100vh - 200px);overflow-y:auto;padding:0;">
<th style="padding:10px;border:1px solid #E5E7EB;background:#F9FAFB;position:sticky;right:0;z-index:2;min-width:100px;">الوحدة</th> <div style="padding:12px 15px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between;">
<?php foreach ($slots as $slot): ?> <strong style="font-size:13px;"><i data-lucide="users" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> المجموعات</strong>
<th style="padding:8px 4px;border:1px solid #E5E7EB;background:#F9FAFB;text-align:center;white-space:nowrap;min-width:70px;"> <button type="button" class="btn btn-sm btn-outline" id="btn-toggle-sidebar" title="إخفاء"><i data-lucide="panel-right-close" style="width:12px;height:12px;"></i></button>
<?= e($slot['start']) ?> </div>
</th> <div id="groups-list" style="padding:8px;">
<?php endforeach; ?> <div style="text-align:center;padding:20px;color:#9CA3AF;font-size:12px;">جاري التحميل...</div>
</tr> </div>
</thead> </div>
<tbody>
<?php foreach ($grid as $unitId => $unitData): ?> <!-- Grid Container -->
<tr> <div class="card" style="flex:1;overflow:hidden;position:relative;">
<td style="padding:10px;border:1px solid #E5E7EB;font-weight:700;background:#FAFAFA;position:sticky;right:0;z-index:1;"> <!-- Legend -->
<?= e($unitData['unit']['name_ar'] ?? '') ?> <div style="padding:8px 12px;border-bottom:1px solid #E5E7EB;display:flex;gap:12px;flex-wrap:wrap;font-size:11px;" id="legend-bar">
</td> <span><span class="legend-dot" style="background:#D1FAE5;"></span> متاح</span>
<?php foreach ($unitData['slots'] as $cell): ?> <span><span class="legend-dot" style="background:#FEF3C7;"></span> جزئي</span>
<?php <span><span class="legend-dot" style="background:#FEE2E2;"></span> ممتلئ</span>
$bgColor = '#FFFFFF'; <span><span class="legend-dot" style="background:#DBEAFE;"></span> تدريب</span>
$cellText = ''; <span><span class="legend-dot" style="background:#D1FAE5;border:2px solid #059669;"></span> عضو</span>
$passIndicator = ''; <span><span class="legend-dot" style="background:#FEF9C3;"></span> زائر</span>
<span><span class="legend-dot" style="background:#FFEDD5;"></span> مؤسسة</span>
if ($cell['status'] === 'free') { <span><span class="legend-dot" style="background:#E5E7EB;"></span> مغلق</span>
$bgColor = '#D1FAE5'; </div>
} elseif ($cell['status'] === 'partial') {
$bgColor = '#FEF3C7'; <!-- Grid Table -->
$cellText = $cell['occupied'] . '/' . $cell['max']; <div id="grid-container" style="overflow-x:auto;position:relative;">
} elseif ($cell['status'] === 'full') { <table id="mirror-grid" style="width:100%;border-collapse:collapse;font-size:11px;min-width:800px;">
$bgColor = '#FEE2E2'; <thead id="grid-header"></thead>
$cellText = $cell['occupied'] . '/' . $cell['max']; <tbody id="grid-body"></tbody>
} elseif ($cell['status'] === 'training') { </table>
$bgColor = '#DBEAFE'; <!-- Now timeline marker -->
if (!empty($cell['bookings'])) { <div id="now-marker" style="position:absolute;top:0;width:2px;background:#EF4444;z-index:10;pointer-events:none;display:none;">
$firstBooking = reset($cell['bookings']); <div style="position:absolute;top:-4px;right:-4px;width:10px;height:10px;background:#EF4444;border-radius:50%;"></div>
$cellText = $firstBooking['group_name'] ?? ''; </div>
} </div>
} elseif ($cell['status'] === 'booked') { </div>
if (!empty($cell['bookings'])) { </div>
$firstBooking = reset($cell['bookings']);
$bookerType = $firstBooking['booker_type'] ?? 'guest'; </div>
if ($bookerType === 'organization') { <!-- Quick Book Modal -->
$bgColor = '#FFEDD5'; <div id="modal-quickbook" class="mirror-modal" style="display:none;">
$cellText = $firstBooking['organization_name'] ?? $firstBooking['booker_name'] ?? ''; <div class="mirror-modal-content" style="max-width:480px;">
} elseif ($bookerType === 'member') { <div class="mirror-modal-header">
$bgColor = '#D1FAE5'; <h3>حجز سريع</h3>
$cellText = $firstBooking['booker_name'] ?? ''; <button type="button" class="mirror-modal-close">&times;</button>
} else { </div>
$bgColor = '#FEF9C3'; <div class="mirror-modal-body">
$cellText = $firstBooking['booker_name'] ?? ''; <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:15px;">
} <div>
<label class="form-label">الوحدة</label>
$passesTotal = (int) ($firstBooking['passes_total'] ?? 0); <input type="text" id="qb-unit-name" class="form-input" readonly>
$passesUsed = (int) ($firstBooking['passes_used'] ?? 0); </div>
if ($passesTotal > 0) { <div>
$passIndicator = $passesUsed . '/' . $passesTotal; <label class="form-label">التاريخ</label>
} <input type="text" id="qb-date" class="form-input" readonly>
} </div>
} elseif ($cell['status'] === 'blocked') { <div>
$bgColor = '#E5E7EB'; <label class="form-label">البداية</label>
} <input type="time" id="qb-start" class="form-input">
?> </div>
<td style="padding:6px 4px;border:1px solid #E5E7EB;background:<?= $bgColor ?>;text-align:center;vertical-align:middle;font-size:11px;font-weight:500;position:relative;"> <div>
<?= e($cellText) ?> <label class="form-label">النهاية</label>
<?php if ($passIndicator !== ''): ?> <input type="time" id="qb-end" class="form-input">
<span style="position:absolute;top:2px;left:2px;font-size:9px;color:#6B7280;background:rgba(255,255,255,0.8);padding:0 2px;border-radius:2px;"><?= e($passIndicator) ?></span> </div>
<?php endif; ?> </div>
</td> <div style="margin-bottom:12px;">
<?php endforeach; ?> <label class="form-label">نوع الحجز</label>
</tr> <select id="qb-type" class="form-input">
<?php endforeach; ?> <option value="training">تدريب مجموعة</option>
</tbody> <option value="hourly">حجز ساعة</option>
</table> <option value="blocked">حجب / مغلق</option>
<option value="maintenance">صيانة</option>
</select>
</div>
<div id="qb-group-row" style="margin-bottom:12px;">
<label class="form-label">المجموعة</label>
<select id="qb-group" class="form-input"></select>
</div>
<div id="qb-coach-row" style="margin-bottom:12px;">
<label class="form-label">المدرب</label>
<select id="qb-coach" class="form-input"><option value="">— تلقائي —</option></select>
</div>
<div id="qb-booker-row" style="margin-bottom:12px;display:none;">
<label class="form-label">اسم الحاجز</label>
<input type="text" id="qb-booker-name" class="form-input" placeholder="اسم الحاجز">
<select id="qb-booker-type" class="form-input" style="margin-top:6px;">
<option value="guest">زائر</option>
<option value="member">عضو</option>
<option value="organization">مؤسسة</option>
</select>
</div>
<div style="margin-bottom:12px;">
<label class="form-label">ملاحظات</label>
<input type="text" id="qb-notes" class="form-input" placeholder="اختياري">
</div>
<div id="qb-conflicts" style="display:none;padding:8px 12px;background:#FEF2F2;border-radius:6px;margin-bottom:12px;font-size:12px;color:#DC2626;"></div>
</div>
<div class="mirror-modal-footer">
<button type="button" class="btn btn-primary" id="qb-submit">إنشاء الحجز</button>
<button type="button" class="btn btn-outline mirror-modal-close">إلغاء</button>
</div>
</div>
</div>
<!-- Context Menu -->
<div id="context-menu" style="display:none;position:fixed;z-index:9999;background:white;border:1px solid #E5E7EB;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);min-width:180px;padding:4px 0;font-size:13px;">
<div class="ctx-item" data-action="details"><i data-lucide="info" style="width:14px;height:14px;"></i> تفاصيل</div>
<div class="ctx-item" data-action="move"><i data-lucide="move" style="width:14px;height:14px;"></i> نقل</div>
<div class="ctx-item" data-action="extend"><i data-lucide="stretch-horizontal" style="width:14px;height:14px;"></i> تمديد</div>
<div class="ctx-item" data-action="swap"><i data-lucide="repeat-2" style="width:14px;height:14px;"></i> تبديل مع...</div>
<div class="ctx-item" data-action="duplicate"><i data-lucide="copy" style="width:14px;height:14px;"></i> نسخ ليوم آخر</div>
<hr style="margin:4px 0;border-color:#E5E7EB;">
<div class="ctx-item ctx-danger" data-action="cancel"><i data-lucide="x-circle" style="width:14px;height:14px;"></i> إلغاء الحجز</div>
</div> </div>
<!-- Booking Details Tooltip -->
<div id="booking-tooltip" style="display:none;position:fixed;z-index:9998;background:white;border:1px solid #E5E7EB;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,0.1);padding:12px 15px;min-width:200px;max-width:300px;font-size:12px;pointer-events:none;"></div>
<!-- Copy Day Modal -->
<div id="modal-copyday" class="mirror-modal" style="display:none;">
<div class="mirror-modal-content" style="max-width:380px;">
<div class="mirror-modal-header"><h3>نسخ جدول اليوم</h3><button type="button" class="mirror-modal-close">&times;</button></div>
<div class="mirror-modal-body">
<p style="font-size:13px;color:#6B7280;margin-bottom:12px;">نسخ جميع حجوزات هذا اليوم إلى يوم آخر</p>
<label class="form-label">نسخ إلى تاريخ</label>
<input type="date" id="copy-target-date" class="form-input">
</div>
<div class="mirror-modal-footer">
<button type="button" class="btn btn-primary" id="copy-submit">نسخ</button>
<button type="button" class="btn btn-outline mirror-modal-close">إلغاء</button>
</div>
</div>
</div>
<!-- Templates Modal -->
<div id="modal-templates" class="mirror-modal" style="display:none;">
<div class="mirror-modal-content" style="max-width:500px;">
<div class="mirror-modal-header"><h3>قوالب الأسبوع</h3><button type="button" class="mirror-modal-close">&times;</button></div>
<div class="mirror-modal-body">
<div style="margin-bottom:15px;padding:12px;background:#F9FAFB;border-radius:6px;">
<strong style="font-size:12px;">حفظ قالب جديد من الأسبوع الحالي</strong>
<div style="display:flex;gap:8px;margin-top:8px;">
<input type="text" id="tpl-name" class="form-input" placeholder="اسم القالب" style="flex:1;">
<button type="button" class="btn btn-sm btn-primary" id="tpl-save">حفظ</button>
</div>
</div>
<div id="tpl-list" style="max-height:300px;overflow-y:auto;"></div>
</div>
<div class="mirror-modal-footer">
<button type="button" class="btn btn-outline mirror-modal-close">إغلاق</button>
</div>
</div>
</div>
<!-- Quick Schedule Modal (from sidebar drag) -->
<div id="modal-schedule" class="mirror-modal" style="display:none;">
<div class="mirror-modal-content" style="max-width:420px;">
<div class="mirror-modal-header"><h3>جدولة المجموعة</h3><button type="button" class="mirror-modal-close">&times;</button></div>
<div class="mirror-modal-body">
<div style="background:#EEF2FF;padding:10px 12px;border-radius:6px;margin-bottom:12px;font-size:13px;">
<strong id="sched-group-name"></strong>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px;">
<div>
<label class="form-label">اليوم</label>
<select id="sched-day" class="form-input">
<option value="6">السبت</option>
<option value="0">الأحد</option>
<option value="1">الاثنين</option>
<option value="2">الثلاثاء</option>
<option value="3">الأربعاء</option>
<option value="4">الخميس</option>
<option value="5">الجمعة</option>
</select>
</div>
<div>
<label class="form-label">الوحدة</label>
<select id="sched-unit" class="form-input"></select>
</div>
<div>
<label class="form-label">البداية</label>
<input type="time" id="sched-start" class="form-input">
</div>
<div>
<label class="form-label">النهاية</label>
<input type="time" id="sched-end" class="form-input">
</div>
</div>
<div style="margin-bottom:12px;">
<label class="form-label">توليد حجوزات لمدة</label>
<select id="sched-weeks" class="form-input">
<option value="4">4 أسابيع</option>
<option value="8">8 أسابيع</option>
<option value="12">12 أسبوع</option>
</select>
</div>
</div>
<div class="mirror-modal-footer">
<button type="button" class="btn btn-primary" id="sched-submit">جدولة + توليد</button>
<button type="button" class="btn btn-outline mirror-modal-close">إلغاء</button>
</div>
</div>
</div>
<style>
@keyframes pulse { 0%,100%{opacity:1;} 50%{opacity:0.4;} }
.legend-dot { display:inline-block;width:12px;height:12px;border-radius:3px;vertical-align:middle;margin-left:3px; }
.mirror-modal { position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:9000;display:flex;align-items:center;justify-content:center; }
.mirror-modal-content { background:white;border-radius:12px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.2); }
.mirror-modal-header { padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between; }
.mirror-modal-header h3 { margin:0;font-size:15px; }
.mirror-modal-body { padding:20px; }
.mirror-modal-footer { padding:12px 20px;border-top:1px solid #E5E7EB;display:flex;gap:8px;justify-content:flex-start; }
.mirror-modal-close { background:none;border:none;font-size:22px;cursor:pointer;color:#6B7280;line-height:1; }
.ctx-item { padding:8px 14px;cursor:pointer;display:flex;align-items:center;gap:8px;transition:background 0.1s; }
.ctx-item:hover { background:#F3F4F6; }
.ctx-danger { color:#DC2626; }
.ctx-danger:hover { background:#FEF2F2; }
.grid-cell { padding:4px 3px;border:1px solid #E5E7EB;text-align:center;vertical-align:middle;font-size:10px;font-weight:500;cursor:pointer;position:relative;min-height:40px;transition:box-shadow 0.15s; }
.grid-cell:hover { box-shadow:inset 0 0 0 2px #6366F1;z-index:5; }
.grid-cell.selected { box-shadow:inset 0 0 0 2px #EF4444;z-index:5; }
.grid-cell.drop-target { box-shadow:inset 0 0 0 3px #10B981;background:#ECFDF5 !important; }
.group-card { padding:8px 10px;margin-bottom:6px;border-radius:6px;background:#F9FAFB;border:1px solid #E5E7EB;cursor:grab;font-size:11px;transition:all 0.15s; }
.group-card:hover { border-color:#6366F1;background:#EEF2FF; }
.group-card.dragging { opacity:0.5;transform:scale(0.95); }
.group-card .group-name { font-weight:600;font-size:12px;margin-bottom:2px; }
.group-card .group-meta { color:#6B7280;font-size:10px; }
.heatmap-green { background:#D1FAE5 !important; }
.heatmap-yellow { background:#FEF3C7 !important; }
.heatmap-orange { background:#FFEDD5 !important; }
.heatmap-red { background:#FEE2E2 !important; }
.week-grid th, .week-grid td { font-size:10px;padding:3px 2px; }
#groups-sidebar.collapsed { width:40px !important;min-width:40px !important;overflow:hidden; }
#groups-sidebar.collapsed > *:not(:first-child) { display:none; }
@media print {
#groups-sidebar, #stats-bar, .btn, #legend-bar, #now-marker, .mirror-modal { display:none !important; }
.card { box-shadow:none !important;border:1px solid #ccc !important; }
#grid-container { overflow:visible !important; }
}
</style>
<script> <script>
(function(){
'use strict';
var facilityId = <?= (int) $facility['id'] ?>;
var currentDate = '<?= e($date) ?>';
var csrfToken = '<?= e(csrf_token()) ?>';
var currentView = 'day';
var gridState = null;
var groupsData = [];
var selectedCells = [];
var swapSource = null;
var pollTimer = null;
var heatmapMode = false;
// ─── Utilities ──────────────────────────────────────────────────────────────
function api(method, path, body) {
var opts = { method: method, headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken } };
if (body) opts.body = JSON.stringify(body);
return fetch(path, opts).then(function(r) { return r.json(); });
}
function showModal(id) { document.getElementById(id).style.display = 'flex'; }
function hideModal(id) { document.getElementById(id).style.display = 'none'; }
function hideAllModals() { document.querySelectorAll('.mirror-modal').forEach(function(m){ m.style.display = 'none'; }); }
function formatTime(t) { return t ? t.substring(0,5) : ''; }
function toast(msg, type) {
var el = document.createElement('div');
el.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);z-index:99999;padding:10px 20px;border-radius:8px;font-size:13px;font-weight:600;color:white;box-shadow:0 4px 12px rgba(0,0,0,0.2);transition:opacity 0.3s;';
el.style.background = type === 'error' ? '#DC2626' : '#059669';
el.textContent = msg;
document.body.appendChild(el);
setTimeout(function(){ el.style.opacity = '0'; setTimeout(function(){ el.remove(); }, 300); }, 3000);
}
// ─── Data Loading ───────────────────────────────────────────────────────────
function loadState() {
var url = '/api/sa/mirror/' + facilityId + '/state?date=' + currentDate;
if (currentView === 'week') {
loadWeekView();
return;
}
api('GET', url).then(function(data) {
if (data.error) { toast(data.error, 'error'); return; }
gridState = data;
renderGrid(data);
updateNowMarker(data);
});
}
function loadStats() {
api('GET', '/api/sa/mirror/' + facilityId + '/stats?date=' + currentDate).then(function(data) {
if (!data.success) return;
document.getElementById('stat-utilization').textContent = data.utilization + '%';
document.getElementById('stat-revenue').textContent = data.revenue.toFixed(2) + ' ج.م';
document.getElementById('stat-bookings').textContent = data.booking_count;
});
}
function loadGroups() {
api('GET', '/api/sa/mirror/' + facilityId + '/groups').then(function(data) {
if (!data.success) return;
groupsData = data.groups;
renderGroups(data.groups);
});
}
function loadTemplates() {
api('GET', '/api/sa/mirror/' + facilityId + '/templates').then(function(data) {
if (!data.success) return;
renderTemplates(data.templates);
});
}
// ─── Grid Rendering ─────────────────────────────────────────────────────────
function renderGrid(data) {
var slots = data.slots;
var grid = data.grid;
var units = data.units;
var thead = document.getElementById('grid-header');
var tbody = document.getElementById('grid-body');
var headerHtml = '<tr><th style="padding:8px;border:1px solid #E5E7EB;background:#F9FAFB;position:sticky;right:0;z-index:2;min-width:90px;font-size:11px;">الوحدة</th>';
for (var i = 0; i < slots.length; i++) {
headerHtml += '<th style="padding:6px 3px;border:1px solid #E5E7EB;background:#F9FAFB;text-align:center;white-space:nowrap;min-width:65px;font-size:10px;">' + slots[i].start + '</th>';
}
headerHtml += '</tr>';
thead.innerHTML = headerHtml;
var bodyHtml = '';
for (var uid in grid) {
var unitData = grid[uid];
var unit = unitData.unit;
bodyHtml += '<tr><td style="padding:8px;border:1px solid #E5E7EB;font-weight:700;background:#FAFAFA;position:sticky;right:0;z-index:1;font-size:11px;">' + escHtml(unit.name_ar) + '</td>';
for (var s = 0; s < unitData.slots.length; s++) {
var cell = unitData.slots[s];
var bg = getCellBg(cell);
var text = getCellText(cell);
var bookingId = (cell.bookings && cell.bookings.length > 0 && cell.bookings[0].id) ? cell.bookings[0].id : '';
var heatClass = '';
if (heatmapMode && cell.bookings.length > 0 && cell.bookings[0].group_id) {
var occ = cell.occupied || 0;
var mx = cell.max || 1;
var ratio = occ / mx;
if (ratio >= 0.9) heatClass = ' heatmap-red';
else if (ratio >= 0.7) heatClass = ' heatmap-orange';
else if (ratio >= 0.4) heatClass = ' heatmap-yellow';
else heatClass = ' heatmap-green';
}
bodyHtml += '<td class="grid-cell' + heatClass + '" style="background:' + bg + ';" '
+ 'data-unit-id="' + uid + '" data-slot-idx="' + s + '" '
+ 'data-start="' + slots[s].start + '" data-end="' + slots[s].end + '" '
+ 'data-booking-id="' + bookingId + '" data-status="' + cell.status + '">'
+ text + '</td>';
}
bodyHtml += '</tr>';
}
tbody.innerHTML = bodyHtml;
attachCellEvents();
}
function getCellBg(cell) {
if (heatmapMode && cell.bookings && cell.bookings.length > 0 && cell.bookings[0].group_id) return '';
if (cell.status === 'free') return '#D1FAE5';
if (cell.status === 'partial') return '#FEF3C7';
if (cell.status === 'full') return '#FEE2E2';
if (cell.status === 'training') return '#DBEAFE';
if (cell.status === 'blocked') return '#E5E7EB';
if (cell.status === 'booked') {
if (cell.bookings && cell.bookings.length > 0) {
var bt = cell.bookings[0].booker_type;
if (bt === 'organization') return '#FFEDD5';
if (bt === 'member') return '#D1FAE5';
return '#FEF9C3';
}
}
return '#FFFFFF';
}
function getCellText(cell) {
if (cell.status === 'partial' || cell.status === 'full') return cell.occupied + '/' + cell.max;
if (cell.status === 'training' && cell.bookings && cell.bookings.length > 0) {
return '<span style="font-size:9px;">' + escHtml(cell.bookings[0].group_name || '') + '</span>';
}
if (cell.status === 'booked' && cell.bookings && cell.bookings.length > 0) {
var b = cell.bookings[0];
return '<span style="font-size:9px;">' + escHtml(b.organization_name || b.booker_name || '') + '</span>';
}
return '';
}
function escHtml(str) {
if (!str) return '';
return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ─── Week View ──────────────────────────────────────────────────────────────
function loadWeekView() {
var startDate = getWeekStart(currentDate);
var days = [];
var dayNames = ['الأحد','الاثنين','الثلاثاء','الأربعاء','الخميس','الجمعة','السبت'];
for (var i = 0; i < 7; i++) {
var d = new Date(startDate);
d.setDate(d.getDate() + i);
days.push(d.toISOString().split('T')[0]);
}
var promises = days.map(function(d) {
return api('GET', '/api/sa/mirror/' + facilityId + '/state?date=' + d);
});
Promise.all(promises).then(function(results) {
renderWeekGrid(results, days, dayNames);
});
}
function getWeekStart(dateStr) {
var d = new Date(dateStr);
var day = d.getDay();
var diff = day === 6 ? 0 : -(day + 1);
d.setDate(d.getDate() + diff);
return d;
}
function renderWeekGrid(results, days, dayNames) {
var thead = document.getElementById('grid-header');
var tbody = document.getElementById('grid-body');
var headerHtml = '<tr><th style="padding:6px;border:1px solid #E5E7EB;background:#F9FAFB;font-size:10px;">الوحدة</th>';
for (var i = 0; i < 7; i++) {
var isToday = days[i] === new Date().toISOString().split('T')[0];
headerHtml += '<th style="padding:6px;border:1px solid #E5E7EB;background:' + (isToday ? '#EEF2FF' : '#F9FAFB') + ';text-align:center;font-size:10px;">'
+ dayNames[new Date(days[i]).getDay()] + '<br><span style="font-size:9px;color:#6B7280;">' + days[i].substring(5) + '</span></th>';
}
headerHtml += '</tr>';
thead.innerHTML = headerHtml;
if (!results[0] || results[0].error) { tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;padding:30px;">لا توجد بيانات</td></tr>'; return; }
var units = results[0].units;
var bodyHtml = '';
for (var u = 0; u < units.length; u++) {
var unit = units[u];
bodyHtml += '<tr><td style="padding:6px;border:1px solid #E5E7EB;font-weight:700;font-size:10px;background:#FAFAFA;">' + escHtml(unit.name_ar) + '</td>';
for (var d = 0; d < 7; d++) {
var dayGrid = results[d] && results[d].grid ? results[d].grid[unit.id] : null;
if (!dayGrid) { bodyHtml += '<td style="border:1px solid #E5E7EB;"></td>'; continue; }
var dayBookings = [];
for (var si = 0; si < dayGrid.slots.length; si++) {
var slot = dayGrid.slots[si];
if (slot.bookings && slot.bookings.length > 0) {
for (var bi = 0; bi < slot.bookings.length; bi++) {
var bk = slot.bookings[bi];
var alreadyAdded = dayBookings.some(function(x){ return x.id === bk.id && x.start_time === bk.start_time; });
if (!alreadyAdded) dayBookings.push(bk);
}
}
}
var cellHtml = '<td style="border:1px solid #E5E7EB;padding:3px;vertical-align:top;font-size:9px;">';
for (var bki = 0; bki < Math.min(dayBookings.length, 4); bki++) {
var bk2 = dayBookings[bki];
var color = bk2.booking_type === 'training' ? '#DBEAFE' : (bk2.booking_type === 'blocked' ? '#E5E7EB' : '#FEF9C3');
cellHtml += '<div style="background:' + color + ';padding:1px 3px;border-radius:3px;margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">'
+ formatTime(bk2.start_time) + ' ' + escHtml(bk2.group_name || bk2.booker_name || '') + '</div>';
}
if (dayBookings.length > 4) cellHtml += '<div style="color:#6B7280;">+' + (dayBookings.length - 4) + '</div>';
cellHtml += '</td>';
bodyHtml += cellHtml;
}
bodyHtml += '</tr>';
}
tbody.innerHTML = bodyHtml;
}
// ─── Groups Sidebar ─────────────────────────────────────────────────────────
function renderGroups(groups) {
var container = document.getElementById('groups-list');
if (groups.length === 0) {
container.innerHTML = '<div style="text-align:center;padding:15px;color:#9CA3AF;font-size:11px;">لا توجد مجموعات</div>';
return;
}
var html = '';
for (var i = 0; i < groups.length; i++) {
var g = groups[i];
var schedText = '';
if (g.schedule_info) {
var parts = g.schedule_info.split('|').slice(0, 3);
var dayMap = {0:'أحد',1:'اثنين',2:'ثلاثاء',3:'أربعاء',4:'خميس',5:'جمعة',6:'سبت'};
schedText = parts.map(function(p){
var segs = p.split(':');
return (dayMap[segs[0]] || '') + ' ' + (segs[1] || '');
}).join(' | ');
}
html += '<div class="group-card" draggable="true" data-group-id="' + g.id + '" data-coach-id="' + (g.coach_id||'') + '" data-duration="' + (g.session_duration_minutes||60) + '">'
+ '<div class="group-name">' + escHtml(g.name_ar) + '</div>'
+ '<div class="group-meta">' + escHtml(g.coach_name || '—') + ' • ' + g.current_count + '/' + (g.max_capacity||'∞') + '</div>'
+ (schedText ? '<div class="group-meta" style="margin-top:2px;">' + escHtml(schedText) + '</div>' : '')
+ (!g.schedule_info ? '<div style="color:#D97706;font-size:9px;margin-top:2px;">⚠ بدون جدول</div>' : '')
+ '</div>';
}
container.innerHTML = html;
attachDragEvents();
}
// ─── Cell Events ────────────────────────────────────────────────────────────
function attachCellEvents() {
var cells = document.querySelectorAll('.grid-cell');
cells.forEach(function(cell) {
cell.addEventListener('click', onCellClick);
cell.addEventListener('contextmenu', onCellContext);
cell.addEventListener('mouseenter', onCellHover);
cell.addEventListener('mouseleave', onCellLeave);
cell.addEventListener('dragover', function(e){ e.preventDefault(); this.classList.add('drop-target'); });
cell.addEventListener('dragleave', function(){ this.classList.remove('drop-target'); });
cell.addEventListener('drop', onCellDrop);
});
}
function onCellClick(e) {
var cell = e.currentTarget;
var status = cell.dataset.status;
var bookingId = cell.dataset.bookingId;
if (e.shiftKey) {
cell.classList.toggle('selected');
if (cell.classList.contains('selected')) {
selectedCells.push(cell);
} else {
selectedCells = selectedCells.filter(function(c){ return c !== cell; });
}
return;
}
if (status === 'free') {
openQuickBook(cell);
}
}
function onCellContext(e) {
e.preventDefault();
var cell = e.currentTarget;
var bookingId = cell.dataset.bookingId;
if (!bookingId) return;
var menu = document.getElementById('context-menu');
menu.style.display = 'block';
menu.style.top = e.clientY + 'px';
menu.style.left = e.clientX + 'px';
menu.dataset.bookingId = bookingId;
menu.dataset.unitId = cell.dataset.unitId;
menu.dataset.start = cell.dataset.start;
menu.dataset.end = cell.dataset.end;
if (typeof lucide !== 'undefined') lucide.createIcons({nodes: menu.querySelectorAll('[data-lucide]')});
}
function onCellHover(e) {
var cell = e.currentTarget;
var bookingId = cell.dataset.bookingId;
if (!bookingId || !gridState) return;
var unitId = cell.dataset.unitId;
var slotIdx = parseInt(cell.dataset.slotIdx);
var unitData = gridState.grid[unitId];
if (!unitData || !unitData.slots[slotIdx]) return;
var slot = unitData.slots[slotIdx];
if (!slot.bookings || slot.bookings.length === 0) return;
var b = slot.bookings[0];
var tooltip = document.getElementById('booking-tooltip');
var html = '<div style="font-weight:700;margin-bottom:4px;">';
if (b.group_name) html += escHtml(b.group_name);
else if (b.booker_name) html += escHtml(b.booker_name);
else html += b.booking_type;
html += '</div>';
html += '<div style="color:#6B7280;">' + formatTime(b.start_time) + ' - ' + formatTime(b.end_time) + '</div>';
if (b.coach_name) html += '<div>المدرب: ' + escHtml(b.coach_name) + '</div>';
if (b.participant_count) html += '<div>المشاركون: ' + b.participant_count + '</div>';
if (b.status) html += '<div>الحالة: ' + escHtml(b.status) + '</div>';
if (b.notes) html += '<div style="margin-top:4px;font-style:italic;">' + escHtml(b.notes) + '</div>';
tooltip.innerHTML = html;
tooltip.style.display = 'block';
tooltip.style.top = (e.clientY + 12) + 'px';
tooltip.style.left = (e.clientX + 12) + 'px';
}
function onCellLeave() {
document.getElementById('booking-tooltip').style.display = 'none';
}
// ─── Drag & Drop ────────────────────────────────────────────────────────────
function attachDragEvents() {
document.querySelectorAll('.group-card').forEach(function(card) {
card.addEventListener('dragstart', function(e) {
e.dataTransfer.setData('text/plain', JSON.stringify({
group_id: card.dataset.groupId,
coach_id: card.dataset.coachId,
duration: card.dataset.duration
}));
card.classList.add('dragging');
});
card.addEventListener('dragend', function() { card.classList.remove('dragging'); });
});
}
function onCellDrop(e) {
e.preventDefault();
var cell = e.currentTarget;
cell.classList.remove('drop-target');
var data;
try { data = JSON.parse(e.dataTransfer.getData('text/plain')); } catch(err) { return; }
if (cell.dataset.status !== 'free') {
toast('هذه الخانة مشغولة', 'error');
return;
}
var groupId = parseInt(data.group_id);
var duration = parseInt(data.duration) || 60;
var startTime = cell.dataset.start;
var startMinutes = parseInt(startTime.split(':')[0]) * 60 + parseInt(startTime.split(':')[1]);
var endMinutes = startMinutes + duration;
var endTime = String(Math.floor(endMinutes/60)).padStart(2,'0') + ':' + String(endMinutes%60).padStart(2,'0');
var group = groupsData.find(function(g){ return g.id == groupId; });
document.getElementById('sched-group-name').textContent = group ? group.name_ar : 'مجموعة #' + groupId;
var dayOfWeek = new Date(currentDate).getDay();
document.getElementById('sched-day').value = dayOfWeek;
document.getElementById('sched-start').value = startTime;
document.getElementById('sched-end').value = endTime;
var unitSelect = document.getElementById('sched-unit');
unitSelect.innerHTML = '';
if (gridState && gridState.units) {
gridState.units.forEach(function(u){
var opt = document.createElement('option');
opt.value = u.id;
opt.textContent = u.name_ar;
if (u.id == cell.dataset.unitId) opt.selected = true;
unitSelect.appendChild(opt);
});
}
document.getElementById('modal-schedule').dataset.groupId = groupId;
document.getElementById('modal-schedule').dataset.coachId = data.coach_id || '';
showModal('modal-schedule');
}
// ─── Quick Book ─────────────────────────────────────────────────────────────
function openQuickBook(cell) {
var unitId = cell.dataset.unitId;
var start = cell.dataset.start;
var end = cell.dataset.end;
var unitName = '';
if (gridState && gridState.grid[unitId]) {
unitName = gridState.grid[unitId].unit.name_ar;
}
document.getElementById('qb-unit-name').value = unitName;
document.getElementById('qb-date').value = currentDate;
document.getElementById('qb-start').value = start;
document.getElementById('qb-end').value = end;
document.getElementById('qb-notes').value = '';
document.getElementById('qb-conflicts').style.display = 'none';
var groupSelect = document.getElementById('qb-group');
groupSelect.innerHTML = '<option value="">— اختر مجموعة —</option>';
groupsData.forEach(function(g){
groupSelect.innerHTML += '<option value="' + g.id + '" data-coach="' + (g.coach_id||'') + '">' + escHtml(g.name_ar) + '</option>';
});
document.getElementById('modal-quickbook').dataset.unitId = unitId;
toggleBookingTypeFields();
showModal('modal-quickbook');
checkConflicts(unitId, currentDate, start, end, 0);
}
function toggleBookingTypeFields() {
var type = document.getElementById('qb-type').value;
document.getElementById('qb-group-row').style.display = (type === 'training') ? '' : 'none';
document.getElementById('qb-coach-row').style.display = (type === 'training') ? '' : 'none';
document.getElementById('qb-booker-row').style.display = (type === 'hourly') ? '' : 'none';
}
function checkConflicts(unitId, date, start, end, coachId) {
api('GET', '/api/sa/mirror/' + facilityId + '/conflicts?unit_id=' + unitId + '&date=' + date + '&start_time=' + start + '&end_time=' + end + '&coach_id=' + (coachId||0))
.then(function(data) {
var el = document.getElementById('qb-conflicts');
if (data.success && data.conflicts && data.conflicts.length > 0) {
el.style.display = 'block';
el.innerHTML = data.conflicts.map(function(c){ return '⚠ ' + escHtml(c.message); }).join('<br>');
} else {
el.style.display = 'none';
}
});
}
// ─── Context Menu Actions ───────────────────────────────────────────────────
function handleContextAction(action) {
var menu = document.getElementById('context-menu');
var bookingId = menu.dataset.bookingId;
menu.style.display = 'none';
if (action === 'cancel') {
if (!confirm('هل تريد إلغاء هذا الحجز؟')) return;
api('POST', '/api/sa/mirror/' + facilityId + '/cancel-booking', { booking_id: parseInt(bookingId) })
.then(function(r) {
if (r.success) { toast('تم إلغاء الحجز'); loadState(); loadStats(); }
else toast(r.error, 'error');
});
} else if (action === 'move') {
toast('انقر على الخانة الجديدة لنقل الحجز إليها');
swapSource = { action: 'move', bookingId: parseInt(bookingId) };
document.querySelectorAll('.grid-cell').forEach(function(c) {
c.addEventListener('click', onMoveTarget, { once: true });
});
} else if (action === 'swap') {
toast('انقر على الحجز الآخر للتبديل');
swapSource = { action: 'swap', bookingId: parseInt(bookingId) };
document.querySelectorAll('.grid-cell').forEach(function(c) {
c.addEventListener('click', onSwapTarget, { once: true });
});
} else if (action === 'extend') {
var newEnd = prompt('وقت الانتهاء الجديد (مثال: 14:00)');
if (!newEnd) return;
api('POST', '/api/sa/mirror/' + facilityId + '/extend-booking', { booking_id: parseInt(bookingId), new_end_time: newEnd })
.then(function(r) {
if (r.success) { toast('تم التمديد'); loadState(); }
else toast(r.error, 'error');
});
} else if (action === 'duplicate') {
var targetDate = prompt('نسخ إلى تاريخ (YYYY-MM-DD)');
if (!targetDate) return;
api('POST', '/api/sa/mirror/' + facilityId + '/copy-day', { source_date: currentDate, target_date: targetDate })
.then(function(r) {
if (r.success) toast('تم النسخ: ' + r.copied + ' حجز');
else toast(r.error, 'error');
});
} else if (action === 'details') {
var b = findBookingById(parseInt(bookingId));
if (b) alert('حجز #' + (b.booking_number||b.id) + '\nنوع: ' + b.booking_type + '\nالوقت: ' + formatTime(b.start_time) + ' - ' + formatTime(b.end_time) + '\nالمجموعة: ' + (b.group_name||'—') + '\nالمدرب: ' + (b.coach_name||'—'));
}
}
function onMoveTarget(e) {
var cell = e.currentTarget;
if (!swapSource || swapSource.action !== 'move') return;
api('POST', '/api/sa/mirror/' + facilityId + '/move-booking', {
booking_id: swapSource.bookingId,
new_unit_id: parseInt(cell.dataset.unitId),
new_date: currentDate,
new_start_time: cell.dataset.start,
new_end_time: cell.dataset.end
}).then(function(r) {
if (r.success) { toast('تم النقل'); loadState(); }
else toast(r.error, 'error');
});
swapSource = null;
clearMoveListeners();
}
function onSwapTarget(e) {
var cell = e.currentTarget;
if (!swapSource || swapSource.action !== 'swap') return;
var targetBookingId = cell.dataset.bookingId;
if (!targetBookingId) { toast('اختر خانة بها حجز', 'error'); swapSource = null; return; }
api('POST', '/api/sa/mirror/' + facilityId + '/swap-bookings', {
booking_id_a: swapSource.bookingId,
booking_id_b: parseInt(targetBookingId)
}).then(function(r) {
if (r.success) { toast('تم التبديل'); loadState(); }
else toast(r.error, 'error');
});
swapSource = null;
clearMoveListeners();
}
function clearMoveListeners() {
document.querySelectorAll('.grid-cell').forEach(function(c) {
c.removeEventListener('click', onMoveTarget);
c.removeEventListener('click', onSwapTarget);
});
}
function findBookingById(id) {
if (!gridState || !gridState.grid) return null;
for (var uid in gridState.grid) {
var slots = gridState.grid[uid].slots;
for (var s = 0; s < slots.length; s++) {
for (var b = 0; b < (slots[s].bookings||[]).length; b++) {
if (slots[s].bookings[b].id == id) return slots[s].bookings[b];
}
}
}
return null;
}
// ─── Now Marker ─────────────────────────────────────────────────────────────
function updateNowMarker(data) {
var marker = document.getElementById('now-marker');
if (currentDate !== new Date().toISOString().split('T')[0]) { marker.style.display = 'none'; return; }
var now = new Date();
var nowMinutes = now.getHours() * 60 + now.getMinutes();
var slots = data.slots;
if (!slots || slots.length === 0) { marker.style.display = 'none'; return; }
var startMinutes = parseInt(slots[0].start.split(':')[0]) * 60 + parseInt(slots[0].start.split(':')[1]);
var endMinutes = parseInt(slots[slots.length-1].end.split(':')[0]) * 60 + parseInt(slots[slots.length-1].end.split(':')[1]);
if (nowMinutes < startMinutes || nowMinutes > endMinutes) { marker.style.display = 'none'; return; }
var table = document.getElementById('mirror-grid');
var headerCells = table.querySelectorAll('thead th');
if (headerCells.length < 2) { marker.style.display = 'none'; return; }
var firstCell = headerCells[1];
var lastCell = headerCells[headerCells.length - 1];
var gridLeft = firstCell.offsetLeft;
var gridWidth = (lastCell.offsetLeft + lastCell.offsetWidth) - gridLeft;
var progress = (nowMinutes - startMinutes) / (endMinutes - startMinutes);
var pos = gridLeft + (progress * gridWidth);
marker.style.display = 'block';
marker.style.right = 'auto';
marker.style.left = pos + 'px';
marker.style.height = table.offsetHeight + 'px';
}
// ─── Templates ──────────────────────────────────────────────────────────────
function renderTemplates(templates) {
var el = document.getElementById('tpl-list');
if (templates.length === 0) {
el.innerHTML = '<div style="text-align:center;padding:15px;color:#9CA3AF;font-size:12px;">لا توجد قوالب محفوظة</div>';
return;
}
var html = '';
templates.forEach(function(t) {
html += '<div style="padding:10px 12px;border-bottom:1px solid #F3F4F6;display:flex;align-items:center;justify-content:space-between;">'
+ '<div><strong style="font-size:12px;">' + escHtml(t.name) + '</strong><div style="font-size:10px;color:#9CA3AF;">' + t.created_at + '</div></div>'
+ '<button type="button" class="btn btn-sm btn-outline" onclick="applyTemplate(' + t.id + ')">تطبيق</button>'
+ '</div>';
});
el.innerHTML = html;
}
window.applyTemplate = function(templateId) {
var weekStart = prompt('بداية الأسبوع المراد التطبيق عليه (YYYY-MM-DD)');
if (!weekStart) return;
api('POST', '/api/sa/mirror/' + facilityId + '/apply-template', { template_id: templateId, week_start: weekStart })
.then(function(r) {
if (r.success) { toast('تم تطبيق القالب: ' + r.applied + ' حجز'); hideAllModals(); loadState(); loadStats(); }
else toast(r.error, 'error');
});
};
// ─── Event Bindings ─────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') { lucide.createIcons(); } loadState();
loadStats();
loadGroups();
pollTimer = setInterval(function() { loadState(); loadStats(); }, 15000);
document.getElementById('mirror-date').addEventListener('change', function() {
currentDate = this.value;
loadState();
loadStats();
});
document.getElementById('btn-today').addEventListener('click', function() {
currentDate = new Date().toISOString().split('T')[0];
document.getElementById('mirror-date').value = currentDate;
loadState(); loadStats();
});
document.getElementById('btn-prev-day').addEventListener('click', function() {
var d = new Date(currentDate); d.setDate(d.getDate() - 1);
currentDate = d.toISOString().split('T')[0];
document.getElementById('mirror-date').value = currentDate;
loadState(); loadStats();
});
document.getElementById('btn-next-day').addEventListener('click', function() {
var d = new Date(currentDate); d.setDate(d.getDate() + 1);
currentDate = d.toISOString().split('T')[0];
document.getElementById('mirror-date').value = currentDate;
loadState(); loadStats();
});
document.querySelectorAll('.view-toggle').forEach(function(btn) {
btn.addEventListener('click', function() {
document.querySelectorAll('.view-toggle').forEach(function(b){ b.classList.remove('active'); b.classList.add('btn-outline'); });
this.classList.add('active'); this.classList.remove('btn-outline');
currentView = this.dataset.view;
loadState();
});
});
document.getElementById('toggle-heatmap').addEventListener('change', function() {
heatmapMode = this.checked;
if (gridState) renderGrid(gridState);
});
document.getElementById('btn-toggle-sidebar').addEventListener('click', function() {
document.getElementById('groups-sidebar').classList.toggle('collapsed');
});
document.getElementById('btn-fullscreen').addEventListener('click', function() {
var app = document.getElementById('mirror-app');
if (document.fullscreenElement) { document.exitFullscreen(); }
else { app.requestFullscreen().catch(function(){}); }
});
// Quick Book submit
document.getElementById('qb-submit').addEventListener('click', function() {
var unitId = parseInt(document.getElementById('modal-quickbook').dataset.unitId);
var type = document.getElementById('qb-type').value;
var data = {
unit_id: unitId,
date: currentDate,
start_time: document.getElementById('qb-start').value,
end_time: document.getElementById('qb-end').value,
booking_type: type,
notes: document.getElementById('qb-notes').value
};
if (type === 'training') {
data.group_id = parseInt(document.getElementById('qb-group').value) || 0;
data.coach_id = parseInt(document.getElementById('qb-coach').value) || 0;
} else if (type === 'hourly') {
data.booker_name = document.getElementById('qb-booker-name').value;
data.booker_type = document.getElementById('qb-booker-type').value;
}
api('POST', '/api/sa/mirror/' + facilityId + '/quick-book', data).then(function(r) {
if (r.success) { toast('تم إنشاء الحجز'); hideAllModals(); loadState(); loadStats(); }
else toast(r.error || 'فشل الحجز', 'error');
});
});
document.getElementById('qb-type').addEventListener('change', toggleBookingTypeFields);
// Schedule submit
document.getElementById('sched-submit').addEventListener('click', function() {
var modal = document.getElementById('modal-schedule');
var data = {
group_id: parseInt(modal.dataset.groupId),
unit_id: parseInt(document.getElementById('sched-unit').value),
day_of_week: parseInt(document.getElementById('sched-day').value),
start_time: document.getElementById('sched-start').value,
end_time: document.getElementById('sched-end').value,
weeks: parseInt(document.getElementById('sched-weeks').value)
};
api('POST', '/api/sa/mirror/' + facilityId + '/quick-schedule', data).then(function(r) {
if (r.success) { toast('تم الجدولة: ' + (r.generated||0) + ' حصة'); hideAllModals(); loadState(); loadStats(); loadGroups(); }
else toast(r.error || 'فشل', 'error');
});
});
// Copy Day
document.getElementById('btn-copy-day').addEventListener('click', function() { showModal('modal-copyday'); });
document.getElementById('copy-submit').addEventListener('click', function() {
var target = document.getElementById('copy-target-date').value;
if (!target) return;
api('POST', '/api/sa/mirror/' + facilityId + '/copy-day', { source_date: currentDate, target_date: target })
.then(function(r) {
if (r.success) { toast('تم نسخ ' + r.copied + ' حجز'); hideAllModals(); }
else toast(r.error, 'error');
});
});
// Templates
document.getElementById('btn-templates').addEventListener('click', function() { loadTemplates(); showModal('modal-templates'); });
document.getElementById('tpl-save').addEventListener('click', function() {
var name = document.getElementById('tpl-name').value.trim();
if (!name) { toast('أدخل اسم القالب', 'error'); return; }
api('POST', '/api/sa/mirror/' + facilityId + '/save-template', { name: name, date: currentDate })
.then(function(r) {
if (r.success) { toast('تم حفظ القالب (' + r.entries + ' حجز)'); loadTemplates(); document.getElementById('tpl-name').value = ''; }
else toast(r.error, 'error');
});
});
// Bulk Block
document.getElementById('btn-bulk-block').addEventListener('click', function() {
if (selectedCells.length === 0) { toast('حدد خلايا أولاً (Shift+Click)', 'error'); return; }
var reason = prompt('سبب الحجب') || 'محجوز';
var cells = selectedCells.map(function(c) {
return { unit_id: parseInt(c.dataset.unitId), date: currentDate, start_time: c.dataset.start, end_time: c.dataset.end };
});
api('POST', '/api/sa/mirror/' + facilityId + '/bulk-block', { cells: cells, reason: reason })
.then(function(r) {
if (r.success) { toast('تم حجب ' + r.blocked + ' خانة'); selectedCells = []; loadState(); loadStats(); }
else toast(r.error, 'error');
});
});
// Context menu
document.querySelectorAll('.ctx-item').forEach(function(item) {
item.addEventListener('click', function() { handleContextAction(this.dataset.action); });
});
document.addEventListener('click', function(e) {
var menu = document.getElementById('context-menu');
if (!menu.contains(e.target)) menu.style.display = 'none';
});
// Modal close handlers
document.querySelectorAll('.mirror-modal-close').forEach(function(btn) {
btn.addEventListener('click', function() { hideAllModals(); });
});
document.querySelectorAll('.mirror-modal').forEach(function(modal) {
modal.addEventListener('click', function(e) { if (e.target === this) hideAllModals(); });
});
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT' || e.target.tagName === 'TEXTAREA') return;
if (e.key === 'Escape') { hideAllModals(); document.getElementById('context-menu').style.display = 'none'; }
if (e.key === 'ArrowRight') { document.getElementById('btn-prev-day').click(); e.preventDefault(); }
if (e.key === 'ArrowLeft') { document.getElementById('btn-next-day').click(); e.preventDefault(); }
if (e.key === 't' || e.key === 'T') { document.getElementById('btn-today').click(); }
if (e.key === 'f' || e.key === 'F') { document.getElementById('btn-fullscreen').click(); }
});
// Load coaches for quick book
document.getElementById('qb-group').addEventListener('change', function() {
var opt = this.options[this.selectedIndex];
var coachId = opt ? opt.dataset.coach : '';
if (coachId) {
var coachSelect = document.getElementById('qb-coach');
for (var i = 0; i < coachSelect.options.length; i++) {
if (coachSelect.options[i].value === coachId) {
coachSelect.selectedIndex = i;
break;
}
}
}
});
api('GET', '/api/sa/mirror/' + facilityId + '/coaches-available?date=' + currentDate).then(function(data) {
if (!data.success) return;
var select = document.getElementById('qb-coach');
data.coaches.forEach(function(c) {
var opt = document.createElement('option');
opt.value = c.id;
opt.textContent = c.name_ar;
select.appendChild(opt);
});
});
if (typeof lucide !== 'undefined') lucide.createIcons();
}); });
})();
</script> </script>
<?php $__template->endSection(); ?> <?php $__template->endSection(); ?>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment