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.
1337 lines
42 KiB
1337 lines
42 KiB
<!DOCTYPE html>
|
|
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
|
|
|
<head th:replace="~{fragments/user-head :: head}"></head>
|
|
<style>
|
|
:root {
|
|
--accent: #12363b;
|
|
--muted: #6b6b6b;
|
|
--card-bg: #ffffff;
|
|
--slot-radius: 10px;
|
|
--green-start: #16a34a;
|
|
--green-end: #09905a;
|
|
}
|
|
|
|
body {
|
|
font-family: "Inter", "Helvetica Neue", Arial, sans-serif;
|
|
min-height: 100vh;
|
|
background-attachment: fixed;
|
|
background-repeat: no-repeat;
|
|
background-size: cover;
|
|
-webkit-font-smoothing: antialiased;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
}
|
|
|
|
/* container */
|
|
.container {
|
|
width: 100%;
|
|
max-width: 1200px;
|
|
background-color: white;
|
|
}
|
|
|
|
/******************************
|
|
PAGE / HERO / PANEL
|
|
******************************/
|
|
.page {
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
.panel-wrap {
|
|
width: 100%;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
margin-top: 18px;
|
|
}
|
|
|
|
.hero {
|
|
width: 100%;
|
|
max-width: 920px;
|
|
text-align: center;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.input {
|
|
width: 100%;
|
|
padding: 14px 16px;
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
|
background: white;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.btn {
|
|
display: inline-block;
|
|
padding: 14px 18px;
|
|
border-radius: 12px;
|
|
background: var(--accent);
|
|
color: #fff;
|
|
font-weight: 700;
|
|
border: none;
|
|
cursor: pointer;
|
|
box-shadow: 0 8px 26px rgba(18, 54, 59, 0.12);
|
|
}
|
|
|
|
/* CSS - responsive behavior */
|
|
|
|
|
|
/* default field behaviour */
|
|
.field {
|
|
width: 100%;
|
|
max-width: 760px;
|
|
/* original maximum */
|
|
margin: 0 auto 18px;
|
|
/* center and spacing */
|
|
margin-top: 0 auto 18px;
|
|
}
|
|
|
|
/* compact variant to match your 536x48 measurement */
|
|
.field .input.compact {
|
|
box-sizing: border-box;
|
|
width: 100%;
|
|
height: 48px;
|
|
/* your requested height */
|
|
padding: 10px 14px;
|
|
/* inner spacing */
|
|
border-radius: 10px;
|
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
|
font-size: 15px;
|
|
background-color: white;
|
|
}
|
|
|
|
|
|
/* People and Contact styles removed */
|
|
|
|
.date-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 10px;
|
|
background: rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
.counter {
|
|
width: 46px;
|
|
text-align: center;
|
|
font-weight: 700;
|
|
}
|
|
|
|
/* slots grid and slot styles */
|
|
.slots-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
/* 4 columns */
|
|
gap: 15px;
|
|
margin-top: 14px;
|
|
}
|
|
|
|
.slot {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 10px 12px;
|
|
border-radius: 10px;
|
|
background: var(--card-bg);
|
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.slot.disabled {
|
|
background: #f1f1f1;
|
|
color: #8b8b8b;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.left {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
.time {
|
|
font-weight: 700;
|
|
}
|
|
|
|
.meta {
|
|
font-size: 13px;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.choose {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 8px;
|
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 700;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.slot .time {
|
|
font-weight: 700;
|
|
color: #000 !important;
|
|
/* always black */
|
|
}
|
|
|
|
.slot.chosen {
|
|
background: #ffffff !important;
|
|
/* keep slot white */
|
|
color: #fff;
|
|
border-color: transparent;
|
|
border-color: rgba(0, 0, 0, 0.06) !important;
|
|
/* optional subtle glow around the entire slot (comment out if not wanted) */
|
|
box-shadow: 0 6px 18px rgba(9, 144, 90, 0.04);
|
|
}
|
|
|
|
/* .slot.chosen .meta{ color: rgba(255,255,255,0.9); } */
|
|
.slot.chosen .choose {
|
|
background: linear-gradient(180deg, var(--green-start), var(--green-end)) !important;
|
|
color: #fff;
|
|
border-color: transparent !important;
|
|
box-shadow: 0 6px 16px rgba(9, 144, 90, 0.12) !important;
|
|
}
|
|
|
|
|
|
/* container that keeps left (multi-line) and right (inline summary) in one row */
|
|
|
|
/* ===== Single-column stacked layout (no left/right) ===== */
|
|
.info-row {
|
|
display: flex !important;
|
|
flex-direction: column !important;
|
|
/* always stacked */
|
|
align-items: center !important;
|
|
/* center items horizontally */
|
|
justify-content: flex-start;
|
|
gap: 14px;
|
|
width: 100%;
|
|
max-width: 820px;
|
|
margin: 0 auto;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
/* preview note becomes a centered block (fluid width) */
|
|
.preview-note-box {
|
|
width: 100%;
|
|
max-width: 640px;
|
|
flex: 0 0 auto;
|
|
font-size: 13px;
|
|
color: #000000;
|
|
line-height: 1.45;
|
|
text-align: left !important;
|
|
/* ← LEFT aligned */
|
|
padding: 6px 8px;
|
|
}
|
|
|
|
/* summary-inline becomes a centered row under the note */
|
|
/* Inline container */
|
|
/* ---- summary inline: left / right alignment (desktop + mobile) ---- */
|
|
/* Strong override to force left / right placement */
|
|
.summary-inline {
|
|
width: 100% !important;
|
|
/* use the full container width */
|
|
max-width: 100% !important;
|
|
/* don't constrain to 420px here */
|
|
display: flex !important;
|
|
align-items: center !important;
|
|
justify-content: flex-end !important;
|
|
/* left / right extremes */
|
|
gap: 24px !important;
|
|
padding: 6px 8px !important;
|
|
box-sizing: border-box !important;
|
|
margin: 0 auto !important;
|
|
white-space: nowrap !important;
|
|
}
|
|
|
|
/* Make each item expand to fill half the line */
|
|
.summary-item {
|
|
display: flex !important;
|
|
justify-content: flex-end !important;
|
|
/* both right-aligned */
|
|
/* align-items: center !important; */
|
|
gap: 8px !important;
|
|
line-height: 1 !important;
|
|
flex: 0 0 auto !important;
|
|
/* allow items to grow and take space */
|
|
min-width: 0 !important;
|
|
/* prevent overflow due to long text */
|
|
text-align: right !important;
|
|
}
|
|
|
|
/* Left block: align contents to left */
|
|
.summary-item.unallocated {
|
|
justify-content: flex-end !important;
|
|
text-align: right !important;
|
|
/* color: #E63946 !important; */
|
|
}
|
|
|
|
/* Right block: align contents to right */
|
|
.summary-item.allocated {
|
|
justify-content: flex-end !important;
|
|
text-align: right !important;
|
|
/* color: #16A34A !important; */
|
|
}
|
|
|
|
/* Keep numeric counter compact and centered inside its area */
|
|
.summary-item strong {
|
|
min-width: 28px !important;
|
|
display: inline-block !important;
|
|
text-align: center !important;
|
|
}
|
|
|
|
/* DEFAULT color = black */
|
|
.summary-item span,
|
|
.summary-item strong {
|
|
color: #000 !important;
|
|
}
|
|
|
|
/* Red */
|
|
.text-red span,
|
|
.text-red strong {
|
|
color: #E63946 !important;
|
|
}
|
|
|
|
/* Green */
|
|
.text-green span,
|
|
.text-green strong {
|
|
color: #16A34A !important;
|
|
}
|
|
|
|
|
|
.submit-note {
|
|
font-family: "Inter", sans-serif;
|
|
font-size: 13px;
|
|
color: #000000;
|
|
text-align: left;
|
|
/* left aligned */
|
|
margin: 3px 0 10px 0;
|
|
/* top:3px, bottom:10px */
|
|
margin-left: 3px;
|
|
}
|
|
|
|
.submit-wrap {
|
|
margin-top: 16px;
|
|
}
|
|
|
|
|
|
/* ===============================
|
|
DEFAULT VISIBILITY STATES
|
|
================================ */
|
|
|
|
/* Book Visit — visible by default */
|
|
.book-vist {
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
/* Cancel Book — HIDDEN by default */
|
|
.cancel-book {
|
|
display: none;
|
|
/* ⬅ start hidden */
|
|
}
|
|
|
|
/* Utility classes that JS toggles */
|
|
.section-hidden {
|
|
display: none !important;
|
|
}
|
|
|
|
.section-enabled {
|
|
display: block !important;
|
|
opacity: 1 !important;
|
|
pointer-events: auto !important;
|
|
}
|
|
|
|
/* When Cancel is visible, style the button normally */
|
|
.cancel-book.section-enabled .btn {
|
|
background: #043B55;
|
|
color: #ffffff;
|
|
border-radius: 10px;
|
|
border: none;
|
|
cursor: pointer;
|
|
font-weight: 700;
|
|
box-shadow: 0 8px 26px rgba(18, 54, 59, 0.12);
|
|
}
|
|
|
|
|
|
|
|
/* summary card inside cancel section (you already had this) */
|
|
.summary-card {
|
|
|
|
max-width: 100%;
|
|
background: #fff;
|
|
border-radius: 10px;
|
|
padding: 18px 20px;
|
|
border: 2px solid rgba(0, 0, 0, 0.12);
|
|
box-sizing: border-box;
|
|
font-family: Inter, sans-serif;
|
|
color: #111;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* Top row */
|
|
.summary-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.summary-left .label {
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
margin-bottom: 10px;
|
|
color: black;
|
|
}
|
|
|
|
.summary-right div {
|
|
font-size: 13px;
|
|
font-weight: 400;
|
|
margin-bottom: 10px;
|
|
text-align: right;
|
|
color: black;
|
|
}
|
|
|
|
/* main buttons (book visit / preview etc.) */
|
|
.submit-wrap {
|
|
margin-top: 16px;
|
|
}
|
|
|
|
.btn {
|
|
width: 100%;
|
|
padding: 12px 26px;
|
|
background: #043B55;
|
|
color: #ffffff;
|
|
border-radius: 10px;
|
|
border: none;
|
|
cursor: pointer;
|
|
font-weight: 700;
|
|
box-sizing: border-box;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ---------- Mobile polish (drop this at the end of your CSS) ---------- */
|
|
@media (max-width: 520px) {
|
|
|
|
/* fields */
|
|
.field {
|
|
width: calc(100% - 20px);
|
|
margin-left: 10px;
|
|
margin-right: 10px;
|
|
max-width: none;
|
|
}
|
|
|
|
.field .input,
|
|
.field .input.compact {
|
|
width: 100%;
|
|
font-size: 15px;
|
|
padding: 12px 14px;
|
|
height: 46px;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Hover effect for Select Date input */
|
|
.date-wrap .input[type="date"]:hover {
|
|
border-color: #043B55;
|
|
/* brand color */
|
|
box-shadow: 0 0 0 2px rgba(4, 59, 85, 0.12);
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Focus (keyboard / click) */
|
|
.date-wrap .input[type="date"]:focus {
|
|
outline: none;
|
|
border-color: #043B55;
|
|
box-shadow: 0 0 0 3px rgba(4, 59, 85, 0.18);
|
|
}
|
|
|
|
/* slots grid -> single column */
|
|
.slots-grid {
|
|
grid-template-columns: 1fr;
|
|
gap: 10px;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.slot {
|
|
padding: 10px 12px;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
/* info row -> stack */
|
|
.info-row {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: 10px;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.preview-note-box {
|
|
width: 100%;
|
|
max-width: none;
|
|
font-size: 13px;
|
|
}
|
|
|
|
|
|
/* submit area */
|
|
.submit-wrap {
|
|
margin-top: 18px;
|
|
padding-bottom: 18px;
|
|
}
|
|
|
|
.btn {
|
|
width: 100%;
|
|
padding: 12px 14px;
|
|
font-size: 16px;
|
|
border-radius: 12px;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* =========================================
|
|
CALENDAR HOVER — FINAL GUARANTEED FIX
|
|
========================================= */
|
|
|
|
/* Target the REAL visible flatpickr input */
|
|
.date-wrap input.input.form-control {
|
|
cursor: pointer;
|
|
background-color: #fff !important;
|
|
transition: border-color 0.15s ease,
|
|
box-shadow 0.15s ease,
|
|
background-color 0.15s ease;
|
|
}
|
|
|
|
/* Hover */
|
|
.date-wrap input.input.form-control:hover {
|
|
border-color: #043B55 !important;
|
|
box-shadow: 0 0 0 2px rgba(4, 59, 85, 0.18) !important;
|
|
background-color: #fafafa !important;
|
|
}
|
|
|
|
/* Focus */
|
|
.date-wrap input.input.form-control:focus {
|
|
outline: none !important;
|
|
border-color: #043B55 !important;
|
|
box-shadow: 0 0 0 3px rgba(4, 59, 85, 0.28) !important;
|
|
}
|
|
|
|
/* Optional: hover anywhere in wrapper */
|
|
.date-wrap:hover input.input.form-control {
|
|
border-color: #043B55 !important;
|
|
}
|
|
|
|
.border-field {
|
|
border: 1px solid rgba(0, 0, 0, 0.15);
|
|
/* visible but subtle */
|
|
border-radius: 12px;
|
|
/* matches your UI */
|
|
padding: 16px 18px;
|
|
/* space inside box */
|
|
background: #ffffff;
|
|
/* white card look */
|
|
}
|
|
</style>
|
|
|
|
<body>
|
|
<div th:replace="~{fragments/preloader :: div}"></div>
|
|
<div id="pcoded" class="pcoded">
|
|
<div class="pcoded-container">
|
|
<!-- TOP NAV -->
|
|
<nav th:replace="~{fragments/nav-admin :: nav}"></nav>
|
|
|
|
<nav th:replace="~{fragments/nav-adminmenu :: nav}"></nav>
|
|
|
|
<div class="pcoded-main-container">
|
|
<div class="pcoded-wrapper">
|
|
<div class="pcoded-content">
|
|
<div class="pcoded-inner-content">
|
|
<div class="page-body">
|
|
<br>
|
|
<br>
|
|
<br>
|
|
<div class="container">
|
|
<br>
|
|
<br>
|
|
<main class="page" role="main" aria-labelledby="hero-heading">
|
|
<div class="panel-wrap">
|
|
|
|
<div class="panel" role="region" aria-label="Booking panel">
|
|
<div id="bookVisitSection" class="book-vist">
|
|
<!-- select date -->
|
|
|
|
<div class="field border-field" style="max-width: 1000px;">
|
|
<h5
|
|
style="text-align: center;font-weight: bold;font-family:Arial, Helvetica, sans-serif;">
|
|
Visit Blocking:
|
|
</h5>
|
|
|
|
<!-- Date Selection -->
|
|
<div style="margin-bottom: 30px;">
|
|
<input type="hidden" id="blockId"> <!-- Hidden ID -->
|
|
<label class="muted-label">Select Blocking Date:</label>
|
|
<div class="date-wrap">
|
|
<input id="dateInput" class="input" type="date"
|
|
aria-label="Select date" placeholder="Select Date">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Slots Section -->
|
|
<div
|
|
style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
|
|
<label style="margin:8px 0 6px 0" class="muted-label">Pick
|
|
the slots to be
|
|
blocked:</label>
|
|
<button id="selectAllBtn" class="btn"
|
|
style="width:auto; padding:8px 16px; font-size:14px; margin:0; display:none;">Select
|
|
All 8 Slots</button>
|
|
</div>
|
|
<div id="slotsGrid" class="slots-grid" aria-live="polite">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="submit-wrap">
|
|
<p class="submit-note">Confirm blocking for the selected
|
|
date and slots.</p>
|
|
<button id="previewBtn" class="btn">Block Visit</button>
|
|
<button id="cancelEditBtn" class="btn"
|
|
style="background:var(--muted); display:none; margin-top:10px;">Cancel
|
|
Edit</button>
|
|
</div>
|
|
|
|
<br>
|
|
<hr><br>
|
|
|
|
<h5
|
|
style="text-align: center;font-weight: bold;font-family:Arial, Helvetica, sans-serif;">
|
|
Visit Blocked List
|
|
</h5>
|
|
<div style="overflow-x:auto;">
|
|
<table class="table table-striped table-bordered"
|
|
style="width:100%; background:white;">
|
|
<thead>
|
|
<tr>
|
|
<th>Visit Blocked Date</th>
|
|
<th>Visit Blocked Slots</th>
|
|
<th style="width: 120px;">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="activeBlocksTableBody">
|
|
<!-- Populated by JS -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</body>
|
|
|
|
|
|
<!-- Conflict Modal -->
|
|
<div id="conflictModal" class="modal fade" role="dialog" data-backdrop="static">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Previously booked Visits on selected block date</h5>
|
|
<button type="button" class="close" data-dismiss="modal">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div style="max-height: 400px; overflow-y: auto;">
|
|
<table class="table table-bordered table-striped">
|
|
<thead>
|
|
<tr>
|
|
<th>Slot ID</th>
|
|
<th>Mob No</th>
|
|
<th>Visit Date</th>
|
|
<th>No of People</th>
|
|
<th>Total Amount</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="conflictTableBody">
|
|
<!-- Populated by JS -->
|
|
</tbody>
|
|
</table>
|
|
<p class="text-danger" style="font-weight:bold; margin-top:10px;">This slot already have bookings.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
|
<button type="button" class="btn btn-danger" id="proceedBlockingBtn">Proceed Blocking</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script th:replace="~{fragments/user-script :: script}"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
|
|
|
<script type="text/javascript">
|
|
|
|
function showLoader() {
|
|
$('#pageLoader').show();
|
|
}
|
|
|
|
function hideLoader() {
|
|
$('#pageLoader').hide();
|
|
}
|
|
|
|
|
|
$('#pageLoader').hide();
|
|
|
|
//checking
|
|
|
|
|
|
/* =========================================
|
|
ADMIN HALL BLOCKING LOGIC (REAL API)
|
|
========================================= */
|
|
|
|
// Core State
|
|
let chosenSlots = new Map(); // time -> { slotId, language, seats }
|
|
let conflictModal; // Added global variable
|
|
let activeBlocksList = []; // Store active blocks for validation
|
|
|
|
// Mock Data for Display (Structure only)
|
|
|
|
|
|
// Elements
|
|
let dateInput, previewBtn, selectAllBtn, slotsGrid, activeBlocksTableBody;
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
dateInput = document.getElementById("dateInput");
|
|
previewBtn = document.getElementById("previewBtn");
|
|
selectAllBtn = document.getElementById("selectAllBtn");
|
|
slotsGrid = document.getElementById("slotsGrid");
|
|
activeBlocksTableBody = document.getElementById("activeBlocksTableBody");
|
|
|
|
// Set min date to today and default to today
|
|
if (dateInput) {
|
|
flatpickr(dateInput, {
|
|
minDate: "today",
|
|
dateFormat: "Y-m-d",
|
|
disable: [
|
|
function (date) {
|
|
return (date.getDay() === 2); // 0=Sun, 1=Mon, 2=Tue
|
|
}
|
|
],
|
|
locale: {
|
|
firstDayOfWeek: 1
|
|
},
|
|
defaultDate: "today",
|
|
onChange: function (selectedDates, dateStr, instance) {
|
|
if (dateStr) {
|
|
loadActiveBlocks();
|
|
loadTimeSlotsAndRender(dateStr); // Reload with availability
|
|
}
|
|
}
|
|
});
|
|
|
|
// Load slots
|
|
const initDate = dateInput.value;
|
|
loadTimeSlotsAndRender(initDate);
|
|
|
|
// Load Active Blocks
|
|
loadActiveBlocks();
|
|
|
|
// Native picker events removed for Flatpickr
|
|
}
|
|
|
|
if (selectAllBtn) {
|
|
selectAllBtn.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
selectAllSlots();
|
|
});
|
|
}
|
|
|
|
if (previewBtn) {
|
|
previewBtn.addEventListener("click", handleBlockSubmit);
|
|
}
|
|
|
|
// Modal events
|
|
document.getElementById("proceedBlockingBtn").addEventListener("click", () => {
|
|
$('#conflictModal').modal('hide');
|
|
|
|
const dateVal = dateInput.value;
|
|
const blockId = document.getElementById("blockId").value;
|
|
|
|
// Collect selected slots
|
|
let slotIds = [];
|
|
chosenSlots.forEach((val, key) => {
|
|
slotIds.push(val.slotId);
|
|
});
|
|
slotIds.sort((a, b) => a - b);
|
|
const slotString = slotIds.join(",");
|
|
|
|
// Call submitBlock with autoCancel = false (as per requirement: dont update reg_details)
|
|
// The 4th argument 'autoCancel' set to false means we FORCE block but DO NOT cancel existing bookings.
|
|
submitBlock(dateVal, slotString, blockId, false);
|
|
});
|
|
});
|
|
|
|
// Load slots from API
|
|
function loadTimeSlotsAndRender(selectedDate) {
|
|
let url = "/victoriapublichall/bookingapi/getAllTimeSlots";
|
|
if (selectedDate) {
|
|
url += "?visitDate=" + selectedDate;
|
|
}
|
|
|
|
$.ajax({
|
|
url: url,
|
|
type: "GET",
|
|
success: function (response) {
|
|
if (response && response.length > 0) {
|
|
renderSlotsMap(response);
|
|
} else {
|
|
console.warn("No time slots found");
|
|
if (slotsGrid) slotsGrid.innerHTML = "<p>No slots found</p>";
|
|
}
|
|
},
|
|
error: function (e) {
|
|
console.error("Failed to load time slots", e);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Render the slots (data from API)
|
|
function renderSlotsMap(slotsData) {
|
|
if (!slotsGrid) return;
|
|
slotsGrid.innerHTML = "";
|
|
chosenSlots.clear();
|
|
|
|
slotsData.forEach(item => {
|
|
const slotEl = document.createElement("div");
|
|
slotEl.className = "slot";
|
|
slotEl.tabIndex = 0;
|
|
|
|
// Map DB fields to UI data attributes
|
|
// item.time_id -> slotId
|
|
// item.timing -> time
|
|
slotEl.dataset.time = item.timing;
|
|
slotEl.dataset.slotId = item.time_id;
|
|
|
|
// Check if this slot is already blocked
|
|
const currentDate = dateInput.value;
|
|
const activeBlock = activeBlocksList.find(b => b.block_date === currentDate);
|
|
let isBlocked = false;
|
|
|
|
if (activeBlock && activeBlock.time_slot) {
|
|
const blockedIds = activeBlock.time_slot.split(',').map(s => s.trim());
|
|
if (blockedIds.includes(String(item.time_id))) {
|
|
isBlocked = true;
|
|
}
|
|
}
|
|
|
|
// Availability
|
|
let avail = item.availCap !== undefined ? item.availCap : item.max_capacity;
|
|
let max = item.max_capacity;
|
|
|
|
// Logic for visual style (optional: disable if full?)
|
|
// For admin blocking, we allow blocking even if full (logic handles conflict),
|
|
// but we show the state.
|
|
|
|
// Green border initially (available)
|
|
slotEl.style.border = "1px solid #28a745";
|
|
|
|
if (isBlocked) {
|
|
// RED BACKGROUND if blocked
|
|
slotEl.style.background = "#dc3545";
|
|
slotEl.style.color = "white";
|
|
slotEl.style.borderColor = "#dc3545";
|
|
} else if (avail <= 0) {
|
|
slotEl.style.border = "1px solid #dc3545"; // Red border if full but not blocked via this tool
|
|
slotEl.style.background = "#fff5f5";
|
|
}
|
|
|
|
slotEl.innerHTML = `
|
|
<div class="left">
|
|
<div class="time" style="${isBlocked ? 'color:white !important;' : ''}">${item.timing}</div>
|
|
<div class="meta" style="font-size:12px; color: ${isBlocked ? '#ffdddd' : (avail > 0 ? 'green' : 'red')};">
|
|
${isBlocked ? 'BLOCKED' : 'Available: ' + avail + ' / ' + max}
|
|
</div>
|
|
</div>
|
|
<div class="choose"></div>
|
|
`;
|
|
|
|
slotEl.addEventListener("click", () => toggleSlot(slotEl));
|
|
slotsGrid.appendChild(slotEl);
|
|
});
|
|
|
|
if (selectAllBtn) selectAllBtn.style.display = "block";
|
|
}
|
|
|
|
// Toggle Slot Selection
|
|
function toggleSlot(slotEl) {
|
|
const time = slotEl.dataset.time;
|
|
|
|
if (chosenSlots.has(time)) {
|
|
// Deselect
|
|
chosenSlots.delete(time);
|
|
slotEl.classList.remove('chosen');
|
|
const choose = slotEl.querySelector('.choose');
|
|
if (choose) choose.textContent = '';
|
|
} else {
|
|
// Select
|
|
chosenSlots.set(time, {
|
|
slotId: slotEl.dataset.slotId
|
|
});
|
|
slotEl.classList.add('chosen');
|
|
const choose = slotEl.querySelector('.choose');
|
|
if (choose) choose.textContent = '✓';
|
|
}
|
|
updateSelectAllText();
|
|
}
|
|
|
|
// Select All Slots
|
|
function selectAllSlots() {
|
|
const allSlots = document.querySelectorAll("#slotsGrid .slot");
|
|
if (allSlots.length === 0) return;
|
|
|
|
let allSelected = true;
|
|
allSlots.forEach(el => {
|
|
if (!el.classList.contains("chosen")) allSelected = false;
|
|
});
|
|
|
|
if (allSelected) {
|
|
// Deselect All
|
|
allSlots.forEach(el => {
|
|
if (el.classList.contains("chosen")) toggleSlot(el);
|
|
});
|
|
} else {
|
|
// Select All
|
|
allSlots.forEach(el => {
|
|
if (!el.classList.contains("chosen")) toggleSlot(el);
|
|
});
|
|
}
|
|
updateSelectAllText();
|
|
}
|
|
|
|
function updateSelectAllText() {
|
|
const allSlots = document.querySelectorAll("#slotsGrid .slot");
|
|
let allSelected = true;
|
|
allSlots.forEach(el => {
|
|
if (!el.classList.contains("chosen")) allSelected = false;
|
|
});
|
|
if (selectAllBtn) {
|
|
selectAllBtn.textContent = allSelected ? "Deselect All" : "Select All 8 Slots";
|
|
}
|
|
}
|
|
|
|
|
|
// API ACTIONS
|
|
|
|
|
|
function loadActiveBlocks() {
|
|
showLoader();
|
|
$.ajax({
|
|
url: "/victoriapublichall/bookingapi/getActiveBlockTimeSlots",
|
|
type: "GET",
|
|
success: function (response) {
|
|
hideLoader();
|
|
renderActiveBlocks(response);
|
|
},
|
|
error: function (xhr) {
|
|
hideLoader();
|
|
console.error("Failed to load blocks", xhr);
|
|
Swal.fire("Error", "Failed to load active blocked slots.", "error");
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderActiveBlocks(data) {
|
|
activeBlocksList = data || []; // Update global list
|
|
activeBlocksTableBody.innerHTML = "";
|
|
if (!data || data.length === 0) {
|
|
activeBlocksTableBody.innerHTML = "<tr><td colspan='3' class='text-center'>No active blocks found.</td></tr>";
|
|
return;
|
|
}
|
|
|
|
data.forEach(row => {
|
|
const tr = document.createElement("tr");
|
|
tr.innerHTML = `
|
|
<td>${row.block_date_formatted}</td>
|
|
<td>${row.time_slot}</td>
|
|
<td>
|
|
<button class="btn btn-primary btn-sm" style="width:auto; padding:6px 12px; margin:0 5px;"
|
|
onclick="editBlock(${row.id}, '${row.block_date}', '${row.time_slot}')" title="Edit">
|
|
<i class="fa-solid fa-pen"></i>
|
|
</button>
|
|
<button class="btn btn-danger btn-sm" style="width:auto; padding:6px 12px; margin:0 5px;"
|
|
onclick="deleteBlock(${row.id})" title="Delete">
|
|
<i class="fa-solid fa-trash"></i>
|
|
</button>
|
|
</td>
|
|
`;
|
|
activeBlocksTableBody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
// Edit Block Function
|
|
window.editBlock = function (id, date, slots) {
|
|
// Set ID
|
|
document.getElementById("blockId").value = id;
|
|
|
|
// Set Date (flatpickr)
|
|
if (dateInput && dateInput._flatpickr) {
|
|
dateInput._flatpickr.setDate(date);
|
|
} else {
|
|
dateInput.value = date;
|
|
}
|
|
|
|
// Clear choices
|
|
chosenSlots.clear();
|
|
document.querySelectorAll("#slotsGrid .slot").forEach(el => {
|
|
el.classList.remove('chosen');
|
|
const choose = el.querySelector('.choose');
|
|
if (choose) choose.textContent = '';
|
|
});
|
|
|
|
// Set slots
|
|
if (slots) {
|
|
const slotIds = slots.split(',').map(s => s.trim());
|
|
slotIds.forEach(sid => {
|
|
// Find element by data-slot-id
|
|
const el = document.querySelector(`.slot[data-slot-id='${sid}']`);
|
|
if (el) {
|
|
toggleSlot(el); // This will add it to chosenSlots map
|
|
}
|
|
});
|
|
}
|
|
|
|
// Scroll to top
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
|
|
// Update UI Text
|
|
previewBtn.textContent = "Update Block";
|
|
document.getElementById("cancelEditBtn").style.display = "inline-block";
|
|
}
|
|
|
|
// Cancel Edit
|
|
document.getElementById("cancelEditBtn").addEventListener("click", function () {
|
|
resetForm();
|
|
});
|
|
|
|
function resetForm() {
|
|
document.getElementById("blockId").value = "";
|
|
previewBtn.textContent = "Block Visit";
|
|
document.getElementById("cancelEditBtn").style.display = "none";
|
|
|
|
// Reset date to today
|
|
if (dateInput && dateInput._flatpickr) {
|
|
dateInput._flatpickr.setDate("today");
|
|
}
|
|
|
|
// Clear slots
|
|
chosenSlots.clear();
|
|
document.querySelectorAll("#slotsGrid .slot").forEach(el => {
|
|
el.classList.remove('chosen');
|
|
const choose = el.querySelector('.choose');
|
|
if (choose) choose.textContent = '';
|
|
});
|
|
updateSelectAllText();
|
|
}
|
|
|
|
function handleBlockSubmit() {
|
|
const dateVal = dateInput.value;
|
|
const blockId = document.getElementById("blockId").value;
|
|
const isEdit = blockId && blockId.trim() !== "";
|
|
|
|
if (!dateVal) {
|
|
Swal.fire("Warning", "Please select a date.", "warning");
|
|
return;
|
|
}
|
|
|
|
// Check for duplicate blocks on the same date
|
|
// If creating new (isEdit false) OR editing (isEdit true) and changing date to one that exists
|
|
const duplicate = activeBlocksList.find(b => b.block_date === dateVal && b.id != blockId);
|
|
if (duplicate) {
|
|
Swal.fire("Warning", "For the selected date, there is already a blocking. Modify it in the table below.", "warning");
|
|
return;
|
|
}
|
|
|
|
if (chosenSlots.size === 0) {
|
|
Swal.fire("Warning", "Please select at least one slot to block.", "warning");
|
|
return;
|
|
}
|
|
|
|
// Collect slot IDs
|
|
let slotIds = [];
|
|
chosenSlots.forEach((val, key) => {
|
|
slotIds.push(val.slotId);
|
|
});
|
|
slotIds.sort((a, b) => a - b);
|
|
const slotString = slotIds.join(",");
|
|
|
|
// Check for conflicts first
|
|
checkConflicts(dateVal, slotString, blockId, isEdit);
|
|
}
|
|
|
|
function checkConflicts(dateVal, slotString, blockId, isEdit) {
|
|
showLoader();
|
|
$.ajax({
|
|
url: "/victoriapublichall/bookingapi/checkConflictingBookings",
|
|
type: "GET",
|
|
data: { blockDate: dateVal, timeSlots: slotString }, // Pass timeSlots
|
|
success: function (response) {
|
|
hideLoader();
|
|
if (response && response.length > 0) {
|
|
showConflictModal(response);
|
|
} else {
|
|
// No conflicts, proceed with normal confirmation
|
|
confirmAndSubmit(dateVal, slotString, blockId, isEdit);
|
|
}
|
|
},
|
|
error: function (xhr) {
|
|
hideLoader();
|
|
// If check fails, maybe warn user or proceed?
|
|
// Creating block shouldn't be blocked by check failure implicitly, but safe to warn.
|
|
console.error("Conflict check failed", xhr);
|
|
confirmAndSubmit(dateVal, slotString, blockId, isEdit);
|
|
}
|
|
});
|
|
}
|
|
|
|
function showConflictModal(conflicts) {
|
|
const tbody = document.getElementById("conflictTableBody");
|
|
tbody.innerHTML = "";
|
|
conflicts.forEach(row => {
|
|
const tr = document.createElement("tr");
|
|
tr.innerHTML = `
|
|
<td style="color:orange; font-weight:bold;">${row.time_id || '-'}</td>
|
|
<td>${row.mob_no || '-'}</td>
|
|
<td>${row.visit_date}</td>
|
|
<td>${row.no_of_people}</td>
|
|
<td>${row.total_amount}</td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
});
|
|
$('#conflictModal').modal('show');
|
|
}
|
|
|
|
function confirmAndSubmit(dateVal, slotString, blockId, isEdit) {
|
|
const actionText = isEdit ? "Update" : "Block";
|
|
const confirmText = isEdit ? "Yes, update it!" : "Yes, block it!";
|
|
|
|
Swal.fire({
|
|
title: `Confirm ${actionText}?`,
|
|
text: `${actionText} ${dateVal} for slots: ${slotString}?`,
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonColor: '#3085d6',
|
|
cancelButtonColor: '#d33',
|
|
confirmButtonText: confirmText
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
submitBlock(dateVal, slotString, blockId, false); // autoCancel = false
|
|
}
|
|
});
|
|
}
|
|
|
|
function submitBlock(dateVal, slotString, blockId, autoCancel) {
|
|
showLoader();
|
|
|
|
let payload = {
|
|
blockDate: dateVal,
|
|
timeSlot: slotString,
|
|
autoCancel: autoCancel
|
|
};
|
|
|
|
if (blockId) {
|
|
payload.id = blockId;
|
|
}
|
|
|
|
$.ajax({
|
|
url: "/victoriapublichall/bookingapi/saveBlockTimeSlot",
|
|
type: "POST",
|
|
data: payload,
|
|
success: function (response) {
|
|
hideLoader();
|
|
if (response.status === "success") {
|
|
const msg = blockId ? "Block updated successfully!" : "Visit blocked successfully!";
|
|
Swal.fire("Success", msg, "success");
|
|
|
|
resetForm();
|
|
loadActiveBlocks();
|
|
const currentSelectedDate = dateInput.value;
|
|
if (currentSelectedDate) {
|
|
loadTimeSlotsAndRender(currentSelectedDate); // Reload with availability
|
|
}
|
|
} else {
|
|
Swal.fire("Error", response.message, "error");
|
|
}
|
|
},
|
|
error: function (xhr) {
|
|
hideLoader();
|
|
Swal.fire("Error", "Server error while saving block.", "error");
|
|
}
|
|
});
|
|
}
|
|
|
|
window.deleteBlock = function (id) {
|
|
Swal.fire({
|
|
title: 'Are you sure?',
|
|
text: "Do you want to delete this block? Slots will become available.",
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonColor: '#3085d6',
|
|
cancelButtonColor: '#d33',
|
|
confirmButtonText: 'Yes, delete it!'
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
executeDelete(id);
|
|
}
|
|
});
|
|
}
|
|
|
|
function executeDelete(id) {
|
|
showLoader();
|
|
$.ajax({
|
|
url: "/victoriapublichall/bookingapi/deleteBlockTimeSlot",
|
|
type: "POST",
|
|
data: { id: id },
|
|
success: function (response) {
|
|
hideLoader();
|
|
if (response.status === "success") {
|
|
Swal.fire("Deleted!", "Block has been deleted.", "success");
|
|
// If we were editing this ID, reset form
|
|
const currentEditId = document.getElementById("blockId").value;
|
|
if (currentEditId == id) {
|
|
resetForm();
|
|
}
|
|
loadActiveBlocks();
|
|
const dateStr = dateInput.value;
|
|
if (dateStr) {
|
|
loadTimeSlotsAndRender(dateStr); // Reload with availability
|
|
}
|
|
} else {
|
|
Swal.fire("Error", response.message, "error");
|
|
}
|
|
},
|
|
error: function (xhr) {
|
|
hideLoader();
|
|
Swal.fire("Error", "Server error while deleting block.", "error");
|
|
}
|
|
});
|
|
}
|
|
|
|
</script>
|
|
|
|
|
|
<script>
|
|
(function () {
|
|
const hambtn = document.getElementById('hambtn');
|
|
const mobileNav = document.getElementById('mobileNav');
|
|
const mobileOverlay = document.getElementById('mobileOverlay');
|
|
const desktopBook = document.getElementById('desktopBookBtn');
|
|
const mobileBook = document.getElementById('mobileBook');
|
|
|
|
if (!hambtn || !mobileNav || !mobileOverlay) {
|
|
// nothing to do if elements missing
|
|
} else {
|
|
function openMenu() {
|
|
hambtn.setAttribute('aria-expanded', 'true');
|
|
mobileNav.classList.add('open'); mobileNav.setAttribute('aria-hidden', 'false');
|
|
mobileOverlay.classList.add('show'); mobileOverlay.setAttribute('aria-hidden', 'false');
|
|
document.body.style.overflow = 'hidden';
|
|
const first = mobileNav.querySelector('a'); if (first) first.focus();
|
|
}
|
|
function closeMenu() {
|
|
hambtn.setAttribute('aria-expanded', 'false');
|
|
mobileNav.classList.remove('open'); mobileNav.setAttribute('aria-hidden', 'true');
|
|
mobileOverlay.classList.remove('show'); mobileOverlay.setAttribute('aria-hidden', 'true');
|
|
document.body.style.overflow = '';
|
|
hambtn.focus();
|
|
}
|
|
|
|
hambtn.addEventListener('click', () => {
|
|
const isOpen = hambtn.getAttribute('aria-expanded') === 'true';
|
|
if (isOpen) closeMenu(); else openMenu();
|
|
});
|
|
|
|
mobileOverlay.addEventListener('click', closeMenu);
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape' && mobileNav.classList.contains('open')) closeMenu();
|
|
});
|
|
|
|
mobileNav.querySelectorAll('a').forEach(a => {
|
|
a.addEventListener('click', () => {
|
|
// close after clicking any mobile link
|
|
closeMenu();
|
|
});
|
|
});
|
|
|
|
// ensure menu closes when resizing to desktop
|
|
window.addEventListener('resize', () => {
|
|
if (window.innerWidth > 900 && mobileNav.classList.contains('open')) closeMenu();
|
|
});
|
|
}
|
|
|
|
// Optional: wire desktop Book a Visit to the same booking action as mobile
|
|
// if (desktopBook && mobileBook) {
|
|
// desktopBook.addEventListener('click', (e) => {
|
|
// e.preventDefault();
|
|
// alert('Book a Visit clicked — open booking flow here.');
|
|
// });
|
|
// mobileBook.addEventListener('click', (e) => {
|
|
// });
|
|
// }
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
</body>
|
|
|
|
</html>
|