911 lines
49 KiB
PHP
911 lines
49 KiB
PHP
@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 |