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.
1054 lines
55 KiB
1054 lines
55 KiB
<!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>
|