Pending Booking dan Booking History

This commit is contained in:
Stephen Gesityan 2025-05-09 02:42:37 +07:00
parent 49d4a07895
commit d4305a4f61
9 changed files with 858 additions and 99 deletions

View File

@ -5,11 +5,13 @@
use App\Models\Booking;
use App\Models\Table;
use App\Models\PendingBooking;
use App\Services\MidtransService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
class BookingController extends Controller
{
@ -20,7 +22,7 @@ public function __construct(MidtransService $midtransService)
$this->midtransService = $midtransService;
}
public function store(Request $request) {
public function createPaymentIntent(Request $request) {
try {
$request->validate([
'table_id' => 'required|exists:tables,id',
@ -28,7 +30,7 @@ public function store(Request $request) {
'end_time' => 'required|date|after:start_time',
]);
// Cek apakah meja sedang dibooking pada waktu tersebut
// Cek apakah meja sedang dibooking pada waktu tersebut (hanya yang sudah paid)
$conflict = Booking::where('table_id', $request->table_id)
->where(function($query) use ($request) {
$query->whereBetween('start_time', [$request->start_time, $request->end_time])
@ -37,8 +39,7 @@ public function store(Request $request) {
->where('end_time', '>', $request->start_time);
});
})
->where('status', '!=', 'cancelled')
->where('status', '!=', 'expired')
->where('status', 'paid') // Hanya cek yang sudah paid
->exists();
if ($conflict) {
@ -52,61 +53,157 @@ public function store(Request $request) {
$duration = $endTime->diffInHours($startTime);
$totalAmount = $duration * $table->price_per_hour;
// Buat booking dengan status pending
$booking = Booking::create([
// Simpan data booking sementara di session untuk digunakan setelah pembayaran
Session::put('temp_booking', [
'table_id' => $request->table_id,
'user_id' => Auth::id(),
'start_time' => $request->start_time,
'end_time' => $request->end_time,
'status' => 'pending',
'total_amount' => $totalAmount,
'payment_expired_at' => now()->addHours(24), // Expired dalam 24 jam
'created_at' => now(),
]);
// Dapatkan snap token dari Midtrans
$snapToken = $this->midtransService->createTransaction($booking);
// Generate unique order ID
$tempOrderId = 'TEMP-' . Auth::id() . '-' . time();
Session::put('temp_order_id', $tempOrderId);
// Simpan booking sementara ke database untuk bisa dilanjutkan nanti
PendingBooking::updateOrCreate(
[
'user_id' => Auth::id(),
'table_id' => $request->table_id,
'start_time' => $request->start_time
],
[
'end_time' => $request->end_time,
'total_amount' => $totalAmount,
'order_id' => $tempOrderId,
'expired_at' => now()->addHours(24), // Kadaluarsa dalam 24 jam
]
);
// Dapatkan snap token dari Midtrans tanpa menyimpan booking
$snapToken = $this->midtransService->createTemporaryTransaction($table, $totalAmount, $tempOrderId, Auth::user());
if (!$snapToken) {
throw new \Exception('Failed to get snap token from Midtrans');
}
\Log::info('Booking created successfully:', [
'booking_id' => $booking->id,
\Log::info('Payment intent created successfully:', [
'order_id' => $tempOrderId,
'snap_token' => $snapToken
]);
return response()->json([
'message' => 'Booking berhasil dibuat, silahkan lakukan pembayaran',
'booking_id' => $booking->id,
'message' => 'Payment intent created, proceed to payment',
'total_amount' => $totalAmount,
'snap_token' => $snapToken
'snap_token' => $snapToken,
'order_id' => $tempOrderId
]);
} catch (\Exception $e) {
\Log::error('Booking error:', [
\Log::error('Payment intent error:', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
if (isset($booking)) {
$booking->delete(); // Hapus booking jika gagal membuat transaksi
}
return response()->json([
'message' => 'Gagal membuat transaksi: ' . $e->getMessage()
], 500);
}
}
public function store(Request $request) {
try {
$request->validate([
'order_id' => 'required|string',
'transaction_id' => 'required|string',
'payment_method' => 'required|string',
'transaction_status' => 'required|string',
]);
// Retrieve booking data from session
$tempBooking = Session::get('temp_booking');
$tempOrderId = Session::get('temp_order_id');
// If not in session, try from pending bookings
if (!$tempBooking || $tempOrderId != $request->order_id) {
$pendingBooking = PendingBooking::where('order_id', $request->order_id)
->where('user_id', Auth::id())
->first();
if (!$pendingBooking) {
throw new \Exception('Invalid or expired booking session');
}
$tempBooking = [
'table_id' => $pendingBooking->table_id,
'user_id' => $pendingBooking->user_id,
'start_time' => $pendingBooking->start_time,
'end_time' => $pendingBooking->end_time,
'total_amount' => $pendingBooking->total_amount,
];
$tempOrderId = $pendingBooking->order_id;
}
// Process based on transaction status
if ($request->transaction_status == 'settlement' || $request->transaction_status == 'capture') {
// Create the actual booking record
$booking = Booking::create([
'table_id' => $tempBooking['table_id'],
'user_id' => $tempBooking['user_id'],
'start_time' => $tempBooking['start_time'],
'end_time' => $tempBooking['end_time'],
'status' => 'paid',
'total_amount' => $tempBooking['total_amount'],
'payment_id' => $request->transaction_id,
'payment_method' => $request->payment_method,
'order_id' => $request->order_id,
]);
// Update table status to booked
$table = Table::findOrFail($tempBooking['table_id']);
$table->update(['status' => 'Booked']);
// Delete pending booking if exists
PendingBooking::where('order_id', $request->order_id)->delete();
// Clear session data
Session::forget('temp_booking');
Session::forget('temp_order_id');
return response()->json([
'message' => 'Booking created successfully',
'booking_id' => $booking->id
]);
} else {
// For pending, deny, cancel, etc. - don't create booking
return response()->json([
'message' => 'Payment ' . $request->transaction_status . ', booking not created',
'status' => $request->transaction_status
]);
}
} catch (\Exception $e) {
\Log::error('Booking store error:', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return response()->json([
'message' => 'Failed to create booking: ' . $e->getMessage()
], 500);
}
}
public function getBookedSchedules(Request $request) {
$request->validate([
'table_id' => 'required|exists:tables,id',
'date' => 'required|date',
]);
// Only get bookings with paid status
$bookings = Booking::where('table_id', $request->table_id)
->whereDate('start_time', $request->date)
->where('status', '!=', 'cancelled')
->where('status', '!=', 'expired')
->where('status', 'paid') // Only include paid bookings
->select('start_time', 'end_time')
->get()
->map(function ($booking) {
@ -127,9 +224,22 @@ public function handleNotification(Request $request)
$transactionStatus = $notification['transaction_status'];
$orderId = $notification['order_id'];
$fraudStatus = $notification['fraud_status'];
$fraudStatus = $notification['fraud_status'] ?? null;
$transactionId = $notification['transaction_id'];
$paymentType = $notification['payment_type'];
// Get booking from order_id
// Check if this is a temporary order (from our new flow)
if (strpos($orderId, 'TEMP-') === 0) {
// This is a notification for a transaction that started with our new flow
// We don't need to do anything here as the frontend will handle creating the booking
// after successful payment via the store method
Log::info('Received notification for temp order, will be handled by frontend', [
'order_id' => $orderId
]);
return response()->json(['message' => 'Notification received for temp order']);
}
// Handle notifications for existing bookings (from old flow or admin-created bookings)
$booking = Booking::where('order_id', $orderId)->first();
if (!$booking) {
Log::error('Booking not found for order_id: ' . $orderId);
@ -163,7 +273,10 @@ public function handleNotification(Request $request)
$booking->status = 'pending';
}
$booking->payment_id = $transactionId;
$booking->payment_method = $paymentType;
$booking->save();
Log::info('Booking status updated:', ['booking_id' => $booking->id, 'status' => $booking->status]);
return response()->json(['message' => 'Notification processed successfully']);
@ -172,4 +285,114 @@ public function handleNotification(Request $request)
return response()->json(['message' => 'Error processing notification'], 500);
}
}
public function getPendingBookings()
{
$pendingBookings = PendingBooking::where('user_id', Auth::id())
->where('expired_at', '>', now())
->with(['table.venue'])
->get();
return response()->json($pendingBookings);
}
public function resumeBooking($id)
{
try {
$pendingBooking = PendingBooking::where('id', $id)
->where('user_id', Auth::id())
->where('expired_at', '>', now())
->firstOrFail();
// Cek apakah meja masih available di waktu tersebut
$conflict = Booking::where('table_id', $pendingBooking->table_id)
->where(function($query) use ($pendingBooking) {
$query->whereBetween('start_time', [$pendingBooking->start_time, $pendingBooking->end_time])
->orWhere(function($query) use ($pendingBooking) {
$query->where('start_time', '<', $pendingBooking->start_time)
->where('end_time', '>', $pendingBooking->start_time);
});
})
->where('status', 'paid')
->exists();
if ($conflict) {
return response()->json([
'success' => false,
'message' => 'Meja ini sudah tidak tersedia pada waktu yang Anda pilih'
], 409);
}
// Simpan ke session
Session::put('temp_booking', [
'table_id' => $pendingBooking->table_id,
'user_id' => Auth::id(),
'start_time' => $pendingBooking->start_time,
'end_time' => $pendingBooking->end_time,
'total_amount' => $pendingBooking->total_amount,
'created_at' => now(),
]);
Session::put('temp_order_id', $pendingBooking->order_id);
// Dapatkan table data
$table = Table::findOrFail($pendingBooking->table_id);
// Dapatkan snap token baru dari Midtrans
$snapToken = $this->midtransService->createTemporaryTransaction(
$table,
$pendingBooking->total_amount,
$pendingBooking->order_id,
Auth::user()
);
if (!$snapToken) {
throw new \Exception('Failed to get snap token from Midtrans');
}
return response()->json([
'success' => true,
'message' => 'Booking dapat dilanjutkan',
'snap_token' => $snapToken,
'order_id' => $pendingBooking->order_id,
'venue_id' => $pendingBooking->table->venue_id,
'table_id' => $pendingBooking->table_id,
'table_name' => $pendingBooking->table->name,
'start_time' => Carbon::parse($pendingBooking->start_time)->format('H:i'),
'duration' => Carbon::parse($pendingBooking->start_time)->diffInHours($pendingBooking->end_time),
'total_amount' => $pendingBooking->total_amount
]);
} catch (\Exception $e) {
Log::error('Resume booking error:', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return response()->json([
'success' => false,
'message' => 'Gagal memproses pembayaran: ' . $e->getMessage()
], 500);
}
}
public function deletePendingBooking($id)
{
try {
$pendingBooking = PendingBooking::where('id', $id)
->where('user_id', Auth::id())
->firstOrFail();
$pendingBooking->delete();
return response()->json([
'success' => true,
'message' => 'Booking berhasil dihapus'
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Gagal menghapus booking: ' . $e->getMessage()
], 500);
}
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Http\Controllers\pages;
use App\Http\Controllers\Controller;
use App\Models\Booking;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class BookingHistoryController extends Controller
{
public function index()
{
$bookings = Booking::where('user_id', Auth::id())
->with(['table.venue'])
->orderBy('start_time', 'desc')
->paginate(10);
return view('pages.booking-history', compact('bookings'));
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class PendingBooking extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'table_id',
'start_time',
'end_time',
'total_amount',
'order_id',
'expired_at',
];
protected $casts = [
'start_time' => 'datetime',
'end_time' => 'datetime',
'expired_at' => 'datetime',
];
public function user()
{
return $this->belongsTo(User::class);
}
public function table()
{
return $this->belongsTo(Table::class);
}
}

View File

@ -3,6 +3,8 @@
namespace App\Services;
use App\Models\Booking;
use App\Models\Table;
use App\Models\User;
use Midtrans\Config;
use Midtrans\Snap;
use Illuminate\Support\Facades\Log;
@ -42,6 +44,67 @@ public function __construct()
]);
}
// Method baru untuk membuat transaksi sementara tanpa booking record
public function createTemporaryTransaction(Table $table, int $amount, string $orderId, User $user)
{
try {
if ($amount <= 0) {
throw new \Exception('Invalid booking amount');
}
$params = [
'transaction_details' => [
'order_id' => $orderId,
'gross_amount' => (int) $amount,
],
'customer_details' => [
'first_name' => $user->name,
'email' => $user->email,
],
'item_details' => [
[
'id' => $table->id,
'price' => (int) $amount,
'quantity' => 1,
'name' => 'Booking Meja ' . $table->name,
],
],
'expiry' => [
'start_time' => now()->format('Y-m-d H:i:s O'),
'unit' => 'hour',
'duration' => 24,
],
];
Log::info('Creating Midtrans temporary transaction:', [
'order_id' => $orderId,
'amount' => $amount,
'params' => $params
]);
$snapToken = Snap::getSnapToken($params);
if (empty($snapToken)) {
throw new \Exception('Empty snap token received from Midtrans');
}
Log::info('Midtrans temporary transaction created successfully:', [
'order_id' => $orderId,
'snap_token' => $snapToken
]);
return $snapToken;
} catch (\Exception $e) {
Log::error('Midtrans temporary transaction failed:', [
'order_id' => $orderId,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
throw new \Exception('Failed to create Midtrans transaction: ' . $e->getMessage());
}
}
// Metode original untuk backward compatibility
public function createTransaction(Booking $booking)
{
try {
@ -120,6 +183,15 @@ public function handleNotification($notification)
'fraud_status' => $fraud
]);
// Check if this is a temporary order
if (strpos($orderId, 'TEMP-') === 0) {
Log::info('Notification for temporary order received, will be handled separately', [
'order_id' => $orderId
]);
return null;
}
// Handle existing bookings
// Extract booking ID from order ID (format: BOOK-{id})
$bookingId = explode('-', $orderId)[1];
$booking = Booking::findOrFail($bookingId);

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('pending_bookings', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('table_id')->constrained()->onDelete('cascade');
$table->dateTime('start_time');
$table->dateTime('end_time');
$table->decimal('total_amount', 10, 2);
$table->string('order_id')->unique();
$table->dateTime('expired_at');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('pending_bookings');
}
};

View File

@ -39,6 +39,9 @@ class="block lg:hidden border-l pl-4 border-gray-300 focus:outline-none">
<!-- Desktop buttons -->
<div class="hidden lg:flex items-center space-x-4">
@auth
<a href="{{ route('booking.history') }}"
class="text-sm font-medium text-gray-700 hover:text-primary transition">Riwayat Booking</a>
<div x-data="{ open: false }" class="relative">
<button @click="open = !open"
class="flex items-center space-x-2 text-sm font-medium text-gray-700 hover:text-primary focus:outline-none">
@ -51,6 +54,10 @@ class="flex items-center space-x-2 text-sm font-medium text-gray-700 hover:text-
<div x-show="open" @click.away="open = false" x-transition
class="absolute right-0 mt-2 w-40 bg-white rounded-lg shadow-lg py-2 z-50">
<a href="{{ route('booking.history') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Riwayat Booking
</a>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit"
@ -69,8 +76,13 @@ class="bg-primary hover:bg-primary-dark text-white px-4 py-2 rounded text-sm fon
</div>
<!-- Mobile menu -->
<div x-show="isMobileMenuOpen" ...>
<div x-show="isMobileMenuOpen"
class="absolute top-full left-0 right-0 bg-white shadow-md mt-1 p-4 z-50">
@auth
<a href="{{ route('booking.history') }}"
class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Riwayat Booking
</a>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit"
@ -79,8 +91,10 @@ class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
</button>
</form>
@else
<button @click="showModal = true; modalType = 'login'" ...>Masuk</button>
<button @click="showModal = true; modalType = 'register'" ...>Daftar</button>
<button @click="showModal = true; modalType = 'login'"
class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Masuk</button>
<button @click="showModal = true; modalType = 'register'"
class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 mt-2">Daftar</button>
@endauth
</div>
</nav>

View File

@ -0,0 +1,74 @@
@extends('layouts.main')
@section('content')
<div class="min-h-96 mx-4 md:w-3/4 md:mx-auto py-8">
<h1 class="text-2xl font-bold mb-6">Riwayat Booking</h1>
@if($bookings->isEmpty())
<div class="bg-white rounded-lg shadow-md p-6 text-center">
<p class="text-gray-500">Anda belum memiliki riwayat booking.</p>
<a href="{{ route('venues.index') }}" class="mt-4 inline-block bg-blue-500 text-white px-4 py-2 rounded-lg">Cari
Venue</a>
</div>
@else
<div class="space-y-4">
@foreach($bookings as $booking)
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<div class="p-4 border-b {{ $booking->start_time > now() ? 'bg-green-50' : 'bg-gray-50' }}">
<div class="flex justify-between items-center">
<h3 class="font-semibold text-lg">{{ $booking->table->venue->name }}</h3>
<span
class="px-3 py-1 rounded-full text-sm {{ $booking->start_time > now() ? 'bg-green-100 text-green-800' : 'bg-gray-200 text-gray-800' }}">
{{ $booking->start_time > now() ? 'Upcoming' : 'Completed' }}
</span>
</div>
</div>
<div class="p-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<p class="text-sm text-gray-500">Meja</p>
<p class="font-medium">{{ $booking->table->name }} ({{ $booking->table->brand }})</p>
</div>
<div>
<p class="text-sm text-gray-500">Tanggal & Waktu</p>
<p class="font-medium">{{ \Carbon\Carbon::parse($booking->start_time)->format('d M Y') }},
{{ \Carbon\Carbon::parse($booking->start_time)->format('H:i') }} -
{{ \Carbon\Carbon::parse($booking->end_time)->format('H:i') }}
</p>
</div>
<div>
<p class="text-sm text-gray-500">Durasi</p>
<p class="font-medium">
{{ \Carbon\Carbon::parse($booking->start_time)->diffInHours($booking->end_time) }} Jam
</p>
</div>
<div>
<p class="text-sm text-gray-500">Total Bayar</p>
<p class="font-medium">Rp {{ number_format($booking->total_amount, 0, ',', '.') }}</p>
</div>
<div>
<p class="text-sm text-gray-500">Status Pembayaran</p>
<p class="font-medium capitalize">{{ $booking->status }}</p>
</div>
<div>
<p class="text-sm text-gray-500">Metode Pembayaran</p>
<p class="font-medium capitalize">{{ $booking->payment_method ?? '-' }}</p>
</div>
</div>
@if($booking->start_time > now() && $booking->status == 'paid')
<div class="mt-4 flex justify-end">
<a href="{{ route('venue', $booking->table->venue->name) }}"
class="text-blue-500 hover:underline">Lihat Venue</a>
</div>
@endif
</div>
</div>
@endforeach
</div>
<div class="mt-6">
{{ $bookings->links() }}
</div>
@endif
</div>
@endsection

View File

@ -16,6 +16,63 @@ class="flex items-center bg-[url('/public/images/map.jpg')] bg-cover bg-center p
<i class="fa-solid fa-map-pin text-red-800 text-3xl"></i>
</div>
</a>
@auth
<!-- Pending Bookings Section -->
<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>
@ -26,8 +83,10 @@ class="flex items-center bg-[url('/public/images/map.jpg')] bg-cover bg-center p
</div>
</div>
@foreach ($venue['tables'] as $table)
<div x-data="booking(@json(auth()->check()), '{{ $table['id'] }}')" 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 x-data="booking(@json(auth()->check()), '{{ $table['id'] }}')"
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">
@ -49,9 +108,8 @@ class="flex items-center bg-[url('/public/images/map.jpg')] bg-cover bg-center p
<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 getHoursInRange(9, 22)" :key="hour">
<option :value="hour + ':00'"
:disabled="isTimeBooked(hour + ':00')"
<template x-for="hour in getHoursInRange(9, 24)" :key="hour">
<option :value="hour + ':00'" :disabled="isTimeBooked(hour + ':00')"
x-text="hour + ':00' + (isTimeBooked(hour + ':00') ? ' (Booked)' : '')">
</option>
</template>
@ -66,7 +124,7 @@ class="flex items-center bg-[url('/public/images/map.jpg')] bg-cover bg-center p
</select>
<button class="mt-3 px-4 py-2 bg-green-500 text-white rounded-lg w-full" :disabled="!selectedTime || !selectedDuration || isLoading"
@click="submitBooking('{{ $table['id'] }}', '{{ addslashes($table['name']) }}')">
@click="initiateBooking('{{ $table['id'] }}', '{{ addslashes($table['name']) }}')">
<template x-if="isLoading">
<span>Loading...</span>
</template>
@ -81,7 +139,8 @@ class="flex items-center bg-[url('/public/images/map.jpg')] bg-cover bg-center p
</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 src="https://app.sandbox.midtrans.com/snap/snap.js"
data-client-key="{{ config('midtrans.client_key') }}"></script>
<script>
function updateClock() {
const now = new Date();
@ -92,7 +151,175 @@ function updateClock() {
setInterval(updateClock, 1000);
updateClock();
// Format functions for pending bookings
function formatDateTime(dateTimeStr) {
const date = new Date(dateTimeStr);
// Explicitly use Jakarta timezone for display
return new Intl.DateTimeFormat('id-ID', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZone: 'Asia/Jakarta'
}).format(date);
}
function formatTime(timeStr) {
const date = new Date(timeStr);
// Explicitly use Jakarta timezone for display
return new Intl.DateTimeFormat('id-ID', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZone: 'Asia/Jakarta'
}).format(date);
}
function formatPrice(price) {
return new Intl.NumberFormat('id-ID').format(price);
}
document.addEventListener('alpine:init', () => {
// Pending bookings component
Alpine.data('pendingBookingsComponent', () => ({
pendingBookings: [],
showPendingBookings: false,
isLoadingPending: false,
init() {
this.fetchPendingBookings();
},
fetchPendingBookings() {
fetch('/booking/pending')
.then(response => response.json())
.then(data => {
// Filter bookings untuk venue saat ini jika diperlukan
const currentVenueId = {{ $venue['id'] ?? 'null' }};
if (currentVenueId) {
this.pendingBookings = data.filter(booking =>
booking.table && booking.table.venue_id === currentVenueId
);
} else {
this.pendingBookings = data;
}
// Log jumlah pending bookings yang ditemukan
console.log("Found", this.pendingBookings.length, "pending bookings");
})
.catch(error => console.error('Error fetching pending bookings:', error));
},
resumeBooking(bookingId) {
this.isLoadingPending = true;
fetch(`/booking/pending/${bookingId}/resume`)
.then(response => response.json())
.then(data => {
if (data.success) {
console.log("Opening payment with snap token:", data.snap_token);
// Open Snap payment
window.snap.pay(data.snap_token, {
onSuccess: (result) => {
this.createBookingAfterPayment(data.order_id, result);
},
onPending: (result) => {
alert('Pembayaran pending, silahkan selesaikan pembayaran');
this.isLoadingPending = false;
},
onError: (result) => {
alert('Pembayaran gagal');
this.isLoadingPending = false;
},
onClose: () => {
alert('Anda menutup popup tanpa menyelesaikan pembayaran');
this.isLoadingPending = false;
}
});
} else {
alert(data.message);
this.isLoadingPending = false;
// Refresh pending bookings list
this.fetchPendingBookings();
}
})
.catch(error => {
console.error('Error resuming booking:', error);
alert('Gagal melanjutkan booking');
this.isLoadingPending = false;
});
},
deletePendingBooking(bookingId) {
if (confirm('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) {
alert('Booking berhasil dihapus');
this.fetchPendingBookings();
} else {
alert(data.message);
}
this.isLoadingPending = false;
})
.catch(error => {
console.error('Error deleting booking:', error);
alert('Gagal menghapus booking');
this.isLoadingPending = false;
});
}
},
createBookingAfterPayment(orderId, paymentResult) {
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 => {
alert('Pembayaran dan booking berhasil!');
this.isLoadingPending = false;
// Refresh pending bookings list
this.fetchPendingBookings();
// Redirect to booking history
window.location.href = '/booking/history';
})
.catch(err => {
console.error('Booking error:', err);
alert('Pembayaran berhasil tetapi gagal menyimpan booking: ' + err.message);
this.isLoadingPending = false;
});
}
}));
// Regular booking component (existing)
Alpine.data('booking', (isLoggedIn, tableId) => ({
isLoggedIn,
tableId,
@ -127,7 +354,7 @@ function updateClock() {
}
},
submitBooking(tableId, tableName) {
initiateBooking(tableId, tableName) {
if (!this.isLoggedIn) {
alert('Silahkan login terlebih dahulu untuk melakukan booking.');
return;
@ -146,10 +373,11 @@ function updateClock() {
const [selectedHour, selectedMinute] = selectedTime.split(':').map(Number);
selectedDateTime.setHours(selectedHour, selectedMinute, 0, 0);
if (selectedDateTime <= now) {
alert('Jam yang dipilih sudah lewat. Silakan pilih jam yang masih tersedia.');
return;
}
// Uncomment this for production to prevent booking past times
// if (selectedDateTime <= now) {
// alert('Jam yang dipilih sudah lewat. Silakan pilih jam yang masih tersedia.');
// return;
// }
this.isLoading = true;
@ -164,8 +392,8 @@ function updateClock() {
const start_time = `${today} ${selectedTime}`;
const end_time = `${today} ${endTimeFormatted}`;
// Kirim ke backend
fetch('/booking', {
// Kirim ke backend untuk membuat payment intent (tanpa membuat booking dulu)
fetch('/booking/payment-intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -180,7 +408,7 @@ function updateClock() {
.then(res => {
if (!res.ok) {
return res.json().then(err => {
throw new Error(err.message || 'Gagal membuat booking');
throw new Error(err.message || 'Gagal membuat payment intent');
});
}
return res.json();
@ -192,30 +420,74 @@ function updateClock() {
// Buka Snap Midtrans
window.snap.pay(data.snap_token, {
onSuccess: function(result) {
alert('Pembayaran berhasil!');
location.reload();
onSuccess: (result) => {
this.createBookingAfterPayment(data.order_id, result);
},
onPending: function(result) {
onPending: (result) => {
alert('Pembayaran pending, silahkan selesaikan pembayaran');
this.isLoading = false;
},
onError: function(result) {
onError: (result) => {
alert('Pembayaran gagal');
this.isLoading = false;
},
onClose: function() {
onClose: () => {
alert('Anda menutup popup tanpa menyelesaikan pembayaran');
this.isLoading = false;
}
});
})
.catch(err => {
console.error('Booking error:', err);
alert('Gagal booking: ' + err.message);
})
.finally(() => {
console.error('Payment intent error:', err);
alert('Gagal membuat payment: ' + err.message);
this.isLoading = false;
});
}
}))
},
// Fungsi untuk menyimpan booking setelah pembayaran berhasil
createBookingAfterPayment(orderId, paymentResult) {
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 => {
alert('Pembayaran dan booking berhasil!');
// Refresh component data
document.dispatchEvent(new CustomEvent('booking-completed'));
// Redirect to booking history page
window.location.href = '/booking/history';
})
.catch(err => {
console.error('Booking error:', err);
alert('Pembayaran berhasil tetapi gagal menyimpan booking: ' + err.message);
this.isLoading = false;
});
},
// Method to refresh booked schedules without reloading the page
async refreshBookedSchedules() {
await this.checkBookedSchedules();
}
}));
});
</script>
@endsection

View File

@ -3,6 +3,7 @@
use App\Http\Controllers\pages\HomeController;
use App\Http\Controllers\pages\VenueController;
use App\Http\Controllers\pages\BookingController;
use App\Http\Controllers\pages\BookingHistoryController;
use App\Http\Controllers\admin\BookingsController;
use App\Http\Controllers\admin\TableController;
use App\Http\Controllers\admin\AdminController;
@ -12,11 +13,24 @@
Auth::routes();
Route::get('/', [HomeController::class, "index"])->name('index');
Route::get('/venue/{venueName}', [VenueController::class, "venue"])->name('venue');
// Changed routes for the new booking flow
Route::post('/booking/payment-intent', [BookingController::class, 'createPaymentIntent'])->name('booking.payment-intent');
Route::post('/booking', [BookingController::class, 'store'])->name('booking.store');
Route::get('/booking/schedules', [BookingController::class, 'getBookedSchedules'])->name('booking.schedules');
Route::post('/booking/payment', [BookingController::class, 'processPayment'])->name('booking.payment');
Route::get('/booking/payment/{bookingId}', [BookingController::class, 'checkPaymentStatus'])->name('booking.payment.status');
Route::post('/payment/notification', [BookingController::class, 'handleNotification'])->name('payment.notification');
// Booking history routes (authenticated only)
Route::middleware(['auth'])->group(function () {
Route::get('/booking/history', [BookingHistoryController::class, 'index'])->name('booking.history');
// Pending bookings routes
Route::get('/booking/pending', [BookingController::class, 'getPendingBookings'])->name('booking.pending');
Route::get('/booking/pending/{id}/resume', [BookingController::class, 'resumeBooking'])->name('booking.resume');
Route::delete('/booking/pending/{id}', [BookingController::class, 'deletePendingBooking'])->name('booking.pending.delete');
});
// Admin routes
Route::middleware(['auth', 'is_admin'])->prefix('admin')->group(function () {
Route::get('/', [AdminController::class, 'index'])->name('admin.dashboard');
Route::get('/bookings', [BookingsController::class, 'index'])->name('admin.bookings.index');
@ -24,5 +38,4 @@
Route::get('/tables/{id}/edit', [TableController::class, 'editTable'])->name('admin.tables.edit');
Route::put('/tables/{id}', [TableController::class, 'updateTable'])->name('admin.tables.update');
});