From 0580940cf5ae485a87f0aeb205d4e393cd7feb6a Mon Sep 17 00:00:00 2001 From: Stephen Gesityan Date: Wed, 4 Jun 2025 15:58:06 +0700 Subject: [PATCH] Jam buka tutup sudah dinamis, booking sbg user sudah bisa, booking sbg admin sudah bisa --- .../Controllers/pages/BookingController.php | 394 +++++++++++------- resources/views/admin/venues/index.blade.php | 0 resources/views/pages/home.blade.php | 7 +- resources/views/pages/venue.blade.php | 133 +++--- routes/web.php | 2 +- 5 files changed, 315 insertions(+), 221 deletions(-) create mode 100644 resources/views/admin/venues/index.blade.php diff --git a/app/Http/Controllers/pages/BookingController.php b/app/Http/Controllers/pages/BookingController.php index f37fc6a..784bdf7 100644 --- a/app/Http/Controllers/pages/BookingController.php +++ b/app/Http/Controllers/pages/BookingController.php @@ -23,179 +23,253 @@ public function __construct(MidtransService $midtransService) } // Tambahkan method baru untuk booking langsung oleh admin - public function adminDirectBooking(Request $request) { - try { - $request->validate([ - 'table_id' => 'required|exists:tables,id', - 'start_time' => 'required|date', - 'end_time' => 'required|date|after:start_time', - ]); - - $user = Auth::user(); - - // Validasi bahwa user adalah admin dan mengelola venue dari meja tersebut - $table = Table::findOrFail($request->table_id); - if ($user->role !== 'admin' || $user->venue_id !== $table->venue_id) { - return response()->json([ - 'message' => 'Unauthorized action' - ], 403); - } - - // 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); - }); - }) - ->where('status', 'paid') - ->exists(); - - if ($conflict) { - 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); - $totalAmount = $duration * $table->price_per_hour; - - // Generate order ID unik untuk admin - $adminOrderId = 'ADMIN-' . $user->id . '-' . time(); - - // Buat booking langsung dengan status paid - $booking = Booking::create([ - 'table_id' => $request->table_id, - 'user_id' => $user->id, - 'start_time' => $request->start_time, - 'end_time' => $request->end_time, - 'status' => 'paid', // langsung set sebagai paid - 'total_amount' => $totalAmount, - 'payment_id' => null, // Admin tidak perlu payment_id - 'payment_method' => 'admin_direct', // Tandai sebagai booking langsung admin - 'order_id' => $adminOrderId, - ]); - - // Update table status menjadi Booked - $table->update(['status' => 'Booked']); - + public function adminDirectBooking($request) { + try { + // 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' => 'Booking created successfully', - 'booking_id' => $booking->id - ]); - - } catch (\Exception $e) { - \Log::error('Admin direct booking error:', [ - 'message' => $e->getMessage(), - 'trace' => $e->getTraceAsString() - ]); - - return response()->json([ - 'message' => 'Failed to create booking: ' . $e->getMessage() - ], 500); + 'message' => 'Missing required fields' + ], 400); } + + $user = Auth::user(); + + // Validasi bahwa user adalah admin dan mengelola venue dari meja tersebut + $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', $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); + }); + }) + ->whereIn('status', ['paid', 'pending']) + ->exists(); + + if ($conflict) { + return response()->json([ + 'message' => 'Meja sudah dibooking di jam tersebut' + ], 409); + } + + // Hitung total biaya dan durasi + $duration = $endDateTime->diffInHours($startDateTime); + $totalAmount = $duration * $table->price_per_hour; + + // Generate order ID unik untuk admin + $adminOrderId = 'ADMIN-' . $user->id . '-' . time(); + + // Buat booking langsung dengan status paid + $booking = Booking::create([ + 'table_id' => $data['table_id'], + 'user_id' => $user->id, + 'start_time' => $startDateTime, + 'end_time' => $endDateTime, + 'status' => 'paid', + 'total_amount' => $totalAmount, + 'payment_id' => null, + 'payment_method' => 'admin_direct', + 'order_id' => $adminOrderId, + ]); + + return response()->json([ + '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(), + 'request_data' => $request instanceof \Illuminate\Http\Request ? $request->all() : $request->toArray() + ]); + + return response()->json([ + 'success' => false, + 'message' => 'Gagal membuat booking: ' . $e->getMessage() + ], 500); } +} 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', - ]); + try { + $request->validate([ + 'table_id' => 'required|exists:tables,id', + '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); + $user = Auth::user(); + $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; - // 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]) - ->orWhere(function($query) use ($request) { - $query->where('start_time', '<', $request->start_time) - ->where('end_time', '>', $request->start_time); - }); - }) - ->where('status', 'paid') // Hanya cek yang sudah paid - ->exists(); - - if ($conflict) { - return response()->json(['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 - Session::put('temp_booking', [ - 'table_id' => $request->table_id, - 'user_id' => Auth::id(), - 'start_time' => $request->start_time, - 'end_time' => $request->end_time, - 'total_amount' => $totalAmount, - 'created_at' => now(), - ]); - - // 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('Payment intent created successfully:', [ - 'order_id' => $tempOrderId, - 'snap_token' => $snapToken - ]); + // 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([ - 'message' => 'Payment intent created, proceed to payment', - 'total_amount' => $totalAmount, - 'snap_token' => $snapToken, - 'order_id' => $tempOrderId - ]); - } catch (\Exception $e) { - \Log::error('Payment intent error:', [ - 'message' => $e->getMessage(), - 'trace' => $e->getTraceAsString() - ]); - - return response()->json([ - 'message' => 'Gagal membuat transaksi: ' . $e->getMessage() - ], 500); + 'success' => false, + 'message' => 'Waktu booking di luar jam operasional venue' + ], 422); } + + // 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 ($startDateTime, $endDateTime) { + $query->where(function($q) use ($startDateTime, $endDateTime) { + $q->where('start_time', '<', $endDateTime) + ->where('end_time', '>', $startDateTime); + }); + }) + ->where('status', 'paid') + ->exists(); + + if ($conflict) { + return response()->json([ + 'success' => false, + 'message' => 'Meja sudah dibooking di jam tersebut' + ], 409); + } + + // Hitung total biaya + $totalAmount = $duration * $table->price_per_hour; + + // Simpan data booking sementara di session + Session::put('temp_booking', [ + 'table_id' => $request->table_id, + 'user_id' => Auth::id(), + 'start_time' => $startDateTime->toDateTimeString(), + 'end_time' => $endDateTime->toDateTimeString(), + 'total_amount' => $totalAmount, + 'created_at' => now(), + ]); + + // Generate unique order ID + $tempOrderId = 'TEMP-' . Auth::id() . '-' . time(); + Session::put('temp_order_id', $tempOrderId); + + // Simpan booking sementara ke database + PendingBooking::updateOrCreate( + [ + 'user_id' => Auth::id(), + 'table_id' => $request->table_id, + 'start_time' => $startDateTime->toDateTimeString() + ], + [ + 'end_time' => $endDateTime->toDateTimeString(), + 'total_amount' => $totalAmount, + 'order_id' => $tempOrderId, + 'expired_at' => now()->addHours(24), + ] + ); + + // Dapatkan snap token dari Midtrans + $snapToken = $this->midtransService->createTemporaryTransaction($table, $totalAmount, $tempOrderId, Auth::user()); + + if (!$snapToken) { + throw new \Exception('Failed to get snap token from Midtrans'); + } + + \Log::info('Payment intent created successfully:', [ + 'order_id' => $tempOrderId, + '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, + 'order_id' => $tempOrderId + ]); + } catch (\Exception $e) { + \Log::error('Payment intent error:', [ + 'message' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return response()->json([ + 'success' => false, + 'message' => 'Gagal membuat transaksi: ' . $e->getMessage() + ], 500); } +} public function store(Request $request) { try { diff --git a/resources/views/admin/venues/index.blade.php b/resources/views/admin/venues/index.blade.php new file mode 100644 index 0000000..e69de29 diff --git a/resources/views/pages/home.blade.php b/resources/views/pages/home.blade.php index ec88fdc..efb92d4 100644 --- a/resources/views/pages/home.blade.php +++ b/resources/views/pages/home.blade.php @@ -52,10 +52,15 @@ class="w-full py-3 md:px-6 rounded-lg text-sm bg-primary text-white font-semibol {{ $venue->name }} +

Venue

{{ $venue->name }}

- {{--

{{ $venue->address }}

--}} +

+ + Buka: {{ date('H:i', strtotime($venue['open_time'])) }} - + {{ date('H:i', strtotime($venue['close_time'])) }} +

Mulai: Rp30,000 / jam diff --git a/resources/views/pages/venue.blade.php b/resources/views/pages/venue.blade.php index d095787..7957cbc 100644 --- a/resources/views/pages/venue.blade.php +++ b/resources/views/pages/venue.blade.php @@ -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" />

{{ $venue['name'] }}

-

{{ $venue['location'] ?? 'Lokasi tidak tersedia' }}

Jam Operasional: {{ date('H:i', strtotime($venue['open_time'])) }} - @@ -183,12 +182,12 @@ function showToast(message, type = 'info', duration = 5000) { toast.className = `${bgColor} text-white px-6 py-4 rounded-lg shadow-lg flex items-center space-x-3 min-w-80 transform transition-all duration-300 translate-x-full opacity-0`; toast.innerHTML = ` - - ${message} - - `; + + ${message} + + `; toastContainer.appendChild(toast); @@ -224,20 +223,20 @@ function showModal(title, message, type = 'info', callback = null) { }[type] || 'fa-info-circle'; modal.innerHTML = ` -

-
- -

${title}

-
-

${message}

-
- -
-
- `; +
+
+ +

${title}

+
+

${message}

+
+ +
+
+ `; document.body.appendChild(modal); @@ -256,22 +255,22 @@ function showConfirmModal(title, message, onConfirm, onCancel = null) { modal.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'; modal.innerHTML = ` -
-
- -

${title}

-
-

${message}

-
- - -
-
- `; +
+
+ +

${title}

+
+

${message}

+
+ + +
+
+ `; document.body.appendChild(modal); @@ -630,29 +629,45 @@ function formatPrice(price) { window.dispatchEvent(new CustomEvent('hide-loading')); 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.createBooking(data.order_id, result); - }, - onPending: (result) => { - showToast('Pembayaran pending, silahkan selesaikan pembayaran', 'warning'); - this.isLoading = false; - }, - onError: (result) => { - showToast('Pembayaran gagal', 'error'); - this.isLoading = false; - }, - onClose: () => { - showToast('Anda menutup popup tanpa menyelesaikan pembayaran', 'warning'); - this.isLoading = false; - window.justClosedPayment = true; + // 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); + window.snap.pay(data.snap_token, { + onSuccess: (result) => { + this.createBooking(data.order_id, result); + }, + onPending: (result) => { + showToast('Pembayaran pending, silahkan selesaikan pembayaran', 'warning'); + this.isLoading = false; + }, + onError: (result) => { + showToast('Pembayaran gagal', 'error'); + this.isLoading = false; + }, + onClose: () => { + showToast('Anda menutup popup tanpa menyelesaikan pembayaran', 'warning'); + this.isLoading = false; + window.justClosedPayment = true; - // Dispatch event to refresh pending bookings - document.dispatchEvent(refreshPendingBookingsEvent); - } - }); + // Dispatch event to refresh pending bookings + 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; diff --git a/routes/web.php b/routes/web.php index 50d8197..b774a5b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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');