diff --git a/app/Http/Controllers/ReviewController.php b/app/Http/Controllers/ReviewController.php new file mode 100644 index 0000000..49285d6 --- /dev/null +++ b/app/Http/Controllers/ReviewController.php @@ -0,0 +1,66 @@ +user_id !== Auth::id()) { + abort(403, 'Akses tidak diizinkan.'); + } + + // Otorisasi: Pastikan booking sudah pernah diulas atau belum + if ($booking->review) { + return redirect()->route('booking.history')->with('error', 'Anda sudah pernah memberikan ulasan untuk booking ini.'); + } + + // Muat relasi yang diperlukan untuk ditampilkan di view + $booking->load('table.venue'); + + return view('reviews.create', compact('booking')); + } + + public function store(Request $request) + { + // 1. Validasi Input + $request->validate([ + 'booking_id' => 'required|exists:bookings,id|unique:reviews,booking_id', + 'rating' => 'required|integer|min:1|max:5', + 'comment' => 'required|string|max:2000', + ], [ + 'booking_id.unique' => 'Anda sudah pernah memberikan ulasan untuk booking ini.', + 'rating.required' => 'Rating bintang wajib diisi.', + 'comment.required' => 'Komentar ulasan wajib diisi.', + ]); + + // 2. Cek Otorisasi + $booking = Booking::find($request->booking_id); + + // Pastikan user yang login adalah pemilik booking tersebut + if ($booking->user_id !== Auth::id()) { + return redirect()->back()->with('error', 'Akses tidak diizinkan.'); + } + + // 3. Simpan Ulasan + Review::create([ + 'user_id' => Auth::id(), + 'venue_id' => $booking->table->venue_id, // Ambil venue_id dari relasi + 'booking_id' => $booking->id, + 'rating' => $request->rating, + 'comment' => $request->comment, + ]); + + // 4. Redirect dengan Pesan Sukses + return redirect()->route('booking.history')->with('success', 'Terima kasih atas ulasan Anda!'); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/admin/BookingsController.php b/app/Http/Controllers/admin/BookingsController.php index d8a4a52..6c4376a 100644 --- a/app/Http/Controllers/admin/BookingsController.php +++ b/app/Http/Controllers/admin/BookingsController.php @@ -76,6 +76,42 @@ public function index(Request $request) return view('admin.bookings.index', compact('bookings')); } + public function showScanner() + { + return view('admin.bookings.scan'); + } + + public function validateBooking($token) + { + $booking = Booking::where('validation_token', $token) + ->with(['user', 'table.venue']) + ->first(); + + if (!$booking) { + return response()->json(['error' => 'Booking tidak ditemukan atau tidak valid.'], 404); + } + + // Otorisasi: Pastikan admin hanya bisa memvalidasi booking di venue miliknya + if ($booking->table->venue_id !== auth()->user()->venue_id) { + return response()->json(['error' => 'Akses tidak diizinkan.'], 403); + } + + // Mengembalikan data yang diformat dengan baik + return response()->json([ + 'success' => true, + 'data' => [ + 'venue_name' => $booking->table->venue->name, + 'booking_id' => $booking->id, + 'user_name' => $booking->user->name, + 'table_name' => $booking->table->name, + 'start_time' => $booking->start_time->format('d M Y, H:i'), + 'end_time' => $booking->end_time->format('H:i'), + 'duration' => $booking->start_time->diffInHours($booking->end_time) . ' Jam', + 'status' => $booking->status, + ] + ]); + } + public function show($id) { // Pastikan booking yang dilihat adalah milik venue admin diff --git a/app/Http/Controllers/admin/VenueController.php b/app/Http/Controllers/admin/VenueController.php index 1b5c41c..23f19f5 100644 --- a/app/Http/Controllers/admin/VenueController.php +++ b/app/Http/Controllers/admin/VenueController.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Controller; use App\Models\Venue; +use App\Models\VenueImage; use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Validator; @@ -36,12 +37,12 @@ public function index() */ public function edit() { - $venue = auth()->user()->venue; - + $venue = auth()->user()->venue()->with('images', 'operatingHours')->first(); // Tambahkan 'operatingHours' + if (!$venue) { return redirect()->route('admin.dashboard')->with('error', 'Anda belum memiliki venue yang ditugaskan.'); } - + return view('admin.venues.edit', compact('venue')); } @@ -51,84 +52,115 @@ public function edit() public function update(Request $request) { $venue = auth()->user()->venue; - if (!$venue) { return redirect()->route('admin.dashboard')->with('error', 'Anda belum memiliki venue yang ditugaskan.'); } - - // Validation rules - $validator = Validator::make($request->all(), [ - 'name' => 'required|string|max:255', - 'address' => 'required|string|max:500', - 'phone' => 'nullable|string|max:20', - 'description' => 'nullable|string|max:1000', - 'open_time' => 'required|date_format:H:i', - 'close_time' => 'required|date_format:H:i', - 'image' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048', // Max 2MB - ], [ - 'name.required' => 'Nama venue harus diisi.', - 'address.required' => 'Alamat venue harus diisi.', - 'open_time.required' => 'Jam buka harus diisi.', - 'open_time.date_format' => 'Format jam buka tidak valid (gunakan format HH:MM).', - 'close_time.required' => 'Jam tutup harus diisi.', - 'close_time.date_format' => 'Format jam tutup tidak valid (gunakan format HH:MM).', - 'image.image' => 'File yang diupload harus berupa gambar.', - 'image.mimes' => 'Gambar harus berformat: jpeg, png, jpg, atau gif.', - 'image.max' => 'Ukuran gambar maksimal 2MB.', - ]); - - if ($validator->fails()) { - return redirect()->back() - ->withErrors($validator) - ->withInput(); + + // 1. Pra-proses data jam operasional sebelum validasi + $input = $request->all(); + if (isset($input['hours'])) { + foreach ($input['hours'] as $day => &$data) { // Gunakan '&' untuk modifikasi langsung + // Jika 'is_closed' dicentang, atau jika input waktu kosong, paksa menjadi null + if (isset($data['is_closed']) && $data['is_closed'] == 'on') { + $data['open_time'] = null; + $data['close_time'] = null; + } + } } + // 2. Buat aturan validasi yang lebih pintar + $validator = Validator::make($input, [ + 'name' => 'required|string|max:255', + 'address' => 'required|string|max:500', + 'latitude' => 'nullable|string|max:255', + 'longitude' => 'nullable|string|max:255', + // ... (validasi lain tetap sama) ... + 'image' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048', + 'gallery_images.*' => 'image|mimes:jpeg,png,jpg,gif|max:2048', + 'hours' => 'required|array|size:7', + // Aturan baru: open_time wajib diisi KECUALI jika is_closed dicentang + 'hours.*.open_time' => 'required_unless:hours.*.is_closed,on|nullable|date_format:H:i', + 'hours.*.close_time' => 'required_unless:hours.*.is_closed,on|nullable|date_format:H:i', + 'link_instagram' => 'nullable|url', + 'link_tiktok' => 'nullable|url', + 'link_facebook' => 'nullable|url', + 'link_x' => 'nullable|url', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + try { - // Handle image upload - $imagePath = $venue->image; // Keep current image by default - - if ($request->hasFile('image')) { - // Delete old image if exists - if ($venue->image && Storage::disk('public')->exists($venue->image)) { - Storage::disk('public')->delete($venue->image); - } - - // Store new image - $imagePath = $request->file('image')->store('venues', 'public'); - } - - // Prepare update data - $updateData = [ + // (Sisa dari kode ini tidak perlu diubah) + $imagePath = $venue->image; + if ($request->hasFile('image')) { /* ... logika upload ... */ } + if ($request->hasFile('gallery_images')) { /* ... logika upload galeri ... */ } + + $venue->update([ 'name' => $request->name, 'address' => $request->address, + 'latitude' => $request->latitude, + 'longitude' => $request->longitude, 'phone' => $request->phone, + 'link_instagram' => $request->link_instagram, + 'link_tiktok' => $request->link_tiktok, + 'link_facebook' => $request->link_facebook, + 'link_x' => $request->link_x, 'description' => $request->description, 'image' => $imagePath, - ]; + ]); - // Only update operating hours if venue is open - if ($venue->status === 'open') { - $updateData['open_time'] = $request->open_time; - $updateData['close_time'] = $request->close_time; - } else { - // If venue is closed, update original times - $updateData['original_open_time'] = $request->open_time; - $updateData['original_close_time'] = $request->close_time; + foreach ($request->hours as $dayNumber => $hoursData) { + $isClosed = isset($hoursData['is_closed']); + $venue->operatingHours()->updateOrCreate( + ['day_of_week' => $dayNumber], + [ + 'open_time' => $isClosed ? null : $hoursData['open_time'], + 'close_time' => $isClosed ? null : $hoursData['close_time'], + 'is_closed' => $isClosed, + ] + ); } - // Update venue data - $venue->update($updateData); - return redirect()->route('admin.venue.index') ->with('success', 'Informasi venue berhasil diperbarui!'); } catch (\Exception $e) { return redirect()->back() - ->with('error', 'Terjadi kesalahan saat memperbarui venue: ' . $e->getMessage()) + ->with('error', 'Terjadi kesalahan: ' . $e->getMessage()) ->withInput(); } } + public function destroyImage(VenueImage $image) + { + // Otorisasi: Pastikan admin yang sedang login adalah pemilik venue dari gambar ini + // Kita bandingkan venue_id milik admin dengan venue_id milik gambar. + if ($image->venue_id !== auth()->user()->venue_id) { + return response()->json(['success' => false, 'message' => 'Akses tidak diizinkan.'], 403); + } + + try { + // Hapus file dari storage + if (Storage::disk('public')->exists($image->path)) { + Storage::disk('public')->delete($image->path); + } + + // Hapus record dari database + $image->delete(); + + return response()->json(['success' => true, 'message' => 'Gambar berhasil dihapus.']); + + } catch (\Exception $e) { + // Catat error untuk debugging jika perlu + // Log::error('Failed to delete gallery image: ' . $e->getMessage()); + + // Kirim respons error yang jelas + return response()->json(['success' => false, 'message' => 'Gagal menghapus gambar di server.'], 500); + } + } + /** * Toggle venue status (open/close) */ diff --git a/app/Http/Controllers/pages/BookingController.php b/app/Http/Controllers/pages/BookingController.php index e7fd2fb..a4e9282 100644 --- a/app/Http/Controllers/pages/BookingController.php +++ b/app/Http/Controllers/pages/BookingController.php @@ -12,6 +12,8 @@ use Carbon\Carbon; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Session; +use Illuminate\Support\Str; // <-- 1. PASTIKAN INI ADA +use SimpleSoftwareIO\QrCode\Facades\QrCode; // <-- 1. PASTIKAN INI JUGA ADA class BookingController extends Controller { @@ -22,6 +24,25 @@ public function __construct(MidtransService $midtransService) $this->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) { @@ -33,10 +54,9 @@ public function adminDirectBooking($request) { } $user = Auth::user(); - $table = Table::with('venue')->findOrFail($data['table_id']); + $table = Table::with(['venue.operatingHours'])->findOrFail($data['table_id']); $venue = $table->venue; - // Validasi otorisasi admin (menggunakan struktur yang konsisten dengan kodemu) if ($user->role !== 'admin' || $user->venue_id !== $table->venue_id) { return response()->json(['message' => 'Unauthorized action'], 403); } @@ -44,49 +64,46 @@ public function adminDirectBooking($request) { $startDateTime = Carbon::parse($data['start_time']); $endDateTime = Carbon::parse($data['end_time']); - // --- Validasi jam operasional (logika ini sudah benar) --- - $operationalDayStart = Carbon::createFromFormat('Y-m-d H:i:s', $startDateTime->format('Y-m-d') . ' ' . $venue->open_time, 'Asia/Jakarta'); - if ($venue->is_overnight && $startDateTime < $operationalDayStart) { - $operationalDayStart->subDay(); + // --- 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); } - $operationalDayEnd = $operationalDayStart->copy()->setTimeFromTimeString($venue->close_time); - if ($venue->is_overnight) { + $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)) { - Log::warning('Admin direct booking attempt outside operational hours.', [ - 'start_time' => $startDateTime->toDateTimeString(), - 'venue_open' => $operationalDayStart->toDateTimeString(), - 'venue_close' => $operationalDayEnd->toDateTimeString(), - ]); return response()->json(['message' => 'Waktu booking di luar jam operasional venue.'], 400); } - // --- Akhir Validasi jam operasional --- + // --- AKHIR VALIDASI BARU --- - // --- PERBAIKAN LOGIKA KONFLIK DIMULAI DI SINI --- - // Kita hapus ->whereDate() dan langsung cek bentrokan waktu. $conflict = Booking::where('table_id', $data['table_id']) ->whereIn('status', ['paid', 'pending']) - ->where(function($query) use ($startDateTime, $endDateTime) { - // Booking yang baru tidak boleh dimulai di tengah booking lain. - // Booking yang baru juga tidak boleh berakhir di tengah booking lain. - // Booking yang baru juga tidak boleh "menelan" booking lain. - $query->where('start_time', '<', $endDateTime) - ->where('end_time', '>', $startDateTime); - }) + ->where('start_time', '<', $endDateTime) + ->where('end_time', '>', $startDateTime) ->exists(); if ($conflict) { return response()->json(['message' => 'Meja sudah dibooking di jam tersebut'], 409); } - // --- AKHIR DARI PERBAIKAN LOGIKA KONFLIK --- - // Hitung total biaya dan durasi + // Sisa fungsi tidak berubah $duration = $endDateTime->diffInHours($startDateTime); $totalAmount = $duration * $table->price_per_hour; - $adminOrderId = 'ADMIN-' . $user->id . '-' . time(); $booking = Booking::create([ @@ -96,86 +113,80 @@ public function adminDirectBooking($request) { 'end_time' => $endDateTime, 'status' => 'paid', 'total_amount' => $totalAmount, - 'payment_id' => null, '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, - '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, ',', '.') - ] + // ... (detail booking lainnya) ]); } 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() - ]); - + \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')->findOrFail($request->table_id); - $venue = $table->venue; - - $bookingDate = $request->booking_date; - $startTimeString = $request->start_time; - $duration = (int) $request->duration; - - // 1. Hitung start & end time yang sebenarnya - $startDateTime = Carbon::createFromFormat('Y-m-d H:i', $bookingDate . ' ' . $startTimeString, 'Asia/Jakarta'); - - // --- AWAL PERBAIKAN LOGIKA STRING COMPARISON --- - $startTimeObject = Carbon::createFromFormat('H:i', $startTimeString); - $openTimeObject = Carbon::parse($venue->open_time); - - // Bandingkan sebagai objek Carbon, bukan string - if ($venue->is_overnight && $startTimeObject->lt($openTimeObject)) { - $startDateTime->addDay(); - } - $endDateTime = $startDateTime->copy()->addHours($duration); - - // 2. --- BLOK VALIDASI YANG DIPERBAIKI --- - $operationalDayStart = Carbon::createFromFormat('Y-m-d H:i:s', $startDateTime->format('Y-m-d') . ' ' . $venue->open_time, 'Asia/Jakarta'); - if ($venue->is_overnight && $startDateTime < $operationalDayStart) { - $operationalDayStart->subDay(); - } - $operationalDayEnd = $operationalDayStart->copy()->setTimeFromTimeString($venue->close_time); - if ($venue->is_overnight) { - $operationalDayEnd->addDay(); - } - if ($startDateTime->lt($operationalDayStart) || $endDateTime->gt($operationalDayEnd)) { - Log::warning('Booking attempt outside operational hours.', [ - 'start_time' => $startDateTime->toDateTimeString(), 'end_time' => $endDateTime->toDateTimeString(), - 'venue_open' => $operationalDayStart->toDateTimeString(), 'venue_close' => $operationalDayEnd->toDateTimeString(), + 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', ]); - return response()->json(['success' => false, 'message' => 'Durasi booking di luar jam operasional venue.'], 422); - } - // --- AKHIR DARI BLOK VALIDASI --- - // 3. Cek untuk admin direct booking (tidak berubah) - if ($user->role === 'admin' && $user->venue_id === $table->venue_id) { + $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(), @@ -183,7 +194,7 @@ public function createPaymentIntent(Request $request) { ])); } - // 4. Cek konflik booking (tidak berubah) + // Cek konflik booking (tidak berubah) $conflict = Booking::where('table_id', $request->table_id) ->where('status', 'paid') ->where(function($query) use ($startDateTime, $endDateTime) { @@ -195,7 +206,7 @@ public function createPaymentIntent(Request $request) { return response()->json(['success' => false, 'message' => 'Meja sudah dibooking di jam tersebut'], 409); } - // 5. Proses ke Midtrans (tidak berubah) + // Proses ke Midtrans (tidak berubah) $totalAmount = $duration * $table->price_per_hour; $tempOrderId = 'TEMP-' . Auth::id() . '-' . time(); @@ -214,18 +225,19 @@ public function createPaymentIntent(Request $request) { '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); + } 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 { @@ -273,6 +285,7 @@ public function store(Request $request) { '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 diff --git a/app/Http/Controllers/pages/VenueController.php b/app/Http/Controllers/pages/VenueController.php index e9a7c5c..f0eae77 100644 --- a/app/Http/Controllers/pages/VenueController.php +++ b/app/Http/Controllers/pages/VenueController.php @@ -1,34 +1,42 @@ first(); + public function venue($venueName) + { + $venue = Venue::where('name', $venueName) + ->with(['tables', 'images', 'operatingHours', 'reviews.user']) + ->firstOrFail(); - // Jika venue tidak ditemukan, tampilkan error 404 - if (!$venue) { - abort(404); + // --- LOGIKA BARU UNTUK JAM OPERASIONAL HARI INI --- + // Day of week: 1 (Senin) - 7 (Minggu). Carbon menggunakan 0 (Minggu) - 6 (Sabtu) + $dayOfWeek = Carbon::now('Asia/Jakarta')->dayOfWeekIso; // ISO standard: 1=Senin, 7=Minggu + + // Cari jadwal untuk hari ini dari data yang sudah dimuat + $todaysHours = $venue->operatingHours->firstWhere('day_of_week', $dayOfWeek); + + // Siapkan data jam buka dan tutup untuk hari ini + // Jika tidak ada jadwal spesifik, atau jika hari ini libur, maka venue dianggap tutup + if ($todaysHours && !$todaysHours->is_closed) { + $openTime = $todaysHours->open_time; + $closeTime = $todaysHours->close_time; + } else { + // Set default ke 'tutup' jika tidak ada jadwal atau is_closed = true + $openTime = '00:00'; + $closeTime = '00:00'; } + // --- AKHIR LOGIKA BARU --- + $averageRating = $venue->reviews->avg('rating'); + $totalReviews = $venue->reviews->count(); - // Ambil tabel-tabel terkait dengan venue - $venue->load('tables'); // Eager loading untuk optimasi - - // Parsing jam operasional dari format H:i:s menjadi integer - $openHour = (int) date('H', strtotime($venue->open_time)); - $closeHour = (int) date('H', strtotime($venue->close_time)); - - // Mengirim data venue dengan jam operasional ke view - return view('pages.venue', [ - 'venue' => $venue, - 'openHour' => $openHour, - 'closeHour' => $closeHour - ]); + // Kirim semua data ke view + return view('pages.venue', compact('venue', 'openTime', 'closeTime', 'averageRating', 'totalReviews')); } } \ No newline at end of file diff --git a/app/Models/Booking.php b/app/Models/Booking.php index 1986494..7fa8b2b 100644 --- a/app/Models/Booking.php +++ b/app/Models/Booking.php @@ -18,7 +18,9 @@ class Booking extends Model 'payment_id', 'payment_method', 'total_amount', - 'payment_expired_at' + 'payment_expired_at', + 'order_id', // Pastikan order_id juga ada di sini jika belum + 'validation_token', // <-- TAMBAHKAN INI ]; protected $casts = [ @@ -38,6 +40,11 @@ public function user() return $this->belongsTo(User::class); } + public function review() + { + return $this->hasOne(Review::class); + } + public function isPending() { return $this->status === 'pending'; diff --git a/app/Models/OperatingHour.php b/app/Models/OperatingHour.php new file mode 100644 index 0000000..ee40eff --- /dev/null +++ b/app/Models/OperatingHour.php @@ -0,0 +1,24 @@ +belongsTo(Venue::class); + } +} \ No newline at end of file diff --git a/app/Models/Review.php b/app/Models/Review.php new file mode 100644 index 0000000..97cb4b5 --- /dev/null +++ b/app/Models/Review.php @@ -0,0 +1,34 @@ +belongsTo(User::class); + } + + public function venue() + { + return $this->belongsTo(Venue::class); + } + + public function booking() + { + return $this->belongsTo(Booking::class); + } +} \ No newline at end of file diff --git a/app/Models/User.php b/app/Models/User.php index bc9e162..bc2f4ed 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -65,6 +65,11 @@ public function hasRole($role) { return $this->role === $role; } + + public function reviews() + { + return $this->hasMany(Review::class); + } /** * Get the venue that the admin belongs to. diff --git a/app/Models/Venue.php b/app/Models/Venue.php index 805d11d..f69ef28 100644 --- a/app/Models/Venue.php +++ b/app/Models/Venue.php @@ -5,6 +5,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Carbon\Carbon; +use App\Models\VenueImage; // <-- Tambahkan ini class Venue extends Model { @@ -19,8 +20,14 @@ public function getIsOvernightAttribute() protected $fillable = [ 'name', 'address', + 'latitude', + 'longitude', 'image', 'phone', + 'link_instagram', + 'link_tiktok', + 'link_facebook', + 'link_x', 'description', 'open_time', 'close_time', @@ -49,6 +56,21 @@ public function tables() return $this->hasMany(Table::class); } + public function images() + { + return $this->hasMany(VenueImage::class); + } + + public function operatingHours() + { + return $this->hasMany(OperatingHour::class); + } + + public function reviews() + { + return $this->hasMany(Review::class); + } + /** * Check if venue should automatically reopen */ diff --git a/app/Models/VenueImage.php b/app/Models/VenueImage.php new file mode 100644 index 0000000..d7e00c0 --- /dev/null +++ b/app/Models/VenueImage.php @@ -0,0 +1,18 @@ +belongsTo(Venue::class); + } +} \ No newline at end of file diff --git a/composer.json b/composer.json index 8e35ca8..70a5f87 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ "laravel/tinker": "^2.8", "laravel/ui": "^4.6", "maatwebsite/excel": "^1.1", - "midtrans/midtrans-php": "^2.6" + "midtrans/midtrans-php": "^2.6", + "simplesoftwareio/simple-qrcode": "^4.2" }, "require-dev": { "fakerphp/faker": "^1.9.1", diff --git a/composer.lock b/composer.lock index b6988ee..78a9acb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,62 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bce8ddce84cf8a2d572b421f6992903e", + "content-hash": "66130e103a2b9c249e50df9d5700a24f", "packages": [ + { + "name": "bacon/bacon-qr-code", + "version": "2.0.8", + "source": { + "type": "git", + "url": "https://github.com/Bacon/BaconQrCode.git", + "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22", + "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22", + "shasum": "" + }, + "require": { + "dasprid/enum": "^1.0.3", + "ext-iconv": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phly/keep-a-changelog": "^2.1", + "phpunit/phpunit": "^7 | ^8 | ^9", + "spatie/phpunit-snapshot-assertions": "^4.2.9", + "squizlabs/php_codesniffer": "^3.4" + }, + "suggest": { + "ext-imagick": "to generate QR code images" + }, + "type": "library", + "autoload": { + "psr-4": { + "BaconQrCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "BaconQrCode is a QR code generator for PHP.", + "homepage": "https://github.com/Bacon/BaconQrCode", + "support": { + "issues": "https://github.com/Bacon/BaconQrCode/issues", + "source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8" + }, + "time": "2022-12-07T17:46:57+00:00" + }, { "name": "brick/math", "version": "0.12.1", @@ -135,6 +189,56 @@ ], "time": "2023-12-11T17:09:12+00:00" }, + { + "name": "dasprid/enum", + "version": "1.0.6", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8dfd07c6d2cf31c8da90c53b83c026c7696dda90", + "reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90", + "shasum": "" + }, + "require": { + "php": ">=7.1 <9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "support": { + "issues": "https://github.com/DASPRiD/Enum/issues", + "source": "https://github.com/DASPRiD/Enum/tree/1.0.6" + }, + "time": "2024-08-09T14:30:48+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.3", @@ -3812,6 +3916,74 @@ ], "time": "2024-04-27T21:32:50+00:00" }, + { + "name": "simplesoftwareio/simple-qrcode", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/SimpleSoftwareIO/simple-qrcode.git", + "reference": "916db7948ca6772d54bb617259c768c9cdc8d537" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SimpleSoftwareIO/simple-qrcode/zipball/916db7948ca6772d54bb617259c768c9cdc8d537", + "reference": "916db7948ca6772d54bb617259c768c9cdc8d537", + "shasum": "" + }, + "require": { + "bacon/bacon-qr-code": "^2.0", + "ext-gd": "*", + "php": ">=7.2|^8.0" + }, + "require-dev": { + "mockery/mockery": "~1", + "phpunit/phpunit": "~9" + }, + "suggest": { + "ext-imagick": "Allows the generation of PNG QrCodes.", + "illuminate/support": "Allows for use within Laravel." + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "QrCode": "SimpleSoftwareIO\\QrCode\\Facades\\QrCode" + }, + "providers": [ + "SimpleSoftwareIO\\QrCode\\QrCodeServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "SimpleSoftwareIO\\QrCode\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Simple Software LLC", + "email": "support@simplesoftware.io" + } + ], + "description": "Simple QrCode is a QR code generator made for Laravel.", + "homepage": "https://www.simplesoftware.io/#/docs/simple-qrcode", + "keywords": [ + "Simple", + "generator", + "laravel", + "qrcode", + "wrapper" + ], + "support": { + "issues": "https://github.com/SimpleSoftwareIO/simple-qrcode/issues", + "source": "https://github.com/SimpleSoftwareIO/simple-qrcode/tree/4.2.0" + }, + "time": "2021-02-08T20:43:55+00:00" + }, { "name": "symfony/console", "version": "v6.4.12", diff --git a/database/migrations/2025_07_07_182501_create_venue_images_table.php b/database/migrations/2025_07_07_182501_create_venue_images_table.php new file mode 100644 index 0000000..3a4a155 --- /dev/null +++ b/database/migrations/2025_07_07_182501_create_venue_images_table.php @@ -0,0 +1,23 @@ +id(); + $table->foreignId('venue_id')->constrained()->onDelete('cascade'); + $table->string('path'); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('venue_images'); + } +} \ No newline at end of file diff --git a/database/migrations/2025_07_08_144048_create_operating_hours_table.php b/database/migrations/2025_07_08_144048_create_operating_hours_table.php new file mode 100644 index 0000000..0929916 --- /dev/null +++ b/database/migrations/2025_07_08_144048_create_operating_hours_table.php @@ -0,0 +1,26 @@ +id(); + $table->foreignId('venue_id')->constrained()->onDelete('cascade'); + $table->tinyInteger('day_of_week'); // 1 untuk Senin, 2 Selasa, ..., 7 Minggu + $table->time('open_time')->nullable(); + $table->time('close_time')->nullable(); + $table->boolean('is_closed')->default(false); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('operating_hours'); + } +}; diff --git a/database/migrations/2025_07_08_181205_add_social_media_links_to_venues_table.php b/database/migrations/2025_07_08_181205_add_social_media_links_to_venues_table.php new file mode 100644 index 0000000..9c086e9 --- /dev/null +++ b/database/migrations/2025_07_08_181205_add_social_media_links_to_venues_table.php @@ -0,0 +1,25 @@ +string('link_instagram')->nullable()->after('phone'); + $table->string('link_tiktok')->nullable()->after('link_instagram'); + $table->string('link_facebook')->nullable()->after('link_tiktok'); + $table->string('link_x')->nullable()->after('link_facebook'); // Untuk Twitter/X + }); + } + + public function down() + { + Schema::table('venues', function (Blueprint $table) { + $table->dropColumn(['link_instagram', 'link_tiktok', 'link_facebook', 'link_x']); + }); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_08_191105_add_coordinates_to_venues_table.php b/database/migrations/2025_07_08_191105_add_coordinates_to_venues_table.php new file mode 100644 index 0000000..cd1fc50 --- /dev/null +++ b/database/migrations/2025_07_08_191105_add_coordinates_to_venues_table.php @@ -0,0 +1,23 @@ +string('latitude')->nullable()->after('address'); + $table->string('longitude')->nullable()->after('latitude'); + }); + } + + public function down() + { + Schema::table('venues', function (Blueprint $table) { + $table->dropColumn(['latitude', 'longitude']); + }); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_08_213405_create_reviews_table.php b/database/migrations/2025_07_08_213405_create_reviews_table.php new file mode 100644 index 0000000..c464288 --- /dev/null +++ b/database/migrations/2025_07_08_213405_create_reviews_table.php @@ -0,0 +1,26 @@ +id(); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + $table->foreignId('venue_id')->constrained()->onDelete('cascade'); + $table->foreignId('booking_id')->constrained()->onDelete('cascade')->unique(); + $table->tinyInteger('rating'); // 1-5 stars + $table->text('comment')->nullable(); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('reviews'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_08_230312_add_validation_token_to_bookings_table.php b/database/migrations/2025_07_08_230312_add_validation_token_to_bookings_table.php new file mode 100644 index 0000000..2776f76 --- /dev/null +++ b/database/migrations/2025_07_08_230312_add_validation_token_to_bookings_table.php @@ -0,0 +1,27 @@ +string('order_id')->nullable()->unique()->after('payment_method'); + + // Kemudian, tambahkan kolom 'validation_token' setelah 'order_id' + $table->string('validation_token')->nullable()->unique()->after('order_id'); + }); + } + + public function down() + { + Schema::table('bookings', function (Blueprint $table) { + $table->dropColumn('validation_token'); + $table->dropColumn('order_id'); + }); + } +}; \ No newline at end of file diff --git a/resources/views/admin/bookings/scan.blade.php b/resources/views/admin/bookings/scan.blade.php new file mode 100644 index 0000000..fdd1aff --- /dev/null +++ b/resources/views/admin/bookings/scan.blade.php @@ -0,0 +1,84 @@ +@extends('layouts.admin') + +@section('content') +
Arahkan kamera ke QR Code...
+Menunggu hasil scan...
+