Browse Source
MF DT unit added in the settlement and Reschedule, Booking status change and NOC document deletion option restriction for EB Reading
master
MF DT unit added in the settlement and Reschedule, Booking status change and NOC document deletion option restriction for EB Reading
master
48 changed files with 3751 additions and 1450 deletions
-
4src/main/java/com/gcc/communityHall/controller/AdminAPIController.java
-
6src/main/java/com/gcc/communityHall/controller/Admincontroller.java
-
504src/main/java/com/gcc/communityHall/controller/CROAPIController.java
-
4src/main/java/com/gcc/communityHall/controller/CROController.java
-
2src/main/java/com/gcc/communityHall/controller/HQAPIController.java
-
3src/main/java/com/gcc/communityHall/controller/HeadquartersController.java
-
25src/main/java/com/gcc/communityHall/controller/OfficerAPIController.java
-
75src/main/java/com/gcc/communityHall/controller/UserAPIController.java
-
41src/main/java/com/gcc/communityHall/controller/UserController.java
-
11src/main/java/com/gcc/communityHall/entity/HallDetails.java
-
2src/main/java/com/gcc/communityHall/repository/BookingDetailsRepository.java
-
17src/main/java/com/gcc/communityHall/repository/BookingDocumentsRepository.java
-
40src/main/java/com/gcc/communityHall/service/HallDetailService.java
-
187src/main/java/com/gcc/communityHall/service/Impl/BookingServiceImpl.java
-
54src/main/java/com/gcc/communityHall/service/Impl/HallDetailServiceImpl.java
-
423src/main/resources/templates/admin/refund-failedlist.html
-
25src/main/resources/templates/admin/reports.html
-
58src/main/resources/templates/admin/settlement-approved.html
-
252src/main/resources/templates/admin/settlement-completed.html
-
58src/main/resources/templates/admin/settlement-initiate.html
-
58src/main/resources/templates/admin/settlement-list.html
-
56src/main/resources/templates/admin/settlement-reject.html
-
31src/main/resources/templates/admin/settlementlist.html
-
294src/main/resources/templates/caretaker/caretakerpage.html
-
115src/main/resources/templates/caretaker/settlementlist.html
-
10src/main/resources/templates/cro/booking-list.html
-
751src/main/resources/templates/cro/booking-status.html
-
860src/main/resources/templates/cro/noc-details.html
-
37src/main/resources/templates/fragments/Details.html
-
171src/main/resources/templates/fragments/croMenu.html
-
2src/main/resources/templates/fragments/menuOfficer.html
-
24src/main/resources/templates/headquarters/halldetailslist.html
-
58src/main/resources/templates/headquarters/refund-failedlist.html
-
25src/main/resources/templates/headquarters/reports.html
-
58src/main/resources/templates/headquarters/settlement-approved.html
-
60src/main/resources/templates/headquarters/settlement-completed.html
-
58src/main/resources/templates/headquarters/settlement-initiate.html
-
58src/main/resources/templates/headquarters/settlement-list.html
-
56src/main/resources/templates/headquarters/settlement-reject.html
-
31src/main/resources/templates/headquarters/settlementlist.html
-
57src/main/resources/templates/headquarters/sixmonthold-transactions.html
-
58src/main/resources/templates/officer/refund-failedlist.html
-
27src/main/resources/templates/officer/report.html
-
56src/main/resources/templates/officer/settlement-approved.html
-
247src/main/resources/templates/officer/settlement-completed.html
-
58src/main/resources/templates/officer/settlement-list.html
-
56src/main/resources/templates/officer/settlement-reject.html
-
38src/main/resources/templates/user/viewhistory.html
@ -1,19 +1,26 @@ |
|||||
package com.gcc.communityHall.repository; |
package com.gcc.communityHall.repository; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
import org.springframework.data.jpa.repository.JpaRepository; |
import org.springframework.data.jpa.repository.JpaRepository; |
||||
import org.springframework.data.jpa.repository.Modifying; |
import org.springframework.data.jpa.repository.Modifying; |
||||
import org.springframework.data.jpa.repository.Query; |
import org.springframework.data.jpa.repository.Query; |
||||
|
import org.springframework.data.repository.query.Param; |
||||
import org.springframework.stereotype.Repository; |
import org.springframework.stereotype.Repository; |
||||
import org.springframework.transaction.annotation.Transactional; |
import org.springframework.transaction.annotation.Transactional; |
||||
|
|
||||
import com.gcc.communityHall.entity.BookingDetails; |
|
||||
import com.gcc.communityHall.entity.BookingDocuments; |
import com.gcc.communityHall.entity.BookingDocuments; |
||||
|
|
||||
@Repository |
@Repository |
||||
public interface BookingDocumentsRepository extends JpaRepository<BookingDocuments,Integer> { |
|
||||
|
|
||||
|
public interface BookingDocumentsRepository extends JpaRepository<BookingDocuments, Integer> { |
||||
|
|
||||
@Transactional |
@Transactional |
||||
@Modifying |
@Modifying |
||||
@Query(value="delete from booking_documents where booking_details_booking_id = :bookingId",nativeQuery=true) |
|
||||
public void deleteByBookingId(int bookingId); |
|
||||
|
@Query(value = "delete from booking_documents where booking_details_booking_id = :bookingId", nativeQuery = true) |
||||
|
public void deleteByBookingId(@Param("bookingId") int bookingId); |
||||
|
|
||||
|
@Query(value = "select * from booking_documents where booking_details_booking_id = :bookingId order by booking_doc_id asc", nativeQuery = true) |
||||
|
public List<BookingDocuments> findByBookingId(@Param("bookingId") int bookingId); |
||||
|
|
||||
} |
} |
||||
|
|
||||
@ -0,0 +1,751 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html xmlns:th="http://www.thymeleaf.org"> |
||||
|
|
||||
|
<head th:replace="~{fragments/head-cro :: head}"></head> |
||||
|
<style> |
||||
|
.card-body table { |
||||
|
width: 100%; |
||||
|
padding: 100px; |
||||
|
} |
||||
|
|
||||
|
.card-body th, |
||||
|
td { |
||||
|
padding: 8px; |
||||
|
text-align: left; |
||||
|
} |
||||
|
|
||||
|
.amount-cell { |
||||
|
text-align: right; |
||||
|
padding-right: 1rem; |
||||
|
} |
||||
|
|
||||
|
.form-control[readonly] { |
||||
|
background-color: white; |
||||
|
} |
||||
|
|
||||
|
.modal { |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
|
||||
|
/* OTP inputs */ |
||||
|
.otp-input-box { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
gap: 10px; |
||||
|
} |
||||
|
|
||||
|
.cro-otp-input { |
||||
|
width: 48px; |
||||
|
height: 52px; |
||||
|
text-align: center; |
||||
|
font-size: 22px; |
||||
|
font-weight: bold; |
||||
|
border: 2px solid #ced4da; |
||||
|
border-radius: 8px; |
||||
|
transition: all 0.2s ease-in-out; |
||||
|
} |
||||
|
|
||||
|
.cro-otp-input:focus { |
||||
|
border-color: #0d6efd; |
||||
|
outline: none; |
||||
|
box-shadow: 0 0 0 4px rgba(13, 110, 253, .2); |
||||
|
} |
||||
|
|
||||
|
#bookingSearchSection { |
||||
|
border: 1px solid #dee2e6; |
||||
|
border-radius: 8px; |
||||
|
padding: 20px; |
||||
|
background: #f8f9fa; |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.detail-section th { |
||||
|
background-color: #f2f2f2; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
.detail-section td, |
||||
|
.detail-section th { |
||||
|
padding: 0.45rem 0.75rem; |
||||
|
border: 1px solid #dee2e6; |
||||
|
} |
||||
|
|
||||
|
.detail-section table { |
||||
|
width: 100%; |
||||
|
border-collapse: collapse; |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
#bookingDetailResult { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.status-change-card { |
||||
|
border: 2px solid #e2e8f0; |
||||
|
border-radius: 10px; |
||||
|
background: #ffffff; |
||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); |
||||
|
padding: 24px; |
||||
|
margin-top: 25px; |
||||
|
} |
||||
|
|
||||
|
.status-badge-lg { |
||||
|
font-size: 0.95rem; |
||||
|
padding: 0.35rem 0.75rem; |
||||
|
border-radius: 6px; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
</style> |
||||
|
|
||||
|
<body> |
||||
|
<!-- Page Container --> |
||||
|
<div id="page-container" |
||||
|
class="sidebar-o sidebar-dark enable-page-overlay side-scroll page-header-fixed page-header-dark"> |
||||
|
<!-- Sidebar Nav--> |
||||
|
<aside th:replace="~{fragments/aside-right :: aside}"></aside> |
||||
|
<!-- END Side Overlay (right)--> |
||||
|
<!-- Sidebar Nav--> |
||||
|
<nav th:replace="~{fragments/nav-cro :: nav}"></nav> |
||||
|
<!-- END Sidebar --> |
||||
|
<!-- Header --> |
||||
|
<header th:replace="~{fragments/header-cro :: header}"></header> |
||||
|
<!-- Main Container --> |
||||
|
<main id="main-container"> |
||||
|
<div class="content"> |
||||
|
|
||||
|
<!-- ═══ Search Booking by ID ═══ --> |
||||
|
<div class="block block-rounded"> |
||||
|
<div class="block-header block-header-default"> |
||||
|
<h3 class="block-title"><i class="fa fa-search me-2 text-primary"></i>Change Booking Status</h3> |
||||
|
</div> |
||||
|
<div class="block-content p-4"> |
||||
|
<div id="bookingSearchSection"> |
||||
|
<div class="row align-items-end"> |
||||
|
<div class="col-md-4 mb-2"> |
||||
|
<label class="form-label fw-semibold" for="searchBookingId">Booking ID <span |
||||
|
class="text-danger">*</span></label> |
||||
|
<input type="number" class="form-control" id="searchBookingId" |
||||
|
placeholder="Enter Booking ID" min="1" /> |
||||
|
</div> |
||||
|
<div class="col-md-3 mb-2"> |
||||
|
<button type="button" class="btn btn-primary" id="btnSearchBooking" |
||||
|
onclick="searchBookingById()"> |
||||
|
<i class="fa fa-search me-1"></i> Search |
||||
|
</button> |
||||
|
<button type="button" class="btn btn-secondary ms-2" id="btnClearBookingSearch" |
||||
|
onclick="clearBookingSearch()"> |
||||
|
<i class="fa fa-times me-1"></i> Clear |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Booking Detail Result --> |
||||
|
<div id="bookingDetailResult"> |
||||
|
<hr /> |
||||
|
<div class="d-flex justify-content-between align-items-center mb-3"> |
||||
|
<h5 class="mb-0">Booking Details <span class="text-primary" id="resultBookingId"></span> |
||||
|
</h5> |
||||
|
<span id="res_bookingStatusBadge" class="badge status-badge-lg bg-info"></span> |
||||
|
</div> |
||||
|
<div class="detail-section"> |
||||
|
<!-- User Details --> |
||||
|
<table> |
||||
|
<tr> |
||||
|
<th colspan="6">User Details</th> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">User Name:</th> |
||||
|
<td id="res_userName"></td> |
||||
|
<th style="font-weight:normal;">Mobile Number:</th> |
||||
|
<td id="res_mobileNumber"></td> |
||||
|
<th style="font-weight:normal;">User Type:</th> |
||||
|
<td id="res_usertype"></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">User Address:</th> |
||||
|
<td colspan="5" id="res_useraddress"></td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
<!-- Hall Details --> |
||||
|
<table> |
||||
|
<tr> |
||||
|
<th colspan="6">Hall Details</th> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">Hall Name:</th> |
||||
|
<td colspan="5" id="res_hallName"></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">Hall Address:</th> |
||||
|
<td colspan="5" id="res_halladdress"></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">Caretaker Name:</th> |
||||
|
<td id="res_caretakername"></td> |
||||
|
<th style="font-weight:normal;">Caretaker Mobile:</th> |
||||
|
<td colspan="3" id="res_caretakermobileNumber"></td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
<!-- Booking Details --> |
||||
|
<table> |
||||
|
<tr> |
||||
|
<th colspan="4">Booking Details (Booking Id: <span id="res_booking_id" |
||||
|
style="color:blue;"></span>)</th> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">Booking Date:</th> |
||||
|
<td id="res_bookingDate"></td> |
||||
|
<th style="font-weight:normal;">Booking Status:</th> |
||||
|
<td id="res_bookingStatus" class="fw-bold"></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">Start Date:</th> |
||||
|
<td id="res_startDate"></td> |
||||
|
<th style="font-weight:normal;">Deposit Amount:</th> |
||||
|
<td class="amount-cell" id="res_depositeAmount"></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">End Date:</th> |
||||
|
<td id="res_endDate"></td> |
||||
|
<th style="font-weight:normal;">Hall Amount:</th> |
||||
|
<td class="amount-cell" id="res_hallAmount"></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">Starting Slot:</th> |
||||
|
<td id="res_startingSlot"></td> |
||||
|
<th style="font-weight:normal;">GST @18%:</th> |
||||
|
<td class="amount-cell" id="res_gst"></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">Ending Slot:</th> |
||||
|
<td id="res_endingSlot"></td> |
||||
|
<th style="font-weight:normal;">Total Amount:</th> |
||||
|
<td class="amount-cell" id="res_totalAmount"></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">Reason:</th> |
||||
|
<td colspan="3" id="res_reason"></td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
<!-- Payment Details --> |
||||
|
<table> |
||||
|
<tr> |
||||
|
<th colspan="4">Booking Payment Details</th> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">Transaction ID:</th> |
||||
|
<td colspan="3" id="res_transactionId"></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">Amount Received:</th> |
||||
|
<td id="res_payamountReceived"></td> |
||||
|
<th style="font-weight:normal;">Transaction Status:</th> |
||||
|
<td id="res_paymentStatus"></td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
</div> |
||||
|
|
||||
|
<!-- ═══ Change Booking Status Section (CRO) ═══ --> |
||||
|
<div id="statusChangeSection" class="status-change-card" style="display: none;"> |
||||
|
<div class="d-flex align-items-center mb-3"> |
||||
|
<div class="bg-primary-light text-primary p-2 rounded-3 me-3" |
||||
|
style="background: rgba(13,110,253,0.1);"> |
||||
|
<i class="fa fa-exchange-alt fa-lg text-primary"></i> |
||||
|
</div> |
||||
|
<div> |
||||
|
<h5 class="mb-0 fw-bold text-dark">Change Booking Status</h5> |
||||
|
<small class="text-muted">CRO can update booking status to REJECTED or |
||||
|
GCC_BLOCKED with OTP verification</small> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="alert alert-warning py-2 mb-3"> |
||||
|
<i class="fa fa-info-circle me-1"></i> |
||||
|
Status changes require OTP verification sent to your registered CRO mobile number. |
||||
|
</div> |
||||
|
|
||||
|
<div class="row g-3"> |
||||
|
<div class="col-md-5"> |
||||
|
<label class="form-label fw-semibold" for="changeBookingStatusSelect"> |
||||
|
Select New Booking Status <span class="text-danger">*</span> |
||||
|
</label> |
||||
|
<select class="form-select" id="changeBookingStatusSelect"> |
||||
|
<option value="">-- Select New Status --</option> |
||||
|
<option value="REJECTED">REJECTED</option> |
||||
|
<option value="GCC_BLOCKED">GCC_BLOCKED</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
<div class="col-md-7"> |
||||
|
<label class="form-label fw-semibold" for="statusChangeRemarks"> |
||||
|
Reason / Remarks <span class="text-danger">*</span> |
||||
|
</label> |
||||
|
<textarea class="form-control" id="statusChangeRemarks" rows="2" |
||||
|
placeholder="Enter reason or comments for changing status (required)..."></textarea> |
||||
|
</div> |
||||
|
<div class="col-12 text-end mt-3"> |
||||
|
<button type="button" class="btn btn-danger px-4" id="btnInitiateStatusChange" |
||||
|
onclick="initiateStatusChange()"> |
||||
|
<i class="fa fa-paper-plane me-1"></i> Submit Status Change |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- ═══ Not Allowed Info Box ═══ --> |
||||
|
<div id="statusChangeNotAllowedMsg" style="display: none; margin-top: 20px;"> |
||||
|
<div class="alert alert-secondary mb-0"> |
||||
|
<i class="fa fa-info-circle me-1"></i> |
||||
|
Status change is <strong>not permitted</strong> for this booking. |
||||
|
<span id="notAllowedDetailMsg">Status change is only permitted for <strong>Past |
||||
|
events</strong> with status <strong>APPROVED</strong>, |
||||
|
<strong>WAITING_FOR_NOC</strong>, or |
||||
|
<strong>WAITING_FOR_NOC_APPROVAL</strong>.</span> |
||||
|
<br /> |
||||
|
<span class="small mt-1 d-inline-block"> |
||||
|
Current status: <span class="badge bg-dark" |
||||
|
id="notAllowedCurrentStatusBadge"></span> |
||||
|
</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</main> |
||||
|
<!-- END Main Container --> |
||||
|
|
||||
|
<!-- ════ CRO OTP MODAL (MATCHES cro/booking-list.html) ════ --> |
||||
|
<div class="modal fade" id="croStatusOtpModal" tabindex="-1" role="dialog" |
||||
|
aria-labelledby="croStatusOtpModalLabel" aria-hidden="true"> |
||||
|
<div class="modal-dialog" role="document"> |
||||
|
<div class="modal-content"> |
||||
|
<div class="modal-header"> |
||||
|
<h6 class="modal-title m-0" id="croStatusOtpModalLabel">OTP Verification</h6> |
||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||
|
</div> |
||||
|
<div class="modal-body"> |
||||
|
<div class="row"> |
||||
|
<div class="mb-3 text-center"> |
||||
|
<img src="media/images/mobileotp.png" width="50%" alt="OTP"> |
||||
|
</div> |
||||
|
<div class="mb-0 text-center"> |
||||
|
<h4>OTP Verification</h4> |
||||
|
<p class="text-mute">Enter the OTP sent to <b class="text-info" |
||||
|
id="croOtpMobileDisplay"></b></p> |
||||
|
<p class="text-danger" id="croOtpErrorAlert"></p> |
||||
|
</div> |
||||
|
<div class="mb-3 text-center"> |
||||
|
<div class="otp-input-box"> |
||||
|
<input type="text" maxlength="1" class="otp-input cro-otp-input" id="croOtp1" |
||||
|
required /> |
||||
|
<input type="text" maxlength="1" class="otp-input cro-otp-input" id="croOtp2" |
||||
|
required /> |
||||
|
<input type="text" maxlength="1" class="otp-input cro-otp-input" id="croOtp3" |
||||
|
required /> |
||||
|
<input type="text" maxlength="1" class="otp-input cro-otp-input" id="croOtp4" |
||||
|
required /> |
||||
|
<input type="text" maxlength="1" class="otp-input cro-otp-input" id="croOtp5" |
||||
|
required /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="mb-3 text-center"> |
||||
|
<p class="mb-0">Didn't receive OTP?</p> |
||||
|
<p class="mb-0 text-info"> |
||||
|
<span id="croResendText">Please wait..</span> |
||||
|
<span id="croCountdown">60</span>s |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="modal-footer"> |
||||
|
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button> |
||||
|
<button type="button" class="btn btn-primary btn-sm" id="btnVerifyCroOtp" |
||||
|
onclick="verifyAndSubmitStatusChange()">Verify OTP</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<footer th:replace="~{fragments/footer2 :: footer}"></footer> |
||||
|
|
||||
|
<div th:replace="~{fragments/resetpassword :: div}"></div> |
||||
|
|
||||
|
<script th:replace="~{fragments/loginscript :: script}"></script> |
||||
|
<script> |
||||
|
var currentSearchedBooking = null; |
||||
|
var croResendTimer = null; |
||||
|
var croMobileNumber = [[${ mobileno }]]; |
||||
|
|
||||
|
// ─── SEARCH BOOKING BY ID ─── |
||||
|
function searchBookingById() { |
||||
|
var input = document.getElementById('searchBookingId'); |
||||
|
var bookingId = input ? input.value.trim() : ''; |
||||
|
if (!bookingId || isNaN(bookingId) || parseInt(bookingId) <= 0) { |
||||
|
Swal.fire({ icon: 'warning', title: 'Invalid Input', text: 'Please enter a valid Booking ID.' }); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
Swal.fire({ |
||||
|
title: 'Fetching Booking...', |
||||
|
text: 'Please wait while booking details are retrieved.', |
||||
|
allowOutsideClick: false, |
||||
|
didOpen: () => { Swal.showLoading(); } |
||||
|
}); |
||||
|
|
||||
|
$.ajax({ |
||||
|
type: 'GET', |
||||
|
url: '/communityhall/cro/api/getBookingById', |
||||
|
data: { bookingId: bookingId }, |
||||
|
success: function (data) { |
||||
|
Swal.close(); |
||||
|
if (data && data.bookingId) { |
||||
|
currentSearchedBooking = data; |
||||
|
populateBookingDetails(data); |
||||
|
$('#bookingDetailResult').show(); |
||||
|
} else { |
||||
|
Swal.fire({ icon: 'error', title: 'Not Found', text: 'No booking found for ID: ' + bookingId }); |
||||
|
$('#bookingDetailResult').hide(); |
||||
|
} |
||||
|
}, |
||||
|
error: function (xhr) { |
||||
|
Swal.close(); |
||||
|
var msg = (xhr.status === 404) ? 'Booking ID ' + bookingId + ' does not exist.' : 'Error fetching booking.'; |
||||
|
Swal.fire({ icon: 'error', title: 'Error', text: msg }); |
||||
|
$('#bookingDetailResult').hide(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// ─── CLEAR BOOKING SEARCH ─── |
||||
|
function clearBookingSearch() { |
||||
|
var input = document.getElementById('searchBookingId'); |
||||
|
if (input) input.value = ''; |
||||
|
$('#bookingDetailResult').hide(); |
||||
|
currentSearchedBooking = null; |
||||
|
} |
||||
|
|
||||
|
$(document).ready(function () { |
||||
|
$('#searchBookingId').on('keypress', function (e) { |
||||
|
if (e.key === 'Enter') searchBookingById(); |
||||
|
}); |
||||
|
|
||||
|
// OTP input auto-advance and backspace handling |
||||
|
$('.cro-otp-input').on('keyup', function (e) { |
||||
|
var $this = $(this); |
||||
|
if (e.key === 'Backspace' || e.key === 'Delete') { |
||||
|
if ($this.val().length === 0) { |
||||
|
$this.prev('.cro-otp-input').focus(); |
||||
|
} |
||||
|
} else if (e.key >= '0' && e.key <= '9') { |
||||
|
if ($this.val().length === 1) { |
||||
|
$this.next('.cro-otp-input').focus(); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// Paste handling for OTP inputs |
||||
|
$('.cro-otp-input').first().on('paste', function (e) { |
||||
|
var pasteData = (e.originalEvent.clipboardData || window.clipboardData).getData('text').trim(); |
||||
|
if (/^\d{5}$/.test(pasteData)) { |
||||
|
e.preventDefault(); |
||||
|
for (var i = 0; i < 5; i++) { |
||||
|
$('#croOtp' + (i + 1)).val(pasteData[i]); |
||||
|
} |
||||
|
$('#croOtp5').focus(); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
// ─── POPULATE BOOKING DETAILS ─── |
||||
|
function populateBookingDetails(list) { |
||||
|
var ud = list.users || {}; |
||||
|
var hd = list.hallDetails || {}; |
||||
|
var py = list.payments || {}; |
||||
|
|
||||
|
document.getElementById('res_userName').innerHTML = ud.name || 'N/A'; |
||||
|
document.getElementById('res_mobileNumber').innerHTML = ud.mobileNo || 'N/A'; |
||||
|
document.getElementById('res_usertype').innerHTML = ud.usertype || 'N/A'; |
||||
|
document.getElementById('res_useraddress').innerHTML = ud.doorNo ? ud.doorNo + ', ' + ud.street + ', ' + ud.area + ', ' + ud.location + '- ' + ud.pinCode : 'N/A'; |
||||
|
document.getElementById('res_hallName').innerHTML = hd.hallName || 'N/A'; |
||||
|
document.getElementById('res_halladdress').innerHTML = hd.doorNo ? hd.doorNo + ', ' + hd.street + ', ' + hd.area + ', ' + hd.city + '- ' + hd.pinCode : 'N/A'; |
||||
|
document.getElementById('res_caretakername').innerHTML = hd.hallInchargeName || 'N/A'; |
||||
|
document.getElementById('res_caretakermobileNumber').innerHTML = hd.hallInchargeMobno || 'N/A'; |
||||
|
document.getElementById('res_booking_id').innerHTML = list.bookingId; |
||||
|
document.getElementById('resultBookingId').innerHTML = ' #' + list.bookingId; |
||||
|
document.getElementById('res_bookingDate').innerHTML = list.bookingDate || 'N/A'; |
||||
|
document.getElementById('res_bookingStatus').innerHTML = list.bookingStatus || 'N/A'; |
||||
|
document.getElementById('res_startDate').innerHTML = list.startDate || 'N/A'; |
||||
|
document.getElementById('res_endDate').innerHTML = list.endDate || 'N/A'; |
||||
|
document.getElementById('res_depositeAmount').innerHTML = list.depositAmount ? '₹ ' + parseFloat(list.depositAmount).toFixed(2) : 'N/A'; |
||||
|
document.getElementById('res_hallAmount').innerHTML = list.hallAmount ? '₹ ' + parseFloat(list.hallAmount).toFixed(2) : 'N/A'; |
||||
|
document.getElementById('res_totalAmount').innerHTML = list.amountReceived ? '₹ ' + parseFloat(list.amountReceived).toFixed(2) : 'N/A'; |
||||
|
var gst = (list.hallAmount && hd.gst) ? (parseFloat(list.hallAmount) * (hd.gst / 100)).toFixed(2) : '0.00'; |
||||
|
document.getElementById('res_gst').innerHTML = '₹ ' + gst; |
||||
|
document.getElementById('res_startingSlot').innerHTML = getSlotLabel(list.startingSlot); |
||||
|
document.getElementById('res_endingSlot').innerHTML = getSlotLabel(list.endingSlot); |
||||
|
document.getElementById('res_reason').innerHTML = list.reason || 'N/A'; |
||||
|
document.getElementById('res_transactionId').innerHTML = py.transactionId || 'NA'; |
||||
|
document.getElementById('res_payamountReceived').innerHTML = py.amountReceived ? '₹ ' + py.amountReceived : 'NA'; |
||||
|
document.getElementById('res_paymentStatus').innerHTML = py.paymentStatus || 'NA'; |
||||
|
|
||||
|
// Status Badge |
||||
|
var status = (list.bookingStatus || '').toUpperCase().trim(); |
||||
|
var badgeEl = document.getElementById('res_bookingStatusBadge'); |
||||
|
badgeEl.textContent = status; |
||||
|
badgeEl.className = 'badge status-badge-lg ' + getStatusBadgeClass(status); |
||||
|
|
||||
|
// ─── Status Change Eligibility Check ─── |
||||
|
// 1. Status must be APPROVED, WAITING_FOR_NOC, or WAITING_FOR_NOC_APPROVAL |
||||
|
// 2. Start date and End date must NOT be future dates (only completed events allowed) |
||||
|
var eligibleStatuses = ['APPROVED', 'WAITING_FOR_NOC', 'WAITING_FOR_NOC_APPROVAL']; |
||||
|
var isStatusEligible = (eligibleStatuses.indexOf(status) !== -1); |
||||
|
var hasEbDetails = list.ebDetails != null && (list.ebDetails.id != null || list.ebDetails.startingReading != null); |
||||
|
|
||||
|
|
||||
|
// Date checks: |
||||
|
var today = new Date(); |
||||
|
var yyyy = today.getFullYear(); |
||||
|
var mm = String(today.getMonth() + 1).padStart(2, '0'); |
||||
|
var dd = String(today.getDate()).padStart(2, '0'); |
||||
|
var todayStr = yyyy + '-' + mm + '-' + dd; |
||||
|
|
||||
|
var startDateStr = (list.startDate || '').trim(); |
||||
|
var endDateStr = (list.endDate || '').trim(); |
||||
|
|
||||
|
var isFutureEvent = false; |
||||
|
if (startDateStr && startDateStr > todayStr) { |
||||
|
isFutureEvent = true; |
||||
|
} else if (endDateStr && endDateStr > todayStr) { |
||||
|
isFutureEvent = true; |
||||
|
} |
||||
|
|
||||
|
if (isStatusEligible && !isFutureEvent && !hasEbDetails) { |
||||
|
$('#statusChangeSection').show(); |
||||
|
$('#statusChangeNotAllowedMsg').hide(); |
||||
|
$('#changeBookingStatusSelect').val(''); |
||||
|
$('#statusChangeRemarks').val(''); |
||||
|
} else { |
||||
|
$('#statusChangeSection').hide(); |
||||
|
$('#statusChangeNotAllowedMsg').show(); |
||||
|
$('#notAllowedCurrentStatusBadge').text(status || 'UNKNOWN'); |
||||
|
if (isFutureEvent) { |
||||
|
$('#notAllowedDetailMsg').html('Booking dates (<strong>' + (startDateStr || 'N/A') + ' to ' + (endDateStr || 'N/A') + '</strong>) are in the future. CRO is <strong>not permitted</strong> to change the status for future events. Only <strong>completed events</strong> can be marked as <strong>REJECTED</strong> or <strong>GCC_BLOCKED</strong>.'); |
||||
|
} else { |
||||
|
$('#notAllowedDetailMsg').html('Status change is only permitted for <strong>Past events</strong> with status <strong>APPROVED</strong>, <strong>WAITING_FOR_NOC</strong>, or <strong>WAITING_FOR_NOC_APPROVAL</strong>. EB Details must not have been entered for the booking.'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function getStatusBadgeClass(status) { |
||||
|
switch (status) { |
||||
|
case 'APPROVED': return 'bg-success'; |
||||
|
case 'WAITING_FOR_NOC': |
||||
|
case 'WAITING_FOR_NOC_APPROVAL': return 'bg-warning text-dark'; |
||||
|
case 'REJECTED': |
||||
|
case 'GCC_BLOCKED': |
||||
|
case 'CANCELED': return 'bg-danger'; |
||||
|
case 'COMPLETED': return 'bg-info text-dark'; |
||||
|
case 'PENDING': return 'bg-secondary'; |
||||
|
default: return 'bg-primary'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function getSlotLabel(v) { |
||||
|
var s = String(v).trim(); |
||||
|
if (s === '1') return 'Morning'; |
||||
|
if (s === '2') return 'Evening'; |
||||
|
return 'Unknown'; |
||||
|
} |
||||
|
|
||||
|
// ─── INITIATE STATUS CHANGE (Trigger OTP) ─── |
||||
|
function initiateStatusChange() { |
||||
|
if (!currentSearchedBooking || !currentSearchedBooking.bookingId) { |
||||
|
Swal.fire({ icon: 'warning', title: 'No Booking Selected', text: 'Please search and select a booking first.' }); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Check future event restriction |
||||
|
var today = new Date(); |
||||
|
var todayStr = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0'); |
||||
|
var sDate = (currentSearchedBooking.startDate || '').trim(); |
||||
|
var eDate = (currentSearchedBooking.endDate || '').trim(); |
||||
|
if ((sDate && sDate > todayStr) || (eDate && eDate > todayStr)) { |
||||
|
Swal.fire({ |
||||
|
icon: 'warning', |
||||
|
title: 'Action Not Allowed', |
||||
|
text: 'Status change is not permitted for future events. Only past events can be marked as REJECTED or GCC_BLOCKED.' |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var newStatus = $('#changeBookingStatusSelect').val(); |
||||
|
if (!newStatus || (newStatus !== 'REJECTED' && newStatus !== 'GCC_BLOCKED')) { |
||||
|
Swal.fire({ icon: 'warning', title: 'Status Required', text: 'Please select a new Booking Status (REJECTED or GCC_BLOCKED).' }); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var remarks = $('#statusChangeRemarks').val().trim(); |
||||
|
if (!remarks) { |
||||
|
Swal.fire({ icon: 'warning', title: 'Remarks Required', text: 'Please enter reason / remarks for this status change.' }); |
||||
|
$('#statusChangeRemarks').focus(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
Swal.fire({ |
||||
|
title: 'Confirm Status Change', |
||||
|
html: 'Are you sure you want to change Booking <b>#' + currentSearchedBooking.bookingId + '</b> status to <b class="text-danger">' + newStatus + '</b>?<br/><small class="text-muted">An OTP will be sent to your CRO mobile number to authorize this change.</small>', |
||||
|
icon: 'question', |
||||
|
showCancelButton: true, |
||||
|
confirmButtonText: 'Yes, Send OTP', |
||||
|
cancelButtonText: 'Cancel', |
||||
|
confirmButtonColor: '#d33' |
||||
|
}).then((result) => { |
||||
|
if (result.isConfirmed) { |
||||
|
sendCroStatusOtp(newStatus, remarks); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// ─── SEND CRO OTP ─── |
||||
|
function sendCroStatusOtp(newStatus, remarks) { |
||||
|
Swal.fire({ |
||||
|
title: 'Sending OTP...', |
||||
|
text: 'Please wait while OTP is sent to your registered mobile number.', |
||||
|
allowOutsideClick: false, |
||||
|
didOpen: () => { Swal.showLoading(); } |
||||
|
}); |
||||
|
|
||||
|
$.ajax({ |
||||
|
type: 'GET', |
||||
|
url: '/communityhall/user/api/sendOTP', |
||||
|
data: { |
||||
|
mobileNo: croMobileNumber, |
||||
|
purpose: 'CRO Status Change' |
||||
|
}, |
||||
|
success: function (bodyMessage) { |
||||
|
Swal.close(); |
||||
|
if (bodyMessage && bodyMessage !== 'Error') { |
||||
|
var mobStr = String(croMobileNumber); |
||||
|
var maskedMobile = mobStr.length >= 10 |
||||
|
? mobStr.substring(0, 2) + 'XXXXXX' + mobStr.substring(mobStr.length - 2) |
||||
|
: mobStr; |
||||
|
|
||||
|
$('#croOtpMobileDisplay').text(maskedMobile || 'Registered CRO Number'); |
||||
|
$('#modalBookingIdText').text('#' + currentSearchedBooking.bookingId); |
||||
|
$('#modalNewStatusText').text(newStatus); |
||||
|
$('#croOtpErrorAlert').hide().text(''); |
||||
|
|
||||
|
// Clear OTP boxes |
||||
|
$('.cro-otp-input').val(''); |
||||
|
|
||||
|
// Show modal |
||||
|
$('#croStatusOtpModal').modal('show'); |
||||
|
setTimeout(function () { $('#croOtp1').focus(); }, 500); |
||||
|
|
||||
|
// Start countdown |
||||
|
startCroCountdown(); |
||||
|
} else { |
||||
|
Swal.fire({ icon: 'error', title: 'OTP Failed', text: 'Failed to send OTP. Please try again.' }); |
||||
|
} |
||||
|
}, |
||||
|
error: function () { |
||||
|
Swal.close(); |
||||
|
Swal.fire({ icon: 'error', title: 'Error', text: 'Failed to send OTP. Please check your network connection.' }); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// ─── COUNTDOWN TIMER FOR RESEND OTP ─── |
||||
|
function startCroCountdown() { |
||||
|
if (croResendTimer) clearInterval(croResendTimer); |
||||
|
var timeLeft = 60; |
||||
|
$('#croResendText').text('Please wait..'); |
||||
|
$('#croCountdown').text(timeLeft); |
||||
|
|
||||
|
croResendTimer = setInterval(function () { |
||||
|
timeLeft--; |
||||
|
$('#croCountdown').text(timeLeft); |
||||
|
if (timeLeft <= 0) { |
||||
|
clearInterval(croResendTimer); |
||||
|
$('#croResendText').html('<a href="javascript:void(0)" class="text-info" onclick="resendCroOtp()">Resend OTP</a>'); |
||||
|
$('#croCountdown').text(''); |
||||
|
} |
||||
|
}, 1000); |
||||
|
} |
||||
|
|
||||
|
function resendCroOtp() { |
||||
|
var newStatus = $('#changeBookingStatusSelect').val(); |
||||
|
var remarks = $('#statusChangeRemarks').val().trim(); |
||||
|
sendCroStatusOtp(newStatus, remarks); |
||||
|
} |
||||
|
|
||||
|
// ─── VERIFY OTP & SUBMIT STATUS CHANGE ─── |
||||
|
function verifyAndSubmitStatusChange() { |
||||
|
var otpValue = ''; |
||||
|
for (var i = 1; i <= 5; i++) { |
||||
|
otpValue += ($('#croOtp' + i).val() || '').trim(); |
||||
|
} |
||||
|
|
||||
|
if (otpValue.length !== 5 || isNaN(otpValue)) { |
||||
|
$('#croOtpErrorAlert').text('Please enter the complete 5-digit OTP.').show(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var newStatus = $('#changeBookingStatusSelect').val(); |
||||
|
var remarks = $('#statusChangeRemarks').val().trim(); |
||||
|
var bookingId = currentSearchedBooking.bookingId; |
||||
|
|
||||
|
$('#btnVerifyCroOtp').prop('disabled', true).html('<i class="fa fa-spinner fa-spin me-1"></i> Verifying...'); |
||||
|
$('#croOtpErrorAlert').hide(); |
||||
|
|
||||
|
$.ajax({ |
||||
|
type: 'POST', |
||||
|
url: '/communityhall/cro/api/updateBookingStatus', |
||||
|
data: { |
||||
|
bookingId: bookingId, |
||||
|
status: newStatus, |
||||
|
comments: remarks, |
||||
|
otp: otpValue |
||||
|
}, |
||||
|
success: function (response) { |
||||
|
$('#btnVerifyCroOtp').prop('disabled', false).html('<i class="fa fa-check-circle me-1"></i> Verify & Update Status'); |
||||
|
if (response && response.status === 'success') { |
||||
|
$('#croStatusOtpModal').modal('hide'); |
||||
|
if (croResendTimer) clearInterval(croResendTimer); |
||||
|
|
||||
|
Swal.fire({ |
||||
|
icon: 'success', |
||||
|
title: 'Status Updated!', |
||||
|
html: 'Booking <b>#' + bookingId + '</b> status has been successfully changed to <b class="text-danger">' + newStatus + '</b>.', |
||||
|
confirmButtonText: 'OK' |
||||
|
}).then(() => { |
||||
|
// Re-fetch booking details to update view in real-time |
||||
|
searchBookingById(); |
||||
|
}); |
||||
|
} else { |
||||
|
var errMsg = (response && response.message) || 'Failed to update status.'; |
||||
|
$('#croOtpErrorAlert').text(errMsg).show(); |
||||
|
} |
||||
|
}, |
||||
|
error: function (xhr) { |
||||
|
$('#btnVerifyCroOtp').prop('disabled', false).html('<i class="fa fa-check-circle me-1"></i> Verify & Update Status'); |
||||
|
var errMsg = 'Verification failed. Please try again.'; |
||||
|
if (xhr.responseJSON && xhr.responseJSON.message) { |
||||
|
errMsg = xhr.responseJSON.message; |
||||
|
} else if (xhr.responseText) { |
||||
|
try { |
||||
|
var parsed = JSON.parse(xhr.responseText); |
||||
|
if (parsed.message) errMsg = parsed.message; |
||||
|
} catch (e) { } |
||||
|
} |
||||
|
$('#croOtpErrorAlert').text(errMsg).show(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
</script> |
||||
|
</div> |
||||
|
</body> |
||||
|
|
||||
|
</html> |
||||
@ -0,0 +1,860 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html xmlns:th="http://www.thymeleaf.org"> |
||||
|
|
||||
|
<head th:replace="~{fragments/head-cro :: head}"></head> |
||||
|
<style> |
||||
|
.card-body table { |
||||
|
width: 100%; |
||||
|
padding: 100px; |
||||
|
} |
||||
|
|
||||
|
.card-body th, |
||||
|
td { |
||||
|
padding: 8px; |
||||
|
text-align: left; |
||||
|
} |
||||
|
|
||||
|
.amount-cell { |
||||
|
text-align: right; |
||||
|
padding-right: 1rem; |
||||
|
} |
||||
|
|
||||
|
.form-control[readonly] { |
||||
|
background-color: white; |
||||
|
} |
||||
|
|
||||
|
.modal { |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
|
||||
|
/* OTP inputs */ |
||||
|
.otp-input-box { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
gap: 10px; |
||||
|
} |
||||
|
|
||||
|
.cro-otp-input { |
||||
|
width: 48px; |
||||
|
height: 52px; |
||||
|
text-align: center; |
||||
|
font-size: 22px; |
||||
|
font-weight: bold; |
||||
|
border: 2px solid #ced4da; |
||||
|
border-radius: 8px; |
||||
|
transition: all 0.2s ease-in-out; |
||||
|
} |
||||
|
|
||||
|
.cro-otp-input:focus { |
||||
|
border-color: #0d6efd; |
||||
|
outline: none; |
||||
|
box-shadow: 0 0 0 4px rgba(13, 110, 253, .2); |
||||
|
} |
||||
|
|
||||
|
#bookingSearchSection { |
||||
|
border: 1px solid #dee2e6; |
||||
|
border-radius: 8px; |
||||
|
padding: 20px; |
||||
|
background: #f8f9fa; |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.detail-section th { |
||||
|
background-color: #f2f2f2; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
.detail-section td, |
||||
|
.detail-section th { |
||||
|
padding: 0.45rem 0.75rem; |
||||
|
border: 1px solid #dee2e6; |
||||
|
} |
||||
|
|
||||
|
.detail-section table { |
||||
|
width: 100%; |
||||
|
border-collapse: collapse; |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
#bookingDetailResult { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.noc-section-card { |
||||
|
border: 2px solid #e2e8f0; |
||||
|
border-radius: 10px; |
||||
|
background: #ffffff; |
||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); |
||||
|
padding: 24px; |
||||
|
margin-top: 25px; |
||||
|
} |
||||
|
|
||||
|
.status-badge-lg { |
||||
|
font-size: 0.95rem; |
||||
|
padding: 0.35rem 0.75rem; |
||||
|
border-radius: 6px; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
</style> |
||||
|
|
||||
|
<body> |
||||
|
<!-- Page Container --> |
||||
|
<div id="page-container" |
||||
|
class="sidebar-o sidebar-dark enable-page-overlay side-scroll page-header-fixed page-header-dark"> |
||||
|
<!-- Sidebar Nav--> |
||||
|
<aside th:replace="~{fragments/aside-right :: aside}"></aside> |
||||
|
<!-- END Side Overlay (right)--> |
||||
|
<!-- Sidebar Nav--> |
||||
|
<nav th:replace="~{fragments/nav-cro :: nav}"></nav> |
||||
|
<!-- END Sidebar --> |
||||
|
<!-- Header --> |
||||
|
<header th:replace="~{fragments/header-cro :: header}"></header> |
||||
|
<!-- Main Container --> |
||||
|
<main id="main-container"> |
||||
|
<div class="content"> |
||||
|
|
||||
|
<!-- ═══ Search Booking by ID ═══ --> |
||||
|
<div class="block block-rounded"> |
||||
|
<div class="block-header block-header-default"> |
||||
|
<h3 class="block-title"><i class="fa fa-file-contract me-2 text-primary"></i>NOC Document |
||||
|
Management</h3> |
||||
|
</div> |
||||
|
<div class="block-content p-4"> |
||||
|
<div id="bookingSearchSection"> |
||||
|
<div class="row align-items-end"> |
||||
|
<div class="col-md-4 mb-2"> |
||||
|
<label class="form-label fw-semibold" for="searchBookingId">Booking ID <span |
||||
|
class="text-danger">*</span></label> |
||||
|
<input type="number" class="form-control" id="searchBookingId" |
||||
|
placeholder="Enter Booking ID" min="1" /> |
||||
|
</div> |
||||
|
<div class="col-md-3 mb-2"> |
||||
|
<button type="button" class="btn btn-primary" id="btnSearchBooking" |
||||
|
onclick="searchBookingById()"> |
||||
|
<i class="fa fa-search me-1"></i> Search |
||||
|
</button> |
||||
|
<button type="button" class="btn btn-secondary ms-2" id="btnClearBookingSearch" |
||||
|
onclick="clearBookingSearch()"> |
||||
|
<i class="fa fa-times me-1"></i> Clear |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Booking Detail Result --> |
||||
|
<div id="bookingDetailResult"> |
||||
|
<hr /> |
||||
|
<div class="d-flex justify-content-between align-items-center mb-3"> |
||||
|
<h5 class="mb-0">Booking Details <span class="text-primary" id="resultBookingId"></span> |
||||
|
</h5> |
||||
|
<div> |
||||
|
<span id="res_bookingReasonBadge" |
||||
|
class="badge status-badge-lg bg-primary me-2"></span> |
||||
|
<span id="res_bookingStatusBadge" class="badge status-badge-lg bg-info"></span> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="detail-section"> |
||||
|
<!-- User Details --> |
||||
|
<table> |
||||
|
<tr> |
||||
|
<th colspan="6">User Details</th> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">User Name:</th> |
||||
|
<td id="res_userName"></td> |
||||
|
<th style="font-weight:normal;">Mobile Number:</th> |
||||
|
<td id="res_mobileNumber"></td> |
||||
|
<th style="font-weight:normal;">User Type:</th> |
||||
|
<td id="res_usertype"></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">User Address:</th> |
||||
|
<td colspan="5" id="res_useraddress"></td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
<!-- Hall Details --> |
||||
|
<table> |
||||
|
<tr> |
||||
|
<th colspan="6">Hall Details</th> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">Hall Name:</th> |
||||
|
<td colspan="5" id="res_hallName"></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">Hall Address:</th> |
||||
|
<td colspan="5" id="res_halladdress"></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">Caretaker Name:</th> |
||||
|
<td id="res_caretakername"></td> |
||||
|
<th style="font-weight:normal;">Caretaker Mobile:</th> |
||||
|
<td colspan="3" id="res_caretakermobileNumber"></td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
<!-- Booking Details --> |
||||
|
<table> |
||||
|
<tr> |
||||
|
<th colspan="4">Booking Details (Booking Id: <span id="res_booking_id" |
||||
|
style="color:blue;"></span>)</th> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">Booking Date:</th> |
||||
|
<td id="res_bookingDate"></td> |
||||
|
<th style="font-weight:normal;">Booking Status:</th> |
||||
|
<td id="res_bookingStatus" class="fw-bold"></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">Start Date:</th> |
||||
|
<td id="res_startDate"></td> |
||||
|
<th style="font-weight:normal;">Deposit Amount:</th> |
||||
|
<td class="amount-cell" id="res_depositeAmount"></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">End Date:</th> |
||||
|
<td id="res_endDate"></td> |
||||
|
<th style="font-weight:normal;">Hall Amount:</th> |
||||
|
<td class="amount-cell" id="res_hallAmount"></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">Starting Slot:</th> |
||||
|
<td id="res_startingSlot"></td> |
||||
|
<th style="font-weight:normal;">GST @18%:</th> |
||||
|
<td class="amount-cell" id="res_gst"></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">Ending Slot:</th> |
||||
|
<td id="res_endingSlot"></td> |
||||
|
<th style="font-weight:normal;">Total Amount:</th> |
||||
|
<td class="amount-cell" id="res_totalAmount"></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">Reason:</th> |
||||
|
<td colspan="3" id="res_reason" class="fw-bold text-primary"></td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
<!-- Payment Details --> |
||||
|
<table> |
||||
|
<tr> |
||||
|
<th colspan="4">Booking Payment Details</th> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">Transaction ID:</th> |
||||
|
<td colspan="3" id="res_transactionId"></td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th style="font-weight:normal;">Amount Received:</th> |
||||
|
<td id="res_payamountReceived"></td> |
||||
|
<th style="font-weight:normal;">Transaction Status:</th> |
||||
|
<td id="res_paymentStatus"></td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
</div> |
||||
|
|
||||
|
<!-- ═══ Uploaded NOC Documents Section (CRO) ═══ --> |
||||
|
<div id="nocDocumentsSection" class="noc-section-card" style="display: none;"> |
||||
|
<div class="d-flex align-items-center justify-content-between mb-3"> |
||||
|
<div class="d-flex align-items-center"> |
||||
|
<div class="p-2 rounded-3 me-3" style="background: rgba(13,110,253,0.1);"> |
||||
|
<i class="fa fa-file-pdf fa-lg text-primary"></i> |
||||
|
</div> |
||||
|
<div> |
||||
|
<h5 class="mb-0 fw-bold text-dark">Uploaded NOC Documents</h5> |
||||
|
<small class="text-muted">CRO can review and delete uploaded NOC documents |
||||
|
with OTP verification</small> |
||||
|
</div> |
||||
|
</div> |
||||
|
<button type="button" class="btn btn-sm btn-outline-primary" |
||||
|
onclick="refreshNocDocuments()"> |
||||
|
<i class="fa fa-sync-alt me-1"></i> Refresh Documents |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<div id="nocDocListContainer"> |
||||
|
<!-- Dynamic NOC documents table loaded here --> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- ═══ Not Allowed Info Box ═══ --> |
||||
|
<div id="nocNotAllowedMsg" style="display: none; margin-top: 20px;"> |
||||
|
<div class="alert alert-secondary mb-0"> |
||||
|
<i class="fa fa-info-circle me-2"></i> |
||||
|
NOC document view and deletion is only permitted for bookings with status |
||||
|
<strong>WAITING_FOR_NOC_APPROVAL</strong> and reason |
||||
|
<strong>Commercial</strong>, <strong>State Govt Event</strong>, or <strong>Central |
||||
|
Govt Event</strong>. EB Details must not have been entered. |
||||
|
<br /> |
||||
|
<span class="small mt-1 d-inline-block"> |
||||
|
Current Status: <span class="badge bg-dark" id="notAllowedStatusBadge"></span> | |
||||
|
Current Reason: <span class="badge bg-dark" id="notAllowedReasonBadge"></span> |
||||
|
</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</main> |
||||
|
<!-- END Main Container --> |
||||
|
|
||||
|
<!-- ═══ NOC Document Preview Modal ═══ --> |
||||
|
<div class="modal fade" id="nocFilePreviewModal" tabindex="-1" role="dialog" |
||||
|
aria-labelledby="nocFilePreviewModalLabel" aria-hidden="true"> |
||||
|
<div class="modal-dialog modal-lg modal-dialog-centered" role="document"> |
||||
|
<div class="modal-content border-0 shadow-lg"> |
||||
|
<div class="modal-header bg-dark text-white"> |
||||
|
<h5 class="modal-title m-0 text-white" id="nocFilePreviewModalLabel"> |
||||
|
<i class="fa fa-file-alt me-2"></i>NOC Document Preview |
||||
|
</h5> |
||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" |
||||
|
aria-label="Close"></button> |
||||
|
</div> |
||||
|
<div class="modal-body p-0 text-center" style="min-height: 480px; background: #525659;"> |
||||
|
<div id="previewSpinner" class="p-5 text-white"> |
||||
|
<i class="fa fa-spinner fa-spin fa-2x"></i> |
||||
|
<p class="mt-2">Loading document preview...</p> |
||||
|
</div> |
||||
|
<iframe id="nocPreviewFrame" src="" |
||||
|
style="width: 100%; height: 550px; border: none; display: none;"></iframe> |
||||
|
<img id="nocPreviewImage" src="" class="img-fluid p-2" style="max-height: 550px; display: none;" |
||||
|
alt="NOC Document" /> |
||||
|
</div> |
||||
|
<div class="modal-footer bg-light justify-content-between"> |
||||
|
<a id="nocDownloadLink" href="" target="_blank" class="btn btn-outline-primary btn-sm"> |
||||
|
<i class="fa fa-external-link-alt me-1"></i> Open in New Tab |
||||
|
</a> |
||||
|
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- ════ CRO NOC DELETE OTP MODAL (MATCHES cro/booking-list.html) ════ --> |
||||
|
<div class="modal fade" id="croDeleteDocOtpModal" tabindex="-1" role="dialog" |
||||
|
aria-labelledby="croDeleteDocOtpModalLabel" aria-hidden="true"> |
||||
|
<div class="modal-dialog" role="document"> |
||||
|
<div class="modal-content"> |
||||
|
<div class="modal-header"> |
||||
|
<h6 class="modal-title m-0" id="croDeleteDocOtpModalLabel">OTP Verification</h6> |
||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||
|
</div> |
||||
|
<div class="modal-body"> |
||||
|
<div class="row"> |
||||
|
<div class="mb-3 text-center"> |
||||
|
<img src="media/images/mobileotp.png" width="50%" alt="OTP"> |
||||
|
</div> |
||||
|
<div class="mb-0 text-center"> |
||||
|
<h4>OTP Verification</h4> |
||||
|
<p class="text-mute">Enter the OTP sent to <b class="text-info" |
||||
|
id="croDeleteDocOtpMobileDisplay"></b></p> |
||||
|
<p class="text-danger" id="croDeleteDocOtpErrorAlert"></p> |
||||
|
</div> |
||||
|
<div class="mb-3 text-start px-4"> |
||||
|
<label class="form-label fw-semibold small" for="croDeleteDocRemarks"> |
||||
|
Reason for Deletion <span class="text-danger">*</span> |
||||
|
</label> |
||||
|
<input type="text" class="form-control form-control-sm" id="croDeleteDocRemarks" |
||||
|
placeholder="Enter reason for deleting NOC document..." /> |
||||
|
</div> |
||||
|
<div class="mb-3 text-center"> |
||||
|
<div class="otp-input-box"> |
||||
|
<input type="text" maxlength="1" class="otp-input cro-otp-input" id="croDelOtp1" |
||||
|
required /> |
||||
|
<input type="text" maxlength="1" class="otp-input cro-otp-input" id="croDelOtp2" |
||||
|
required /> |
||||
|
<input type="text" maxlength="1" class="otp-input cro-otp-input" id="croDelOtp3" |
||||
|
required /> |
||||
|
<input type="text" maxlength="1" class="otp-input cro-otp-input" id="croDelOtp4" |
||||
|
required /> |
||||
|
<input type="text" maxlength="1" class="otp-input cro-otp-input" id="croDelOtp5" |
||||
|
required /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="mb-3 text-center"> |
||||
|
<p class="mb-0">Didn't receive OTP?</p> |
||||
|
<p class="mb-0 text-info"> |
||||
|
<span id="croDeleteDocResendText">Please wait..</span> |
||||
|
<span id="croDeleteDocCountdown">60</span>s |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="modal-footer"> |
||||
|
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button> |
||||
|
<button type="button" class="btn btn-primary btn-sm" id="btnVerifyDeleteDocOtp" |
||||
|
onclick="verifyAndSubmitDocDelete()">Verify OTP</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<footer th:replace="~{fragments/footer2 :: footer}"></footer> |
||||
|
|
||||
|
<div th:replace="~{fragments/resetpassword :: div}"></div> |
||||
|
|
||||
|
<script th:replace="~{fragments/loginscript :: script}"></script> |
||||
|
<script> |
||||
|
var currentSearchedBooking = null; |
||||
|
var croMobileNumber = [[${ mobileno }]]; |
||||
|
var pendingDeleteDocId = null; |
||||
|
var pendingDeleteDocName = null; |
||||
|
var croDeleteDocResendTimer = null; |
||||
|
|
||||
|
// ─── SEARCH BOOKING BY ID ─── |
||||
|
function searchBookingById() { |
||||
|
var input = document.getElementById('searchBookingId'); |
||||
|
var bookingId = input ? input.value.trim() : ''; |
||||
|
if (!bookingId || isNaN(bookingId) || parseInt(bookingId) <= 0) { |
||||
|
Swal.fire({ icon: 'warning', title: 'Invalid Input', text: 'Please enter a valid Booking ID.' }); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
Swal.fire({ |
||||
|
title: 'Fetching Booking...', |
||||
|
text: 'Please wait while booking details are retrieved.', |
||||
|
allowOutsideClick: false, |
||||
|
didOpen: () => { Swal.showLoading(); } |
||||
|
}); |
||||
|
|
||||
|
$.ajax({ |
||||
|
type: 'GET', |
||||
|
url: '/communityhall/cro/api/getBookingById', |
||||
|
data: { bookingId: bookingId }, |
||||
|
success: function (data) { |
||||
|
Swal.close(); |
||||
|
if (data && data.bookingId) { |
||||
|
currentSearchedBooking = data; |
||||
|
populateBookingDetails(data); |
||||
|
$('#bookingDetailResult').show(); |
||||
|
} else { |
||||
|
Swal.fire({ icon: 'error', title: 'Not Found', text: 'No booking found for ID: ' + bookingId }); |
||||
|
$('#bookingDetailResult').hide(); |
||||
|
} |
||||
|
}, |
||||
|
error: function (xhr) { |
||||
|
Swal.close(); |
||||
|
var msg = (xhr.status === 404) ? 'Booking ID ' + bookingId + ' does not exist.' : 'Error fetching booking.'; |
||||
|
Swal.fire({ icon: 'error', title: 'Error', text: msg }); |
||||
|
$('#bookingDetailResult').hide(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// ─── CLEAR BOOKING SEARCH ─── |
||||
|
function clearBookingSearch() { |
||||
|
var input = document.getElementById('searchBookingId'); |
||||
|
if (input) input.value = ''; |
||||
|
$('#bookingDetailResult').hide(); |
||||
|
currentSearchedBooking = null; |
||||
|
} |
||||
|
|
||||
|
$(document).ready(function () { |
||||
|
$('#searchBookingId').on('keypress', function (e) { |
||||
|
if (e.key === 'Enter') searchBookingById(); |
||||
|
}); |
||||
|
|
||||
|
// OTP inputs navigation |
||||
|
$('.cro-otp-input').on('keyup', function (e) { |
||||
|
var $this = $(this); |
||||
|
if (e.key === 'Backspace' || e.key === 'Delete') { |
||||
|
if ($this.val().length === 0) { |
||||
|
$this.prev('.cro-otp-input').focus(); |
||||
|
} |
||||
|
} else if (e.key >= '0' && e.key <= '9') { |
||||
|
if ($this.val().length === 1) { |
||||
|
$this.next('.cro-otp-input').focus(); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// Paste handling for OTP inputs |
||||
|
$('#croDelOtp1').on('paste', function (e) { |
||||
|
var pasteData = (e.originalEvent.clipboardData || window.clipboardData).getData('text').trim(); |
||||
|
if (/^\d{5}$/.test(pasteData)) { |
||||
|
e.preventDefault(); |
||||
|
for (var i = 0; i < 5; i++) { |
||||
|
$('#croDelOtp' + (i + 1)).val(pasteData[i]); |
||||
|
} |
||||
|
$('#croDelOtp5').focus(); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
// ─── POPULATE BOOKING DETAILS ─── |
||||
|
function populateBookingDetails(list) { |
||||
|
var ud = list.users || {}; |
||||
|
var hd = list.hallDetails || {}; |
||||
|
var py = list.payments || {}; |
||||
|
|
||||
|
document.getElementById('res_userName').innerHTML = ud.name || 'N/A'; |
||||
|
document.getElementById('res_mobileNumber').innerHTML = ud.mobileNo || 'N/A'; |
||||
|
document.getElementById('res_usertype').innerHTML = ud.usertype || 'N/A'; |
||||
|
document.getElementById('res_useraddress').innerHTML = ud.doorNo ? ud.doorNo + ', ' + ud.street + ', ' + ud.area + ', ' + ud.location + '- ' + ud.pinCode : 'N/A'; |
||||
|
document.getElementById('res_hallName').innerHTML = hd.hallName || 'N/A'; |
||||
|
document.getElementById('res_halladdress').innerHTML = hd.doorNo ? hd.doorNo + ', ' + hd.street + ', ' + hd.area + ', ' + hd.city + '- ' + hd.pinCode : 'N/A'; |
||||
|
document.getElementById('res_caretakername').innerHTML = hd.hallInchargeName || 'N/A'; |
||||
|
document.getElementById('res_caretakermobileNumber').innerHTML = hd.hallInchargeMobno || 'N/A'; |
||||
|
document.getElementById('res_booking_id').innerHTML = list.bookingId; |
||||
|
document.getElementById('resultBookingId').innerHTML = ' #' + list.bookingId; |
||||
|
document.getElementById('res_bookingDate').innerHTML = list.bookingDate || 'N/A'; |
||||
|
document.getElementById('res_bookingStatus').innerHTML = list.bookingStatus || 'N/A'; |
||||
|
document.getElementById('res_startDate').innerHTML = list.startDate || 'N/A'; |
||||
|
document.getElementById('res_endDate').innerHTML = list.endDate || 'N/A'; |
||||
|
document.getElementById('res_depositeAmount').innerHTML = list.depositAmount ? '₹ ' + parseFloat(list.depositAmount).toFixed(2) : 'N/A'; |
||||
|
document.getElementById('res_hallAmount').innerHTML = list.hallAmount ? '₹ ' + parseFloat(list.hallAmount).toFixed(2) : 'N/A'; |
||||
|
document.getElementById('res_totalAmount').innerHTML = list.amountReceived ? '₹ ' + parseFloat(list.amountReceived).toFixed(2) : 'N/A'; |
||||
|
var gst = (list.hallAmount && hd.gst) ? (parseFloat(list.hallAmount) * (hd.gst / 100)).toFixed(2) : '0.00'; |
||||
|
document.getElementById('res_gst').innerHTML = '₹ ' + gst; |
||||
|
document.getElementById('res_startingSlot').innerHTML = getSlotLabel(list.startingSlot); |
||||
|
document.getElementById('res_endingSlot').innerHTML = getSlotLabel(list.endingSlot); |
||||
|
document.getElementById('res_reason').innerHTML = list.reason || 'N/A'; |
||||
|
document.getElementById('res_transactionId').innerHTML = py.transactionId || 'NA'; |
||||
|
document.getElementById('res_payamountReceived').innerHTML = py.amountReceived ? '₹ ' + py.amountReceived : 'NA'; |
||||
|
document.getElementById('res_paymentStatus').innerHTML = py.paymentStatus || 'NA'; |
||||
|
|
||||
|
// Reason & Status Badges |
||||
|
var reason = (list.reason || '').trim(); |
||||
|
var status = (list.bookingStatus || '').toUpperCase().trim(); |
||||
|
|
||||
|
var reasonBadge = document.getElementById('res_bookingReasonBadge'); |
||||
|
reasonBadge.textContent = 'Reason: ' + (reason || 'N/A'); |
||||
|
|
||||
|
var statusBadge = document.getElementById('res_bookingStatusBadge'); |
||||
|
statusBadge.textContent = status; |
||||
|
statusBadge.className = 'badge status-badge-lg ' + getStatusBadgeClass(status); |
||||
|
|
||||
|
// ─── Check NOC Eligibility ─── |
||||
|
// 1) Reason in ['Commercial', 'State Govt Event', 'Central Govt Event'] |
||||
|
// 2) Status is WAITING_FOR_NOC_APPROVAL |
||||
|
var eligibleReasons = ['commercial', 'state govt event', 'central govt event']; |
||||
|
var isReasonEligible = eligibleReasons.indexOf(reason.toLowerCase()) !== -1; |
||||
|
var isStatusEligible = (status === 'WAITING_FOR_NOC_APPROVAL'); |
||||
|
var hasEbDetails = list.ebDetails != null && (list.ebDetails.id != null || list.ebDetails.startingReading != null); |
||||
|
|
||||
|
|
||||
|
if (isReasonEligible && isStatusEligible && !hasEbDetails) { |
||||
|
$('#nocDocumentsSection').show(); |
||||
|
$('#nocNotAllowedMsg').hide(); |
||||
|
loadNocDocuments(list.bookingId); |
||||
|
} else { |
||||
|
$('#nocDocumentsSection').hide(); |
||||
|
$('#nocNotAllowedMsg').show(); |
||||
|
$('#notAllowedStatusBadge').text(status || 'UNKNOWN'); |
||||
|
$('#notAllowedReasonBadge').text(reason || 'UNKNOWN'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function getStatusBadgeClass(status) { |
||||
|
switch (status) { |
||||
|
case 'APPROVED': return 'bg-success'; |
||||
|
case 'WAITING_FOR_NOC': |
||||
|
case 'WAITING_FOR_NOC_APPROVAL': return 'bg-warning text-dark'; |
||||
|
case 'REJECTED': |
||||
|
case 'GCC_BLOCKED': |
||||
|
case 'CANCELED': return 'bg-danger'; |
||||
|
case 'COMPLETED': return 'bg-info text-dark'; |
||||
|
case 'PENDING': return 'bg-secondary'; |
||||
|
default: return 'bg-primary'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function getSlotLabel(v) { |
||||
|
var s = String(v).trim(); |
||||
|
if (s === '1') return 'Morning'; |
||||
|
if (s === '2') return 'Evening'; |
||||
|
return 'Unknown'; |
||||
|
} |
||||
|
|
||||
|
function refreshNocDocuments() { |
||||
|
if (currentSearchedBooking && currentSearchedBooking.bookingId) { |
||||
|
loadNocDocuments(currentSearchedBooking.bookingId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ─── LOAD NOC DOCUMENTS ─── |
||||
|
function loadNocDocuments(bookingId) { |
||||
|
$('#nocDocListContainer').html('<div class="text-center py-4 text-muted"><i class="fa fa-spinner fa-spin me-2"></i>Loading uploaded NOC documents...</div>'); |
||||
|
|
||||
|
$.ajax({ |
||||
|
type: 'GET', |
||||
|
url: '/communityhall/cro/api/getBookingDocuments', |
||||
|
data: { bookingId: bookingId }, |
||||
|
success: function (docs) { |
||||
|
if (!docs || !Array.isArray(docs) || docs.length === 0) { |
||||
|
$('#nocDocListContainer').html('<div class="alert alert-info mb-0"><i class="fa fa-info-circle me-2"></i>No documents uploaded for this booking.</div>'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Filter documents where bookingDocUrl contains /communityhall/images/NOC/ or /images/noc/ |
||||
|
var nocDocs = docs.filter(function (doc) { |
||||
|
var url = (doc.bookingDocUrl || '').toLowerCase(); |
||||
|
return url.indexOf('/images/noc/') !== -1; |
||||
|
}); |
||||
|
|
||||
|
if (nocDocs.length === 0) { |
||||
|
$('#nocDocListContainer').html('<div class="alert alert-warning mb-0"><i class="fa fa-exclamation-triangle me-2"></i>No NOC document uploaded for this booking yet (other general documents may exist).</div>'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Determine currently/latest uploaded NOC document by highest bookingDocId |
||||
|
var latestDocId = Math.max.apply(Math, nocDocs.map(function (d) { return d.bookingDocId; })); |
||||
|
|
||||
|
var html = '<div class="table-responsive"><table class="table table-bordered table-striped align-middle mb-0">'; |
||||
|
html += '<thead class="table-light"><tr>'; |
||||
|
html += '<th style="width: 50px;">#</th>'; |
||||
|
html += '<th>Document Name / File</th>'; |
||||
|
html += '<th>Document Type</th>'; |
||||
|
html += '<th style="width: 120px;" class="text-center">Preview</th>'; |
||||
|
html += '<th style="width: 170px;" class="text-center">Action</th>'; |
||||
|
html += '</tr></thead><tbody>'; |
||||
|
|
||||
|
nocDocs.forEach(function (doc, idx) { |
||||
|
var docName = doc.documentName || doc.name || ('NOC_Document_' + doc.bookingDocId); |
||||
|
var docType = doc.documentType || 'application/pdf'; |
||||
|
var docUrl = doc.bookingDocUrl || ''; |
||||
|
var isCurrent = (doc.bookingDocId === latestDocId); |
||||
|
|
||||
|
html += '<tr>'; |
||||
|
html += '<td>' + (idx + 1) + '</td>'; |
||||
|
html += '<td><i class="fa fa-file-pdf text-danger me-2"></i><strong>' + escapeHtml(docName) + '</strong>'; |
||||
|
if (isCurrent) { |
||||
|
html += ' <span class="badge bg-success ms-2">Current NOC</span>'; |
||||
|
} else { |
||||
|
html += ' <span class="badge bg-secondary ms-2">Previous NOC</span>'; |
||||
|
} |
||||
|
html += '</td>'; |
||||
|
html += '<td><span class="badge bg-secondary">' + escapeHtml(docType) + '</span></td>'; |
||||
|
html += '<td class="text-center">'; |
||||
|
html += '<button type="button" class="btn btn-sm btn-info text-white" onclick="previewNocDoc(\'' + encodeURIComponent(docUrl) + '\', \'' + encodeURIComponent(docName) + '\')">'; |
||||
|
html += '<i class="fa fa-eye me-1"></i> View'; |
||||
|
html += '</button>'; |
||||
|
html += '</td>'; |
||||
|
html += '<td class="text-center">'; |
||||
|
if (isCurrent) { |
||||
|
html += '<button type="button" class="btn btn-sm btn-danger" onclick="initiateDeleteNocDoc(' + doc.bookingDocId + ', \'' + escapeHtml(docName) + '\')">'; |
||||
|
html += '<i class="fa fa-trash-alt me-1"></i> Delete Document'; |
||||
|
html += '</button>'; |
||||
|
} else { |
||||
|
html += '<span class="text-muted small"><i class="fa fa-lock me-1"></i>View Only</span>'; |
||||
|
} |
||||
|
html += '</td>'; |
||||
|
html += '</tr>'; |
||||
|
}); |
||||
|
|
||||
|
html += '</tbody></table></div>'; |
||||
|
$('#nocDocListContainer').html(html); |
||||
|
}, |
||||
|
error: function () { |
||||
|
$('#nocDocListContainer').html('<div class="alert alert-danger mb-0"><i class="fa fa-times me-2"></i>Error loading documents. Please try again.</div>'); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function escapeHtml(str) { |
||||
|
if (!str) return ''; |
||||
|
return String(str) |
||||
|
.replace(/&/g, '&') |
||||
|
.replace(/</g, '<') |
||||
|
.replace(/>/g, '>') |
||||
|
.replace(/"/g, '"') |
||||
|
.replace(/'/g, '''); |
||||
|
} |
||||
|
|
||||
|
// ─── PREVIEW NOC DOCUMENT ─── |
||||
|
function previewNocDoc(encodedUrl, encodedName) { |
||||
|
var url = decodeURIComponent(encodedUrl); |
||||
|
var name = decodeURIComponent(encodedName); |
||||
|
|
||||
|
$('#nocFilePreviewModalLabel').html('<i class="fa fa-file-pdf me-2 text-danger"></i>' + escapeHtml(name)); |
||||
|
$('#nocDownloadLink').attr('href', url); |
||||
|
|
||||
|
$('#previewSpinner').show(); |
||||
|
$('#nocPreviewFrame').hide(); |
||||
|
$('#nocPreviewImage').hide(); |
||||
|
$('#nocFilePreviewModal').modal('show'); |
||||
|
|
||||
|
var isImage = url.match(/\.(jpeg|jpg|gif|png|webp)$/i); |
||||
|
if (isImage) { |
||||
|
$('#nocPreviewImage').attr('src', url).on('load', function () { |
||||
|
$('#previewSpinner').hide(); |
||||
|
$('#nocPreviewImage').show(); |
||||
|
}); |
||||
|
} else { |
||||
|
$('#nocPreviewFrame').attr('src', url).on('load', function () { |
||||
|
$('#previewSpinner').hide(); |
||||
|
$('#nocPreviewFrame').show(); |
||||
|
}); |
||||
|
setTimeout(function () { |
||||
|
$('#previewSpinner').hide(); |
||||
|
$('#nocPreviewFrame').show(); |
||||
|
}, 800); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ─── INITIATE NOC DOCUMENT DELETE (TRIGGER OTP) ─── |
||||
|
function initiateDeleteNocDoc(docId, docName) { |
||||
|
if (!currentSearchedBooking || !currentSearchedBooking.bookingId) { |
||||
|
Swal.fire({ icon: 'warning', title: 'No Booking Selected', text: 'Please search and select a booking first.' }); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
pendingDeleteDocId = docId; |
||||
|
pendingDeleteDocName = docName; |
||||
|
|
||||
|
Swal.fire({ |
||||
|
title: 'Delete NOC Document?', |
||||
|
html: 'Are you sure you want to delete NOC Document <b>"' + escapeHtml(docName) + '"</b> for Booking <b>#' + currentSearchedBooking.bookingId + '</b>?<br/><small class="text-danger">An OTP will be sent to your CRO registered mobile number to authorize this deletion.</small>', |
||||
|
icon: 'warning', |
||||
|
showCancelButton: true, |
||||
|
confirmButtonText: 'Yes, Send OTP & Proceed', |
||||
|
cancelButtonText: 'Cancel', |
||||
|
confirmButtonColor: '#dc3545' |
||||
|
}).then((result) => { |
||||
|
if (result.isConfirmed) { |
||||
|
sendDeleteDocOtp(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// ─── SEND OTP FOR DOCUMENT DELETION ─── |
||||
|
function sendDeleteDocOtp() { |
||||
|
Swal.fire({ |
||||
|
title: 'Sending OTP...', |
||||
|
text: 'Please wait while OTP is sent to your registered CRO mobile number.', |
||||
|
allowOutsideClick: false, |
||||
|
didOpen: () => { Swal.showLoading(); } |
||||
|
}); |
||||
|
|
||||
|
$.ajax({ |
||||
|
type: 'GET', |
||||
|
url: '/communityhall/user/api/sendOTP', |
||||
|
data: { |
||||
|
mobileNo: croMobileNumber, |
||||
|
purpose: 'Delete NOC Document' |
||||
|
}, |
||||
|
success: function (bodyMessage) { |
||||
|
Swal.close(); |
||||
|
if (bodyMessage && bodyMessage !== 'Error') { |
||||
|
var mobStr = String(croMobileNumber); |
||||
|
var maskedMobile = mobStr.length >= 10 |
||||
|
? mobStr.substring(0, 2) + 'XXXXXX' + mobStr.substring(mobStr.length - 2) |
||||
|
: mobStr; |
||||
|
|
||||
|
$('#croDeleteDocOtpMobileDisplay').text(maskedMobile || 'Registered CRO Number'); |
||||
|
$('#modalDeleteBookingIdText').text('#' + currentSearchedBooking.bookingId); |
||||
|
$('#modalDeleteDocNameText').text(pendingDeleteDocName || ('ID: ' + pendingDeleteDocId)); |
||||
|
$('#croDeleteDocRemarks').val(''); |
||||
|
$('#croDeleteDocOtpErrorAlert').hide().text(''); |
||||
|
|
||||
|
// Clear OTP inputs |
||||
|
$('.cro-otp-input').val(''); |
||||
|
|
||||
|
// Show modal |
||||
|
$('#croDeleteDocOtpModal').modal('show'); |
||||
|
setTimeout(function () { $('#croDelOtp1').focus(); }, 500); |
||||
|
|
||||
|
// Start countdown |
||||
|
startDeleteDocCountdown(); |
||||
|
} else { |
||||
|
Swal.fire({ icon: 'error', title: 'OTP Failed', text: 'Failed to send OTP. Please try again.' }); |
||||
|
} |
||||
|
}, |
||||
|
error: function () { |
||||
|
Swal.close(); |
||||
|
Swal.fire({ icon: 'error', title: 'Error', text: 'Failed to send OTP. Please check your network connection.' }); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function startDeleteDocCountdown() { |
||||
|
if (croDeleteDocResendTimer) clearInterval(croDeleteDocResendTimer); |
||||
|
var timeLeft = 60; |
||||
|
$('#croDeleteDocResendText').text('Please wait..'); |
||||
|
$('#croDeleteDocCountdown').text(timeLeft); |
||||
|
|
||||
|
croDeleteDocResendTimer = setInterval(function () { |
||||
|
timeLeft--; |
||||
|
$('#croDeleteDocCountdown').text(timeLeft); |
||||
|
if (timeLeft <= 0) { |
||||
|
clearInterval(croDeleteDocResendTimer); |
||||
|
$('#croDeleteDocResendText').html('<a href="javascript:void(0)" class="text-info" onclick="sendDeleteDocOtp()">Resend OTP</a>'); |
||||
|
$('#croDeleteDocCountdown').text(''); |
||||
|
} |
||||
|
}, 1000); |
||||
|
} |
||||
|
|
||||
|
// ─── VERIFY OTP & SUBMIT DOCUMENT DELETE ─── |
||||
|
function verifyAndSubmitDocDelete() { |
||||
|
var otpValue = ''; |
||||
|
for (var i = 1; i <= 5; i++) { |
||||
|
otpValue += ($('#croDelOtp' + i).val() || '').trim(); |
||||
|
} |
||||
|
|
||||
|
if (otpValue.length !== 5 || isNaN(otpValue)) { |
||||
|
$('#croDeleteDocOtpErrorAlert').text('Please enter the complete 5-digit OTP.').show(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var remarks = $('#croDeleteDocRemarks').val().trim(); |
||||
|
if (!remarks) { |
||||
|
$('#croDeleteDocOtpErrorAlert').text('Please enter a reason for deletion.').show(); |
||||
|
$('#croDeleteDocRemarks').focus(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var bookingId = currentSearchedBooking.bookingId; |
||||
|
var bookingDocId = pendingDeleteDocId; |
||||
|
|
||||
|
$('#btnVerifyDeleteDocOtp').prop('disabled', true).html('<i class="fa fa-spinner fa-spin me-1"></i> Deleting...'); |
||||
|
$('#croDeleteDocOtpErrorAlert').hide(); |
||||
|
|
||||
|
$.ajax({ |
||||
|
type: 'POST', |
||||
|
url: '/communityhall/cro/api/deleteNocDocument', |
||||
|
data: { |
||||
|
bookingId: bookingId, |
||||
|
bookingDocId: bookingDocId, |
||||
|
otp: otpValue, |
||||
|
comments: remarks |
||||
|
}, |
||||
|
success: function (response) { |
||||
|
$('#btnVerifyDeleteDocOtp').prop('disabled', false).html('<i class="fa fa-trash-alt me-1"></i> Verify & Delete Document'); |
||||
|
if (response && response.status === 'success') { |
||||
|
$('#croDeleteDocOtpModal').modal('hide'); |
||||
|
if (croDeleteDocResendTimer) clearInterval(croDeleteDocResendTimer); |
||||
|
|
||||
|
Swal.fire({ |
||||
|
icon: 'success', |
||||
|
title: 'NOC Status Updated!', |
||||
|
html: 'NOC document rejected/removed successfully.<br/>Booking <b>#' + bookingId + '</b> status is now <b class="text-warning">WAITING_FOR_NOC</b>.', |
||||
|
confirmButtonText: 'OK' |
||||
|
}).then(() => { |
||||
|
// Re-fetch booking details to update view in real-time |
||||
|
searchBookingById(); |
||||
|
}); |
||||
|
} else { |
||||
|
var errMsg = (response && response.message) || 'Failed to delete NOC document.'; |
||||
|
$('#croDeleteDocOtpErrorAlert').text(errMsg).show(); |
||||
|
} |
||||
|
}, |
||||
|
error: function (xhr) { |
||||
|
$('#btnVerifyDeleteDocOtp').prop('disabled', false).html('<i class="fa fa-trash-alt me-1"></i> Verify & Delete Document'); |
||||
|
var errMsg = 'Verification failed. Please try again.'; |
||||
|
if (xhr.responseJSON && xhr.responseJSON.message) { |
||||
|
errMsg = xhr.responseJSON.message; |
||||
|
} else if (xhr.responseText) { |
||||
|
try { |
||||
|
var parsed = JSON.parse(xhr.responseText); |
||||
|
if (parsed.message) errMsg = parsed.message; |
||||
|
} catch (e) { } |
||||
|
} |
||||
|
$('#croDeleteDocOtpErrorAlert').text(errMsg).show(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
</script> |
||||
|
</div> |
||||
|
</body> |
||||
|
|
||||
|
</html> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue