Jam buka tutup sudah dinamis, booking sbg user sudah bisa, booking sbg admin sudah bisa

This commit is contained in:
Stephen Gesityan 2025-06-04 15:58:06 +07:00
parent b8f70e7f6f
commit 0580940cf5
5 changed files with 315 additions and 221 deletions

View File

@ -23,44 +23,75 @@ public function __construct(MidtransService $midtransService)
}
// Tambahkan method baru untuk booking langsung oleh admin
public function adminDirectBooking(Request $request) {
public function adminDirectBooking($request) {
try {
$request->validate([
'table_id' => 'required|exists:tables,id',
'start_time' => 'required|date',
'end_time' => 'required|date|after:start_time',
]);
// Handle both Request object dan Collection
$data = $request instanceof \Illuminate\Http\Request ? $request->all() : $request->toArray();
// Validasi manual karena bisa dari collection
if (!isset($data['table_id']) || !isset($data['start_time']) || !isset($data['end_time'])) {
return response()->json([
'message' => 'Missing required fields'
], 400);
}
$user = Auth::user();
// Validasi bahwa user adalah admin dan mengelola venue dari meja tersebut
$table = Table::findOrFail($request->table_id);
$table = Table::findOrFail($data['table_id']);
if ($user->role !== 'admin' || $user->venue_id !== $table->venue_id) {
return response()->json([
'message' => 'Unauthorized action'
], 403);
}
// Parse start_time dan end_time yang sudah dalam format datetime string
$startDateTime = Carbon::parse($data['start_time']);
$endDateTime = Carbon::parse($data['end_time']);
// Validasi jam operasional venue (opsional, karena sudah divalidasi di createPaymentIntent)
$venue = $table->venue;
$venueOpenTime = Carbon::parse($venue->open_time);
$venueCloseTime = Carbon::parse($venue->close_time);
$startTimeOnly = $startDateTime->format('H:i');
$endTimeOnly = $endDateTime->format('H:i');
if ($startTimeOnly < $venueOpenTime->format('H:i') || $endTimeOnly > $venueCloseTime->format('H:i')) {
return response()->json([
'message' => 'Waktu booking di luar jam operasional venue'
], 400);
}
// Cek konflik booking
$conflict = Booking::where('table_id', $request->table_id)
->where(function($query) use ($request) {
$query->whereBetween('start_time', [$request->start_time, $request->end_time])
->orWhere(function($query) use ($request) {
$query->where('start_time', '<', $request->start_time)
->where('end_time', '>', $request->start_time);
$conflict = Booking::where('table_id', $data['table_id'])
->whereDate('start_time', $startDateTime->format('Y-m-d'))
->where(function($query) use ($startDateTime, $endDateTime) {
$query->where(function($q) use ($startDateTime, $endDateTime) {
// Case 1: Booking baru mulai di tengah booking yang ada
$q->where('start_time', '<=', $startDateTime)
->where('end_time', '>', $startDateTime);
})->orWhere(function($q) use ($startDateTime, $endDateTime) {
// Case 2: Booking baru berakhir di tengah booking yang ada
$q->where('start_time', '<', $endDateTime)
->where('end_time', '>=', $endDateTime);
})->orWhere(function($q) use ($startDateTime, $endDateTime) {
// Case 3: Booking baru mencakup seluruh booking yang ada
$q->where('start_time', '>=', $startDateTime)
->where('end_time', '<=', $endDateTime);
});
})
->where('status', 'paid')
->whereIn('status', ['paid', 'pending'])
->exists();
if ($conflict) {
return response()->json(['message' => 'Meja sudah dibooking di jam tersebut'], 409);
return response()->json([
'message' => 'Meja sudah dibooking di jam tersebut'
], 409);
}
// Hitung total biaya (meskipun admin tidak membayar, kita tetap catat nilainya)
$startTime = Carbon::parse($request->start_time);
$endTime = Carbon::parse($request->end_time);
$duration = $endTime->diffInHours($startTime);
// Hitung total biaya dan durasi
$duration = $endDateTime->diffInHours($startDateTime);
$totalAmount = $duration * $table->price_per_hour;
// Generate order ID unik untuk admin
@ -68,33 +99,40 @@ public function adminDirectBooking(Request $request) {
// Buat booking langsung dengan status paid
$booking = Booking::create([
'table_id' => $request->table_id,
'table_id' => $data['table_id'],
'user_id' => $user->id,
'start_time' => $request->start_time,
'end_time' => $request->end_time,
'status' => 'paid', // langsung set sebagai paid
'start_time' => $startDateTime,
'end_time' => $endDateTime,
'status' => 'paid',
'total_amount' => $totalAmount,
'payment_id' => null, // Admin tidak perlu payment_id
'payment_method' => 'admin_direct', // Tandai sebagai booking langsung admin
'payment_id' => null,
'payment_method' => 'admin_direct',
'order_id' => $adminOrderId,
]);
// Update table status menjadi Booked
$table->update(['status' => 'Booked']);
return response()->json([
'message' => 'Booking created successfully',
'booking_id' => $booking->id
'success' => true,
'message' => 'Booking berhasil dibuat oleh admin',
'booking_id' => $booking->id,
'booking_details' => [
'table_name' => $table->name,
'start_time' => $startDateTime->format('Y-m-d H:i:s'),
'end_time' => $endDateTime->format('Y-m-d H:i:s'),
'duration' => $duration . ' jam',
'total_amount' => 'Rp ' . number_format($totalAmount, 0, ',', '.')
]
]);
} catch (\Exception $e) {
\Log::error('Admin direct booking error:', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString()
'trace' => $e->getTraceAsString(),
'request_data' => $request instanceof \Illuminate\Http\Request ? $request->all() : $request->toArray()
]);
return response()->json([
'message' => 'Failed to create booking: ' . $e->getMessage()
'success' => false,
'message' => 'Gagal membuat booking: ' . $e->getMessage()
], 500);
}
}
@ -104,46 +142,78 @@ public function createPaymentIntent(Request $request) {
try {
$request->validate([
'table_id' => 'required|exists:tables,id',
'start_time' => 'required|date',
'end_time' => 'required|date|after:start_time',
'start_time' => 'required', // Ubah dari date menjadi string untuk format H:i
'duration' => 'required|integer|min:1|max:12', // Validasi durasi
'booking_date' => 'required|date_format:Y-m-d', // Validasi tanggal booking
]);
$user = Auth::user();
$table = Table::findOrFail($request->table_id);
$table = Table::with('venue')->findOrFail($request->table_id);
if ($user->role === 'admin' && $user->venue_id === $table->venue_id) {
return $this->adminDirectBooking($request);
// Buat datetime lengkap dari booking_date dan start_time
$bookingDate = $request->booking_date;
$startTime = $request->start_time; // Format H:i (contoh: "14:00")
$duration = (int) $request->duration;
// Gabungkan tanggal dan waktu untuk membuat datetime lengkap
$startDateTime = Carbon::createFromFormat('Y-m-d H:i', $bookingDate . ' ' . $startTime, 'Asia/Jakarta');
$endDateTime = $startDateTime->copy()->addHours($duration);
// Validasi waktu booking dalam jam operasional venue
$venueOpenTime = Carbon::createFromFormat('H:i:s', $table->venue->open_time)->format('H:i');
$venueCloseTime = Carbon::createFromFormat('H:i:s', $table->venue->close_time)->format('H:i');
if ($startTime < $venueOpenTime || $startTime >= $venueCloseTime) {
return response()->json([
'success' => false,
'message' => 'Waktu booking di luar jam operasional venue'
], 422);
}
// Cek apakah meja sedang dibooking pada waktu tersebut (hanya yang sudah paid)
// Validasi bahwa end time tidak melebihi jam tutup venue
if ($endDateTime->format('H:i') > $venueCloseTime) {
return response()->json([
'success' => false,
'message' => 'Durasi booking melebihi jam tutup venue'
], 422);
}
// Cek untuk admin direct booking
if ($user->role === 'admin' && $user->venue_id === $table->venue_id) {
return $this->adminDirectBooking(collect([
'table_id' => $request->table_id,
'start_time' => $startDateTime->toDateTimeString(),
'end_time' => $endDateTime->toDateTimeString(),
]));
}
// Cek konflik booking dengan format datetime lengkap
$conflict = Booking::where('table_id', $request->table_id)
->where(function($query) use ($request) {
$query->whereBetween('start_time', [$request->start_time, $request->end_time])
->orWhere(function($query) use ($request) {
$query->where('start_time', '<', $request->start_time)
->where('end_time', '>', $request->start_time);
->where(function($query) use ($startDateTime, $endDateTime) {
$query->where(function($q) use ($startDateTime, $endDateTime) {
$q->where('start_time', '<', $endDateTime)
->where('end_time', '>', $startDateTime);
});
})
->where('status', 'paid') // Hanya cek yang sudah paid
->where('status', 'paid')
->exists();
if ($conflict) {
return response()->json(['message' => 'Meja sudah dibooking di jam tersebut'], 409);
return response()->json([
'success' => false,
'message' => 'Meja sudah dibooking di jam tersebut'
], 409);
}
// Hitung total biaya
$table = Table::findOrFail($request->table_id);
$startTime = Carbon::parse($request->start_time);
$endTime = Carbon::parse($request->end_time);
$duration = $endTime->diffInHours($startTime);
$totalAmount = $duration * $table->price_per_hour;
// Simpan data booking sementara di session untuk digunakan setelah pembayaran
// Simpan data booking sementara di session
Session::put('temp_booking', [
'table_id' => $request->table_id,
'user_id' => Auth::id(),
'start_time' => $request->start_time,
'end_time' => $request->end_time,
'start_time' => $startDateTime->toDateTimeString(),
'end_time' => $endDateTime->toDateTimeString(),
'total_amount' => $totalAmount,
'created_at' => now(),
]);
@ -152,22 +222,22 @@ public function createPaymentIntent(Request $request) {
$tempOrderId = 'TEMP-' . Auth::id() . '-' . time();
Session::put('temp_order_id', $tempOrderId);
// Simpan booking sementara ke database untuk bisa dilanjutkan nanti
// Simpan booking sementara ke database
PendingBooking::updateOrCreate(
[
'user_id' => Auth::id(),
'table_id' => $request->table_id,
'start_time' => $request->start_time
'start_time' => $startDateTime->toDateTimeString()
],
[
'end_time' => $request->end_time,
'end_time' => $endDateTime->toDateTimeString(),
'total_amount' => $totalAmount,
'order_id' => $tempOrderId,
'expired_at' => now()->addHours(24), // Kadaluarsa dalam 24 jam
'expired_at' => now()->addHours(24),
]
);
// Dapatkan snap token dari Midtrans tanpa menyimpan booking
// Dapatkan snap token dari Midtrans
$snapToken = $this->midtransService->createTemporaryTransaction($table, $totalAmount, $tempOrderId, Auth::user());
if (!$snapToken) {
@ -176,10 +246,13 @@ public function createPaymentIntent(Request $request) {
\Log::info('Payment intent created successfully:', [
'order_id' => $tempOrderId,
'snap_token' => $snapToken
'snap_token' => $snapToken,
'start_time' => $startDateTime->toDateTimeString(),
'end_time' => $endDateTime->toDateTimeString()
]);
return response()->json([
'success' => true,
'message' => 'Payment intent created, proceed to payment',
'total_amount' => $totalAmount,
'snap_token' => $snapToken,
@ -192,6 +265,7 @@ public function createPaymentIntent(Request $request) {
]);
return response()->json([
'success' => false,
'message' => 'Gagal membuat transaksi: ' . $e->getMessage()
], 500);
}

View File

@ -52,10 +52,15 @@ class="w-full py-3 md:px-6 rounded-lg text-sm bg-primary text-white font-semibol
<a href="{{ route('venue', ['venueName' => $venue->name]) }}"
class="flex flex-col h-full border border-gray-400 rounded-lg overflow-hidden">
<img src="{{ Storage::url($venue->image) }}" alt="{{ $venue->name }}" class="w-full h-48 object-cover">
<div class="flex-grow px-4 py-2">
<h3 class="text-sm text-gray-400 font-semibold mb-2">Venue</h3>
<h1 class="text-xl text-gray-800 font-semibold">{{ $venue->name }}</h1>
{{-- <p class="text-sm text-gray-500">{{ $venue->address }}</p> --}}
<p class="text-sm text-gray-600 mt-1">
<i class="fa-regular fa-clock"></i>
Buka: {{ date('H:i', strtotime($venue['open_time'])) }} -
{{ date('H:i', strtotime($venue['close_time'])) }}
</p>
<p class="mt-10 text-gray-500 text-sm">Mulai:
<span class="font-bold text-gray-800">Rp30,000</span>
<span class="text-gray-400 font-thin text-sm">/ jam</span>

View File

@ -21,7 +21,6 @@ class="fixed inset-0 bg-black bg-opacity-50 z-40 flex items-center justify-cente
class="w-full h-full object-cover rounded-lg mb-4 mt-8" />
<h1 class="text-xl text-gray-800 font-semibold">{{ $venue['name'] }}</h1>
<p class="text-sm text-gray-500">{{ $venue['location'] ?? 'Lokasi tidak tersedia' }}</p>
<p class="text-sm text-gray-600 mt-1">
<i class="fa-regular fa-clock"></i>
Jam Operasional: {{ date('H:i', strtotime($venue['open_time'])) }} -
@ -630,8 +629,10 @@ function formatPrice(price) {
window.dispatchEvent(new CustomEvent('hide-loading'));
if (data.success) {
// Cek apakah ini admin direct booking atau customer payment
if (data.snap_token) {
// Customer biasa - perlu payment
console.log("Opening payment with snap token:", data.snap_token);
// Open Snap payment
window.snap.pay(data.snap_token, {
onSuccess: (result) => {
this.createBooking(data.order_id, result);
@ -653,6 +654,20 @@ function formatPrice(price) {
document.dispatchEvent(refreshPendingBookingsEvent);
}
});
} else if (data.booking_id) {
// Admin direct booking - langsung berhasil
showToast(data.message || 'Booking berhasil dibuat!', 'success');
this.isLoading = false;
// Refresh halaman atau reload available times
setTimeout(() => {
window.location.reload(); // Atau panggil method refresh yang sudah ada
}, 1000);
} else {
// Response success tapi tidak ada snap_token atau booking_id
showToast(data.message || 'Booking berhasil diproses', 'success');
this.isLoading = false;
}
} else {
showToast(data.message, 'error');
this.isLoading = false;

View File

@ -26,7 +26,7 @@
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/initiate', [BookingController::class, 'createPaymentIntent'])->name('booking.initiate');
Route::post('/booking', [BookingController::class, 'store'])->name('booking.store');
Route::get('/booking/schedules', [BookingController::class, 'getBookedSchedules'])->name('booking.schedules');
Route::post('/payment/notification', [BookingController::class, 'handleNotification'])->name('payment.notification');