MIF_E31222882/resources/views/pages/venue.blade.php

911 lines
49 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@extends('layouts.main')
@section('content')
<div id="toast-container" class="fixed top-4 right-4 z-50 space-y-2"></div>
<div x-data="{ show: false }" x-show="show" x-cloak x-on:show-loading.window="show = true"
x-on:hide-loading.window="show = false"
class="fixed inset-0 bg-black bg-opacity-50 z-40 flex items-center justify-center">
<div class="bg-white rounded-lg p-6 shadow-xl">
<div class="flex items-center space-x-3">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span class="text-gray-700">Memproses booking...</span>
</div>
</div>
</div>
<div class="min-h-96 mx-4 md:w-3/4 md:mx-auto">
@php
// Menyiapkan semua gambar (cover + galeri) untuk Alpine.js
$galleryPaths = collect();
if ($venue->image) {
// Tambahkan gambar utama sebagai gambar pertama
$galleryPaths->push(asset('storage/' . $venue->image));
}
// Tambahkan gambar-gambar dari relasi 'images'
if(isset($venue->images) && $venue->images->isNotEmpty()) {
foreach ($venue->images as $img) {
$galleryPaths->push(asset('storage/' . $img->path));
}
}
@endphp
<div x-data="galleryViewer({ images: {{ $galleryPaths->toJson() }} })" class="mb-6 mt-8">
<div class="grid grid-cols-1 md:grid-cols-3 md:gap-2">
<div class="md:col-span-2 mb-2 md:mb-0">
<div @if($galleryPaths->isNotEmpty()) @click="open(0)" @endif class="relative w-full h-64 md:h-[33rem] cursor-pointer group">
<img src="{{ $venue->image ? asset('storage/' . $venue->image) : 'https://via.placeholder.com/800x400.png?text=Venue+Image' }}" alt="{{ $venue['name'] }}"
class="w-full h-full object-cover rounded-lg shadow-lg" />
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-20 transition-all duration-300 rounded-lg"></div>
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-1 gap-2">
@if(isset($venue->images) && $venue->images->isNotEmpty())
@foreach($venue->images->take(2) as $image)
@php
// Indeks ini untuk membuka gambar yang benar di modal
// +1 karena gambar utama (cover) ada di indeks 0
$modalIndex = $loop->index + 1;
@endphp
<div @click="open({{ $modalIndex }})" class="relative w-full h-40 md:h-64 cursor-pointer group">
<img src="{{ asset('storage/' . $image->path) }}" alt="Gallery image {{ $loop->iteration }}"
class="w-full h-full object-cover rounded-lg shadow-lg" />
@if($loop->last && $galleryPaths->count() > 1)
<div class="absolute inset-0 bg-black bg-opacity-40 flex items-center justify-center rounded-lg opacity-100 group-hover:bg-opacity-50 transition-all duration-300">
<span class="text-white font-semibold text-center">
<i class="fa-regular fa-images mr-1"></i>
Lihat Semua Foto
</span>
</div>
@else
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-20 transition-all duration-300 rounded-lg"></div>
@endif
</div>
@endforeach
@endif
</div>
</div>
<div x-show="isOpen" @keydown.escape.window="close()" @keydown.arrow-right.window="next()" @keydown.arrow-left.window="prev()"
class="fixed inset-0 z-[999] flex items-center justify-center bg-black bg-opacity-80" x-cloak>
<button @click="close()" class="absolute top-4 right-5 text-white text-4xl z-50 hover:text-gray-300">×</button>
<div class="relative w-full h-full flex items-center justify-center p-4 md:p-8">
<button @click="prev()" class="absolute left-2 md:left-5 text-white text-3xl md:text-5xl opacity-70 hover:opacity-100 p-2 z-50">
</button>
<img :src="images[currentIndex]" class="w-auto h-auto object-contain" style="max-height: 90vh; max-width: 90vw;">
<button @click="next()" class="absolute right-2 md:right-5 text-white text-3xl md:text-5xl opacity-70 hover:opacity-100 p-2 z-50">
</button>
<div class="absolute bottom-4 text-white text-sm bg-black bg-opacity-50 px-2 py-1 rounded">
<span x-text="currentIndex + 1"></span> / <span x-text="images.length"></span>
</div>
</div>
</div>
<div class="mt-4">
<h1 class="text-xl text-gray-800 font-semibold">{{ $venue['name'] }}</h1>
{{-- <p class="text-sm text-gray-500">{{ $venue['description'] ?? 'Tidak ada deskripsi.' }}</p> --}}
<div x-data="{ isOpen: false }" class="text-sm">
@if($venue['status'] === 'open')
@php
// Siapkan data untuk ditampilkan
$days = [1 => 'Senin', 2 => 'Selasa', 3 => 'Rabu', 4 => 'Kamis', 5 => 'Jumat', 6 => 'Sabtu', 7 => 'Minggu'];
$hoursByDay = $venue->operatingHours->keyBy('day_of_week');
$todayDayNumber = now('Asia/Jakarta')->dayOfWeekIso; // 1 for Monday, 7 for Sunday
$todaysHours = $hoursByDay->get($todayDayNumber);
$isOpenNow = false;
$statusText = 'Tutup';
$statusColor = 'text-red-600';
if ($todaysHours && !$todaysHours->is_closed) {
$openTimeToday = \Carbon\Carbon::parse($todaysHours->open_time);
$closeTimeToday = \Carbon\Carbon::parse($todaysHours->close_time);
$now = now('Asia/Jakarta');
// Logika untuk 'Buka 24 Jam'
if ($openTimeToday->format('H:i') == '00:00' && $closeTimeToday->format('H:i') == '23:59') {
$isOpenNow = true;
$statusText = 'Buka 24 jam';
}
// Logika untuk jam overnight (lewat tengah malam)
elseif ($closeTimeToday->lt($openTimeToday)) {
if ($now->between($openTimeToday, $closeTimeToday->copy()->addDay())) {
$isOpenNow = true;
}
}
// Logika untuk jam normal
else {
if ($now->between($openTimeToday, $closeTimeToday)) {
$isOpenNow = true;
}
}
if($isOpenNow && $statusText == 'Tutup'){
$statusText = 'Buka sekarang';
}
$statusColor = $isOpenNow ? 'text-green-600' : 'text-red-600';
}
@endphp
<button @click="isOpen = !isOpen" class="flex items-center space-x-2 w-full text-left">
<i class="fa-regular fa-clock {{ $statusColor }}"></i>
<span class="font-medium {{ $statusColor }}">{{ $statusText }}</span>
<span class="text-gray-500">·</span>
<span class="text-gray-600">
@if($todaysHours && !$todaysHours->is_closed)
{{ \Carbon\Carbon::parse($todaysHours->open_time)->format('H:i') }} - {{ \Carbon\Carbon::parse($todaysHours->close_time)->format('H:i') }}
@else
Tutup
@endif
</span>
<i class="fa-solid fa-chevron-down text-xs text-gray-500 transition-transform" :class="{'rotate-180': isOpen}"></i>
</button>
<div x-show="isOpen" x-collapse class="mt-3 pl-6">
<table class="w-full text-left">
<tbody>
@foreach($days as $dayNumber => $dayName)
@php
$schedule = $hoursByDay->get($dayNumber);
$isToday = ($dayNumber == $todayDayNumber);
@endphp
<tr class="{{ $isToday ? 'font-bold' : '' }}">
<td class="py-1 pr-4">{{ $dayName }}</td>
<td class="py-1 text-gray-800">
@if($schedule && !$schedule->is_closed)
@if($schedule->open_time == '00:00:00' && $schedule->close_time == '23:59:00')
Buka 24 jam
@else
{{ \Carbon\Carbon::parse($schedule->open_time)->format('H:i') }} - {{ \Carbon\Carbon::parse($schedule->close_time)->format('H:i') }}
@endif
@else
Tutup
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else <div class="mt-1 flex items-center space-x-2">
<i class="fa-solid fa-circle-xmark text-red-500"></i>
<p class="text-sm text-red-600 font-medium">
Tutup Sementara - {{ $venue['close_reason'] }}
</p>
</div>
@endif
</div>
<div class="mt-4 flex items-center space-x-4">
@if($venue->link_instagram)
<a href="{{ $venue->link_instagram }}" target="_blank" class="text-2xl text-orange-500 hover:text-orange-800">
<i class="fab fa-instagram fa-lg"></i>
</a>
@endif
@if($venue->link_tiktok)
<a href="{{ $venue->link_tiktok }}" target="_blank" class="text-2xl text-gray-700 hover:text-black">
<i class="fab fa-tiktok fa-lg"></i>
</a>
@endif
@if($venue->link_facebook)
<a href="{{ $venue->link_facebook }}" target="_blank" class="text-2xl text-blue-600 hover:text-blue-900">
<i class="fab fa-facebook-f fa-lg"></i>
</a>
@endif
@if($venue->link_x)
<a href="{{ $venue->link_x }}" target="_blank" class="text-2xl text-blue-500 hover:text-blue-800">
<i class="fab fa-twitter fa-lg"></i>
</a>
@endif
</div>
<div x-data="{ open: false }" class="relative inline-block text-left mt-4">
<div>
<button @click="open = !open" type="button" class="inline-flex items-center justify-center w-full rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
<i class="fa-solid fa-map-location-dot mr-2"></i>
Di Sekitar
<i class="fa-solid fa-chevron-down text-xs ml-2"></i>
</button>
</div>
<div x-show="open" @click.away="open = false" x-transition
class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-10">
<div class="py-1" role="none">
@if($venue->latitude && $venue->longitude)
@php
// PASTIKAN BARIS INI MEMILIKI "https://www."
$baseUrl = "https://www.google.com/maps/search/";
$coordinates = "@" . $venue->latitude . ',' . $venue->longitude;
$restoUrl = $baseUrl . "Restoran/" . $coordinates . ",15z";
$hotelUrl = $baseUrl . "Hotel/" . $coordinates . ",15z";
$minimarketUrl = $baseUrl . "Minimarket/" . $coordinates . ",15z";
$atmUrl = $baseUrl . "ATM/" . $coordinates . ",17z";
@endphp
<a href="{{ $restoUrl }}" target="_blank" class="text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100">
<i class="fa-solid fa-utensils w-5 mr-2"></i>Restoran
</a>
<a href="{{ $hotelUrl }}" target="_blank" class="text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100">
<i class="fa-solid fa-hotel w-5 mr-2"></i>Hotel
</a>
<a href="{{ $minimarketUrl }}" target="_blank" class="text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100">
<i class="fa-solid fa-store w-5 mr-2"></i>Minimarket
</a>
<a href="{{ $atmUrl }}" target="_blank" class="text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100">
<i class="fa-solid fa-credit-card w-5 mr-2"></i>ATM
</a>
@else
<span class="text-gray-400 block px-4 py-2 text-sm">Koordinat venue belum diatur.</span>
@endif
</div>
</div>
</div>
</div>
</div>
<a href="http://maps.google.com/?q={{ urlencode($venue['address']) }}" target="_blank"
class="flex items-center bg-[url('/public/images/map.jpg')] bg-cover bg-center p-4">
<div>
<h1 class="font-semibold">Lokasi Venue</h1>
<p>{{ $venue['address'] }}</p>
</div>
<div>
<i class="fa-solid fa-map-pin text-red-800 text-3xl"></i>
</div>
</a>
@auth
<div x-data="pendingBookingsComponent" class="mt-6">
<template x-if="pendingBookings.length > 0">
<div class="bg-orange-50 border border-orange-200 rounded-lg p-4 mb-6">
<div class="flex justify-between items-center mb-2">
<h2 class="font-semibold text-orange-700">
<i class="fa-solid fa-clock"></i> Booking yang Belum Diselesaikan
</h2>
<button @click="showPendingBookings = !showPendingBookings"
class="text-orange-700 hover:text-orange-900">
<span x-show="!showPendingBookings"> Lihat</span>
<span x-show="showPendingBookings"> Tutup</span>
</button>
</div>
<div x-show="showPendingBookings" x-transition class="mt-3">
<p class="text-sm text-orange-700 mb-2">Anda memiliki booking yang belum diselesaikan:</p>
<template x-for="booking in pendingBookings" :key="booking . id">
<div class="bg-white rounded-md p-3 mb-2 shadow-sm border border-orange-200">
<div class="flex justify-between">
<div>
<p class="font-medium" x-text="booking.table.name"></p>
<p class="text-sm text-gray-600"
x-text="formatDateTime(booking.start_time) + ' - ' + formatTime(booking.end_time)">
</p>
<p class="text-sm font-medium text-gray-800 mt-1">
<span>Rp </span>
<span x-text="formatPrice(booking.total_amount)"></span>
</p>
</div>
<div class="flex space-x-2">
<button @click="resumeBooking(booking.id)"
class="bg-blue-500 text-white text-sm px-3 py-1 rounded-md hover:bg-blue-600"
:disabled="isLoadingPending">
<template x-if="isLoadingPending">
<span>Loading...</span>
</template>
<template x-if="!isLoadingPending">
<span>Lanjutkan</span>
</template>
</button>
<button @click="deletePendingBooking(booking.id)"
class="bg-gray-200 text-gray-700 text-sm px-3 py-1 rounded-md hover:bg-gray-300"
:disabled="isLoadingPending">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</div>
</div>
</template>
</div>
</div>
</template>
</div>
@endauth
<div class="mt-6">
<div class="flex justify-between">
<div>
<h1 class="text-xl text-gray-800 font-semibold">Pilih Meja</h1>
</div>
<div>
<h1 id="realTimeClock"></h1>
</div>
</div>
@foreach ($venue['tables'] as $table)
<div x-data="booking(
@json(auth()->check()),
'{{ $table['id'] }}',
{{ date('G', strtotime($openTime)) }}, {{-- <-- Gunakan variabel baru --}}
{{ date('G', strtotime($closeTime)) }}, {{-- <-- Gunakan variabel baru --}}
{{ (strtotime($closeTime) < strtotime($openTime)) ? 'true' : 'false' }} {{-- <-- Logika is_overnight dinamis --}}
)"
class="border rounded-lg shadow-md p-4 mb-4">
<div class="flex items-center justify-between cursor-pointer"
@click="open = !open; if(open) checkBookedSchedules()">
<div class="flex items-center">
<img src="{{ asset('images/meja.jpg') }}" class="w-24">
<div class="ml-4">
<h3 class="font-semibold">{{ $table['name'] }} ({{ $table['brand'] }})</h3>
<p class="text-sm font-semibold text-gray-500">
Rp. {{ number_format($table['price_per_hour'], 0, ',', '.') }} / jam
</p>
</div>
</div>
<div class="px-3 py-2 bg-gray-200 rounded-lg">
<span x-show="!open"></span>
<span x-show="open"></span>
</div>
</div>
<div x-show="open" x-collapse class="mt-4 p-4 border-t bg-gray-100 rounded-lg">
<h4 class="font-semibold mb-2">Pilih Jam Booking:</h4>
<select class="w-full border p-2 rounded-lg" x-model="selectedTime">
<option value="">-- Pilih Jam --</option>
<template x-for="hour in getAvailableHours()" :key="hour">
<option :value="hour + ':00'" :disabled="isTimeBooked(hour + ':00')"
x-text="hour + ':00' + (isTimeBooked(hour + ':00') ? ' (Booked)' : '')">
</option>
</template>
</select>
<h4 class="font-semibold mb-2 mt-4">Pilih Durasi Main:</h4>
@if ($venue['status'] === 'open')
<select class="w-full border p-2 rounded-lg" x-model="selectedDuration">
<option value="">-- Pilih Durasi --</option>
<option value="1">1 Jam</option>
<option value="2">2 Jam</option>
<option value="3">3 Jam</option>
</select>
@else
<select class="w-full border p-2 rounded-lg bg-gray-100 text-gray-400 cursor-not-allowed" disabled>
<option value="">Venue sedang tutup</option>
</select>
@endif
<button
class="mt-3 px-4 py-2 rounded-lg w-full
{{ $venue['status'] === 'open' ? 'bg-green-500 text-white' : 'bg-gray-400 text-gray-700 cursor-not-allowed' }}"
:disabled="!selectedTime || !selectedDuration || isLoading || '{{ $venue['status'] }}' !== 'open'"
@click="initiateBooking('{{ $table['id'] }}', '{{ addslashes($table['name']) }}')">
<template x-if="isLoading">
<span>Loading...</span>
</template>
<template x-if="!isLoading">
<span>Confirm Booking</span>
</template>
</button>
</div>
</div>
@endforeach
<div class="mt-8 pt-6 border-t">
<h2 class="text-xl font-bold text-gray-800 mb-4">Ulasan Pengguna</h2>
@if($totalReviews > 0)
<div class="flex items-center mb-6 bg-gray-50 p-4 rounded-lg">
<div class="text-5xl font-bold text-gray-800">{{ number_format($averageRating, 1) }}</div>
<div class="ml-4">
<div class="flex items-center">
@for ($i = 1; $i <= 5; $i++)
@if ($i <= round($averageRating))
<i class="fa-solid fa-star text-yellow-400"></i>
@else
<i class="fa-regular fa-star text-gray-300"></i>
@endif
@endfor
</div>
<p class="text-sm text-gray-600">Berdasarkan {{ $totalReviews }} ulasan</p>
</div>
</div>
<div class="space-y-6">
@foreach($venue->reviews->sortByDesc('created_at') as $review)
<div class="flex items-start space-x-4">
<div class="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center font-bold text-gray-600">
{{-- Ambil inisial nama --}}
{{ strtoupper(substr($review->user->name, 0, 1)) }}
</div>
<div class="flex-1">
<div class="flex items-center justify-between">
<div>
<p class="font-semibold text-gray-800">{{ $review->user->name }}</p>
<p class="text-xs text-gray-500">{{ $review->created_at->diffForHumans() }}</p>
</div>
<div class="flex items-center">
<span class="text-sm font-bold mr-1">{{ $review->rating }}</span>
<i class="fa-solid fa-star text-yellow-400"></i>
</div>
</div>
<p class="mt-2 text-gray-700">
{{ $review->comment }}
</p>
</div>
</div>
@endforeach
</div>
@else
<div class="text-center py-8 bg-gray-50 rounded-lg">
<p class="text-gray-500">Belum ada ulasan untuk venue ini.</p>
</div>
@endif
</div>
</div>
</div>
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<script src="https://app.sandbox.midtrans.com/snap/snap.js"
data-client-key="{{ config('midtrans.client_key') }}"></script>
<script>
// Toast Notification Function
function showToast(message, type = 'info', duration = 5000) {
const toastContainer = document.getElementById('toast-container');
const toast = document.createElement('div');
const bgColor = {
'success': 'bg-green-500',
'error': 'bg-red-500',
'warning': 'bg-yellow-500',
'info': 'bg-blue-500'
}[type] || 'bg-blue-500';
const icon = {
'success': 'fa-check-circle',
'error': 'fa-exclamation-circle',
'warning': 'fa-exclamation-triangle',
'info': 'fa-info-circle'
}[type] || 'fa-info-circle';
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 = `
<i class="fas ${icon}"></i>
<span class="flex-1">${message}</span>
<button onclick="this.parentElement.remove()" class="text-white hover:text-gray-200">
<i class="fas fa-times"></i>
</button>
`;
toastContainer.appendChild(toast);
setTimeout(() => {
toast.classList.remove('translate-x-full', 'opacity-0');
}, 100);
setTimeout(() => {
toast.classList.add('translate-x-full', 'opacity-0');
setTimeout(() => toast.remove(), 300);
}, duration);
}
// Modal Notification Function
function showModal(title, message, type = 'info', callback = null) {
const modal = document.createElement('div');
modal.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4';
const iconColor = {
'success': 'text-green-500',
'error': 'text-red-500',
'warning': 'text-yellow-500',
'info': 'text-blue-500'
}[type] || 'text-blue-500';
const icon = {
'success': 'fa-check-circle',
'error': 'fa-exclamation-circle',
'warning': 'fa-exclamation-triangle',
'info': 'fa-info-circle'
}[type] || 'fa-info-circle';
modal.innerHTML = `
<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">
<i class="fas ${icon} text-2xl ${iconColor}"></i>
<h3 class="text-lg font-semibold text-gray-800">${title}</h3>
</div>
<p class="text-gray-600 mb-6">${message}</p>
<div class="flex justify-end space-x-3">
<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">
OK
</button>
</div>
</div>
`;
document.body.appendChild(modal);
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.remove();
if (callback) callback();
}
});
}
// Confirmation Modal Function
function showConfirmModal(title, message, onConfirm, onCancel = null) {
const modal = document.createElement('div');
modal.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4';
modal.innerHTML = `
<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">
<i class="fas fa-question-circle text-2xl text-yellow-500"></i>
<h3 class="text-lg font-semibold text-gray-800">${title}</h3>
</div>
<p class="text-gray-600 mb-6">${message}</p>
<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">
Batal
</button>
<button id="confirmBtn" class="bg-red-500 text-white px-4 py-2 rounded-md hover:bg-red-600 transition-colors">
Ya, Hapus
</button>
</div>
</div>
`;
document.body.appendChild(modal);
const confirmBtn = modal.querySelector('#confirmBtn');
const cancelBtn = modal.querySelector('#cancelBtn');
confirmBtn.addEventListener('click', () => {
modal.remove();
if (onConfirm) onConfirm();
});
cancelBtn.addEventListener('click', () => {
modal.remove();
if (onCancel) onCancel();
});
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.remove();
if (onCancel) onCancel();
}
});
}
const refreshPendingBookingsEvent = new Event('refresh-pending-bookings');
function getJakartaDate() {
const now = new Date();
const jakartaTime = new Date(now.toLocaleString("en-US", { timeZone: "Asia/Jakarta" }));
return jakartaTime;
}
function getJakartaDateString() {
const jakartaTime = getJakartaDate();
const year = jakartaTime.getFullYear();
const month = String(jakartaTime.getMonth() + 1).padStart(2, '0');
const day = String(jakartaTime.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function updateClock() {
const jakartaTime = getJakartaDate();
const timeFormatter = new Intl.DateTimeFormat('id-ID', {
timeZone: 'Asia/Jakarta',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
document.getElementById('realTimeClock').textContent = timeFormatter.format(jakartaTime);
}
function formatDateTime(dateTimeStr) {
const parts = dateTimeStr.split(/[^0-9]/);
const year = parseInt(parts[0]);
const month = parseInt(parts[1]) - 1;
const day = parseInt(parts[2]);
const hour = parseInt(parts[3]);
const minute = parseInt(parts[4]);
const adjustedHour = (hour + 7) % 24;
const dateFormatter = new Intl.DateTimeFormat('id-ID', {
day: '2-digit',
month: 'short',
year: 'numeric',
});
const dateObj = new Date(year, month, day);
return dateFormatter.format(dateObj) + ' ' +
(adjustedHour.toString().padStart(2, '0') + ':' +
minute.toString().padStart(2, '0'));
}
function formatTime(timeStr) {
const parts = timeStr.split(/[^0-9]/);
const hour = parseInt(parts[3]);
const minute = parseInt(parts[4]);
const adjustedHour = (hour + 7) % 24;
return adjustedHour.toString().padStart(2, '0') + ':' +
minute.toString().padStart(2, '0');
}
function formatPrice(price) {
return new Intl.NumberFormat('id-ID').format(price);
}
document.addEventListener('alpine:init', () => {
// --- START: Gallery Viewer Alpine Component (KODE BARU) ---
Alpine.data('galleryViewer', (config) => ({
isOpen: false,
currentIndex: 0,
images: config.images || [],
open(index) {
if (this.images.length === 0) return;
this.currentIndex = index;
this.isOpen = true;
document.body.style.overflow = 'hidden';
},
close() {
this.isOpen = false;
document.body.style.overflow = 'auto';
},
next() {
this.currentIndex = (this.currentIndex + 1) % this.images.length;
},
prev() {
this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length;
}
}));
// --- END: Gallery Viewer Alpine Component ---
// Pending bookings component
Alpine.data('pendingBookingsComponent', () => ({
pendingBookings: [],
showPendingBookings: false,
isLoadingPending: false,
init() {
this.fetchPendingBookings();
document.addEventListener('refresh-pending-bookings', () => {
console.log('Refreshing pending bookings from event');
this.fetchPendingBookings();
this.showPendingBookings = true;
});
},
fetchPendingBookings() {
fetch('/booking/pending')
.then(response => response.json())
.then(data => {
const currentVenueId = {{ $venue['id'] ?? 'null' }};
if (currentVenueId) {
this.pendingBookings = data.filter(booking =>
booking.table && booking.table.venue_id === currentVenueId
);
} else {
this.pendingBookings = data;
}
console.log("Found", this.pendingBookings.length, "pending bookings");
if (this.pendingBookings.length > 0 && window.justClosedPayment) {
this.showPendingBookings = true;
window.justClosedPayment = false;
}
})
.catch(error => console.error('Error fetching pending bookings:', error));
},
resumeBooking(bookingId) {
this.isLoadingPending = true;
window.dispatchEvent(new CustomEvent('show-loading'));
fetch(`/booking/pending/${bookingId}/resume`)
.then(response => response.json())
.then(data => {
window.dispatchEvent(new CustomEvent('hide-loading'));
if (data.success) {
console.log("Opening payment with snap token:", data.snap_token);
window.snap.pay(data.snap_token, {
onSuccess: (result) => this.createBookingAfterPayment(data.order_id, result),
onPending: (result) => { showToast('Pembayaran pending...', 'warning'); this.isLoadingPending = false; },
onError: (result) => { showToast('Pembayaran gagal', 'error'); this.isLoadingPending = false; },
onClose: () => { showToast('Anda menutup popup pembayaran', 'warning'); this.isLoadingPending = false; }
});
} else {
showToast(data.message, 'error');
this.isLoadingPending = false;
this.fetchPendingBookings();
}
})
.catch(error => {
window.dispatchEvent(new CustomEvent('hide-loading'));
console.error('Error resuming booking:', error);
showToast('Gagal melanjutkan booking', 'error');
this.isLoadingPending = false;
});
},
deletePendingBooking(bookingId) {
showConfirmModal(
'Konfirmasi Hapus',
'Apakah Anda yakin ingin menghapus booking ini?',
() => {
this.isLoadingPending = true;
fetch(`/booking/pending/${bookingId}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content }
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('Booking berhasil dihapus', 'success');
this.fetchPendingBookings();
} else {
showToast(data.message, 'error');
}
this.isLoadingPending = false;
})
.catch(error => {
console.error('Error deleting booking:', error);
showToast('Gagal menghapus booking', 'error');
this.isLoadingPending = false;
});
}
);
},
createBookingAfterPayment(orderId, paymentResult) {
window.dispatchEvent(new CustomEvent('show-loading'));
fetch('/booking', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content },
body: JSON.stringify({ order_id: orderId, transaction_id: paymentResult.transaction_id, payment_method: paymentResult.payment_type, transaction_status: paymentResult.transaction_status }),
})
.then(res => {
if (!res.ok) return res.json().then(err => { throw new Error(err.message || 'Gagal menyimpan booking'); });
return res.json();
})
.then(data => {
window.dispatchEvent(new CustomEvent('hide-loading'));
showToast('Pembayaran dan booking berhasil!', 'success');
this.isLoadingPending = false;
this.fetchPendingBookings();
setTimeout(() => { window.location.href = '/booking/history'; }, 2000);
})
.catch(err => {
window.dispatchEvent(new CustomEvent('hide-loading'));
console.error('Booking error:', err);
showToast('Gagal menyimpan booking: ' + err.message, 'error');
this.isLoadingPending = false;
});
}
}));
// Regular booking component
Alpine.data('booking', (isLoggedIn, tableId, openHour, closeHour, isOvernight) => ({
isLoggedIn, tableId, openHour, closeHour, isOvernight, open: false, selectedTime: '', selectedDuration: '', isLoading: false, bookedSchedules: [],
getAvailableHours() {
let hours = [];
const currentJakartaHour = getJakartaDate().getHours();
if (this.isOvernight) {
for (let i = this.openHour; i < 24; i++) { if (i >= currentJakartaHour) hours.push(i.toString().padStart(2, '0')); }
for (let i = 0; i <= this.closeHour; i++) { hours.push(i.toString().padStart(2, '0')); }
} else {
for (let i = this.openHour; i <= this.closeHour; i++) { if (i >= currentJakartaHour) hours.push(i.toString().padStart(2, '0')); }
}
return hours;
},
isTimeBooked(time) {
const timeFormatted = time.padStart(5, '0');
return this.bookedSchedules.some(schedule => {
const isOvernightBooking = schedule.end < schedule.start;
if (isOvernightBooking) return (timeFormatted >= schedule.start || timeFormatted < schedule.end);
else return (timeFormatted >= schedule.start && timeFormatted < schedule.end);
});
},
async checkBookedSchedules() {
const today = getJakartaDateString();
try {
const response = await fetch(`/booking/schedules?table_id=${this.tableId}&date=${today}`);
this.bookedSchedules = await response.json();
} catch (error) { console.error('Error checking booked schedules:', error); }
},
initiateBooking(tableId, tableName) {
if (!this.isLoggedIn) { showToast('Silahkan login terlebih dahulu', 'warning'); return; }
if (!this.selectedTime || !this.selectedDuration) { showToast('Pilih jam dan durasi', 'warning'); return; }
const now = getJakartaDate();
const selectedDateTime = new Date(now);
const [selectedHour, selectedMinute] = this.selectedTime.split(':').map(Number);
selectedDateTime.setHours(selectedHour, selectedMinute, 0, 0);
if (this.isOvernight && selectedHour < this.openHour) selectedDateTime.setDate(selectedDateTime.getDate() + 1);
if (selectedDateTime <= now) { showToast('Tidak bisa booking untuk waktu yang sudah berlalu', 'warning'); return; }
this.isLoading = true;
window.dispatchEvent(new CustomEvent('show-loading'));
const bookingDate = getJakartaDateString();
fetch('/booking/initiate', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content },
body: JSON.stringify({ table_id: tableId, start_time: this.selectedTime, duration: this.selectedDuration, booking_date: bookingDate }),
})
.then(res => res.json())
.then(data => {
window.dispatchEvent(new CustomEvent('hide-loading'));
if (data.success) {
if (data.snap_token) {
window.snap.pay(data.snap_token, {
onSuccess: (result) => this.createBooking(data.order_id, result),
onPending: (result) => { showToast('Pembayaran pending...', 'warning'); this.isLoading = false; },
onError: (result) => { showToast('Pembayaran gagal', 'error'); this.isLoading = false; },
onClose: () => {
showToast('Anda menutup popup pembayaran', 'warning');
this.isLoading = false;
window.justClosedPayment = true;
document.dispatchEvent(refreshPendingBookingsEvent);
}
});
} else {
showToast(data.message || 'Booking berhasil diproses', 'success');
this.isLoading = false;
if (data.booking_id) setTimeout(() => window.location.reload(), 1000);
}
} else {
showToast(data.message, 'error');
this.isLoading = false;
}
})
.catch(err => {
window.dispatchEvent(new CustomEvent('hide-loading'));
console.error('Booking initiation error:', err);
showToast('Gagal melakukan booking', 'error');
this.isLoading = false;
});
},
createBooking(orderId, paymentResult) {
window.dispatchEvent(new CustomEvent('show-loading'));
fetch('/booking', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content },
body: JSON.stringify({ order_id: orderId, transaction_id: paymentResult.transaction_id, payment_method: paymentResult.payment_type, transaction_status: paymentResult.transaction_status }),
})
.then(res => {
if (!res.ok) return res.json().then(err => { throw new Error(err.message || 'Gagal menyimpan booking'); });
return res.json();
})
.then(data => {
window.dispatchEvent(new CustomEvent('hide-loading'));
showToast('Pembayaran dan booking berhasil!', 'success');
this.isLoading = false;
this.selectedTime = '';
this.selectedDuration = '';
this.open = false;
this.checkBookedSchedules();
setTimeout(() => { window.location.href = '/booking/history'; }, 2000);
})
.catch(err => {
window.dispatchEvent(new CustomEvent('hide-loading'));
console.error('Booking error:', err);
showToast('Gagal menyimpan booking: ' + err.message, 'error');
this.isLoading = false;
});
}
}));
});
updateClock();
setInterval(updateClock, 1000);
</script>
@endsection