You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1167 lines
61 KiB

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/head-officer :: 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;
}
.pill-free {
background: #e8f5e9;
color: #198754;
border: 1px solid #198754;
}
/* ── 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-officer :: nav}"></nav>
<header th:replace="~{fragments/header-officer :: header}"></header>
<main id="main-container">
<div class="content">
<!-- ═══════════════════════════════════════════════════════
PANEL A – Currently Blocked Halls (loads on page 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 -->
<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>
<!-- Shown only when officer has exactly 1 hall (prefilled, readonly) -->
<input type="text" id="zonePrefilled" class="form-control" readonly
style="display:none; background:#f8f9fa; cursor:default;">
</div>
<!-- Hall -->
<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" id="hallSelect">
<option>Select Halls</option>
</select>
<div class="overSelect"></div>
</div>
<div id="hallSelectOptions" class="checkboxes"></div>
</div>
<!-- Shown only when officer has exactly 1 hall (prefilled, readonly) -->
<input type="text" id="hallPrefilled" class="form-control" readonly
style="display:none; background:#f8f9fa; cursor:default;">
</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" name="document"
accept=".pdf">
<div class="file-hint">Only PDF files up to 2 MB are accepted.</div>
</div>
<!-- Document 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.
</p>
</div>
</div>
<div class="row mt-3">
<div class="col text-center">
<button type="button" class="btn btn-primary px-5" id="viewButton"
onclick="loadBookings()">
<i class="fa fa-search me-1"></i> View Bookings &amp; 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">
&nbsp;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 (&#x20B9;)</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 &amp; 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 &amp; Block</strong> — marks existing bookings as GCC-blocked and
initiates refund.&nbsp;|&nbsp;
<strong>Block Only</strong> — marks dates as blocked; existing bookings remain
untouched.
</p>
</div>
</div>
</div>
<!-- Confirm (no bookings) -->
<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 &amp; 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 th:inline="javascript">
/* ═══════════════════════════════════════════════════════════════
CONSTANTS (Officer namespace)
═══════════════════════════════════════════════════════════════ */
const ADMIN_BASE = '/communityhall/api/officer';
const CANCEL_ALL_URL = ADMIN_BASE + '/cancelAll';
const BLOCK_ONLY_URL = ADMIN_BASE + '/blockWithoutCancel';
const FILTER_URL = ADMIN_BASE + '/filter';
const UNBLOCK_URL = ADMIN_BASE + '/cancelByid';
const BLOCKED_URL = ADMIN_BASE + '/blockedevents';
const ZONE_URL = '/communityhall/user/api/getZone';
const HALLS_URL = '/communityhall/user/api/hallsbyzone';
// No zones param → returns all halls (reuses same endpoint)
const ALL_HALLS_URL = '/communityhall/user/api/hallsbyzone';
const MAX_FILE_BYTES = 2 * 1024 * 1024;
/* ── Officer's allocated hall IDs & zone (injected from session via Thymeleaf) ── */
const OFFICER_HALL_IDS = new Set(
/*[[${hallIds}]]*/[]
);
const OFFICER_ZONE = /*[[${zone}]]*/ null;
/* ═══════════════════════════════════════════════════════════════
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');
});
loadOfficerHalls();
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 = new Date(val);
if (isNaN(d.getTime())) return String(val).substring(0, 10);
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')}`;
// ── Fetch all halls, then scope to officer's allocated hall IDs ──
$.get(ALL_HALLS_URL)
.done(function (allHalls) {
// Build hallMap for ALL halls (needed to resolve names from blocked entries)
const hallMap = {};
(allHalls || []).forEach(h => {
const id = h.hallId || h.hall_id;
const name = h.hallName || h.hall_name || ('Hall#' + id);
hallMap[String(id)] = name;
});
// Filter to only officer's allocated halls
const officerHalls = (allHalls || []).filter(h => {
const id = h.hallId || h.hall_id;
return OFFICER_HALL_IDS.size === 0 || OFFICER_HALL_IDS.has(id) || OFFICER_HALL_IDS.has(String(id));
});
if (!officerHalls.length) {
$('#blockedListMsg').text('No allocated halls found.').show();
return;
}
const officerHallIdSet = new Set(officerHalls.map(h => String(h.hallId || h.hall_id)));
$.ajax({
url: BLOCKED_URL,
type: 'GET',
success: function (blocked) {
// Filter blocked entries to only those involving the officer's halls
const list = (blocked || []).filter(function (item) {
const ids = (item.hallId || '').split(',').map(s => s.trim()).filter(Boolean);
return ids.some(id => officerHallIdSet.has(id));
});
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(', ');
// Use fmtDate() to correctly parse java.util.Date JSON values
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">${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
═══════════════════════════════════════════════════════════════ */
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 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.'
});
}
});
});
}
/* ═══════════════════════════════════════════════════════════════
HELPERS
═══════════════════════════════════════════════════════════════ */
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 !== '');
}
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 check)
═══════════════════════════════════════════════════════════════ */
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 || [];
if (blockedEvents && blockedEvents.length > 0) {
// Build per-hall conflict list
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">&#128274; ${escHtml(h.name)}</span>
is <strong>already blocked</strong> for <strong>${from}</strong> to <strong>${to}</strong>.
Please unblock it first or choose different dates.
</li>`;
}
});
html += '</ul>';
$('#conflictDetails').html(html);
$('#conflictAlertBox').show();
document.getElementById('conflictAlertBox').scrollIntoView({ behavior: 'smooth' });
return; // stop – do not show blocking actions
}
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&#39;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 THEN block
═══════════════════════════════════════════════════════════════ */
function proceedCancelAndBlock() {
// Collect 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">&#9888; ${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 booking cancellation)
═══════════════════════════════════════════════════════════════ */
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)
═══════════════════════════════════════════════════════════════ */
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); });
}
console.log("selectedBookings...." + selectedBookings);
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) — calls smart endpoint
═══════════════════════════════════════════════════════════════ */
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 – Officer variant
Loads the zone and hall fields pre-filled with the officer's
allocated zone (from session) and their assigned halls.
Key facts from the backend:
• OFFICER_ZONE – injected by Thymeleaf from session "zone"
• OFFICER_HALL_IDS – injected by Thymeleaf from session "hall_ids"
• HALLS_URL (/hallsbyzone?zones=X) returns { hallId, hallName }
(getAllHalls SQL has NO zone column, so zone must come from session)
═══════════════════════════════════════════════════════════════ */
function loadOfficerHalls() {
if (!OFFICER_ZONE) {
document.getElementById('zoneSelectOptions').innerHTML =
'<label class="text-danger px-2">Zone not assigned to this account.</label>';
return;
}
// Fetch halls for the officer's zone → returns { hallId, hallName }
$.get(HALLS_URL, { zones: OFFICER_ZONE }).done(function (hallsInZone) {
// Keep only the officer's allocated hall IDs
const myHalls = (hallsInZone || []).filter(function (h) {
const id = h.hallId || h.hall_id;
return OFFICER_HALL_IDS.size === 0 ||
OFFICER_HALL_IDS.has(id) ||
OFFICER_HALL_IDS.has(Number(id)) ||
OFFICER_HALL_IDS.has(String(id));
});
if (!myHalls.length) {
document.getElementById('zoneSelectOptions').innerHTML =
'<label class="text-danger px-2">No allocated halls found for zone: ' + OFFICER_ZONE + '</label>';
return;
}
/* ── SINGLE HALL: show readonly prefilled inputs ── */
if (myHalls.length === 1) {
const hall = myHalls[0];
const zone = OFFICER_ZONE;
const hallName = (hall.hallName || hall.hall_name || '').trim().replace(/^, */, '');
const hallId = String(hall.hallId || hall.hall_id);
// Zone – hide multiselect, show readonly text input
document.getElementById('zoneMultiselect').style.display = 'none';
const zonePrefilled = document.getElementById('zonePrefilled');
zonePrefilled.value = zone;
zonePrefilled.style.display = '';
// Inject hidden checked checkbox so getSelectedZones() works
const zoneMenu = document.getElementById('zoneSelectOptions');
zoneMenu.innerHTML = '';
const zCb = document.createElement('input');
zCb.type = 'checkbox'; zCb.value = zone; zCb.checked = true;
zCb.style.display = 'none';
zoneMenu.appendChild(zCb);
// Hall – hide multiselect, show readonly text input
document.getElementById('hallMultiselect').style.display = 'none';
const hallPrefilled = document.getElementById('hallPrefilled');
hallPrefilled.value = hallName;
hallPrefilled.style.display = '';
// Inject hidden checked checkbox so getSelectedHalls() works
const hallMenu = document.getElementById('hallSelectOptions');
hallMenu.innerHTML = '';
const hCb = document.createElement('input');
hCb.type = 'checkbox'; hCb.value = hallId; hCb.checked = true;
hCb.dataset.name = hallName;
hCb.style.display = 'none';
hallMenu.appendChild(hCb);
return; // done
}
/* ── MULTIPLE HALLS: populate dropdowns ── */
// Zone dropdown – officer always belongs to one zone
const zoneMenu = document.getElementById('zoneSelectOptions');
zoneMenu.innerHTML = '';
appendCheckbox(zoneMenu, 'zone_' + OFFICER_ZONE, OFFICER_ZONE, OFFICER_ZONE, null, function () {
updateLabel('zoneSelectOptions', 'zoneSelectLabel');
});
const zoneCb = document.querySelector('#zoneSelectOptions input');
if (zoneCb) { zoneCb.checked = true; }
updateLabel('zoneSelectOptions', 'zoneSelectLabel');
// Hall dropdown – all allocated halls
populateHallDropdown(myHalls);
}).fail(function () {
document.getElementById('zoneSelectOptions').innerHTML =
'<label class="text-danger px-2">Failed to load halls. Please refresh.</label>';
});
}
function toggleAllZones(checked, myHalls) {
document.querySelectorAll('#zoneSelectOptions input').forEach(cb => { cb.checked = checked; });
loadHallsForSelectedZones(myHalls);
updateLabel('zoneSelectOptions', 'zoneSelectLabel');
}
function loadHallsForSelectedZones(myHalls) {
const selected = getSelectedZones();
const menu = document.getElementById('hallSelectOptions');
if (!selected.length) {
// No zone selected — show all officer's halls
populateHallDropdown(myHalls);
return;
}
// Show only halls from selected zones
const filtered = myHalls.filter(h => selected.includes(h.zone || h.zoneName));
populateHallDropdown(filtered);
}
function populateHallDropdown(halls) {
const menu = document.getElementById('hallSelectOptions');
menu.innerHTML = '';
if (!halls.length) return;
appendCheckbox(menu, 'allHallsCb', 'All Halls', '', null,
checked => toggleAllHalls(checked));
halls.forEach(hall => {
const name = (hall.hallName || '').trim().replace(/^, */, '');
appendCheckbox(menu, `hall_${hall.hallId}`, name, hall.hallId, name, () => {
updateLabel('hallSelectOptions', 'hallSelectLabel');
});
});
}
/* Legacy alias — kept for safety in case any other code calls getZone() */
function getZone() { loadOfficerHalls(); }
/* Legacy alias — kept for safety */
function loadHallsbyzone() {
$.get(ALL_HALLS_URL).done(function (allHalls) {
const myHalls = (allHalls || []).filter(h => {
const id = h.hallId || h.hall_id;
return OFFICER_HALL_IDS.size === 0 || OFFICER_HALL_IDS.has(id) || OFFICER_HALL_IDS.has(String(id));
});
loadHallsForSelectedZones(myHalls);
});
}
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, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
</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>