|
|
<!DOCTYPE html><html xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/head-sm :: head}"></head><meta charset="UTF-8"><style> /* ── Multiselect ─────────────────────────────────────── */ .multiselect { text-wrap: balance; }
.selectBox { position: relative; }
.selectBox select { width: 100%; }
.overSelect { position: absolute; left: 0; right: 0; top: 0; bottom: 0; }
.checkboxes { display: none; border: 0.5px #7c7c7c solid; background-color: #fff; max-height: 200px; overflow-y: auto; z-index: 9999; position: relative; }
.checkboxes label { display: block; font-weight: normal; white-space: nowrap; min-height: 1.2em; padding: 0 2.25rem 0 .75rem; cursor: pointer; }
.checkboxes label:hover { background-color: #1e90ff; color: #fff; }
.multiselect .selectBox { min-width: 150px; max-width: 100%; overflow: hidden; border: 0.5px #7c7c7c solid; }
.multiselect .selectBox select { height: auto; overflow: hidden; }
/* ── Step badge ──────────────────────────────────────── */ .step-badge { display: inline-block; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; background: #0d6efd; color: #fff; font-weight: 700; font-size: 13px; text-align: center; margin-right: 6px; }
/* ── Visibility helpers ──────────────────────────────── */ #step2Section { display: none; }
#blockActionCard { display: none; }
#noConflictCard { display: none; }
/* ── Conflict pill ───────────────────────────────────── */ .conflict-pill { display: inline-block; border-radius: 20px; padding: 3px 10px; font-size: 12px; font-weight: 600; }
.pill-blocked { background: #fde8e8; color: #dc3545; border: 1px solid #dc3545; }
/* ── Decision card ───────────────────────────────────── */ .decision-card { border: 2px solid #0d6efd; border-radius: 8px; padding: 16px 20px; background: #f0f6ff; }
.btn-decision { min-width: 230px; }
/* ── File hint ───────────────────────────────────────── */ .file-hint { font-size: 11px; color: #6c757d; margin-top: 2px; }
.req { color: #dc3545; }</style>
<body> <div id="page-container" class="sidebar-o sidebar-dark enable-page-overlay side-scroll page-header-fixed page-header-dark"> <aside th:replace="~{fragments/aside-right :: aside}"></aside> <nav th:replace="~{fragments/nav-sm :: nav}"></nav> <header th:replace="~{fragments/header-sm :: header}"></header>
<main id="main-container"> <div class="content">
<!-- ═══════════════════════════════════════════════════════
PANEL A – Currently Blocked Halls (auto-loads on open) ══════════════════════════════════════════════════════════ --> <div class="block block-rounded" id="blockedListSection"> <div class="block-header block-header-default d-flex justify-content-between align-items-center"> <h3 class="block-title mb-0"> <i class="fa fa-lock text-danger me-2"></i>Currently Blocked Halls </h3> <button class="btn btn-sm btn-outline-secondary" onclick="loadCurrentlyBlockedHalls()"> <i class="fa fa-refresh me-1"></i>Refresh </button> </div> <div class="block-content"> <div id="blockedListMsg" class="text-muted fst-italic mb-2">Loading…</div> <div class="table-responsive"> <table class="table table-bordered table-striped table-vcenter" id="blockedHallsTable" style="display:none;"> <thead class="thead-light"> <tr> <th class="text-center">#</th> <th class="text-center">Hall Name</th> <!-- <th class="text-center">Zone</th> --> <th class="text-center">Blocked From</th> <th class="text-center">Blocked To</th> <th class="text-center">Reason</th> <th class="text-center">Status</th> <th class="text-center">Action</th> </tr> </thead> <tbody id="blockedHallsBody"></tbody> </table> </div> </div> </div>
<!-- ═══════════════════════════════════════════════════════
STEP 1 – New Blocking Form ══════════════════════════════════════════════════════════ --> <div class="block block-rounded"> <div class="block-header block-header-default"> <h3 class="block-title"> <span class="step-badge">1</span> New GCC Hall Blocking – Selection </h3> </div> <div class="block-content"> <div class="row g-3">
<!-- Zone multiselect --> <div class="col-md-6"> <label class="fw-semibold">Zone <span class="req">*</span></label> <div id="zoneMultiselect" class="multiselect"> <div id="zoneSelectLabel" class="selectBox" onclick="toggleDropdown('zoneSelectOptions')"> <select class="form-select"> <option>Select Zones</option> </select> <div class="overSelect"></div> </div> <div id="zoneSelectOptions" class="checkboxes"></div> </div> </div>
<!-- Hall multiselect --> <div class="col-md-6"> <label class="fw-semibold">Hall(s) <span class="req">*</span></label> <div id="hallMultiselect" class="multiselect"> <div id="hallSelectLabel" class="selectBox" onclick="toggleDropdown('hallSelectOptions')"> <select class="form-select"> <option>Select Halls</option> </select> <div class="overSelect"></div> </div> <div id="hallSelectOptions" class="checkboxes"></div> </div> </div>
<!-- From Date --> <div class="col-md-3"> <label class="fw-semibold">From Date <span class="req">*</span></label> <input type="date" class="form-control" id="fromDate" onkeydown="return false;"> </div>
<!-- To Date --> <div class="col-md-3"> <label class="fw-semibold">To Date <span class="req">*</span></label> <input type="date" class="form-control" id="toDate" onkeydown="return false;"> </div>
<!-- Reason --> <div class="col-md-6"> <label class="fw-semibold">Reason for Blocking <span class="req">*</span></label> <input type="text" class="form-control" id="reasonforBlocking" placeholder="Enter reason for blocking" maxlength="300"> </div>
<!-- Document --> <div class="col-md-6"> <label class="fw-semibold"> Upload Document <span class="req">*</span> <small class="text-muted">(PDF only, max 2 MB)</small> </label> <input type="file" class="form-control" id="uploadDocument" accept=".pdf"> <div class="file-hint">Only PDF files up to 2 MB are accepted.</div> </div>
<!-- Description --> <div class="col-md-6"> <label class="fw-semibold">Document Description</label> <input type="text" class="form-control" id="documentDescription" placeholder="Optional description"> </div>
</div><!-- /row -->
<!-- Per-hall conflict alert --> <div id="conflictAlertBox" class="mt-3" style="display:none;"> <div class="alert alert-danger"> <strong><i class="fa fa-exclamation-circle me-1"></i>Blocking Conflict Detected</strong> <div id="conflictDetails" class="mt-2"></div> <hr class="my-2"> <p class="mb-0 text-danger fw-semibold"> Blocking cannot proceed because one or more halls are already blocked for the selected period. Please choose different dates or unblock the existing entry first from the table above. </p> </div> </div>
<div class="row mt-3"> <div class="col text-center"> <button type="button" class="btn btn-primary px-5" onclick="loadBookings()"> <i class="fa fa-search me-1"></i> View Bookings & Check Availability </button> </div> </div> </div> </div>
<!-- ═══════════════════════════════════════════════════════
STEP 2 – Bookings Preview + Decision ══════════════════════════════════════════════════════════ --> <div id="step2Section">
<!-- Booking Table --> <div class="block block-rounded"> <div class="block-header block-header-default"> <h3 class="block-title"> <span class="step-badge">2</span> Existing Bookings in Selected Date Range </h3> </div> <div class="block-content"> <div id="noBookingsMsg" class="alert alert-success" style="display:none;"> <i class="fa fa-check-circle me-1"></i> No existing bookings found for the selected halls and dates. </div> <div id="bookingsFoundMsg" class="alert alert-warning" style="display:none;"> <i class="fa fa-exclamation-triangle me-1"></i> <strong>Attention:</strong> The following bookings exist within the selected date range. Choose how to proceed below. </div> <!-- Today-booking warning --> <div id="todayBookingWarning" class="alert alert-danger mt-2" style="display:none;"> <i class="fa fa-exclamation-circle me-1"></i> <strong>Note:</strong> One or more bookings are for <strong>today's date</strong> and <strong>cannot be cancelled</strong>. Use <em>Block Without Cancelling Bookings</em> to block dates without affecting any bookings. </div> <div class="table-responsive" id="bookingTableWrapper" style="display:none;"> <table class="table table-bordered table-striped table-vcenter"> <thead class="thead-light"> <tr> <th class="text-center"> <input type="checkbox" id="selectAllBookings" title="Select / Deselect all cancellable bookings"> Select All </th> <th class="text-center">#</th> <th class="text-center">Hall Name</th> <th class="text-center">Zone</th> <th class="text-center">Division</th> <th class="text-center">Reason</th> <th class="text-center">Days</th> <th class="text-center">Amount (₹)</th> <th class="text-center">From</th> <th class="text-center">To</th> <th class="text-center">Status</th> </tr> </thead> <tbody id="bookingTableBody"></tbody> </table> </div> </div> </div>
<!-- Decision (bookings exist) --> <div class="block block-rounded" id="blockActionCard"> <div class="block-header block-header-default"> <h3 class="block-title"> <span class="step-badge">3</span> Choose Blocking Action </h3> </div> <div class="block-content"> <div class="decision-card"> <p class="mb-3 fw-semibold">How would you like to proceed?</p> <div class="d-flex flex-wrap gap-3"> <button type="button" class="btn btn-danger btn-decision" id="cancelAndBlockBtn" onclick="proceedCancelAndBlock()"> <i class="fa fa-ban me-2"></i>Cancel Bookings & Block Dates </button> <button type="button" class="btn btn-warning btn-decision text-dark" onclick="proceedBlockOnly()"> <i class="fa fa-lock me-2"></i>Block Without Cancelling Bookings </button> </div> <p class="mt-3 mb-0 text-muted small"> <strong>Cancel & Block</strong> — marks existing bookings as GCC-blocked and initiates refund. | <strong>Block Only</strong> — marks dates as blocked; existing bookings remain untouched. </p> </div> </div> </div>
<!-- Confirm (no bookings exist) --> <div class="block block-rounded" id="noConflictCard"> <div class="block-content text-center py-3"> <p class="text-muted mb-3">No booking conflicts found. You can safely block the selected dates.</p> <button type="button" class="btn btn-success px-5" onclick="proceedBlockOnly()"> <i class="fa fa-check me-2"></i> Confirm & Block Dates </button> </div> </div>
</div><!-- /#step2Section -->
</div><!-- /content --> </main>
<div th:replace="~{fragments/resetpassword :: div}"></div> <footer th:replace="~{fragments/footer2 :: footer}"></footer> </div>
<script th:replace="~{fragments/loginscript :: script}"></script> <script> /* ═══════════════════════════════════════════════════════════════ CONSTANTS (HQ namespace) ═══════════════════════════════════════════════════════════════ */ const HQ_BASE = '/communityhall/headquarters/api'; const CANCEL_ALL_URL = HQ_BASE + '/cancelAll'; const BLOCK_ONLY_URL = HQ_BASE + '/blockWithoutCancel'; const FILTER_URL = HQ_BASE + '/filter'; const UNBLOCK_URL = HQ_BASE + '/cancelByid'; const BLOCKED_URL = HQ_BASE + '/blockedevents'; const ZONE_URL = '/communityhall/user/api/getZone'; const HALLS_URL = '/communityhall/user/api/hallsbyzone'; // No zones param -> returns all halls (same endpoint) const ALL_HALLS_URL = '/communityhall/user/api/hallsbyzone'; const MAX_FILE_BYTES = 2 * 1024 * 1024;
/* ═══════════════════════════════════════════════════════════════ PAGE INIT ═══════════════════════════════════════════════════════════════ */ document.addEventListener('DOMContentLoaded', function () { const today = new Date(); const pad = n => String(n).padStart(2, '0'); const todayStr = `${today.getFullYear()}-${pad(today.getMonth() + 1)}-${pad(today.getDate())}`; document.getElementById('fromDate').min = todayStr; document.getElementById('toDate').min = todayStr;
document.getElementById('fromDate').addEventListener('change', function () { const toEl = document.getElementById('toDate'); toEl.min = this.value; toEl.value = ''; });
document.getElementById('uploadDocument').addEventListener('change', function () { const file = this.files[0]; if (!file) return; if (file.type !== 'application/pdf') { Swal.fire('Invalid File', 'Only PDF files are accepted.', 'error'); this.value = ''; return; } if (file.size > MAX_FILE_BYTES) { Swal.fire('File Too Large', 'Document must be 2 MB or smaller.', 'error'); this.value = ''; } });
window.addEventListener('click', function (e) { let t = e.target; while (t) { if (t === document.getElementById('zoneMultiselect') || t === document.getElementById('hallMultiselect')) return; t = t.parentNode; } closeDropdown('zoneSelectOptions'); closeDropdown('hallSelectOptions'); });
getZone(); loadCurrentlyBlockedHalls(); });
/* ═══════════════════════════════════════════════════════════════ LOAD CURRENTLY BLOCKED HALLS ═══════════════════════════════════════════════════════════════ */ /* ── Format a java.util.Date JSON value (epoch ms or ISO string) to a yyyy-MM-dd string using LOCAL date (no timezone shift) ── */ function fmtDate(val) { if (!val) return '-'; var d; if (typeof val === 'number') { // Jackson serialized as epoch milliseconds d = new Date(val); } else { // ISO string e.g. "2026-07-01T00:00:00.000+05:30" d = new Date(val); } if (isNaN(d.getTime())) return String(val).substring(0, 10); // Use local date parts to avoid UTC-midnight shifting to previous day var y = d.getFullYear(); var m = String(d.getMonth() + 1).padStart(2, '0'); var day = String(d.getDate()).padStart(2, '0'); return y + '-' + m + '-' + day; }
function loadCurrentlyBlockedHalls() { $('#blockedListMsg').text('Loading currently blocked halls…').show(); $('#blockedHallsTable').hide();
const today = new Date(); // Use 5 years in the PAST as fromDate so blocks that started before // today are still returned by the backend and shown correctly. const past = new Date(today.getFullYear() - 5, 0, 1); const far = new Date(today.getFullYear() + 5, 11, 31); const fmt = d => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
// Step 1: get all halls to build id→name map $.get(ALL_HALLS_URL) .done(function (allHalls) { const hallMap = {}; (allHalls || []).forEach(h => { const id = h.hallId || h.hall_id; const name = h.hallName || h.hall_name || ('Hall #' + id); if (id) hallMap[String(id)] = name; });
const allHallIds = (allHalls || []) .map(h => h.hallId || h.hall_id) .filter(Boolean);
if (!allHallIds.length) { $('#blockedListMsg').text('No halls found.'); return; }
// Step 2: get all blocked events across all halls // fromDate=past ensures blocks that started before today are included $.ajax({ url: BLOCKED_URL, type: 'GET', // data: { fromDate: fmt(past), toDate: fmt(far), hallIds: allHallIds.join(',') }, success: function (blocked) { const list = blocked || []; if (!list.length) { $('#blockedListMsg').text('No halls are currently blocked.').show(); $('#blockedHallsTable').hide(); return; }
$('#blockedListMsg').hide(); const tbody = document.getElementById('blockedHallsBody'); tbody.innerHTML = '';
const seen = new Set(); let rowNum = 0; list.forEach(function (item) { if (seen.has(item.blockingId)) return; seen.add(item.blockingId); rowNum++;
const hallIds = (item.hallId || '').split(',').map(s => s.trim()).filter(Boolean); const hallNames = hallIds.map(id => hallMap[id] || ('Hall #' + id)).join(', '); const startDate = fmtDate(item.startDate); const endDate = fmtDate(item.endDate);
const tr = document.createElement('tr'); tr.innerHTML = ` <td class="text-center">${rowNum}</td> <td class="text-center">${escHtml(hallNames)}</td> <!-- <td class="text-center">${escHtml(item.zone || '-')}</td> --> <td class="text-center">${startDate}</td> <td class="text-center">${endDate}</td> <td class="text-center">${escHtml(item.blockingReason || '-')}</td> <td class="text-center"><span class="badge bg-danger">BLOCKED</span></td> <td class="text-center"> <button class="btn btn-sm btn-outline-success" onclick="unblockEntry(${item.blockingId}, '${startDate}', '${endDate}')"> <i class="fa fa-unlock me-1"></i>Unblock </button> </td> `; tbody.appendChild(tr); });
// ── Init / reinit DataTable ────────────────────────────────────── if ($.fn.DataTable.isDataTable('#blockedHallsTable')) { $('#blockedHallsTable').DataTable().destroy(); } $('#blockedHallsTable').show(); $('#blockedHallsTable').DataTable({ pageLength: 10, lengthChange: false, order: [], dom: 'Bfrtip', buttons: [ { extend: 'excelHtml5', text: '<i class="fa fa-file-excel-o me-1"></i>Excel', className: 'btn btn-sm btn-success me-1', title: 'Currently Blocked Halls' }, { extend: 'pdfHtml5', text: '<i class="fa fa-file-pdf-o me-1"></i>PDF', className: 'btn btn-sm btn-danger', title: 'Currently Blocked Halls', orientation: 'landscape', pageSize: 'A4' } ] }); }, error: function () { $('#blockedListMsg').text('Failed to load blocked halls. Please refresh.'); } }); }) .fail(function () { $('#blockedListMsg').text('Failed to load hall list.'); }); }
/* ═══════════════════════════════════════════════════════════════ UNBLOCK ENTRY ═══════════════════════════════════════════════════════════════ */ function unblockEntry(blockingId, fromDate, endDate) { Swal.fire({ title: 'Unblock these dates?', html: `This will unblock <strong>${fromDate}</strong> to <strong>${endDate}</strong> (ID: ${blockingId}).<br> Existing bookings linked to this entry will be restored to active status.`, icon: 'question', showCancelButton: true, confirmButtonColor: '#198754', cancelButtonColor: '#6c757d', confirmButtonText: '<i class="fa fa-unlock me-1"></i> Yes, Unblock', cancelButtonText: 'Cancel' }).then(result => { if (!result.isConfirmed) return; Swal.fire({ title: 'Processing…', allowOutsideClick: false, didOpen: () => Swal.showLoading() }); $.ajax({ url: UNBLOCK_URL, type: 'GET', data: { blockingId: blockingId, fromDate: fromDate, endDate: endDate }, success: function () { Swal.fire({ icon: 'success', title: 'Unblocked', text: 'The halls have been unblocked successfully.' }) .then(() => { location.reload(); }); }, error: function (xhr) { Swal.fire({ icon: 'error', title: 'Unblock Failed', text: xhr.responseText || 'An error occurred. Please try again.' }); } }); }); }
/* ═══════════════════════════════════════════════════════════════ HELPERS – get selected values ═══════════════════════════════════════════════════════════════ */ function getSelectedZones() { return Array.from(document.querySelectorAll('#zoneSelectOptions input:checked')) .map(cb => cb.value).filter(v => v && v !== 'All Zones'); } function getSelectedHalls() { return Array.from(document.querySelectorAll('#hallSelectOptions input:checked')) .map(cb => ({ id: cb.value, name: cb.dataset.name || cb.value })) .filter(h => !isNaN(h.id) && h.id !== ''); }
/* ═══════════════════════════════════════════════════════════════ FORM VALIDATION ═══════════════════════════════════════════════════════════════ */ function validateForm() { const zones = getSelectedZones(); const halls = getSelectedHalls(); const from = document.getElementById('fromDate').value; const to = document.getElementById('toDate').value; const reason = document.getElementById('reasonforBlocking').value.trim(); const fileEl = document.getElementById('uploadDocument');
if (!zones.length) { Swal.fire('Validation', 'Please select at least one Zone.', 'error'); return false; } if (!halls.length) { Swal.fire('Validation', 'Please select at least one Hall.', 'error'); return false; } if (!from) { Swal.fire('Validation', 'From Date is required.', 'error'); return false; } if (!to) { Swal.fire('Validation', 'To Date is required.', 'error'); return false; } if (new Date(to) < new Date(from)) { Swal.fire('Validation', 'To Date cannot be earlier than From Date.', 'error'); return false; } if (!reason) { Swal.fire('Validation', 'Reason for Blocking is mandatory.', 'error'); return false; } if (!fileEl.files || !fileEl.files[0]) { Swal.fire('Validation', 'Please upload a supporting document (PDF, max 2 MB).', 'error'); return false; } const file = fileEl.files[0]; if (file.type !== 'application/pdf') { Swal.fire('Invalid File', 'Only PDF files are allowed.', 'error'); fileEl.value = ''; return false; } if (file.size > MAX_FILE_BYTES) { Swal.fire('File Too Large', 'Document must be 2 MB or smaller.', 'error'); fileEl.value = ''; return false; } return true; }
/* ═══════════════════════════════════════════════════════════════ STEP 1 – VIEW BOOKINGS + per-hall conflict detection ═══════════════════════════════════════════════════════════════ */ function loadBookings() { if (!validateForm()) return;
$('#step2Section').hide(); $('#conflictAlertBox').hide(); $('#conflictDetails').html('');
const halls = getSelectedHalls(); const from = document.getElementById('fromDate').value; const to = document.getElementById('toDate').value; const hallIds = halls.map(h => h.id);
$.ajax({ url: FILTER_URL, type: 'GET', data: { hallIds: hallIds.join(','), fromDate: from, toDate: to }, success: function (response) { const bookings = response.bookings || []; const blockedEvents = response.blockedEvents || [];
// ── Per-hall conflict check ───────────────────────── if (blockedEvents && blockedEvents.length > 0) { const conflictHallIds = new Set(); blockedEvents.forEach(ev => { (ev.hallId || '').split(',').forEach(id => conflictHallIds.add(id.trim())); });
let html = '<ul class="mb-0">'; halls.forEach(function (h) { if (conflictHallIds.has(String(h.id))) { html += `<li> <span class="conflict-pill pill-blocked">🔒 ${escHtml(h.name)}</span> is <strong>already blocked</strong> for the period <strong>${from}</strong> to <strong>${to}</strong>. Please unblock it first using the table above, or select different dates. </li>`; } }); html += '</ul>';
$('#conflictDetails').html(html); $('#conflictAlertBox').show(); document.getElementById('conflictAlertBox').scrollIntoView({ behavior: 'smooth' }); return; // STOP – do not proceed to step 2 }
// ── No conflicts – show bookings ──────────────────── renderBookingsTable(bookings); $('#step2Section').show();
if (bookings.length === 0) { $('#noBookingsMsg').show(); $('#bookingsFoundMsg').hide(); $('#bookingTableWrapper').hide(); $('#blockActionCard').hide(); $('#noConflictCard').show(); } else { $('#noBookingsMsg').hide(); $('#bookingsFoundMsg').show(); $('#bookingTableWrapper').show(); $('#blockActionCard').show(); $('#noConflictCard').hide(); } document.getElementById('step2Section').scrollIntoView({ behavior: 'smooth' }); }, error: function (xhr) { console.error('Filter error:', xhr.responseText); Swal.fire('Error', 'Failed to check hall availability. Please try again.', 'error'); } }); }
function renderBookingsTable(bookings) { const tbody = document.getElementById('bookingTableBody'); tbody.innerHTML = ''; $('#todayBookingWarning').hide(); $('#selectAllBookings').prop('checked', true);
const todayStr = (() => { const d = new Date(); return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); })();
let hasTodayBooking = false; let allToday = true;
bookings.forEach(function (b, idx) { const bStart = (b.startDate || '').slice(0, 10); const isToday = (bStart === todayStr); if (isToday) hasTodayBooking = true; else allToday = false;
const cbDisabled = isToday ? 'disabled title="Cannot cancel today's active booking"' : ''; const cbChecked = isToday ? '' : 'checked'; const statusBadge = isToday ? '<span class="badge bg-warning text-dark">Today – Cannot Cancel</span>' : '<span class="badge bg-primary">User Booked</span>';
const tr = document.createElement('tr'); tr.setAttribute('data-booking-id', b.bookingId || ''); tr.innerHTML = ` <td class="text-center"> <input type="checkbox" class="booking-cb" data-booking-id="${b.bookingId || ''}" ${cbChecked} ${cbDisabled}> </td> <td class="text-center">${idx + 1}</td> <td class="text-center">${escHtml(b.hallDetails ? b.hallDetails.hallName : '-')}</td> <td class="text-center">${escHtml(b.hallDetails ? b.hallDetails.zone : '-')}</td> <td class="text-center">${escHtml(b.hallDetails ? b.hallDetails.division : '-')}</td> <td class="text-center">${escHtml(b.reason || '-')}</td> <td class="text-center">${b.noOfDays || '-'}</td> <td class="text-center">${b.hallAmount || '-'}</td> <td class="text-center">${escHtml(b.startDate || '-')}</td> <td class="text-center">${escHtml(b.endDate || '-')}</td> <td class="text-center">${statusBadge}</td> `; tbody.appendChild(tr); });
// Wire select-all checkbox const selAll = document.getElementById('selectAllBookings'); if (selAll) { // Select All → toggle every enabled row checkbox selAll.onchange = function () { document.querySelectorAll('.booking-cb:not(:disabled)') .forEach(cb => { cb.checked = selAll.checked; }); };
// Individual row checkboxes → sync Select All state document.querySelectorAll('.booking-cb:not(:disabled)').forEach(cb => { cb.addEventListener('change', function () { const total = document.querySelectorAll('.booking-cb:not(:disabled)').length; const checked = document.querySelectorAll('.booking-cb:not(:disabled):checked').length; selAll.checked = (total > 0 && checked === total); }); }); }
if (hasTodayBooking) { $('#todayBookingWarning').show(); }
// Disable Cancel & Block button if ALL bookings are today's if (allToday && bookings.length > 0) { $('#cancelAndBlockBtn') .prop('disabled', true) .attr('title', 'Cannot cancel — all conflicting bookings are for today. Use Block Without Cancelling instead.'); } else { $('#cancelAndBlockBtn').prop('disabled', false).removeAttr('title'); } }
/* ═══════════════════════════════════════════════════════════════ STEP 3A – Cancel bookings + block ═══════════════════════════════════════════════════════════════ */ function proceedCancelAndBlock() { // Count enabled+checked booking IDs const selectedIds = []; document.querySelectorAll('.booking-cb:not(:disabled):checked') .forEach(cb => { if (cb.dataset.bookingId) selectedIds.push(cb.dataset.bookingId); });
if (selectedIds.length === 0) { Swal.fire({ icon: 'warning', title: 'No Bookings Selected', html: 'Please tick at least one booking to cancel, or use <em>Block Without Cancelling Bookings</em>.' }); return; }
const todayCount = document.querySelectorAll('.booking-cb:disabled').length; const extraNote = todayCount > 0 ? `<br><small class="text-warning">⚠ ${todayCount} today's booking(s) will be skipped and remain active.</small>` : '';
Swal.fire({ title: 'Cancel Bookings & Block?', html: `This will <strong>cancel ${selectedIds.length} selected booking(s)</strong> and block the halls.<br><br> <span class="text-danger fw-bold">This action cannot be undone.</span>${extraNote}`, icon: 'warning', showCancelButton: true, confirmButtonColor: '#dc3545', cancelButtonColor: '#6c757d', confirmButtonText: '<i class="fa fa-ban me-1"></i> Yes, Cancel & Block', cancelButtonText: 'Go Back' }).then(result => { if (result.isConfirmed) submitBlockRequest(true); }); }
/* ═══════════════════════════════════════════════════════════════ STEP 3B – Block only (no cancellation of existing bookings) ═══════════════════════════════════════════════════════════════ */ function proceedBlockOnly() { Swal.fire({ title: 'Block Dates?', html: `The selected halls will be blocked for the chosen period.<br> <strong>Existing bookings will NOT be cancelled.</strong>`, icon: 'info', showCancelButton: true, confirmButtonColor: '#ffc107', cancelButtonColor: '#6c757d', confirmButtonText: '<i class="fa fa-lock me-1"></i> Yes, Block', cancelButtonText: 'Go Back' }).then(result => { if (result.isConfirmed) submitBlockOnlyRequest(); }); }
/* ═══════════════════════════════════════════════════════════════ SUBMIT (Cancel & Block) — POSTs to /cancelAll ═══════════════════════════════════════════════════════════════ */ function submitBlockRequest(cancelExisting) { const halls = getSelectedHalls(); const from = document.getElementById('fromDate').value; const to = document.getElementById('toDate').value; const reason = document.getElementById('reasonforBlocking').value.trim(); const desc = document.getElementById('documentDescription').value.trim(); const fileEl = document.getElementById('uploadDocument');
let selectedBookings = []; if (cancelExisting) { // Only collect IDs from checked & enabled checkboxes (skips today's bookings) document.querySelectorAll('.booking-cb:not(:disabled):checked') .forEach(cb => { if (cb.dataset.bookingId) selectedBookings.push(cb.dataset.bookingId); }); }
const fd = new FormData(); fd.append('fromDate', from); fd.append('toDate', to); fd.append('hallID', halls.map(h => h.id).join(',')); fd.append('reasonforblocking', reason); if (desc) fd.append('description', desc); if (fileEl.files && fileEl.files[0]) fd.append('document', fileEl.files[0]); if (selectedBookings.length) fd.append('selectedBookings', selectedBookings.join(','));
Swal.fire({ title: 'Processing…', allowOutsideClick: false, didOpen: () => Swal.showLoading() });
$.ajax({ url: CANCEL_ALL_URL, type: 'POST', data: fd, processData: false, contentType: false, success: function () { Swal.fire({ icon: 'success', title: 'Success', text: 'Bookings have been cancelled and the dates are now blocked.' }).then(() => location.reload()); }, error: function (xhr) { Swal.fire({ icon: 'error', title: 'Request Failed', text: 'Something went wrong. Please try again.' }); } }); }
/* ═══════════════════════════════════════════════════════════════ SUBMIT (Block Without Cancelling) — POSTs to /blockWithoutCancel ═══════════════════════════════════════════════════════════════ */ function submitBlockOnlyRequest() { const halls = getSelectedHalls(); const from = document.getElementById('fromDate').value; const to = document.getElementById('toDate').value; const reason = document.getElementById('reasonforBlocking').value.trim(); const desc = document.getElementById('documentDescription').value.trim(); const fileEl = document.getElementById('uploadDocument');
const fd = new FormData(); fd.append('fromDate', from); fd.append('toDate', to); fd.append('hallID', halls.map(h => h.id).join(',')); fd.append('reasonforblocking', reason); if (desc) fd.append('description', desc); if (fileEl.files && fileEl.files[0]) fd.append('document', fileEl.files[0]);
Swal.fire({ title: 'Processing…', allowOutsideClick: false, didOpen: () => Swal.showLoading() });
$.ajax({ url: BLOCK_ONLY_URL, type: 'POST', data: fd, processData: false, contentType: false, success: function () { Swal.fire({ icon: 'success', title: 'Success', text: 'The selected dates have been blocked. Existing bookings were not affected.' }).then(() => location.reload()); }, error: function (xhr) { Swal.fire({ icon: 'error', title: 'Request Failed', text: 'Something went wrong. Please try again.' }); } }); }
/* ═══════════════════════════════════════════════════════════════ ZONE / HALL MULTISELECT ═══════════════════════════════════════════════════════════════ */ function getZone() { $.get(ZONE_URL).done(function (data) { const uniqueZones = [...new Set(data.map(i => i.zone))]; const menu = document.getElementById('zoneSelectOptions'); menu.innerHTML = ''; appendCheckbox(menu, 'allZonesCb', 'All Zones', 'All Zones', null, checked => toggleAllZones(checked)); uniqueZones.forEach(zone => { appendCheckbox(menu, `zone_${zone}`, zone, zone, null, () => { loadHallsbyzone(); updateLabel('zoneSelectOptions', 'zoneSelectLabel'); }); }); }); } function toggleAllZones(checked) { document.querySelectorAll('#zoneSelectOptions input').forEach(cb => { cb.checked = checked; }); loadHallsbyzone(); updateLabel('zoneSelectOptions', 'zoneSelectLabel'); } function loadHallsbyzone() { const selected = getSelectedZones(); const menu = document.getElementById('hallSelectOptions'); if (!selected.length) { menu.innerHTML = ''; return; } $.ajax({ url: HALLS_URL, method: 'GET', data: { zones: selected.join(',') }, success: function (halls) { menu.innerHTML = ''; appendCheckbox(menu, 'allHallsCb', 'All Halls', '', null, checked => toggleAllHalls(checked)); halls.forEach(hall => { const id = hall.hallId || hall.hall_id; const name = (hall.hallName || hall.hall_name || '').trim().replace(/^, */, ''); appendCheckbox(menu, `hall_${id}`, name, id, name, () => { updateLabel('hallSelectOptions', 'hallSelectLabel'); }); }); } }); } function appendCheckbox(parent, id, text, value, dataName, onChange) { const label = document.createElement('label'); const cb = document.createElement('input'); cb.type = 'checkbox'; cb.id = id; cb.value = value; if (dataName) cb.dataset.name = dataName; cb.addEventListener('change', function () { onChange(this.checked); }); label.textContent = text; label.prepend(cb); parent.appendChild(label); } function toggleAllHalls(checked) { document.querySelectorAll('#hallSelectOptions input').forEach(cb => { cb.checked = checked; }); updateLabel('hallSelectOptions', 'hallSelectLabel'); } function updateLabel(optId, labelId) { const opt0 = document.getElementById(labelId).getElementsByTagName('option')[0]; const names = Array.from(document.querySelectorAll(`#${optId} input:checked`)) .map(cb => cb.dataset.name || cb.value).filter(v => v && v !== 'All Zones' && v !== ''); opt0.innerText = names.length ? names.join(', ') : 'Nothing selected'; } function toggleDropdown(id) { const el = document.getElementById(id); el.style.display = el.style.display === 'block' ? 'none' : 'block'; } function closeDropdown(id) { document.getElementById(id).style.display = 'none'; } function escHtml(str) { if (!str) return ''; return String(str).replace(/&/g, '&').replace(/</g, '<') .replace(/>/g, '>').replace(/"/g, '"'); } </script>
<!-- DataTables 1.13 + Buttons extension (Excel / PDF) --> <link rel="stylesheet" href="https://cdn.datatables.net/1.13.8/css/dataTables.bootstrap5.min.css"> <link rel="stylesheet" href="https://cdn.datatables.net/buttons/2.4.2/css/buttons.bootstrap5.min.css"> <script src="https://cdn.datatables.net/1.13.8/js/jquery.dataTables.min.js"></script> <script src="https://cdn.datatables.net/1.13.8/js/dataTables.bootstrap5.min.js"></script> <script src="https://cdn.datatables.net/buttons/2.4.2/js/dataTables.buttons.min.js"></script> <script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.bootstrap5.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/pdfmake.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/vfs_fonts.js"></script> <script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.html5.min.js"></script></body>
</html>
|