Switch remote
This commit is contained in:
parent
79307fb0d6
commit
f0f2d5299d
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Review;
|
||||
use App\Models\Booking;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ReviewController extends Controller
|
||||
{
|
||||
/**
|
||||
* Menyimpan ulasan baru ke dalam database.
|
||||
*/
|
||||
public function create(Booking $booking)
|
||||
{
|
||||
// Otorisasi: Pastikan user yang login adalah pemilik booking
|
||||
if ($booking->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!');
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,7 +37,7 @@ 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.');
|
||||
|
@ -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(), [
|
||||
// 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',
|
||||
'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.',
|
||||
'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();
|
||||
return redirect()->back()->withErrors($validator)->withInput();
|
||||
}
|
||||
|
||||
try {
|
||||
// Handle image upload
|
||||
$imagePath = $venue->image; // Keep current image by default
|
||||
// (Sisa dari kode ini tidak perlu diubah)
|
||||
$imagePath = $venue->image;
|
||||
if ($request->hasFile('image')) { /* ... logika upload ... */ }
|
||||
if ($request->hasFile('gallery_images')) { /* ... logika upload galeri ... */ }
|
||||
|
||||
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 = [
|
||||
$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)
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,34 +1,42 @@
|
|||
<?php
|
||||
// app/Http/Controllers/pages/VenueController.php
|
||||
|
||||
namespace App\Http\Controllers\pages;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Venue; // Pastikan model Venue di-import
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Venue;
|
||||
use Carbon\Carbon; // <-- Pastikan Carbon di-import
|
||||
|
||||
class VenueController extends Controller
|
||||
{
|
||||
public function venue($venueName) {
|
||||
// Mengambil venue berdasarkan nama yang diberikan
|
||||
$venue = Venue::where('name', 'like', '%' . ucfirst($venueName) . '%')->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'));
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class OperatingHour extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'venue_id',
|
||||
'day_of_week',
|
||||
'open_time',
|
||||
'close_time',
|
||||
'is_closed',
|
||||
];
|
||||
|
||||
public function venue()
|
||||
{
|
||||
return $this->belongsTo(Venue::class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Review extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'venue_id',
|
||||
'booking_id',
|
||||
'rating',
|
||||
'comment',
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function venue()
|
||||
{
|
||||
return $this->belongsTo(Venue::class);
|
||||
}
|
||||
|
||||
public function booking()
|
||||
{
|
||||
return $this->belongsTo(Booking::class);
|
||||
}
|
||||
}
|
|
@ -66,6 +66,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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class VenueImage extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['venue_id', 'path'];
|
||||
|
||||
public function venue()
|
||||
{
|
||||
return $this->belongsTo(Venue::class);
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateVenueImagesTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('venue_images', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('venue_id')->constrained()->onDelete('cascade');
|
||||
$table->string('path');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('venue_images');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateOperatingHoursTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('operating_hours', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddSocialMediaLinksToVenuesTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('venues', function (Blueprint $table) {
|
||||
$table->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']);
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('venues', function (Blueprint $table) {
|
||||
$table->string('latitude')->nullable()->after('address');
|
||||
$table->string('longitude')->nullable()->after('latitude');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('venues', function (Blueprint $table) {
|
||||
$table->dropColumn(['latitude', 'longitude']);
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('reviews', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('bookings', function (Blueprint $table) {
|
||||
// Tambahkan kolom 'order_id' yang hilang terlebih dahulu
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,84 @@
|
|||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="p-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-6">Scan QR Code Validasi</h1>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<div id="qr-reader" class="w-full"></div>
|
||||
<p id="qr-reader-status" class="text-center text-sm text-gray-500 mt-2">Arahkan kamera ke QR Code...</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">Hasil Scan</h3>
|
||||
<div id="qr-scan-result">
|
||||
<div class="text-center py-10">
|
||||
<p class="text-gray-400">Menunggu hasil scan...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Script untuk library QR Scanner --}}
|
||||
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const resultContainer = document.getElementById('qr-scan-result');
|
||||
const statusContainer = document.getElementById('qr-reader-status');
|
||||
|
||||
function onScanSuccess(decodedText, decodedResult) {
|
||||
// Hentikan scanner setelah berhasil
|
||||
html5QrcodeScanner.clear();
|
||||
statusContainer.innerHTML = `<span class="font-bold text-green-600">Scan Berhasil!</span> Memuat data...`;
|
||||
|
||||
// Kirim token ke server untuk divalidasi
|
||||
fetch(`/admin/bookings/validate/${decodedText}`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
// Jika booking tidak ditemukan (error 404) atau error lainnya
|
||||
throw new Error(`Booking tidak valid atau terjadi error (Status: ${response.status})`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const booking = data.data;
|
||||
// Tampilkan hasil yang valid
|
||||
resultContainer.innerHTML = `
|
||||
<div class="p-4 bg-green-50 border-l-4 border-green-500">
|
||||
<h4 class="text-xl font-bold text-green-800">✅ Valid!</h4>
|
||||
<div class="mt-4 space-y-2 text-sm">
|
||||
<p><strong class="w-24 inline-block">Nama User:</strong> ${booking.user_name}</p>
|
||||
<p><strong class="w-24 inline-block">Venue:</strong> ${booking.venue_name}</p>
|
||||
<p><strong class="w-24 inline-block">Meja:</strong> ${booking.table_name}</p>
|
||||
<p><strong class="w-24 inline-block">Waktu:</strong> ${booking.start_time} - ${booking.end_time}</p>
|
||||
<p><strong class="w-24 inline-block">Durasi:</strong> ${booking.duration}</p>
|
||||
<p><strong class="w-24 inline-block">Status:</strong> <span class="font-bold capitalize">${booking.status}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
// Jika ada pesan error dari server
|
||||
resultContainer.innerHTML = `<div class="p-4 bg-red-50 border-l-4 border-red-500"><h4 class="text-xl font-bold text-red-800">❌ Tidak Valid!</h4><p class="mt-2 text-red-700">${data.error || 'Data booking tidak bisa diambil.'}</p></div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error:", error);
|
||||
// Tampilkan error jika fetch gagal
|
||||
resultContainer.innerHTML = `<div class="p-4 bg-red-50 border-l-4 border-red-500"><h4 class="text-xl font-bold text-red-800">❌ Error!</h4><p class="mt-2 text-red-700">${error.message}</p></div>`;
|
||||
});
|
||||
}
|
||||
|
||||
// Inisialisasi scanner
|
||||
var html5QrcodeScanner = new Html5QrcodeScanner(
|
||||
"qr-reader", { fps: 10, qrbox: 250 }
|
||||
);
|
||||
|
||||
// Render scanner
|
||||
html5QrcodeScanner.render(onScanSuccess);
|
||||
});
|
||||
</script>
|
||||
@endsection
|
|
@ -2,14 +2,10 @@
|
|||
|
||||
@section('content')
|
||||
<div class="p-6">
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="{{ route('admin.venue.index') }}" class="text-gray-500 hover:text-gray-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg>
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Edit Venue</h1>
|
||||
|
@ -18,189 +14,192 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alert Messages -->
|
||||
@if(session('success'))
|
||||
<div class="mb-6 bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded-lg">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
<div class="mb-6 bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded-lg">{{ session('success') }}</div>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<div class="mb-6 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-lg">{{ session('error') }}</div>
|
||||
@endif
|
||||
@if ($errors->any())
|
||||
<div class="mb-6 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-lg">
|
||||
{{ session('error') }}
|
||||
<ul>
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Edit Form -->
|
||||
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200">
|
||||
<form action="{{ route('admin.venue.update') }}" method="POST" enctype="multipart/form-data" class="p-6">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Left Column -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-x-8 gap-y-6">
|
||||
<div class="space-y-6">
|
||||
<!-- Venue Name -->
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Nama Venue <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text" id="name" name="name" value="{{ old('name', $venue->name) }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('name') border-red-500 @enderror"
|
||||
placeholder="Masukkan nama venue">
|
||||
@error('name')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">Nama Venue <span class="text-red-500">*</span></label>
|
||||
<input type="text" id="name" name="name" value="{{ old('name', $venue->name) }}" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" required>
|
||||
</div>
|
||||
|
||||
<!-- Address -->
|
||||
<div>
|
||||
<label for="address" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Alamat <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea id="address" name="address" rows="3"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('address') border-red-500 @enderror"
|
||||
placeholder="Masukkan alamat lengkap venue">{{ old('address', $venue->address) }}</textarea>
|
||||
@error('address')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
<label for="address" class="block text-sm font-medium text-gray-700 mb-2">Alamat <span class="text-red-500">*</span></label>
|
||||
<textarea id="address" name="address" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" required>{{ old('address', $venue->address) }}</textarea>
|
||||
</div>
|
||||
|
||||
<!-- Phone -->
|
||||
<div>
|
||||
<label for="phone" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Nomor Telepon
|
||||
</label>
|
||||
<input type="text" id="phone" name="phone" value="{{ old('phone', $venue->phone) }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('phone') border-red-500 @enderror"
|
||||
placeholder="Contoh: 08123456789">
|
||||
@error('phone')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Operating Hours -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="open_time" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Jam Buka <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="time" id="open_time" name="open_time"
|
||||
value="{{ old('open_time', $venue->open_time ? \Carbon\Carbon::parse($venue->open_time)->format('H:i') : '') }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('open_time') border-red-500 @enderror">
|
||||
@error('open_time')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
<label for="latitude" class="block text-sm font-medium text-gray-700 mb-2">Latitude</label>
|
||||
<input type="text" id="latitude" name="latitude" value="{{ old('latitude', $venue->latitude) }}" class="w-full px-3 py-2 border border-gray-300 rounded-lg" placeholder="-8.123456">
|
||||
</div>
|
||||
<div>
|
||||
<label for="close_time" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Jam Tutup <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="time" id="close_time" name="close_time"
|
||||
value="{{ old('close_time', $venue->close_time ? \Carbon\Carbon::parse($venue->close_time)->format('H:i') : '') }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('close_time') border-red-500 @enderror">
|
||||
@error('close_time')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
<label for="longitude" class="block text-sm font-medium text-gray-700 mb-2">Longitude</label>
|
||||
<input type="text" id="longitude" name="longitude" value="{{ old('longitude', $venue->longitude) }}" class="w-full px-3 py-2 border border-gray-300 rounded-lg" placeholder="113.123456">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div class="space-y-6">
|
||||
<!-- Current Image -->
|
||||
<p class="text-xs text-gray-500 -mt-4">
|
||||
*Cara mendapatkan: Buka Google Maps, klik kanan pada lokasi venue, lalu klik pada angka koordinat untuk menyalinnya.
|
||||
</p>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Foto Venue Saat Ini
|
||||
</label>
|
||||
<div class="w-full h-48 bg-gray-100 rounded-lg overflow-hidden">
|
||||
@if($venue->image)
|
||||
<img id="current-image" src="{{ asset('storage/' . $venue->image) }}"
|
||||
alt="{{ $venue->name }}" class="w-full h-full object-cover">
|
||||
@else
|
||||
<div class="w-full h-full flex items-center justify-center text-gray-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<label for="phone" class="block text-sm font-medium text-gray-700 mb-2">Nomor Telepon</label>
|
||||
<input type="text" id="phone" name="phone" value="{{ old('phone', $venue->phone) }}" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<!-- New Image Upload -->
|
||||
<div>
|
||||
<label for="image" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Upload Foto Baru (Opsional)
|
||||
</label>
|
||||
<input type="file" id="image" name="image" accept="image/*" onchange="previewImage(this)"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('image') border-red-500 @enderror">
|
||||
@error('image')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="text-xs text-gray-500 mt-1">Format: JPEG, PNG, JPG, GIF. Maksimal 2MB</p>
|
||||
|
||||
<!-- Image Preview -->
|
||||
<div id="image-preview" class="mt-4 hidden">
|
||||
<p class="text-sm font-medium text-gray-700 mb-2">Preview Foto Baru:</p>
|
||||
<div class="w-full h-48 bg-gray-100 rounded-lg overflow-hidden">
|
||||
<img id="preview-img" src="" alt="Preview" class="w-full h-full object-cover">
|
||||
<div class="pt-4 border-t">
|
||||
<h3 class="text-base font-medium text-gray-800 mb-3">Link Media Sosial</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="link_instagram" class="block text-sm font-medium text-gray-700 mb-2">Instagram</label>
|
||||
<input type="url" id="link_instagram" name="link_instagram" value="{{ old('link_instagram', $venue->link_instagram) }}" class="w-full px-3 py-2 border border-gray-300 rounded-lg" placeholder="https://instagram.com/venueanda">
|
||||
</div>
|
||||
<div>
|
||||
<label for="link_tiktok" class="block text-sm font-medium text-gray-700 mb-2">TikTok</label>
|
||||
<input type="url" id="link_tiktok" name="link_tiktok" value="{{ old('link_tiktok', $venue->link_tiktok) }}" class="w-full px-3 py-2 border border-gray-300 rounded-lg" placeholder="https://tiktok.com/@venueanda">
|
||||
</div>
|
||||
<div>
|
||||
<label for="link_facebook" class="block text-sm font-medium text-gray-700 mb-2">Facebook</label>
|
||||
<input type="url" id="link_facebook" name="link_facebook" value="{{ old('link_facebook', $venue->link_facebook) }}" class="w-full px-3 py-2 border border-gray-300 rounded-lg" placeholder="https://facebook.com/venueanda">
|
||||
</div>
|
||||
<div>
|
||||
<label for="link_x" class="block text-sm font-medium text-gray-700 mb-2">X (Twitter)</label>
|
||||
<input type="url" id="link_x" name="link_x" value="{{ old('link_x', $venue->link_x) }}" class="w-full px-3 py-2 border border-gray-300 rounded-lg" placeholder="https://x.com/venueanda">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 border border-gray-200 rounded-lg">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Jam Operasional Harian</h3>
|
||||
<div class="space-y-4">
|
||||
@php
|
||||
$days = [1 => 'Senin', 2 => 'Selasa', 3 => 'Rabu', 4 => 'Kamis', 5 => 'Jumat', 6 => 'Sabtu', 7 => 'Minggu'];
|
||||
// Siapkan data jam operasional yang ada agar mudah diakses
|
||||
$hoursByDay = $venue->operatingHours->keyBy('day_of_week');
|
||||
@endphp
|
||||
|
||||
@foreach($days as $dayNumber => $dayName)
|
||||
@php
|
||||
$hour = $hoursByDay->get($dayNumber);
|
||||
@endphp
|
||||
<div x-data="{ isClosed: {{ old('hours.'.$dayNumber.'.is_closed', $hour->is_closed ?? true) ? 'true' : 'false' }} }" class="grid grid-cols-1 sm:grid-cols-4 gap-3 items-center">
|
||||
<div class="sm:col-span-1">
|
||||
<label class="font-medium text-gray-700">{{ $dayName }}</label>
|
||||
</div>
|
||||
<div class="sm:col-span-3 flex items-center space-x-4">
|
||||
<input type="checkbox" x-model="isClosed" name="hours[{{ $dayNumber }}][is_closed]" class="h-4 w-4 rounded">
|
||||
<label class="text-sm text-gray-600">Tutup</label>
|
||||
|
||||
<div class="flex items-center space-x-2" x-show="!isClosed">
|
||||
<input type="time" name="hours[{{ $dayNumber }}][open_time]" value="{{ old('hours.'.$dayNumber.'.open_time', $hour && $hour->open_time ? \Carbon\Carbon::parse($hour->open_time)->format('H:i') : '09:00') }}" :disabled="isClosed" class="w-full px-2 py-1 border border-gray-300 rounded-md text-sm">
|
||||
<span>-</span>
|
||||
<input type="time" name="hours[{{ $dayNumber }}][close_time]" value="{{ old('hours.'.$dayNumber.'.close_time', $hour && $hour->close_time ? \Carbon\Carbon::parse($hour->close_time)->format('H:i') : '22:00') }}" :disabled="isClosed" class="w-full px-2 py-1 border border-gray-300 rounded-md text-sm">
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="hours[{{ $dayNumber }}][day_of_week]" value="{{ $dayNumber }}">
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">Deskripsi Venue</label>
|
||||
<textarea id="description" name="description" rows="5" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500">{{ old('description', $venue->description) }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Foto Sampul (Utama)</label>
|
||||
<img id="current-image" src="{{ $venue->image ? asset('storage/' . $venue->image) : 'https://via.placeholder.com/300' }}" alt="{{ $venue->name }}" class="w-full h-48 object-cover rounded-lg bg-gray-100">
|
||||
<input type="file" id="image" name="image" accept="image/*" class="w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 mt-2">
|
||||
</div>
|
||||
|
||||
<div class="pt-6 border-t">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">Galeri Venue</h3>
|
||||
<div id="gallery-container" class="grid grid-cols-3 sm:grid-cols-4 gap-4">
|
||||
@foreach($venue->images as $image)
|
||||
<div id="gallery-image-{{ $image->id }}" class="relative group">
|
||||
<img src="{{ asset('storage/' . $image->path) }}" alt="Gallery Image" class="w-full h-24 object-cover rounded-lg">
|
||||
<button type="button" onclick="deleteImage({{ $image->id }}, event)"
|
||||
class="absolute top-1 right-1 bg-red-600 text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity focus:outline-none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<p id="no-gallery-text" class="text-sm text-gray-500 mt-2 {{ $venue->images->isNotEmpty() ? 'hidden' : '' }}">Belum ada gambar di galeri.</p>
|
||||
|
||||
<div class="mt-4">
|
||||
<label for="gallery_images" class="block text-sm font-medium text-gray-700 mb-2">Tambah Gambar ke Galeri</label>
|
||||
<input type="file" id="gallery_images" name="gallery_images[]" accept="image/*" multiple class="w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-green-50 file:text-green-700 hover:file:bg-green-100">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="mt-6">
|
||||
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Deskripsi Venue
|
||||
</label>
|
||||
<textarea id="description" name="description" rows="4"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('description') border-red-500 @enderror"
|
||||
placeholder="Masukkan deskripsi venue (fasilitas, suasana, dll)">{{ old('description', $venue->description) }}</textarea>
|
||||
@error('description')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex items-center justify-end space-x-4 mt-8 pt-6 border-t border-gray-200">
|
||||
<a href="{{ route('admin.venue.index') }}"
|
||||
class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg font-medium transition-colors">
|
||||
Batal
|
||||
</a>
|
||||
<button type="submit"
|
||||
class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Simpan Perubahan
|
||||
</button>
|
||||
<a href="{{ route('admin.venue.index') }}" class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg font-medium transition-colors">Batal</a>
|
||||
<button type="submit" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors">Simpan Perubahan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function previewImage(input) {
|
||||
const preview = document.getElementById('image-preview');
|
||||
const previewImg = document.getElementById('preview-img');
|
||||
function deleteImage(imageId, event) {
|
||||
event.preventDefault();
|
||||
if (!confirm('Apakah Anda yakin ingin menghapus gambar ini?')) return;
|
||||
|
||||
if (input.files && input.files[0]) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function (e) {
|
||||
previewImg.src = e.target.result;
|
||||
preview.classList.remove('hidden');
|
||||
}
|
||||
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
} else {
|
||||
preview.classList.add('hidden');
|
||||
fetch(`/admin/venue-image/${imageId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json' // Memberitahu server kita ingin respons JSON
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
// Cek jika respons dari server tidak OK (misal: error 403, 404, 500)
|
||||
if (!response.ok) {
|
||||
// Ubah respons menjadi error agar bisa ditangkap di .catch()
|
||||
return response.json().then(err => { throw err; });
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const imageElement = document.getElementById(`gallery-image-${imageId}`);
|
||||
imageElement.remove();
|
||||
|
||||
const galleryContainer = document.getElementById('gallery-container');
|
||||
if (galleryContainer.children.length === 0) {
|
||||
document.getElementById('no-gallery-text').classList.remove('hidden');
|
||||
}
|
||||
} else {
|
||||
// Menampilkan pesan error dari server jika success = false
|
||||
alert(data.message || 'Gagal menghapus gambar.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// Menangkap error dari server (seperti 403, 500) atau error koneksi
|
||||
console.error('Error:', error);
|
||||
// Menampilkan pesan error yang lebih spesifik jika ada, jika tidak tampilkan pesan default
|
||||
alert('Terjadi kesalahan: ' + (error.message || 'Periksa koneksi Anda.'));
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@endsection
|
|
@ -166,6 +166,14 @@ class="nav-item flex items-center px-3 py-2.5 rounded-lg {{ request()->routeIs('
|
|||
</svg>
|
||||
<span x-show="sidebarOpen">Kelola Booking</span>
|
||||
</a>
|
||||
<a href="{{ route('admin.bookings.scanner') }}"
|
||||
class="nav-item flex items-center px-3 py-2.5 rounded-lg {{ request()->routeIs('admin.bookings.scanner') ? 'active' : '' }}">
|
||||
{{-- Ikon Baru untuk QR Code --}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4h6v6H4V4zM4 14h6v6H4v-6zM14 4h6v6h-6V4zM14 14h6v6h-6v-6z" />
|
||||
</svg>
|
||||
<span x-show="sidebarOpen">Scan QR</span>
|
||||
</a>
|
||||
<a href="{{ route('admin.revenues.index') }}"
|
||||
class="nav-item flex items-center px-3 py-2.5 rounded-lg {{ request()->routeIs('admin.revenues.*') ? 'active' : '' }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24"
|
||||
|
|
|
@ -1,4 +1,45 @@
|
|||
@extends('layouts.main') @section('content') <div class="min-h-96 mx-4 md:w-3/4 md:mx-auto py-8">
|
||||
@extends('layouts.main')
|
||||
@section('content')
|
||||
<div x-data="{
|
||||
showModal: false,
|
||||
qrCodeUrl: '',
|
||||
venueName: '',
|
||||
bookingDate: '',
|
||||
bookingTime: ''
|
||||
}"
|
||||
x-on:keydown.escape.window="showModal = false; document.body.style.overflow = 'auto';"
|
||||
class="min-h-screen bg-gray-100">
|
||||
|
||||
{{-- ======================================================= --}}
|
||||
{{-- START: MODAL UNTUK MENAMPILKAN QR CODE --}}
|
||||
{{-- ======================================================= --}}
|
||||
<div x-show="showModal"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-75 p-4"
|
||||
x-cloak>
|
||||
|
||||
<div @click.away="showModal = false; document.body.style.overflow = 'auto';" class="bg-white rounded-lg shadow-xl p-6 max-w-sm w-full text-center">
|
||||
<h3 class="text-lg font-bold text-gray-800" x-text="venueName"></h3>
|
||||
<p class="text-sm text-gray-500 mb-4">
|
||||
<span x-text="bookingDate"></span> - <span x-text="bookingTime"></span>
|
||||
</p>
|
||||
|
||||
<div class="p-4 border rounded-lg bg-white">
|
||||
<img :src="qrCodeUrl" alt="Booking QR Code" class="w-full h-auto">
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500 mt-4">Tunjukkan QR Code ini kepada kasir saat Anda tiba di venue.</p>
|
||||
|
||||
<div class="mt-6 flex space-x-4">
|
||||
<button @click="showModal = false; document.body.style.overflow = 'auto';" class="w-full px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 font-medium">Tutup</button>
|
||||
<a :href="qrCodeUrl" download="tiket-carimeja.svg" class="w-full flex items-center justify-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium">Download</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-h-96 mx-4 md:w-3/4 md:mx-auto py-8">
|
||||
<h1 class="text-2xl font-bold mb-6">Riwayat Bookingg</h1>
|
||||
|
||||
@if($bookings->isEmpty())
|
||||
|
@ -64,17 +105,52 @@ class="px-3 py-1 rounded-full text-sm {{ $booking->start_time > now() ? 'bg-gree
|
|||
@endif
|
||||
</div>
|
||||
|
||||
@if($booking->start_time > now() && $booking->status == 'paid')
|
||||
<div class="mt-4 flex justify-end space-x-4">
|
||||
<a href="{{ route('venue', $booking->table->venue->name) }}"
|
||||
class="text-blue-500 hover:underline">Lihat Venue</a>
|
||||
<div class="border-t mt-4 pt-4 flex justify-end items-center space-x-4">
|
||||
<a href="{{ route('venue', $booking->table->venue->name) }}" class="text-sm text-blue-600 hover:underline font-medium">
|
||||
Lihat Venue
|
||||
</a>
|
||||
|
||||
<a href="{{ route('booking.reschedule.form', $booking->id) }}"
|
||||
class="text-orange-500 hover:underline">
|
||||
Reschedule
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
{{-- Hanya tampilkan tombol jika status lunas --}}
|
||||
@if($booking->status == 'paid')
|
||||
|
||||
{{-- Opsi untuk booking yang AKAN DATANG --}}
|
||||
@if(now()->lt($booking->start_time))
|
||||
{{-- Tombol Reschedule, dengan asumsi ada kolom reschedule_count di tabel bookings --}}
|
||||
@if(isset($booking->reschedule_count) && $booking->reschedule_count < 1)
|
||||
<a href="{{ route('booking.reschedule.form', $booking) }}" class="text-sm text-orange-600 hover:underline font-medium">
|
||||
Reschedule
|
||||
</a>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
{{-- Opsi untuk booking yang SUDAH SELESAI --}}
|
||||
@if(now()->gt($booking->end_time))
|
||||
@if(!$booking->review)
|
||||
<a href="{{ route('reviews.create', $booking) }}" class="text-sm text-green-600 hover:underline font-medium">
|
||||
Beri Ulasan
|
||||
</a>
|
||||
@else
|
||||
<span class="text-sm text-gray-500">Ulasan diberikan</span>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
{{-- Tombol QR selalu muncul untuk booking lunas yang punya token --}}
|
||||
@if($booking->validation_token)
|
||||
<button @click="
|
||||
showModal = true;
|
||||
qrCodeUrl = '{{ route('booking.qrcode', $booking) }}';
|
||||
venueName = '{{ addslashes($booking->table->venue->name) }}';
|
||||
bookingDate = '{{ $booking->start_time->format("d M Y") }}';
|
||||
bookingTime = '{{ $booking->start_time->format("H:i") }} - {{ $booking->end_time->format("H:i") }}';
|
||||
document.body.style.overflow = 'hidden';
|
||||
"
|
||||
class="bg-blue-600 text-white text-sm font-bold px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors">
|
||||
Lihat Tiket (QR)
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,75 @@
|
|||
@extends('layouts.main')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
/* CSS untuk rating bintang */
|
||||
.rating {
|
||||
display: inline-block;
|
||||
direction: rtl; /* Bintang dari kanan ke kiri */
|
||||
}
|
||||
.rating input {
|
||||
display: none;
|
||||
}
|
||||
.rating label {
|
||||
font-size: 2.5rem;
|
||||
color: #ddd;
|
||||
cursor: pointer;
|
||||
padding: 0 0.1em;
|
||||
}
|
||||
.rating input:checked ~ label,
|
||||
.rating label:hover,
|
||||
.rating label:hover ~ label {
|
||||
color: #f5b301;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="min-h-screen container mx-auto px-4 py-8">
|
||||
<div class="max-w-2xl mx-auto bg-white rounded-lg shadow-md p-8">
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-2">Beri Ulasan Anda</h1>
|
||||
<p class="text-gray-600 mb-6">
|
||||
Untuk booking di **{{ $booking->table->venue->name }}** pada tanggal **{{ $booking->start_time->format('d M Y') }}**.
|
||||
</p>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-lg">
|
||||
<ul>
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('reviews.store') }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="booking_id" value="{{ $booking->id }}">
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Rating Anda *</label>
|
||||
<div class="rating">
|
||||
<input type="radio" id="star5" name="rating" value="5" required /><label for="star5">★</label>
|
||||
<input type="radio" id="star4" name="rating" value="4" /><label for="star4">★</label>
|
||||
<input type="radio" id="star3" name="rating" value="3" /><label for="star3">★</label>
|
||||
<input type="radio" id="star2" name="rating" value="2" /><label for="star2">★</label>
|
||||
<input type="radio" id="star1" name="rating" value="1" /><label for="star1">★</label>
|
||||
</div>
|
||||
@error('rating') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="comment" class="block text-sm font-medium text-gray-700 mb-2">Ulasan Anda *</label>
|
||||
<textarea name="comment" id="comment" rows="5"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Bagaimana pengalaman Anda di venue ini? Ceritakan di sini...">{{ old('comment') }}</textarea>
|
||||
@error('comment') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors">
|
||||
Kirim Ulasan
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
|
@ -16,6 +16,7 @@
|
|||
use App\Http\Controllers\superadmin\VenueManagementController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Http\Controllers\ReviewController;
|
||||
|
||||
// Authentication Routes (dengan verifikasi email aktif)
|
||||
Auth::routes(['verify' => true]);
|
||||
|
@ -68,10 +69,15 @@
|
|||
// Any sensitive operations that should still require password confirmation can go here
|
||||
});
|
||||
|
||||
Route::post('/reviews', [ReviewController::class, 'store'])->name('reviews.store');
|
||||
Route::get('/reviews/create/{booking}', [ReviewController::class, 'create'])->name('reviews.create');
|
||||
|
||||
Route::get('/booking/{booking}/qrcode', [BookingController::class, 'showQrCode'])->name('booking.qrcode');
|
||||
|
||||
});
|
||||
|
||||
// Admin routes (admin tetap perlu verified untuk keamanan)
|
||||
Route::middleware(['auth', 'verified', 'is_admin'])->prefix('admin')->group(function () {
|
||||
Route::middleware(['auth', 'verified', 'is_admin'])->prefix('admin')->group(function () {
|
||||
Route::get('/', [AdminController::class, 'index'])->name('admin.dashboard');
|
||||
|
||||
// Admin Profile Routes
|
||||
|
@ -81,12 +87,14 @@
|
|||
|
||||
// Booking management routes
|
||||
Route::get('/bookings', [BookingsController::class, 'index'])->name('admin.bookings.index');
|
||||
Route::get('/bookings/export', [BookingsController::class, 'export'])->name('admin.bookings.export');
|
||||
Route::get('/bookings/{id}', [BookingsController::class, 'show'])->name('admin.bookings.show');
|
||||
Route::get('/bookings/{id}/edit', [BookingsController::class, 'edit'])->name('admin.bookings.edit');
|
||||
Route::put('/bookings/{id}', [BookingsController::class, 'update'])->name('admin.bookings.update');
|
||||
Route::patch('/bookings/{id}/complete', [BookingsController::class, 'complete'])->name('admin.bookings.complete');
|
||||
Route::patch('/bookings/{id}/cancel', [BookingsController::class, 'cancel'])->name('admin.bookings.cancel');
|
||||
Route::get('/bookings/export', [BookingsController::class, 'export'])->name('admin.bookings.export');
|
||||
Route::get('/bookings/scan', [BookingsController::class, 'showScanner'])->name('admin.bookings.scanner'); // <-- Pindahkan ke atas
|
||||
Route::get('/bookings/validate/{token}', [BookingsController::class, 'validateBooking'])->name('admin.bookings.validate'); // <-- Pindahkan ke atas
|
||||
Route::get('/bookings/{id}', [BookingsController::class, 'show'])->name('admin.bookings.show'); // <-- Route dengan {id} sekarang di bawah
|
||||
Route::get('/bookings/{id}/edit', [BookingsController::class, 'edit'])->name('admin.bookings.edit');
|
||||
Route::put('/bookings/{id}', [BookingsController::class, 'update'])->name('admin.bookings.update');
|
||||
Route::patch('/bookings/{id}/complete', [BookingsController::class, 'complete'])->name('admin.bookings.complete');
|
||||
Route::patch('/bookings/{id}/cancel', [BookingsController::class, 'cancel'])->name('admin.bookings.cancel');
|
||||
|
||||
// Table management routes
|
||||
Route::get('/tables', [TableController::class, 'index'])->name('admin.tables.index');
|
||||
|
@ -101,11 +109,14 @@
|
|||
Route::get('/venue/edit', [AdminVenueController::class, 'edit'])->name('admin.venue.edit');
|
||||
Route::put('/venue/update', [AdminVenueController::class, 'update'])->name('admin.venue.update');
|
||||
Route::post('/venue/toggle-status', [AdminVenueController::class, 'toggleStatus'])->name('admin.venue.toggle-status');
|
||||
Route::delete('/venue-image/{image}', [AdminVenueController::class, 'destroyImage'])->name('admin.venue.image.destroy');
|
||||
|
||||
// Revenue management routes
|
||||
Route::get('/revenues', [RevenueController::class, 'index'])->name('admin.revenues.index');
|
||||
Route::get('/revenues/detail/{tableId}', [RevenueController::class, 'detail'])->name('admin.revenues.detail');
|
||||
Route::get('/revenues/export', [RevenueController::class, 'export'])->name('admin.revenues.export');
|
||||
|
||||
|
||||
});
|
||||
|
||||
// Superadmin routes
|
||||
|
|
Loading…
Reference in New Issue