Jam buka tutup dinamis, tapi booking nya jadi gabisa

This commit is contained in:
Stephen Gesityan 2025-06-04 14:25:42 +07:00
parent d59f1fb75e
commit b8f70e7f6f
2 changed files with 106 additions and 145 deletions

View File

@ -18,12 +18,17 @@ public function venue($venueName) {
} }
// Ambil tabel-tabel terkait dengan venue // Ambil tabel-tabel terkait dengan venue
$tables = $venue->tables; $venue->load('tables'); // Eager loading untuk optimasi
// Mengirim data venue dan tabel ke view // Parsing jam operasional dari format H:i:s menjadi integer
$openHour = (int) date('H', strtotime($venue->open_time));
$closeHour = (int) date('H', strtotime($venue->close_time));
// Mengirim data venue dengan jam operasional ke view
return view('pages.venue', [ return view('pages.venue', [
'venue' => $venue, 'venue' => $venue,
'tables' => $tables 'openHour' => $openHour,
'closeHour' => $closeHour
]); ]);
} }
} }

View File

@ -21,7 +21,12 @@ class="fixed inset-0 bg-black bg-opacity-50 z-40 flex items-center justify-cente
class="w-full h-full object-cover rounded-lg mb-4 mt-8" /> class="w-full h-full object-cover rounded-lg mb-4 mt-8" />
<h1 class="text-xl text-gray-800 font-semibold">{{ $venue['name'] }}</h1> <h1 class="text-xl text-gray-800 font-semibold">{{ $venue['name'] }}</h1>
<p class="text-sm text-gray-500">{{ $venue['location'] }}</p> <p class="text-sm text-gray-500">{{ $venue['location'] ?? 'Lokasi tidak tersedia' }}</p>
<p class="text-sm text-gray-600 mt-1">
<i class="fa-regular fa-clock"></i>
Jam Operasional: {{ date('H:i', strtotime($venue['open_time'])) }} -
{{ date('H:i', strtotime($venue['close_time'])) }}
</p>
</div> </div>
<a href="https://www.google.com/maps/search/?api=1&query={{ urlencode($venue['address']) }}" target="_blank" <a href="https://www.google.com/maps/search/?api=1&query={{ urlencode($venue['address']) }}" target="_blank"
class="flex items-center bg-[url('/public/images/map.jpg')] bg-cover bg-center p-4"> class="flex items-center bg-[url('/public/images/map.jpg')] bg-cover bg-center p-4">
@ -100,7 +105,7 @@ class="bg-gray-200 text-gray-700 text-sm px-3 py-1 rounded-md hover:bg-gray-300"
</div> </div>
</div> </div>
@foreach ($venue['tables'] as $table) @foreach ($venue['tables'] as $table)
<div x-data="booking(@json(auth()->check()), '{{ $table['id'] }}')" <div x-data="booking(@json(auth()->check()), '{{ $table['id'] }}', {{ $openHour }}, {{ $closeHour }})"
class="border rounded-lg shadow-md p-4 mb-4"> class="border rounded-lg shadow-md p-4 mb-4">
<div class="flex items-center justify-between cursor-pointer" <div class="flex items-center justify-between cursor-pointer"
@click="open = !open; if(open) checkBookedSchedules()"> @click="open = !open; if(open) checkBookedSchedules()">
@ -123,7 +128,7 @@ class="border rounded-lg shadow-md p-4 mb-4">
<h4 class="font-semibold mb-2">Pilih Jam Booking:</h4> <h4 class="font-semibold mb-2">Pilih Jam Booking:</h4>
<select class="w-full border p-2 rounded-lg" x-model="selectedTime"> <select class="w-full border p-2 rounded-lg" x-model="selectedTime">
<option value="">-- Pilih Jam --</option> <option value="">-- Pilih Jam --</option>
<template x-for="hour in getHoursInRange(9, 24)" :key="hour"> <template x-for="hour in getAvailableHours()" :key="hour">
<option :value="hour + ':00'" :disabled="isTimeBooked(hour + ':00')" <option :value="hour + ':00'" :disabled="isTimeBooked(hour + ':00')"
x-text="hour + ':00' + (isTimeBooked(hour + ':00') ? ' (Booked)' : '')"> x-text="hour + ':00' + (isTimeBooked(hour + ':00') ? ' (Booked)' : '')">
</option> </option>
@ -178,12 +183,12 @@ function showToast(message, type = 'info', duration = 5000) {
toast.className = `${bgColor} text-white px-6 py-4 rounded-lg shadow-lg flex items-center space-x-3 min-w-80 transform transition-all duration-300 translate-x-full opacity-0`; toast.className = `${bgColor} text-white px-6 py-4 rounded-lg shadow-lg flex items-center space-x-3 min-w-80 transform transition-all duration-300 translate-x-full opacity-0`;
toast.innerHTML = ` toast.innerHTML = `
<i class="fas ${icon}"></i> <i class="fas ${icon}"></i>
<span class="flex-1">${message}</span> <span class="flex-1">${message}</span>
<button onclick="this.parentElement.remove()" class="text-white hover:text-gray-200"> <button onclick="this.parentElement.remove()" class="text-white hover:text-gray-200">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
`; `;
toastContainer.appendChild(toast); toastContainer.appendChild(toast);
@ -219,20 +224,20 @@ function showModal(title, message, type = 'info', callback = null) {
}[type] || 'fa-info-circle'; }[type] || 'fa-info-circle';
modal.innerHTML = ` modal.innerHTML = `
<div class="bg-white rounded-lg p-6 max-w-md w-full shadow-2xl transform transition-all"> <div class="bg-white rounded-lg p-6 max-w-md w-full shadow-2xl transform transition-all">
<div class="flex items-center space-x-3 mb-4"> <div class="flex items-center space-x-3 mb-4">
<i class="fas ${icon} text-2xl ${iconColor}"></i> <i class="fas ${icon} text-2xl ${iconColor}"></i>
<h3 class="text-lg font-semibold text-gray-800">${title}</h3> <h3 class="text-lg font-semibold text-gray-800">${title}</h3>
</div> </div>
<p class="text-gray-600 mb-6">${message}</p> <p class="text-gray-600 mb-6">${message}</p>
<div class="flex justify-end space-x-3"> <div class="flex justify-end space-x-3">
<button onclick="this.closest('.fixed').remove(); ${callback ? callback + '()' : ''}" <button onclick="this.closest('.fixed').remove(); ${callback ? callback + '()' : ''}"
class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 transition-colors"> class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 transition-colors">
OK OK
</button> </button>
</div> </div>
</div> </div>
`; `;
document.body.appendChild(modal); document.body.appendChild(modal);
@ -251,22 +256,22 @@ function showConfirmModal(title, message, onConfirm, onCancel = null) {
modal.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'; modal.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4';
modal.innerHTML = ` modal.innerHTML = `
<div class="bg-white rounded-lg p-6 max-w-md w-full shadow-2xl transform transition-all"> <div class="bg-white rounded-lg p-6 max-w-md w-full shadow-2xl transform transition-all">
<div class="flex items-center space-x-3 mb-4"> <div class="flex items-center space-x-3 mb-4">
<i class="fas fa-question-circle text-2xl text-yellow-500"></i> <i class="fas fa-question-circle text-2xl text-yellow-500"></i>
<h3 class="text-lg font-semibold text-gray-800">${title}</h3> <h3 class="text-lg font-semibold text-gray-800">${title}</h3>
</div> </div>
<p class="text-gray-600 mb-6">${message}</p> <p class="text-gray-600 mb-6">${message}</p>
<div class="flex justify-end space-x-3"> <div class="flex justify-end space-x-3">
<button id="cancelBtn" class="bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400 transition-colors"> <button id="cancelBtn" class="bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400 transition-colors">
Batal Batal
</button> </button>
<button id="confirmBtn" class="bg-red-500 text-white px-4 py-2 rounded-md hover:bg-red-600 transition-colors"> <button id="confirmBtn" class="bg-red-500 text-white px-4 py-2 rounded-md hover:bg-red-600 transition-colors">
Ya, Hapus Ya, Hapus
</button> </button>
</div> </div>
</div> </div>
`; `;
document.body.appendChild(modal); document.body.appendChild(modal);
@ -535,19 +540,22 @@ function formatPrice(price) {
} }
})); }));
// Regular booking component (existing) // Regular booking component (updated with dynamic hours)
Alpine.data('booking', (isLoggedIn, tableId) => ({ Alpine.data('booking', (isLoggedIn, tableId, openHour, closeHour) => ({
isLoggedIn, isLoggedIn,
tableId, tableId,
openHour, // Dynamic open hour from venue
closeHour, // Dynamic close hour from venue
open: false, open: false,
selectedTime: '', selectedTime: '',
selectedDuration: '', selectedDuration: '',
isLoading: false, isLoading: false,
bookedSchedules: [], bookedSchedules: [],
getHoursInRange(startHour, endHour) { // Updated method to use dynamic hours from venue
getAvailableHours() {
let hours = []; let hours = [];
for (let i = startHour; i <= endHour; i++) { for (let i = this.openHour; i <= this.closeHour; i++) {
hours.push(i.toString().padStart(2, '0')); hours.push(i.toString().padStart(2, '0'));
} }
return hours; return hours;
@ -594,34 +602,17 @@ function formatPrice(price) {
// Uncomment this for production to prevent booking past times // Uncomment this for production to prevent booking past times
if (selectedDateTime <= now) { if (selectedDateTime <= now) {
showToast('Jam yang dipilih sudah lewat. Silakan pilih jam yang masih tersedia.', 'warning'); showToast('Tidak bisa booking untuk waktu yang sudah berlalu', 'warning');
return; return;
} }
this.isLoading = true; this.isLoading = true;
window.dispatchEvent(new CustomEvent('show-loading')); window.dispatchEvent(new CustomEvent('show-loading'));
// Hitung end time
const bookingStart = new Date();
bookingStart.setHours(selectedHour, selectedMinute, 0, 0);
const bookingEnd = new Date(bookingStart);
bookingEnd.setHours(bookingEnd.getHours() + parseInt(selectedDuration));
const endTimeFormatted = ('0' + bookingEnd.getHours()).slice(-2) + ':' + ('0' + bookingEnd.getMinutes()).slice(-2);
// Gunakan tanggal Jakarta yang konsisten // Gunakan tanggal Jakarta yang konsisten
const today = getJakartaDateString(); const bookingDate = getJakartaDateString();
const start_time = `${today} ${selectedTime}`; fetch('/booking/initiate', {
const end_time = `${today} ${endTimeFormatted}`;
console.log('Booking data:', { start_time, end_time, today });
// Track that we're creating a new booking
window.creatingNewBooking = true;
// Kirim ke backend untuk membuat payment intent
fetch('/booking/payment-intent', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -629,86 +620,53 @@ function formatPrice(price) {
}, },
body: JSON.stringify({ body: JSON.stringify({
table_id: tableId, table_id: tableId,
start_time: start_time, start_time: selectedTime,
end_time: end_time, duration: selectedDuration,
booking_date: bookingDate
}), }),
}) })
.then(res => { .then(res => res.json())
if (!res.ok) {
return res.json().then(err => {
throw new Error(err.message || 'Gagal membuat booking');
});
}
return res.json();
})
.then(data => { .then(data => {
window.dispatchEvent(new CustomEvent('hide-loading')); window.dispatchEvent(new CustomEvent('hide-loading'));
// Cek apakah ini response dari admin direct booking if (data.success) {
if (data.booking_id && data.message === 'Booking created successfully') { console.log("Opening payment with snap token:", data.snap_token);
// Admin booking berhasil tanpa payment // Open Snap payment
showToast('Booking berhasil dibuat!', 'success'); window.snap.pay(data.snap_token, {
this.isLoading = false; onSuccess: (result) => {
this.createBooking(data.order_id, result);
},
onPending: (result) => {
showToast('Pembayaran pending, silahkan selesaikan pembayaran', 'warning');
this.isLoading = false;
},
onError: (result) => {
showToast('Pembayaran gagal', 'error');
this.isLoading = false;
},
onClose: () => {
showToast('Anda menutup popup tanpa menyelesaikan pembayaran', 'warning');
this.isLoading = false;
window.justClosedPayment = true;
// Reset form // Dispatch event to refresh pending bookings
this.selectedTime = '';
this.selectedDuration = '';
this.open = false;
// Refresh halaman atau komponen yang diperlukan
this.checkBookedSchedules();
// Refresh pending bookings jika ada
document.dispatchEvent(refreshPendingBookingsEvent);
return; // Exit early untuk admin booking
}
// Flow normal untuk customer (membutuhkan payment)
if (!data.snap_token) {
throw new Error('Snap token tidak ditemukan');
}
// Track bahwa kita sedang membuat booking baru
window.creatingNewBooking = true;
// Buka Snap Midtrans untuk customer
window.snap.pay(data.snap_token, {
onSuccess: (result) => {
this.createBookingAfterPayment(data.order_id, result);
},
onPending: (result) => {
showToast('Pembayaran pending, silahkan selesaikan pembayaran', 'warning');
this.isLoading = false;
},
onError: (result) => {
showToast('Pembayaran gagal', 'error');
this.isLoading = false;
window.creatingNewBooking = false;
},
onClose: () => {
showToast('Anda menutup popup tanpa menyelesaikan pembayaran', 'warning');
this.isLoading = false;
window.justClosedPayment = true;
if (window.creatingNewBooking) {
window.creatingNewBooking = false;
document.dispatchEvent(refreshPendingBookingsEvent); document.dispatchEvent(refreshPendingBookingsEvent);
} }
} });
}); } else {
showToast(data.message, 'error');
this.isLoading = false;
}
}) })
.catch(err => { .catch(err => {
window.dispatchEvent(new CustomEvent('hide-loading')); window.dispatchEvent(new CustomEvent('hide-loading'));
console.error('Booking error:', err); console.error('Booking initiation error:', err);
showToast('Gagal membuat booking: ' + err.message, 'error'); showToast('Gagal melakukan booking', 'error');
this.isLoading = false; this.isLoading = false;
window.creatingNewBooking = false;
}); });
}, },
// Fungsi untuk menyimpan booking setelah pembayaran berhasil createBooking(orderId, paymentResult) {
createBookingAfterPayment(orderId, paymentResult) {
window.dispatchEvent(new CustomEvent('show-loading')); window.dispatchEvent(new CustomEvent('show-loading'));
fetch('/booking', { fetch('/booking', {
@ -735,14 +693,17 @@ function formatPrice(price) {
.then(data => { .then(data => {
window.dispatchEvent(new CustomEvent('hide-loading')); window.dispatchEvent(new CustomEvent('hide-loading'));
showToast('Pembayaran dan booking berhasil!', 'success'); showToast('Pembayaran dan booking berhasil!', 'success');
this.isLoading = false;
// Reset the flag // Reset form
window.creatingNewBooking = false; this.selectedTime = '';
this.selectedDuration = '';
this.open = false;
// Refresh component data // Refresh booked schedules
document.dispatchEvent(new CustomEvent('booking-completed')); this.checkBookedSchedules();
// Redirect to booking history page // Redirect to booking history
setTimeout(() => { setTimeout(() => {
window.location.href = '/booking/history'; window.location.href = '/booking/history';
}, 2000); }, 2000);
@ -752,19 +713,14 @@ function formatPrice(price) {
console.error('Booking error:', err); console.error('Booking error:', err);
showToast('Pembayaran berhasil tetapi gagal menyimpan booking: ' + err.message, 'error'); showToast('Pembayaran berhasil tetapi gagal menyimpan booking: ' + err.message, 'error');
this.isLoading = false; this.isLoading = false;
window.creatingNewBooking = false;
}); });
},
// Method to refresh booked schedules without reloading the page
async refreshBookedSchedules() {
await this.checkBookedSchedules();
} }
})); }));
}); });
// Initialize clock update // Initialize clock
updateClock();
setInterval(updateClock, 1000); setInterval(updateClock, 1000);
updateClock(); // Initial call
</script> </script>
@endsection @endsection