midtransService = $midtransService; } public function showQrCode(Booking $booking) { // Otorisasi: Pastikan user yang login adalah pemilik booking if ($booking->user_id !== Auth::id()) { abort(403); } // Pastikan booking memiliki token validasi if (!$booking->validation_token) { abort(404, 'QR Code tidak ditemukan.'); } // Buat gambar QR Code dari token validasi $qrCode = QrCode::size(300)->generate($booking->validation_token); // Kembalikan sebagai respons gambar SVG return response($qrCode)->header('Content-Type', 'image/svg+xml'); } // Tambahkan method baru untuk booking langsung oleh admin // Ganti seluruh fungsi adminDirectBooking dengan ini public function adminDirectBooking($request) { try { $data = $request instanceof \Illuminate\Http\Request ? $request->all() : $request->toArray(); if (!isset($data['table_id']) || !isset($data['start_time']) || !isset($data['end_time'])) { return response()->json(['message' => 'Missing required fields'], 400); } $user = Auth::user(); $table = Table::with(['venue.operatingHours'])->findOrFail($data['table_id']); $venue = $table->venue; if ($user->role !== 'admin' || $user->venue_id !== $table->venue_id) { return response()->json(['message' => 'Unauthorized action'], 403); } $startDateTime = Carbon::parse($data['start_time']); $endDateTime = Carbon::parse($data['end_time']); // --- LOGIKA BARU: Validasi Jam Operasional Harian --- $bookingDate = $startDateTime->copy()->setTimezone('Asia/Jakarta'); $dayOfWeek = $bookingDate->dayOfWeekIso; $todaysHours = $venue->operatingHours->firstWhere('day_of_week', $dayOfWeek); if (!$todaysHours || $todaysHours->is_closed) { return response()->json(['message' => 'Venue tutup pada tanggal yang dipilih.'], 400); } $openTimeToday = $todaysHours->open_time; $closeTimeToday = $todaysHours->close_time; $isOvernightToday = strtotime($closeTimeToday) < strtotime($openTimeToday); $operationalDayStart = Carbon::createFromFormat('Y-m-d H:i:s', $startDateTime->format('Y-m-d') . ' ' . $openTimeToday, 'Asia/Jakarta'); if ($isOvernightToday && $startDateTime < $operationalDayStart) { $operationalDayStart->subDay(); } $operationalDayEnd = $operationalDayStart->copy()->setTimeFromTimeString($closeTimeToday); if ($isOvernightToday) { $operationalDayEnd->addDay(); } if ($startDateTime->lt($operationalDayStart) || $endDateTime->gt($operationalDayEnd)) { return response()->json(['message' => 'Waktu booking di luar jam operasional venue.'], 400); } // --- AKHIR VALIDASI BARU --- $conflict = Booking::where('table_id', $data['table_id']) ->whereIn('status', ['paid', 'pending']) ->where('start_time', '<', $endDateTime) ->where('end_time', '>', $startDateTime) ->exists(); if ($conflict) { return response()->json(['message' => 'Meja sudah dibooking di jam tersebut'], 409); } // Sisa fungsi tidak berubah $duration = $endDateTime->diffInHours($startDateTime); $totalAmount = $duration * $table->price_per_hour; $adminOrderId = 'ADMIN-' . $user->id . '-' . time(); $booking = Booking::create([ 'table_id' => $data['table_id'], 'user_id' => $user->id, 'start_time' => $startDateTime, 'end_time' => $endDateTime, 'status' => 'paid', 'total_amount' => $totalAmount, 'payment_method' => 'admin_direct', 'order_id' => $adminOrderId, 'validation_token' => (string) Str::uuid(), ]); return response()->json([ 'success' => true, 'message' => 'Booking berhasil dibuat oleh admin', 'booking_id' => $booking->id, // ... (detail booking lainnya) ]); } catch (\Exception $e) { \Log::error('Admin direct booking error:', ['message' => $e->getMessage()]); 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', 'duration' => 'required|integer|min:1|max:12', 'booking_date' => 'required|date_format:Y-m-d', ]); $user = Auth::user(); $table = Table::with(['venue.operatingHours'])->findOrFail($request->table_id); // Muat relasi operatingHours $venue = $table->venue; $bookingDate = Carbon::createFromFormat('Y-m-d', $request->booking_date, 'Asia/Jakarta'); $startTimeString = $request->start_time; $duration = (int) $request->duration; // --- START LOGIKA BARU: Ambil Jadwal Hari Ini --- $dayOfWeek = $bookingDate->dayOfWeekIso; // 1=Senin, 7=Minggu $todaysHours = $venue->operatingHours->firstWhere('day_of_week', $dayOfWeek); if (!$todaysHours || $todaysHours->is_closed) { return response()->json(['success' => false, 'message' => 'Venue tutup pada tanggal yang dipilih.'], 422); } // Gunakan jam dari jadwal harian, bukan dari $venue->open_time $openTimeToday = $todaysHours->open_time; $closeTimeToday = $todaysHours->close_time; $isOvernightToday = strtotime($closeTimeToday) < strtotime($openTimeToday); // --- END LOGIKA BARU --- $startDateTime = Carbon::createFromFormat('Y-m-d H:i', $bookingDate->format('Y-m-d') . ' ' . $startTimeString, 'Asia/Jakarta'); if ($isOvernightToday && (strtotime($startTimeString) < strtotime($openTimeToday))) { $startDateTime->addDay(); } $endDateTime = $startDateTime->copy()->addHours($duration); // Validasi jam operasional menggunakan data dinamis $operationalDayStart = Carbon::createFromFormat('Y-m-d H:i:s', $startDateTime->format('Y-m-d') . ' ' . $openTimeToday, 'Asia/Jakarta'); if ($isOvernightToday && $startDateTime < $operationalDayStart) { $operationalDayStart->subDay(); } $operationalDayEnd = $operationalDayStart->copy()->setTimeFromTimeString($closeTimeToday); if ($isOvernightToday) { $operationalDayEnd->addDay(); } if ($startDateTime->lt($operationalDayStart) || $endDateTime->gt($operationalDayEnd)) { return response()->json(['success' => false, 'message' => 'Durasi booking di luar jam operasional venue.'], 422); } // Sisa dari fungsi ini (cek admin, cek konflik, proses Midtrans) tidak perlu diubah. // ... (kode lama Anda untuk cek admin, cek konflik, dll, tetap di sini) ... 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 (tidak berubah) $conflict = Booking::where('table_id', $request->table_id) ->where('status', 'paid') ->where(function($query) use ($startDateTime, $endDateTime) { $query->where('start_time', '<', $endDateTime) ->where('end_time', '>', $startDateTime); }) ->exists(); if ($conflict) { return response()->json(['success' => false, 'message' => 'Meja sudah dibooking di jam tersebut'], 409); } // Proses ke Midtrans (tidak berubah) $totalAmount = $duration * $table->price_per_hour; $tempOrderId = 'TEMP-' . Auth::id() . '-' . time(); 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) ] ); $snapToken = $this->midtransService->createTemporaryTransaction($table, $totalAmount, $tempOrderId, Auth::user()); if (!$snapToken) { throw new \Exception('Failed to get snap token from Midtrans'); } return response()->json([ 'success' => true, '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 { $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, 'validation_token' => (string) Str::uuid(), ]); // 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', ]); $table = Table::with('venue')->findOrFail($request->table_id); $venue = $table->venue; $requestDate = Carbon::parse($request->date); // Only get bookings with paid status $query = Booking::where('table_id', $request->table_id) ->where('status', 'paid'); if ($venue->is_overnight) { // Jika overnight, ambil booking dari jam buka di hari H // sampai jam tutup di hari H+1 $startOperationalDay = $requestDate->copy()->setTimeFromTimeString($venue->open_time); $endOperationalDay = $requestDate->copy()->addDay()->setTimeFromTimeString($venue->close_time); $query->whereBetween('start_time', [$startOperationalDay, $endOperationalDay]); } else { // Jika tidak overnight, ambil booking hanya di hari H $query->whereDate('start_time', $requestDate); } $bookings = $query->select('start_time', 'end_time') ->get() ->map(function ($booking) { return [ // Format H:i tetap sama, karena frontend hanya butuh jamnya 'start' => Carbon::parse($booking->start_time)->format('H:i'), 'end' => Carbon::parse($booking->end_time)->format('H:i') ]; }); return response()->json($bookings); } public function handleNotification(Request $request) { try { $notification = $request->all(); Log::info('Midtrans notification received:', $notification); $transactionStatus = $notification['transaction_status']; $orderId = $notification['order_id']; $fraudStatus = $notification['fraud_status'] ?? null; $transactionId = $notification['transaction_id']; $paymentType = $notification['payment_type']; // 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); return response()->json(['message' => 'Booking not found'], 404); } // Update booking status based on transaction status if ($transactionStatus == 'capture') { if ($fraudStatus == 'challenge') { $booking->status = 'challenge'; } else if ($fraudStatus == 'accept') { $booking->status = 'paid'; // Update table status to booked $booking->table->update(['status' => 'Booked']); } } else if ($transactionStatus == 'settlement') { $booking->status = 'paid'; // Update table status to booked $booking->table->update(['status' => 'Booked']); } else if ($transactionStatus == 'cancel' || $transactionStatus == 'deny' || $transactionStatus == 'expire') { $booking->status = 'cancelled'; // Reset table status to available if no other active bookings $hasActiveBookings = $booking->table->bookings() ->where('status', 'paid') ->where('id', '!=', $booking->id) ->exists(); if (!$hasActiveBookings) { $booking->table->update(['status' => 'Available']); } } else if ($transactionStatus == 'pending') { $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']); } catch (\Exception $e) { Log::error('Error processing Midtrans notification: ' . $e->getMessage()); 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); } } // GANTI SELURUH FUNGSI showReschedule DENGAN YANG INI public function showReschedule($id) { $booking = Booking::with(['table.venue', 'table.venue.tables'])->findOrFail($id); // Validasi kepemilikan dan status booking (tidak ada perubahan) if ($booking->user_id !== auth()->id() || $booking->status !== 'paid' || $booking->reschedule_count >= 1) { return redirect()->route('booking.history')->with('error', 'Batas maksimal reschedule telah digunakan (1x).'); } $rescheduleDeadline = Carbon::parse($booking->start_time)->subHour(); if (now() > $rescheduleDeadline) { return redirect()->route('booking.history')->with('error', 'Batas waktu reschedule telah berakhir (1 jam sebelum mulai).'); } $venue = $booking->table->venue; $duration = Carbon::parse($booking->start_time)->diffInHours($booking->end_time); // --- AWAL LOGIKA BARU UNTUK MENENTUKAN TANGGAL OPERASIONAL --- $startTime = Carbon::parse($booking->start_time); $operational_date = $startTime->copy(); // Mulai dengan tanggal kalender // Jika venue-nya overnight DAN jam booking lebih pagi dari jam buka, // maka tanggal operasionalnya adalah H-1 dari tanggal kalender. if ($venue->is_overnight && $startTime->format('H:i:s') < $venue->open_time) { $operational_date->subDay(); } // Ubah ke format Y-m-d untuk dikirim ke view $operational_date_string = $operational_date->format('Y-m-d'); // --- AKHIR DARI LOGIKA BARU --- // Kirim $operational_date_string ke view, bukan lagi tanggal dari $booking return view('pages.reschedule', compact('booking', 'venue', 'duration', 'operational_date_string')); } /** * Process a reschedule request. */ public function processReschedule(Request $request, $id) { $request->validate([ 'table_id' => 'required|exists:tables,id', 'start_time' => 'required|date_format:Y-m-d H:i:s', 'end_time' => 'required|date_format:Y-m-d H:i:s|after:start_time', ]); $booking = Booking::findOrFail($id); // Perform validation if ($booking->user_id !== auth()->id() || $booking->start_time <= now() || $booking->status !== 'paid' || now() > Carbon::parse($booking->start_time)->subHour()) { return response()->json([ 'success' => false, 'message' => 'Booking ini tidak dapat di-reschedule.' ], 422); } // Check if the selected time is available (exclude current booking when checking conflicts) $existingBookings = Booking::where('table_id', $request->table_id) ->where('id', '!=', $booking->id) ->where('status', 'paid') ->where(function ($query) use ($request) { $query->where(function ($q) use ($request) { $q->where('start_time', '<', $request->end_time) ->where('end_time', '>', $request->start_time); }); })->count(); if ($existingBookings > 0) { return response()->json([ 'success' => false, 'message' => 'Jam yang dipilih sudah dibooking oleh orang lain.' ], 422); } // Update the booking with new schedule $booking->start_time = $request->start_time; $booking->end_time = $request->end_time; $booking->table_id = $request->table_id; $booking->save(); // Increment reschedule count $booking->increment('reschedule_count'); return response()->json([ 'success' => true, 'message' => 'Booking berhasil di-reschedule.', 'redirect' => route('booking.history') ]); } /** * Check availability for reschedule. */ public function checkRescheduleAvailability(Request $request) { $request->validate([ 'table_id' => 'required|exists:tables,id', 'date' => 'required|date_format:Y-m-d', 'booking_id' => 'required|exists:bookings,id' ]); $table = Table::with('venue')->findOrFail($request->table_id); $venue = $table->venue; $requestDate = Carbon::parse($request->date); // Query untuk mengambil booking lain di meja yang sama $query = Booking::where('table_id', $table->id) ->where('id', '!=', $request->booking_id) // Jangan ikut sertakan booking yang sedang di-reschedule ->where('status', 'paid'); // --- LOGIKA OVERNIGHT DITERAPKAN DI SINI --- if ($venue->is_overnight) { // Ambil booking dari jam buka di hari H sampai jam tutup di hari H+1 $startOperationalDay = $requestDate->copy()->setTimeFromTimeString($venue->open_time); $endOperationalDay = $requestDate->copy()->addDay()->setTimeFromTimeString($venue->close_time); $query->whereBetween('start_time', [$startOperationalDay, $endOperationalDay]); } else { // Logika standar untuk venue yang tidak overnight $query->whereDate('start_time', $requestDate); } // --- AKHIR DARI LOGIKA OVERNIGHT --- $bookings = $query->get(['start_time', 'end_time']) ->map(function ($booking) { return [ 'start' => Carbon::parse($booking->start_time)->format('H:i'), 'end' => Carbon::parse($booking->end_time)->format('H:i'), ]; }); return response()->json($bookings); } }