new one
new one
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Museum Chatbot</title>
<script src="https://unpkg.com/@tailwindcss/browser@4"></script>
<link href="https://fonts.googleapis.com/css2?
family=Inter:wght@400;600&family=Noto+Sans+Devanagari&family=Noto+Sans+Tamil&displa
y=swap" rel="stylesheet">
<script
src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.9.3/html2pdf.bundle.min.j
s"></script>
<!-- EmailJS SDK -->
<script type="text/javascript"
src="https://cdn.jsdelivr.net/npm/@emailjs/browser@3/dist/email.min.js">
</script>
<script src="https://cdn.jsdelivr.net/npm/qrcode/build/qrcode.min.js"></script>
<style>
body {
font-family: 'Inter', 'Noto Sans Tamil', 'Noto Sans Devanagari', sans-
serif;
}
#ticket-pdf-content {
display: none;
}
#chat-container {
display: flex;
flex-direction: column;
height: 400px;
overflow-y: auto;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
padding: 1rem;
background-color: #f7fafc;
margin-bottom: 1rem;
}
.chat-message {
display: flex;
margin-bottom: 0.75rem;
}
.sender {
justify-content: flex-end;
}
.receiver {
justify-content: flex-start;
}
.message-content {
max-width: 80%;
padding: 0.75rem 1rem;
border-radius: 1.25rem;
white-space: pre-wrap; /* Ensure line breaks are rendered */
word-wrap: break-word; /* Break long words */
}
.sender .message-content {
background-color: #3b82f6;
color: white;
border-top-right-radius: 0.375rem;
border-bottom-right-radius: 0.375rem;
border-top-left-radius: 1.25rem;
border-bottom-left-radius: 1.25rem;
}
.receiver .message-content {
background-color: #e2e8f0;
color: #1a202c;
border-top-left-radius: 0.375rem;
border-bottom-left-radius: 0.375rem;
border-top-right-radius: 1.25rem;
}
.input-area {
display: flex;
align-items: center;
gap: 0.5rem;
}
#user-input {
flex: 1;
padding: 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 1.25rem; /* Make input rounder */
font-size: 1rem;
outline: none;
}
#user-input:focus {
ring: 2 ring-blue-500 border-transparent;
}
#send-button {
padding: 0.75rem 1.5rem;
background-color: #3b82f6;
color: white;
border-radius: 1.25rem; /* Make button rounder */
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s ease;
border: none;
display: flex;
align-items: center;
gap: 0.5rem;
}
#send-button:hover {
background-color: #2563eb;
}
#send-button:focus {
outline: none;
ring: 2 ring-blue-500 ring-opacity-50;
}
.action-button {
background-color: #3b82f6;
color: white;
font-weight: 600;
border-radius: 1.25rem; /* Make button rounder */
padding: 0.75rem 1rem;
cursor: pointer;
transition: background-color 0.3s ease;
border: none;
margin: 0.5rem;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.action-button:hover {
background-color: #2563eb;
}
.action-button:focus {
outline: none;
ring: 2 ring-blue-500 ring-opacity-50;
}
.action-button:disabled {
background-color: #a0aec0;
cursor: not-allowed;
}
.cancel-button {
background-color: #ef4444;
color: white;
font-weight: 600;
border-radius: 1.25rem; /* Make button rounder */
padding: 0.75rem 1rem;
cursor: pointer;
transition: background-color 0.3s ease;
border: none;
margin: 0.5rem;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.cancel-button:hover {
background-color: #dc2626;
}
.logout-button {
background-color: #ef4444;
color: white;
font-weight: 600;
border-radius: 1.25rem; /* Make button rounder */
padding: 0.75rem 1rem;
cursor: pointer;
transition: background-color 0.3s ease;
border: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.logout-button:hover {
background-color: #dc2626;
}
.history-button {
background-color: #10b981;
color: white;
font-weight: 600;
border-radius: 1.25rem; /* Make button rounder */
padding: 0.75rem 1rem;
cursor: pointer;
transition: background-color 0.3s ease;
border: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.history-button:hover {
background-color: #059669;
}
.hidden {
display: none;
}
.typing-indicator {
display: flex;
align-items: center;
margin-left: 1rem;
margin-top: 0.5rem; /* Added margin top */
}
.typing-dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background-color: #d1d5db; /* Lighter dot in light mode */
margin: 0 0.25rem;
animation: typing 1s infinite;
}
body.dark-mode .typing-dot {
background-color: #9ca3af; /* Darker dot in dark mode */
}
.typing-dot:nth-child(2) {
animation-delay: 0.2s;
}
.typing-dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0% { opacity: 0.4; transform: translateY(0); }
50% { opacity: 1; transform: translateY(-0.25rem); }
100% { opacity: 0.4; transform: translateY(0); }
}
.museum-button,
.service-button {
background-color: #3b82f6;
color: white;
font-weight: 600;
border-radius: 1.25rem;
padding: 0.8rem 1.6rem;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
border: none;
margin: 0.5rem;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
width: 100%;
text-align: center;
}
.service-button:disabled { /* Style for disabled time slot buttons */
background-color: #a0aec0;
cursor: not-allowed;
transform: none;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
body.dark-mode .service-button:disabled {
background-color: #4a5568;
color: #718096;
}
.museum-button:hover,
.service-button:hover:not(:disabled) { /* Apply hover only if not disabled
*/
background-color: #2563eb;
transform: translateY(-0.125rem);
box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
}
.museum-button:active,
.service-button:active:not(:disabled) {
transform: translateY(0);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.museum-button:focus,
.service-button:focus {
outline: none;
ring: 2 ring-blue-500 ring-opacity-50;
border-radius: 1.5rem;
}
.button-icon {
width: 1.25rem;
height: 1.25rem;
}
#owner-qr-code {
margin-top: 1rem;
text-align: center;
}
#ticket-pdf-content img {
display: block;
margin: 10px auto;
}
#payment-status {
margin-top: 1rem;
text-align: center;
color: #1a202c;
font-weight: 600; /* Made bold */
}
#persons-selection select {
padding: 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
font-size: 1rem;
outline: none;
width: 100%;
margin-top: 0.5rem;
}
#previous-tickets {
margin-top: 1rem;
padding: 1rem;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
background-color: #f7fafc;
}
#previous-tickets h3 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
#previous-tickets-list {
max-height: 150px;
overflow-y: auto;
}
.ticket-item {
padding: 0.5rem;
border-bottom: 1px solid #e2e8f0;
white-space: pre-line; /* Render line breaks in history */
}
body.dark-mode .ticket-item {
border-bottom-color: #718096;
}
.ticket-item:last-child {
border-bottom: none;
}
#date-picker {
width: 100%;
padding: 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
font-size: 1rem;
margin-top: 0.5rem;
}
.person-details-form h4 {
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.person-details-form input,
.person-details-form select {
width: 100%;
padding: 0.5rem;
margin-bottom: 0.5rem;
border: 1px solid #e2e8f0;
border-radius: 0.25rem;
}
.person-details-container {
margin-top: 1rem;
}
.person-details {
margin-bottom: 1rem;
padding: 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
background-color: #f7fafc;
}
.attendee-list {
margin-top: 0.5rem;
padding-left: 1rem;
}
.attendee-item {
margin-bottom: 0.25rem;
}
.lookup-button:hover {
background-color: #7c3aed;
}
<script>
// Initialize EmailJS with your public key
(function() {
try {
// Use the provided Public Key
emailjs.init("9BHIclDoDXmRMDi4t");
console.log("EmailJS Initialized Successfully");
} catch (error) {
console.error("EmailJS initialization failed:", error);
// Optionally display an error to the user or disable email
functionality
alert("Could not initialize email service. Email confirmation might
not be available.");
}
})();
// UPI Base URL (Replace with your actual UPI details if needed)
const upiBaseUrl = "upi://pay?pa=punithchowdary11@oksbi&pn=punith
%20chowdary&aid=uGICAgMDQlZymZQ";
// Set default to April 5, 2025, for demo purposes, but min should be
today
const defaultDateStr = "2025-04-05";
function setTheme(theme) {
if (theme === 'dark') {
document.body.classList.add('dark-mode');
darkModeIcon.innerHTML = `<path d="M12 16a4 4 0 0 0 0-8 4 4 0 0 0-
3.465 2H7a5 5 0 0 1 5 5v1.535A3.978 3.978 0 0 0 12 16ZM2 10a8 8 0 1 1 8 8 8 8 0 0
1-8-8Zm8-6a.75.75 0 0 0 0 1.5A1.5 1.5 0 0 1 11.5 7 .75.75 0 0 0 10 7.75 3 3 0 0 0 7
4.75.75.75 0 0 0 6.25 4H5a.75.75 0 0 0 0 1.5h.325A4.5 4.5 0 0 1 10 9.5a4.5 4.5 0 0
1 4.5 4.326V14a.75.75 0 0 0 1.5 0v-.674A6 6 0 0 0 10 4Z"/>`; // Moon icon
darkModeText.textContent = translations[currentLanguage]?.lightMode
|| "Light Mode"; // Show option to switch to Light
} else {
document.body.classList.remove('dark-mode');
darkModeIcon.innerHTML = `<path fill-rule="evenodd" d="M10 2a.75.75
0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 10 2Zm0 12a.75.75 0 0
1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 10 14Zm-5.657-2.843a.75.75 0 0 1
0 1.06l-1.061 1.061a.75.75 0 1 1-1.06-1.06l1.06-1.061a.75.75 0 0 1 1.061 0Zm12.728
0a.75.75 0 0 1 1.06 0l1.061 1.061a.75.75 0 1 1-1.06 1.06l-1.061-1.06a.75.75 0 0 1
0-1.061ZM10 5a5 5 0 1 0 0 10 5 5 0 0 0 0-10Z" clip-rule="evenodd" />`; // Sun icon
darkModeText.textContent = translations[currentLanguage]?.darkMode
|| "Dark Mode"; // Show option to switch to Dark
}
}
function updateLanguage() {
const lang = currentLanguage;
const tr = translations[lang];
if (!tr) {
console.error(`Translations not found for language: ${lang}`);
return; // Exit if translations are missing
}
checkPaymentButtonText.childNodes[checkPaymentButtonText.childNodes.length -
1].nodeValue = ` ${tr.checkPayment}`;
confirmTicketButtonText.childNodes[confirmTicketButtonText.childNodes.length -
1].nodeValue = ` ${tr.confirmAndEmail}`;
cancelTicketText.textContent = tr.cancelTicket;
previousTicketsHeader.textContent = tr.previousTicketsHeader;
historyButtonText.textContent = isHistoryVisible ? tr.hideTicketHistory
: tr.viewTicketHistory;
logoutButtonText.textContent = tr.logout;
lookupButtonText.textContent = tr.lookupBooking;
sendButtonText.textContent = tr.send;
// Language Selection
languageSelect.addEventListener('change', (e) => {
currentLanguage = e.target.value;
localStorage.setItem('language', currentLanguage);
updateLanguage(); // Update all UI text and re-render chat
});
// Logout Button
logoutButton.addEventListener('click', () => {
console.log("Logout initiated");
localStorage.removeItem('chatHistory');
localStorage.removeItem('bookedTickets');
alert("You have been logged out (simulation).");
resetChatState(true); // Full reset including user details
chatHistory = [];
bookedTickets = [];
chatContainer.innerHTML = '';
appendMessage('receiver', translations[currentLanguage].helloMessage);
setTimeout(() => {
appendMessage('receiver',
translations[currentLanguage].selectMuseumPrompt);
museumSelection.classList.remove('hidden');
}, 1000);
});
function sendMessage() {
const message = userInput.value.trim();
if (message === '') return;
function showTypingIndicator() {
typingIndicator.classList.remove('hidden');
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function hideTypingIndicator() {
typingIndicator.classList.add('hidden');
}
function getUserDetails() {
const phoneInput = document.getElementById('phone');
const emailInput = document.getElementById('email');
const phone = phoneInput.value.trim();
const email = emailInput.value.trim();
let isValid = true;
const lang = currentLanguage;
const tr = translations[lang];
if (!/^\d{10}$/.test(phone)) { showError(tr.invalidPhone);
phoneInput.classList.add('border-red-500'); isValid = false; }
else { phoneInput.classList.remove('border-red-500'); }
if (!/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(email))
{ showError(tr.invalidEmail); emailInput.classList.add('border-red-500'); isValid =
false; }
else { emailInput.classList.remove('border-red-500'); }
function selectMuseum(museumKey) {
selectedMuseum = museumKey;
const lang = currentLanguage;
const museumName = serviceData[museumKey]?.[`name_${lang}`] ||
serviceData[museumKey]?.name; // Get translated name
appendMessage('receiver', `Okay, you selected: ${museumName}.`);
appendMessage('receiver',
translations[lang].selectServicePrompt.replace('{museum}', museumName));
museumSelection.classList.add('hidden');
updateLanguage(); // Ensure service names are updated
serviceSelection.classList.remove('hidden');
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function selectService(serviceKey) {
selectedService = serviceKey;
const lang = currentLanguage;
const tr = translations[lang];
const serviceName = serviceData[selectedMuseum].services[serviceKey]?.
[`name_${lang}`] || serviceData[selectedMuseum].services[serviceKey]?.name;
appendMessage('receiver', `Service selected: ${serviceName}.`);
function updateAvailableTimes() {
if (!selectedMuseum || !selectedService) return;
const timesForDate =
serviceData[selectedMuseum]?.services[selectedService]?.times?.
[currentSelectedDate] || [];
const lang = currentLanguage;
const tr = translations[lang];
if (timesForDate.length === 0 || !
Object.keys(serviceData[selectedMuseum].services[selectedService].times).includes(c
urrentSelectedDate)) {
// Check if the date itself is invalid or just has no slots
timeSelectionText.textContent = tr.noTimeSlots.replace('{date}',
formatDate(currentSelectedDate));
}
else {
timeSelectionText.textContent =
tr.timeSlotsPrompt.replace('{date}', formatDate(currentSelectedDate));
timesForDate.forEach((slot) => {
const button = document.createElement('button');
button.className = 'service-button';
const isSoldOut = slot.ticketsLeft === 0;
button.disabled = isSoldOut; // Disable if no tickets
button.textContent = buttonText;
button.onclick = () => selectTime(slot); // Pass the whole slot
object
timeSlots.appendChild(button);
});
}
dateSelection.classList.add('hidden');
timeSelection.classList.remove('hidden');
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function selectTime(timeSlot) {
selectedTime = { ...timeSlot, date: selectedDate }; // Store selected
time info including date
timeAndCostText.textContent = tr.timeAndCost
.replace('{time}', selectedTime.time)
.replace('{date}', formatDate(selectedTime.date))
.replace('{cost}', selectedTime.price);
timeAndCost.classList.remove('hidden');
timeSelection.classList.add('hidden');
personsSelection.classList.remove('hidden');
updatePersonsDropdown();
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function updatePersonsDropdown() {
if (!selectedTime) return;
const maxAllowed = 5;
const ticketsAvailable = selectedTime.ticketsLeft;
const maxPersons = Math.max(1, Math.min(maxAllowed,
ticketsAvailable));
personsCount.innerHTML = '';
for (let i = 1; i <= maxPersons; i++) {
const option = document.createElement('option');
option.value = i;
option.textContent = `${i} ${i > 1 ? 'Persons' : 'Person'}`;
personsCount.appendChild(option);
}
personsCount.value = '1';
numberOfPersons = 1;
}
function updatePersons() {
numberOfPersons = parseInt(personsCount.value);
}
function showPersonDetailsForm() {
if (!selectedTime || numberOfPersons <= 0) return;
personsSelection.classList.add('hidden');
personDetailsContainer.classList.remove('hidden');
personDetailsForms.innerHTML = '';
personDetails = [];
personDetailsForms.appendChild(formDiv);
}
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function confirmPersonDetails() {
hideErrorMessage();
const inputs = personDetailsForms.querySelectorAll('input[required],
select[required]');
let allValid = true;
personDetails = [];
inputs.forEach(input => {
input.classList.remove('border-red-500');
let isInvalid = false;
if (!input.value) { isInvalid = true; }
if (input.tagName === 'SELECT' && input.value === "") { isInvalid
= true; } // Check for placeholder
if (input.type === 'number' && (parseInt(input.value) <
parseInt(input.min) || parseInt(input.value) > parseInt(input.max))) { isInvalid =
true; }
if (isInvalid) {
input.classList.add('border-red-500');
allValid = false;
}
});
if (!allValid) {
showError('Please fill in all fields correctly for all persons.');
return;
}
function displayTicketSummary() {
if (!selectedMuseum || !selectedService || !selectedTime || !userPhone
|| !userEmail) {
console.error("Cannot display ticket summary: Missing required
data.");
showError("Error displaying ticket summary. Please try again.");
return;
}
if (personDetails.length > 0) {
ticketDetailsText += `\n\n${tr.attendees}`;
personDetails.forEach(person => {
const genderDisplay = tr[person.gender.toLowerCase()] ||
person.gender;
ticketDetailsText += `\n- ${person.name} (${person.age}, $
{genderDisplay})`;
});
}
function resetPaymentAndConfirmation() {
paymentVerifiedGlobally = false;
paymentStatus.classList.add('hidden'); paymentStatus.textContent = '';
ownerQrCodeDiv.classList.add('hidden');
validationQrCodeDiv.classList.add('hidden'); // Hide validation QR
referenceNumberDiv.classList.add('hidden'); // Hide reference number
confirmTicketButton.classList.add('hidden');
confirmTicketButton.disabled = true;
confirmationMessage.classList.add('hidden');
// Reset print buttons based on whether it's free or paid (initial
state)
printAlertButton.disabled = totalCost > 0;
printPdfButton.disabled = totalCost > 0;
checkPaymentButtonText.disabled = totalCost <= 0;
showQrButtonText.disabled = totalCost <= 0;
}
function showUpiQrCode() {
hideErrorMessage();
const lang = currentLanguage;
const tr = translations[lang];
upiQrData = `${upiBaseUrl}&am=$
{totalCost.toFixed(2)}&cu=INR&tn=MuseumTicket${Date.now()}`;
upiAmountSpan.textContent = `${totalCost.toFixed(2)}`;
scanPayText.textContent = tr.scanPayText.replace('{amount}',
totalCost.toFixed(2));
generateQRCode(upiQrData, ownerQrCodeDiv);
ownerQrCodeDiv.classList.remove('hidden');
paymentStatus.textContent = tr.scanUpi.replace('{cost}',
totalCost.toFixed(2));
paymentStatus.classList.remove('hidden');
appendMessage('receiver', tr.scanUpi.replace('{cost}',
totalCost.toFixed(2)));
confirmTicketButton.classList.add('hidden');
confirmTicketButton.disabled = true;
checkPaymentButtonText.disabled = false; showQrButtonText.disabled =
false;
printAlertButton.disabled = true; printPdfButton.disabled = true;
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function checkPayment() {
hideErrorMessage();
const lang = currentLanguage;
const tr = translations[lang];
if (userConfirmedPayment) {
paymentVerifiedGlobally = true;
paymentStatus.textContent = tr.paymentVerified;
paymentStatus.classList.remove('hidden');
confirmTicketButton.classList.remove('hidden');
confirmTicketButton.disabled = false;
ownerQrCodeDiv.classList.add('hidden'); // Hide QR now
checkPaymentButtonText.disabled = true; // Disable check btn
showQrButtonText.disabled = true; // Disable show QR btn
printAlertButton.disabled = false; // Enable print
printPdfButton.disabled = false;
appendMessage('receiver', tr.paymentVerified);
} else {
paymentVerifiedGlobally = false;
paymentStatus.textContent = tr.paymentNotConfirmed;
paymentStatus.classList.remove('hidden');
confirmTicketButton.classList.add('hidden');
confirmTicketButton.disabled = true;
checkPaymentButtonText.disabled = false; // Keep enabled
showQrButtonText.disabled = false; // Keep enabled
appendMessage('receiver', tr.paymentNotConfirmedMessage);
}
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function printTicket(type) {
if (!paymentVerifiedGlobally) {
appendMessage('receiver', "Please confirm payment before
printing.");
showError("Payment not verified. Cannot print ticket."); return;
}
if (!selectedMuseum || !selectedService || !selectedTime ||
personDetails.length !== numberOfPersons) {
appendMessage('receiver', "Ticket details incomplete. Cannot
print.");
showError("Cannot print ticket, details missing."); return;
}
function generatePdfFromContent() {
const element = document.getElementById('ticket-content');
const pdfFilename = `Museum_Ticket_${referenceNumber ||
Date.now()}.pdf`;
const pdfOptions = { margin: 15, filename: pdfFilename, image:
{ type: 'jpeg', quality: 0.95 }, html2canvas: { scale: 2, useCORS: true }, jsPDF: {
unit: 'mm', format: 'a5', orientation: 'portrait' } };
try {
html2pdf().from(element).set(pdfOptions).save();
appendMessage('receiver', `Ticket PDF generated ($
{pdfFilename}).`);
} catch (error) {
console.error("PDF generation failed:", error);
appendMessage('receiver', "Failed to generate PDF.");
showError("Failed to generate PDF.");
}
}
function generateValidationQRData() {
const ticketData = { ref: referenceNumber, m: selectedMuseum, s:
selectedService, dt: selectedTime.date, tm: selectedTime.time, pax:
numberOfPersons, ph: userPhone, ts: Date.now() };
try { return JSON.stringify(ticketData); }
catch (error) { console.error("Failed to stringify QR data:", error);
return `ERROR_QR_${Date.now()}`; }
}
confirmTicketButton.disabled = true;
confirmTicketButton.childNodes[confirmTicketButton.childNodes.length -
1].nodeValue = " Confirming..."; // Update button text
try {
referenceNumber = generateReferenceNumber();
validationQrData = generateValidationQRData();
currentTicket = {
referenceNumber: referenceNumber, museumKey: selectedMuseum,
serviceKey: selectedService,
museum: serviceData[selectedMuseum]?.[`name_${lang}`] ||
serviceData[selectedMuseum]?.name,
service:
serviceData[selectedMuseum]?.services[selectedService]?.[`name_${lang}`] ||
serviceData[selectedMuseum]?.services[selectedService]?.name,
time: selectedTime.time, date: selectedTime.date, persons:
numberOfPersons, cost: totalCost, phone: userPhone, email: userEmail,
personDetails: personDetails, validationQrData:
validationQrData, confirmedAt: new Date().toISOString()
};
bookedTickets.push(currentTicket);
saveBookedTickets();
// Update UI immediately
referenceNumberValue.textContent = referenceNumber;
referenceNumberDiv.classList.remove('hidden');
referenceNumberPdf.innerHTML = `<strong>Reference:</strong> $
{referenceNumber}`;
validationQrCodeDiv.innerHTML = `<p>${tr.validationQR}</p>`;
generateQRCode(validationQrData, validationQrCodeDiv);
validationQrCodeDiv.classList.remove('hidden');
displayTicketSummary(); // Update summary with Ref#
// Prepare Email
let attendeeDetailsString = "";
personDetails.forEach((p, i) => {
const genderDisplay = tr[p.gender.toLowerCase()] || p.gender;
attendeeDetailsString += `${i + 1}. ${p.name} (Age: ${p.age},
Gender: ${genderDisplay})\n`;
});
const templateParams = {
to_email: userEmail, user_name: personDetails[0]?.name ||
'Valued Customer',
museum: currentTicket.museum, service: currentTicket.service,
date: formatDate(currentTicket.date),
time: currentTicket.time, persons: currentTicket.persons,
cost: currentTicket.cost.toFixed(2),
reference_number: currentTicket.referenceNumber, phone_number:
currentTicket.phone,
attendee_details: attendeeDetailsString.trim(),
};
confirmTicketButton.childNodes[confirmTicketButton.childNodes.length - 1].nodeValue
= ` ${tr.confirmAndEmail}`; // Reset text
ticketSection.classList.remove('hidden'); // Show actions again
confirmationMessage.classList.add('hidden');
} finally {
if (isHistoryVisible) { displayPreviousTickets(); }
chatContainer.scrollTop = chatContainer.scrollHeight;
}
}
function cancelTicket() {
if (!currentTicket || !currentTicket.referenceNumber) return;
cancelTicketButton.disabled = true;
bookedTickets = bookedTickets.filter(ticket =>
ticket.referenceNumber !== refToCancel);
saveBookedTickets();
confirmationMessage.classList.add('hidden');
appendMessage('receiver',
tr.ticketCancelledMessage.replace('{referenceNumber}', refToCancel));
resetChatState(); // Partial reset
setTimeout(() => {
appendMessage('receiver', tr.selectMuseumPrompt);
museumSelection.classList.remove('hidden');
chatContainer.scrollTop = chatContainer.scrollHeight;
}, 1000);
if (isHistoryVisible) { displayPreviousTickets(); }
currentTicket = null;
}
// --- Utility Functions ---
function formatDate(dateString) {
if (!dateString || dateString.length < 10) return "Invalid Date"; //
Basic check
try {
const date = new Date(dateString + 'T00:00:00Z'); // Assume UTC
date input
const options = { year: 'numeric', month: 'long', day: 'numeric',
timeZone: 'UTC' };
let locale = 'en-US';
if (currentLanguage === 'hi') locale = 'hi-IN';
if (currentLanguage === 'ta') locale = 'ta-IN';
// Check if Intl is supported and locale is valid, otherwise
fallback
if (typeof Intl?.DateTimeFormat.supportedLocalesOf === 'function'
&& Intl.DateTimeFormat.supportedLocalesOf(locale).length > 0) {
return date.toLocaleDateString(locale, options);
} else {
// Simple fallback if locale isn't supported
return date.toISOString().slice(0, 10); // YYYY-MM-DD
}
} catch (e) {
console.error("Error formatting date:", e, "Input:", dateString);
return dateString; // Fallback
}
}
function generateReferenceNumber() {
const datePart = new Date().toISOString().slice(0, 10).replace(/-/g,
"");
const randomPart = Math.random().toString(36).substring(2,
8).toUpperCase();
return `M${datePart}-${randomPart}`;
}
function showError(message) {
errorMessageText.textContent = message;
errorMessage.classList.remove('hidden');
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function hideErrorMessage() {
if(!errorMessage.classList.contains('hidden')){
errorMessage.classList.add('hidden');
errorMessageText.textContent = '';
}
}
function saveBookedTickets() {
try {
localStorage.setItem('bookedTickets',
JSON.stringify(bookedTickets));
} catch (error) {
console.error("Failed to save booked tickets:", error);
}
}
function displayPreviousTickets() {
previousTicketsList.innerHTML = ''; // Clear list
const lang = currentLanguage;
const tr = translations[lang];
if (bookedTickets.length === 0) {
previousTicketsList.innerHTML = `<p class="italic text-gray-500
dark:text-gray-400 p-2">${tr.noPreviousTickets}</p>`;
} else {
[...bookedTickets].reverse().forEach((ticket) => {
const ticketItem = document.createElement('div');
ticketItem.className = 'ticket-item p-2 border-b dark:border-
gray-600';
const museumName = serviceData[ticket.museumKey]?.[`name_$
{lang}`] || ticket.museum;
const serviceName =
serviceData[ticket.museumKey]?.services[ticket.serviceKey]?.[`name_${lang}`] ||
ticket.service;
if (userBookings.length === 1)
{ displayBookingDetails(userBookings[0]); }
else if (userBookings.length > 1) {
let bookingListText = ""; const newestFirst =
[...userBookings].reverse();
newestFirst.forEach((b, i) => {
const museumName = serviceData[b.museumKey]?.[`name_$
{lang}`] || b.museum;
bookingListText += `${i + 1}. ${museumName} on $
{formatDate(b.date)} (${b.referenceNumber})\n`;
});
const selectionPrompt = tr.bookingsFound.replace('{list}',
bookingListText);
const chosenIndexStr = prompt(selectionPrompt);
const chosenIndex = parseInt(chosenIndexStr) - 1;
if (!isNaN(chosenIndex) && chosenIndex >= 0 && chosenIndex <
newestFirst.length) {
displayBookingDetails(newestFirst[chosenIndex]);
} else if (chosenIndexStr !== null) { alert("Invalid
selection."); }
} else { alert(tr.bookingNotFound); }
} else if (lookupType !== null) { alert("Invalid lookup type
selected."); }
}
function displayBookingDetails(booking) {
const lang = currentLanguage;
const tr = translations[lang];
const museumName = serviceData[booking.museumKey]?.[`name_${lang}`] ||
booking.museum;
const serviceName =
serviceData[booking.museumKey]?.services[booking.serviceKey]?.[`name_${lang}`] ||
booking.service;
alert(details);
appendMessage('receiver', `Booking details for Ref: $
{booking.referenceNumber} displayed.`);
}
</script>
</body>
</html>
```