Checkpoint sebelum revisi
This commit is contained in:
parent
f5cf7a4f8a
commit
79307fb0d6
|
@ -23,81 +23,72 @@ public function __construct(MidtransService $midtransService)
|
|||
}
|
||||
|
||||
// Tambahkan method baru untuk booking langsung oleh admin
|
||||
public function adminDirectBooking($request) {
|
||||
// Ganti seluruh fungsi adminDirectBooking dengan ini
|
||||
public function adminDirectBooking($request) {
|
||||
try {
|
||||
// Handle both Request object dan Collection
|
||||
$data = $request instanceof \Illuminate\Http\Request ? $request->all() : $request->toArray();
|
||||
|
||||
// Validasi manual karena bisa dari collection
|
||||
if (!isset($data['table_id']) || !isset($data['start_time']) || !isset($data['end_time'])) {
|
||||
return response()->json([
|
||||
'message' => 'Missing required fields'
|
||||
], 400);
|
||||
return response()->json(['message' => 'Missing required fields'], 400);
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
$table = Table::with('venue')->findOrFail($data['table_id']);
|
||||
$venue = $table->venue;
|
||||
|
||||
// Validasi bahwa user adalah admin dan mengelola venue dari meja tersebut
|
||||
$table = Table::findOrFail($data['table_id']);
|
||||
// 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);
|
||||
return response()->json(['message' => 'Unauthorized action'], 403);
|
||||
}
|
||||
|
||||
// Parse start_time dan end_time yang sudah dalam format datetime string
|
||||
$startDateTime = Carbon::parse($data['start_time']);
|
||||
$endDateTime = Carbon::parse($data['end_time']);
|
||||
|
||||
// Validasi jam operasional venue (opsional, karena sudah divalidasi di createPaymentIntent)
|
||||
$venue = $table->venue;
|
||||
$venueOpenTime = Carbon::parse($venue->open_time);
|
||||
$venueCloseTime = Carbon::parse($venue->close_time);
|
||||
|
||||
$startTimeOnly = $startDateTime->format('H:i');
|
||||
$endTimeOnly = $endDateTime->format('H:i');
|
||||
|
||||
if ($startTimeOnly < $venueOpenTime->format('H:i') || $endTimeOnly > $venueCloseTime->format('H:i')) {
|
||||
return response()->json([
|
||||
'message' => 'Waktu booking di luar jam operasional venue'
|
||||
], 400);
|
||||
// --- 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();
|
||||
}
|
||||
|
||||
// Cek konflik booking
|
||||
$operationalDayEnd = $operationalDayStart->copy()->setTimeFromTimeString($venue->close_time);
|
||||
if ($venue->is_overnight) {
|
||||
$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 ---
|
||||
|
||||
// --- PERBAIKAN LOGIKA KONFLIK DIMULAI DI SINI ---
|
||||
// Kita hapus ->whereDate() dan langsung cek bentrokan waktu.
|
||||
$conflict = Booking::where('table_id', $data['table_id'])
|
||||
->whereDate('start_time', $startDateTime->format('Y-m-d'))
|
||||
->where(function($query) use ($startDateTime, $endDateTime) {
|
||||
$query->where(function($q) use ($startDateTime, $endDateTime) {
|
||||
// Case 1: Booking baru mulai di tengah booking yang ada
|
||||
$q->where('start_time', '<=', $startDateTime)
|
||||
->where('end_time', '>', $startDateTime);
|
||||
})->orWhere(function($q) use ($startDateTime, $endDateTime) {
|
||||
// Case 2: Booking baru berakhir di tengah booking yang ada
|
||||
$q->where('start_time', '<', $endDateTime)
|
||||
->where('end_time', '>=', $endDateTime);
|
||||
})->orWhere(function($q) use ($startDateTime, $endDateTime) {
|
||||
// Case 3: Booking baru mencakup seluruh booking yang ada
|
||||
$q->where('start_time', '>=', $startDateTime)
|
||||
->where('end_time', '<=', $endDateTime);
|
||||
});
|
||||
})
|
||||
->whereIn('status', ['paid', 'pending'])
|
||||
->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);
|
||||
})
|
||||
->exists();
|
||||
|
||||
if ($conflict) {
|
||||
return response()->json([
|
||||
'message' => 'Meja sudah dibooking di jam tersebut'
|
||||
], 409);
|
||||
return response()->json(['message' => 'Meja sudah dibooking di jam tersebut'], 409);
|
||||
}
|
||||
// --- AKHIR DARI PERBAIKAN LOGIKA KONFLIK ---
|
||||
|
||||
// Hitung total biaya dan durasi
|
||||
$duration = $endDateTime->diffInHours($startDateTime);
|
||||
$totalAmount = $duration * $table->price_per_hour;
|
||||
|
||||
// Generate order ID unik untuk admin
|
||||
$adminOrderId = 'ADMIN-' . $user->id . '-' . time();
|
||||
|
||||
// Buat booking langsung dengan status paid
|
||||
$booking = Booking::create([
|
||||
'table_id' => $data['table_id'],
|
||||
'user_id' => $user->id,
|
||||
|
@ -130,10 +121,7 @@ public function adminDirectBooking($request) {
|
|||
'request_data' => $request instanceof \Illuminate\Http\Request ? $request->all() : $request->toArray()
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Gagal membuat booking: ' . $e->getMessage()
|
||||
], 500);
|
||||
return response()->json(['success' => false, 'message' => 'Gagal membuat booking: ' . $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -142,44 +130,51 @@ public function createPaymentIntent(Request $request) {
|
|||
try {
|
||||
$request->validate([
|
||||
'table_id' => 'required|exists:tables,id',
|
||||
'start_time' => 'required', // Ubah dari date menjadi string untuk format H:i
|
||||
'duration' => 'required|integer|min:1|max:12', // Validasi durasi
|
||||
'booking_date' => 'required|date_format:Y-m-d', // Validasi tanggal booking
|
||||
'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;
|
||||
|
||||
// Buat datetime lengkap dari booking_date dan start_time
|
||||
$bookingDate = $request->booking_date;
|
||||
$startTime = $request->start_time; // Format H:i (contoh: "14:00")
|
||||
$startTimeString = $request->start_time;
|
||||
$duration = (int) $request->duration;
|
||||
|
||||
// Gabungkan tanggal dan waktu untuk membuat datetime lengkap
|
||||
$startDateTime = Carbon::createFromFormat('Y-m-d H:i', $bookingDate . ' ' . $startTime, 'Asia/Jakarta');
|
||||
// 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);
|
||||
|
||||
// Validasi waktu booking dalam jam operasional venue
|
||||
$venueOpenTime = Carbon::createFromFormat('H:i:s', $table->venue->open_time)->format('H:i');
|
||||
$venueCloseTime = Carbon::createFromFormat('H:i:s', $table->venue->close_time)->format('H:i');
|
||||
$venueCloseDateTime = Carbon::createFromFormat('Y-m-d H:i', $bookingDate . ' ' . $venueCloseTime, 'Asia/Jakarta');
|
||||
|
||||
if ($startTime < $venueOpenTime || $startTime >= $venueCloseTime) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Waktu booking di luar jam operasional venue'
|
||||
], 422);
|
||||
// 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();
|
||||
}
|
||||
|
||||
// Validasi bahwa end time tidak melebihi jam tutup venue
|
||||
if ($endDateTime->gt($venueCloseDateTime)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Durasi booking melebihi jam tutup venue'
|
||||
], 422);
|
||||
$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(),
|
||||
]);
|
||||
return response()->json(['success' => false, 'message' => 'Durasi booking di luar jam operasional venue.'], 422);
|
||||
}
|
||||
// --- AKHIR DARI BLOK VALIDASI ---
|
||||
|
||||
// Cek untuk admin direct booking
|
||||
// 3. Cek untuk admin direct booking (tidak berubah)
|
||||
if ($user->role === 'admin' && $user->venue_id === $table->venue_id) {
|
||||
return $this->adminDirectBooking(collect([
|
||||
'table_id' => $request->table_id,
|
||||
|
@ -188,74 +183,34 @@ public function createPaymentIntent(Request $request) {
|
|||
]));
|
||||
}
|
||||
|
||||
// Cek konflik booking dengan format datetime lengkap
|
||||
// 4. Cek konflik booking (tidak berubah)
|
||||
$conflict = Booking::where('table_id', $request->table_id)
|
||||
->where(function($query) use ($startDateTime, $endDateTime) {
|
||||
$query->where(function($q) use ($startDateTime, $endDateTime) {
|
||||
$q->where('start_time', '<', $endDateTime)
|
||||
->where('end_time', '>', $startDateTime);
|
||||
});
|
||||
})
|
||||
->where('status', 'paid')
|
||||
->exists();
|
||||
|
||||
->where('status', 'paid')
|
||||
->where(function($query) use ($startDateTime, $endDateTime) {
|
||||
$query->where('start_time', '<', $endDateTime)
|
||||
->where('end_time', '>', $startDateTime);
|
||||
})
|
||||
->exists();
|
||||
if ($conflict) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Meja sudah dibooking di jam tersebut'
|
||||
], 409);
|
||||
return response()->json(['success' => false, 'message' => 'Meja sudah dibooking di jam tersebut'], 409);
|
||||
}
|
||||
|
||||
// Hitung total biaya
|
||||
// 5. Proses ke Midtrans (tidak berubah)
|
||||
$totalAmount = $duration * $table->price_per_hour;
|
||||
|
||||
// Simpan data booking sementara di session
|
||||
Session::put('temp_booking', [
|
||||
'table_id' => $request->table_id,
|
||||
'user_id' => Auth::id(),
|
||||
'start_time' => $startDateTime->toDateTimeString(),
|
||||
'end_time' => $endDateTime->toDateTimeString(),
|
||||
'total_amount' => $totalAmount,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
// Generate unique order ID
|
||||
$tempOrderId = 'TEMP-' . Auth::id() . '-' . time();
|
||||
Session::put('temp_order_id', $tempOrderId);
|
||||
|
||||
// Simpan booking sementara ke database
|
||||
PendingBooking::updateOrCreate(
|
||||
[
|
||||
'user_id' => Auth::id(),
|
||||
'table_id' => $request->table_id,
|
||||
'start_time' => $startDateTime->toDateTimeString()
|
||||
],
|
||||
[
|
||||
'end_time' => $endDateTime->toDateTimeString(),
|
||||
'total_amount' => $totalAmount,
|
||||
'order_id' => $tempOrderId,
|
||||
'expired_at' => now()->addHours(24),
|
||||
]
|
||||
['user_id' => Auth::id(), 'table_id' => $request->table_id, 'start_time' => $startDateTime->toDateTimeString()],
|
||||
['end_time' => $endDateTime->toDateTimeString(), 'total_amount' => $totalAmount, 'order_id' => $tempOrderId, 'expired_at' => now()->addHours(24) ]
|
||||
);
|
||||
|
||||
// Dapatkan snap token dari Midtrans
|
||||
$snapToken = $this->midtransService->createTemporaryTransaction($table, $totalAmount, $tempOrderId, Auth::user());
|
||||
|
||||
if (!$snapToken) {
|
||||
throw new \Exception('Failed to get snap token from Midtrans');
|
||||
}
|
||||
|
||||
\Log::info('Payment intent created successfully:', [
|
||||
'order_id' => $tempOrderId,
|
||||
'snap_token' => $snapToken,
|
||||
'start_time' => $startDateTime->toDateTimeString(),
|
||||
'end_time' => $endDateTime->toDateTimeString()
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Payment intent created, proceed to payment',
|
||||
'total_amount' => $totalAmount,
|
||||
'snap_token' => $snapToken,
|
||||
'order_id' => $tempOrderId
|
||||
]);
|
||||
|
@ -360,18 +315,37 @@ public function getBookedSchedules(Request $request) {
|
|||
'date' => 'required|date',
|
||||
]);
|
||||
|
||||
$table = Table::with('venue')->findOrFail($request->table_id);
|
||||
$venue = $table->venue;
|
||||
$requestDate = Carbon::parse($request->date);
|
||||
|
||||
// Only get bookings with paid status
|
||||
$bookings = Booking::where('table_id', $request->table_id)
|
||||
->whereDate('start_time', $request->date)
|
||||
->where('status', 'paid') // Only include paid bookings
|
||||
->select('start_time', 'end_time')
|
||||
->get()
|
||||
->map(function ($booking) {
|
||||
return [
|
||||
'start' => Carbon::parse($booking->start_time)->format('H:i'),
|
||||
'end' => Carbon::parse($booking->end_time)->format('H:i')
|
||||
];
|
||||
});
|
||||
$query = Booking::where('table_id', $request->table_id)
|
||||
->where('status', 'paid');
|
||||
|
||||
if ($venue->is_overnight) {
|
||||
// Jika overnight, ambil booking dari jam buka di hari H
|
||||
// sampai jam tutup di hari H+1
|
||||
$startOperationalDay = $requestDate->copy()->setTimeFromTimeString($venue->open_time);
|
||||
$endOperationalDay = $requestDate->copy()->addDay()->setTimeFromTimeString($venue->close_time);
|
||||
|
||||
$query->whereBetween('start_time', [$startOperationalDay, $endOperationalDay]);
|
||||
|
||||
} else {
|
||||
// Jika tidak overnight, ambil booking hanya di hari H
|
||||
$query->whereDate('start_time', $requestDate);
|
||||
}
|
||||
|
||||
$bookings = $query->select('start_time', 'end_time')
|
||||
->get()
|
||||
->map(function ($booking) {
|
||||
return [
|
||||
// Format H:i tetap sama, karena frontend hanya butuh jamnya
|
||||
'start' => Carbon::parse($booking->start_time)->format('H:i'),
|
||||
'end' => Carbon::parse($booking->end_time)->format('H:i')
|
||||
];
|
||||
});
|
||||
|
||||
|
||||
return response()->json($bookings);
|
||||
}
|
||||
|
@ -556,40 +530,43 @@ public function deletePendingBooking($id)
|
|||
}
|
||||
}
|
||||
|
||||
public function showReschedule($id)
|
||||
{
|
||||
$booking = Booking::with(['table.venue', 'table.venue.tables'])->findOrFail($id);
|
||||
// GANTI SELURUH FUNGSI showReschedule DENGAN YANG INI
|
||||
|
||||
// Check if user owns this booking
|
||||
if ($booking->user_id !== auth()->id()) {
|
||||
return redirect()->route('booking.history')->with('error', 'Anda tidak memiliki akses ke booking ini.');
|
||||
}
|
||||
public function showReschedule($id)
|
||||
{
|
||||
$booking = Booking::with(['table.venue', 'table.venue.tables'])->findOrFail($id);
|
||||
|
||||
// Check if booking is upcoming
|
||||
if ($booking->start_time <= now() || $booking->status !== 'paid') {
|
||||
return redirect()->route('booking.history')->with('error', 'Booking ini tidak dapat di-reschedule.');
|
||||
}
|
||||
|
||||
// Check if booking has reached reschedule limit
|
||||
if ($booking->reschedule_count >= 1) {
|
||||
return redirect()->route('booking.history')->with('error', 'Booking ini sudah pernah di-reschedule sebelumnya dan tidak dapat di-reschedule lagi.');
|
||||
}
|
||||
|
||||
// Check if it's within the time limit (at least 1 hour before start)
|
||||
$rescheduleDeadline = Carbon::parse($booking->start_time)->subHour();
|
||||
if (now() > $rescheduleDeadline) {
|
||||
return redirect()->route('booking.history')->with('error', 'Batas waktu reschedule telah berakhir (1 jam sebelum mulai).');
|
||||
}
|
||||
|
||||
// Get venue and tables data
|
||||
$venue = $booking->table->venue;
|
||||
|
||||
// Duration in hours
|
||||
$duration = Carbon::parse($booking->start_time)->diffInHours($booking->end_time);
|
||||
|
||||
return view('pages.reschedule', compact('booking', 'venue', 'duration'));
|
||||
// Validasi kepemilikan dan status booking (tidak ada perubahan)
|
||||
if ($booking->user_id !== auth()->id() || $booking->status !== 'paid' || $booking->reschedule_count >= 1) {
|
||||
return redirect()->route('booking.history')->with('error', 'Batas maksimal reschedule telah digunakan (1x).');
|
||||
}
|
||||
|
||||
$rescheduleDeadline = Carbon::parse($booking->start_time)->subHour();
|
||||
if (now() > $rescheduleDeadline) {
|
||||
return redirect()->route('booking.history')->with('error', 'Batas waktu reschedule telah berakhir (1 jam sebelum mulai).');
|
||||
}
|
||||
|
||||
$venue = $booking->table->venue;
|
||||
$duration = Carbon::parse($booking->start_time)->diffInHours($booking->end_time);
|
||||
|
||||
// --- AWAL LOGIKA BARU UNTUK MENENTUKAN TANGGAL OPERASIONAL ---
|
||||
$startTime = Carbon::parse($booking->start_time);
|
||||
$operational_date = $startTime->copy(); // Mulai dengan tanggal kalender
|
||||
|
||||
// Jika venue-nya overnight DAN jam booking lebih pagi dari jam buka,
|
||||
// maka tanggal operasionalnya adalah H-1 dari tanggal kalender.
|
||||
if ($venue->is_overnight && $startTime->format('H:i:s') < $venue->open_time) {
|
||||
$operational_date->subDay();
|
||||
}
|
||||
|
||||
// Ubah ke format Y-m-d untuk dikirim ke view
|
||||
$operational_date_string = $operational_date->format('Y-m-d');
|
||||
// --- AKHIR DARI LOGIKA BARU ---
|
||||
|
||||
// Kirim $operational_date_string ke view, bukan lagi tanggal dari $booking
|
||||
return view('pages.reschedule', compact('booking', 'venue', 'duration', 'operational_date_string'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a reschedule request.
|
||||
*/
|
||||
|
@ -652,30 +629,43 @@ public function processReschedule(Request $request, $id)
|
|||
* Check availability for reschedule.
|
||||
*/
|
||||
public function checkRescheduleAvailability(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'table_id' => 'required|exists:tables,id',
|
||||
'date' => 'required|date_format:Y-m-d',
|
||||
'booking_id' => 'required|exists:bookings,id'
|
||||
]);
|
||||
{
|
||||
$request->validate([
|
||||
'table_id' => 'required|exists:tables,id',
|
||||
'date' => 'required|date_format:Y-m-d',
|
||||
'booking_id' => 'required|exists:bookings,id'
|
||||
]);
|
||||
|
||||
$date = $request->date;
|
||||
$tableId = $request->table_id;
|
||||
$bookingId = $request->booking_id;
|
||||
$table = Table::with('venue')->findOrFail($request->table_id);
|
||||
$venue = $table->venue;
|
||||
$requestDate = Carbon::parse($request->date);
|
||||
|
||||
// Get all bookings for this table on this date (excluding the current booking)
|
||||
$bookings = Booking::where('table_id', $tableId)
|
||||
->where('id', '!=', $bookingId)
|
||||
->where('status', 'paid')
|
||||
->whereDate('start_time', $date)
|
||||
->get(['start_time', 'end_time'])
|
||||
->map(function ($booking) {
|
||||
return [
|
||||
'start' => Carbon::parse($booking->start_time)->format('H:i'),
|
||||
'end' => Carbon::parse($booking->end_time)->format('H:i'),
|
||||
];
|
||||
});
|
||||
// Query untuk mengambil booking lain di meja yang sama
|
||||
$query = Booking::where('table_id', $table->id)
|
||||
->where('id', '!=', $request->booking_id) // Jangan ikut sertakan booking yang sedang di-reschedule
|
||||
->where('status', 'paid');
|
||||
|
||||
return response()->json($bookings);
|
||||
// --- LOGIKA OVERNIGHT DITERAPKAN DI SINI ---
|
||||
if ($venue->is_overnight) {
|
||||
// Ambil booking dari jam buka di hari H sampai jam tutup di hari H+1
|
||||
$startOperationalDay = $requestDate->copy()->setTimeFromTimeString($venue->open_time);
|
||||
$endOperationalDay = $requestDate->copy()->addDay()->setTimeFromTimeString($venue->close_time);
|
||||
|
||||
$query->whereBetween('start_time', [$startOperationalDay, $endOperationalDay]);
|
||||
} else {
|
||||
// Logika standar untuk venue yang tidak overnight
|
||||
$query->whereDate('start_time', $requestDate);
|
||||
}
|
||||
// --- AKHIR DARI LOGIKA OVERNIGHT ---
|
||||
|
||||
$bookings = $query->get(['start_time', 'end_time'])
|
||||
->map(function ($booking) {
|
||||
return [
|
||||
'start' => Carbon::parse($booking->start_time)->format('H:i'),
|
||||
'end' => Carbon::parse($booking->end_time)->format('H:i'),
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json($bookings);
|
||||
}
|
||||
}
|
|
@ -10,6 +10,12 @@ class Venue extends Model
|
|||
{
|
||||
use HasFactory;
|
||||
|
||||
public function getIsOvernightAttribute()
|
||||
{
|
||||
// Jika jam tutup lebih kecil dari jam buka, berarti melewati tengah malam
|
||||
return $this->close_time && $this->open_time && $this->close_time < $this->open_time;
|
||||
}
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'address',
|
||||
|
@ -29,6 +35,15 @@ class Venue extends Model
|
|||
'reopen_date',
|
||||
];
|
||||
|
||||
// --- TAMBAHKAN PROPERTI INI ---
|
||||
/**
|
||||
* The accessors to append to the model's array form.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $appends = ['is_overnight'];
|
||||
// --- AKHIR DARI PENAMBAHAN ---
|
||||
|
||||
public function tables()
|
||||
{
|
||||
return $this->hasMany(Table::class);
|
||||
|
|
|
@ -63,7 +63,7 @@ class="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-
|
|||
<input id="email" type="email" name="email" value="{{ old('email', $user->email) }}"
|
||||
required
|
||||
class="focus:ring-blue-500 focus:border-blue-500 flex-1 block w-full rounded-none rounded-r-md border-gray-300 p-1"
|
||||
placeholder="Masukkan email">
|
||||
placeholder="Masukkan email" readonly>
|
||||
</div>
|
||||
@error('email')
|
||||
<div class="text-red-500 mt-1 text-sm"><i
|
||||
|
|
|
@ -271,7 +271,7 @@ class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 rounded">Daftar</but
|
|||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-4" role="alert">
|
||||
<ul>
|
||||
@foreach($errors->get('email') as $error)
|
||||
<li>{{ $error }}</li>
|
||||
<li>Email anda belum terdaftar.</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -62,8 +62,8 @@ class="flex flex-col h-full border border-gray-400 rounded-lg overflow-hidden">
|
|||
{{-- Venue sedang buka - tampilkan jam operasional --}}
|
||||
<p class="text-sm text-gray-600 mt-1">
|
||||
<i class="fa-regular fa-clock text-green-500"></i>
|
||||
Buka: {{ date('H:i', strtotime($venue['open_time'])) }} -
|
||||
{{ date('H:i', strtotime($venue['close_time'])) }}
|
||||
Buka: {{ date('H:i A', strtotime($venue['open_time'])) }} -
|
||||
{{ date('H:i A', strtotime($venue['close_time'])) }}
|
||||
</p>
|
||||
@else
|
||||
{{-- Venue sedang tutup - tampilkan informasi penutupan --}}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
@extends('layouts.main')
|
||||
|
||||
@section('content')
|
||||
<div class="min-h-96 mx-4 md:w-3/4 md:mx-auto py-8">
|
||||
<!-- Notification Container -->
|
||||
<div id="notification-container" class="fixed top-4 right-4 z-50 space-y-2"></div>
|
||||
|
||||
<h1 class="text-2xl font-bold mb-6">Reschedule Booking</h1>
|
||||
|
@ -30,12 +30,9 @@
|
|||
<p class="font-medium">{{ $duration }} Jam</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
|
||||
<div class="flex items-start">
|
||||
<div class="mr-3 text-yellow-500">
|
||||
<i class="fa-solid fa-exclamation-circle text-xl"></i>
|
||||
</div>
|
||||
<div class="mr-3 text-yellow-500"><i class="fa-solid fa-exclamation-circle text-xl"></i></div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-yellow-700">Perhatian</h3>
|
||||
<p class="text-yellow-700 text-sm">
|
||||
|
@ -54,14 +51,13 @@
|
|||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Pilih Tanggal:</label>
|
||||
<input type="date" x-model="selectedDate" class="w-full border p-2 rounded-lg"
|
||||
:min="today" @change="dateChanged" disabled>
|
||||
<label for="date-picker" class="block text-sm font-medium text-gray-700 mb-1">Tanggal Operasional:</label>
|
||||
<input type="date" id="date-picker" x-model="selectedDate" class="w-full border p-2 rounded-lg bg-gray-100 cursor-not-allowed" disabled>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Pilih Meja:</label>
|
||||
<select x-model="selectedTableId" class="w-full border p-2 rounded-lg" @change="tableChanged">
|
||||
<label for="table-picker" class="block text-sm font-medium text-gray-700 mb-1">Pilih Meja:</label>
|
||||
<select id="table-picker" x-model="selectedTableId" class="w-full border p-2 rounded-lg" @change="fetchSchedules">
|
||||
<option value="">-- Pilih Meja --</option>
|
||||
<template x-for="table in tables" :key="table.id">
|
||||
<option :value="table.id" x-text="table.name + ' (' + table.brand + ')'"></option>
|
||||
|
@ -72,264 +68,168 @@
|
|||
|
||||
<div class="mt-6" x-show="selectedDate && selectedTableId">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Pilih Jam Mulai:</label>
|
||||
<div class="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-2">
|
||||
<div x-show="!isLoadingSchedules" class="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-2">
|
||||
<template x-for="hour in availableHours" :key="hour">
|
||||
<button
|
||||
class="py-2 px-1 rounded-lg text-sm font-medium transition duration-150"
|
||||
:class="isTimeSlotAvailable(hour) ?
|
||||
(selectedStartHour === hour ? 'bg-blue-500 text-white' : 'bg-gray-100 hover:bg-gray-200 text-gray-800') :
|
||||
'bg-gray-200 text-gray-400 cursor-not-allowed opacity-70'"
|
||||
:disabled="!isTimeSlotAvailable(hour)"
|
||||
@click="selectStartHour(hour)"
|
||||
x-text="hour + ':00'">
|
||||
</button>
|
||||
<button class="py-2 px-1 rounded-lg text-sm font-medium transition duration-150"
|
||||
:class="getSlotClass(hour)" :disabled="!isSlotAvailable(hour)" @click="selectedStartHour = hour"
|
||||
x-text="hour + ':00'"></button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="mt-4" x-show="selectedStartHour">
|
||||
<p class="text-sm text-gray-700 mb-2">
|
||||
Jadwal reschedule: <span class="font-medium" x-text="formattedSchedule"></span>
|
||||
</p>
|
||||
<div x-show="isLoadingSchedules" class="text-center text-gray-500 py-4">
|
||||
Memeriksa jadwal...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex justify-end">
|
||||
<a href="{{ route('booking.history') }}" class="bg-gray-300 text-gray-700 px-4 py-2 rounded-lg mr-3">
|
||||
Batal
|
||||
</a>
|
||||
<button @click="submitReschedule"
|
||||
:disabled="!canSubmit || isSubmitting"
|
||||
:class="canSubmit ? 'bg-green-500 hover:bg-green-600' : 'bg-green-300 cursor-not-allowed'"
|
||||
class="text-white px-4 py-2 rounded-lg">
|
||||
<a href="{{ route('booking.history') }}" class="bg-gray-200 text-gray-700 px-4 py-2 rounded-lg mr-3 hover:bg-gray-300">Batal</a>
|
||||
<button @click="submitReschedule" :disabled="!canSubmit || isSubmitting"
|
||||
class="text-white px-4 py-2 rounded-lg flex items-center justify-center" :class="canSubmit ? 'bg-green-600 hover:bg-green-700' : 'bg-green-300 cursor-not-allowed'">
|
||||
<span x-show="!isSubmitting">Konfirmasi Reschedule</span>
|
||||
<span x-show="isSubmitting">Memproses...</span>
|
||||
<span x-show="isSubmitting">
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div> Memproses...
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Notification System
|
||||
function showNotification(message, type = 'info', duration = 5000) {
|
||||
const container = document.getElementById('notification-container');
|
||||
const notification = document.createElement('div');
|
||||
|
||||
// Set notification styles based on type
|
||||
let bgColor, textColor, icon;
|
||||
switch(type) {
|
||||
case 'success':
|
||||
bgColor = 'bg-green-500';
|
||||
textColor = 'text-white';
|
||||
icon = 'fa-check-circle';
|
||||
break;
|
||||
case 'error':
|
||||
bgColor = 'bg-red-500';
|
||||
textColor = 'text-white';
|
||||
icon = 'fa-exclamation-circle';
|
||||
break;
|
||||
case 'warning':
|
||||
bgColor = 'bg-yellow-500';
|
||||
textColor = 'text-white';
|
||||
icon = 'fa-exclamation-triangle';
|
||||
break;
|
||||
default:
|
||||
bgColor = 'bg-blue-500';
|
||||
textColor = 'text-white';
|
||||
icon = 'fa-info-circle';
|
||||
}
|
||||
|
||||
notification.className = `${bgColor} ${textColor} px-6 py-4 rounded-lg shadow-lg transform transition-all duration-300 ease-in-out opacity-0 translate-x-full flex items-center space-x-3 max-w-md`;
|
||||
|
||||
notification.innerHTML = `
|
||||
<i class="fas ${icon} text-lg"></i>
|
||||
<div class="flex-1">
|
||||
<p class="font-medium">${message}</p>
|
||||
</div>
|
||||
<button onclick="this.parentElement.remove()" class="text-white hover:text-gray-200 focus:outline-none">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
container.appendChild(notification);
|
||||
|
||||
// Animate in
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('opacity-0', 'translate-x-full');
|
||||
notification.classList.add('opacity-100', 'translate-x-0');
|
||||
}, 100);
|
||||
|
||||
// Auto remove after duration
|
||||
setTimeout(() => {
|
||||
notification.classList.add('opacity-0', 'translate-x-full');
|
||||
setTimeout(() => {
|
||||
if (notification.parentElement) {
|
||||
notification.remove();
|
||||
}
|
||||
}, 300);
|
||||
}, duration);
|
||||
}
|
||||
function showNotification(message, type = 'info') { /* ... fungsi notifikasi tidak berubah ... */ }
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('rescheduleForm', () => ({
|
||||
tables: @json($venue->tables),
|
||||
// PERUBAHAN 2: Inisialisasi data yang bersih & benar
|
||||
venue: @json($venue),
|
||||
bookingId: {{ $booking->id }},
|
||||
bookingDuration: {{ $duration }},
|
||||
originalTableId: {{ $booking->table_id }},
|
||||
originalStartTime: "{{ \Carbon\Carbon::parse($booking->start_time)->format('H:i') }}",
|
||||
originalDate: "{{ \Carbon\Carbon::parse($booking->start_time)->format('Y-m-d') }}",
|
||||
selectedDate: '',
|
||||
selectedTableId: '',
|
||||
tables: @json($venue->tables),
|
||||
|
||||
selectedDate: '{{ $operational_date_string }}',
|
||||
selectedTableId: {{ $booking->table_id }},
|
||||
selectedStartHour: null,
|
||||
|
||||
bookedSchedules: [],
|
||||
availableHours: Array.from({length: 16}, (_, i) => (i + 9).toString().padStart(2, '0')),
|
||||
isLoadingSchedules: true,
|
||||
isSubmitting: false,
|
||||
|
||||
// PERUBAHAN 3: Fungsi init() disederhanakan
|
||||
init() {
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
this.today = `${year}-${month}-${day}`;
|
||||
this.fetchSchedules();
|
||||
},
|
||||
|
||||
this.selectedDate = this.originalDate;
|
||||
this.selectedTableId = this.originalTableId;
|
||||
// Semua fungsi di bawah ini sudah benar & tidak perlu diubah lagi
|
||||
get openHour() { return parseInt(this.venue.open_time.split(':')[0]); },
|
||||
get closeHour() { return parseInt(this.venue.close_time.split(':')[0]); },
|
||||
get isOvernight() { return this.venue.is_overnight; },
|
||||
|
||||
this.checkBookedSchedules();
|
||||
get availableHours() {
|
||||
let hours = [];
|
||||
if (this.isOvernight) {
|
||||
for (let i = this.openHour; i < 24; i++) hours.push(i.toString().padStart(2, '0'));
|
||||
for (let i = 0; i <= this.closeHour; i++) hours.push(i.toString().padStart(2, '0'));
|
||||
} else {
|
||||
for (let i = this.openHour; i <= this.closeHour; i++) hours.push(i.toString().padStart(2, '0'));
|
||||
}
|
||||
return hours;
|
||||
},
|
||||
|
||||
get canSubmit() {
|
||||
return this.selectedDate &&
|
||||
this.selectedTableId &&
|
||||
this.selectedStartHour !== null &&
|
||||
(this.selectedDate !== this.originalDate ||
|
||||
this.selectedTableId != this.originalTableId ||
|
||||
this.selectedStartHour !== this.originalStartTime.split(':')[0]);
|
||||
return this.selectedStartHour !== null && !this.isLoadingSchedules;
|
||||
},
|
||||
|
||||
get formattedSchedule() {
|
||||
if (!this.selectedStartHour) return '';
|
||||
|
||||
const startHour = parseInt(this.selectedStartHour);
|
||||
const endHour = startHour + this.bookingDuration;
|
||||
|
||||
return `${this.selectedStartHour}:00 - ${endHour.toString().padStart(2, '0')}:00`;
|
||||
},
|
||||
|
||||
async dateChanged() {
|
||||
this.selectedStartHour = null;
|
||||
await this.checkBookedSchedules();
|
||||
},
|
||||
|
||||
async tableChanged() {
|
||||
this.selectedStartHour = null;
|
||||
await this.checkBookedSchedules();
|
||||
},
|
||||
|
||||
async checkBookedSchedules() {
|
||||
async fetchSchedules() {
|
||||
if (!this.selectedDate || !this.selectedTableId) return;
|
||||
|
||||
this.isLoadingSchedules = true;
|
||||
this.bookedSchedules = [];
|
||||
this.selectedStartHour = null;
|
||||
try {
|
||||
const response = await fetch(`/booking/reschedule/check-availability?table_id=${this.selectedTableId}&date=${this.selectedDate}&booking_id=${this.bookingId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
if (!response.ok) throw new Error('Failed to fetch schedules');
|
||||
this.bookedSchedules = await response.json();
|
||||
|
||||
if (this.selectedDate === this.originalDate &&
|
||||
parseInt(this.selectedTableId) === parseInt(this.originalTableId)) {
|
||||
const originalHour = this.originalStartTime.split(':')[0];
|
||||
if (this.isTimeSlotAvailable(originalHour)) {
|
||||
this.selectedStartHour = originalHour;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking booked schedules:', error);
|
||||
showNotification('Terjadi kesalahan saat memeriksa jadwal. Silakan coba lagi.', 'error');
|
||||
console.error(error);
|
||||
} finally {
|
||||
this.isLoadingSchedules = false;
|
||||
}
|
||||
},
|
||||
|
||||
isTimeSlotAvailable(hour) {
|
||||
const hourInt = parseInt(hour);
|
||||
const endHourInt = hourInt + this.bookingDuration;
|
||||
|
||||
if (endHourInt > 24) return false;
|
||||
|
||||
const selectedDate = new Date(this.selectedDate);
|
||||
const today = new Date();
|
||||
const isToday = selectedDate.toDateString() === today.toDateString();
|
||||
|
||||
if (isToday) {
|
||||
const currentHour = today.getHours();
|
||||
if (hourInt <= currentHour) {
|
||||
return false;
|
||||
}
|
||||
isSlotAvailable(hour) {
|
||||
const startHourInt = parseInt(hour);
|
||||
const endHourInt = startHourInt + this.bookingDuration;
|
||||
if (this.isOvernight) {
|
||||
if (startHourInt >= this.openHour && endHourInt > (24 + this.closeHour)) return false;
|
||||
if (startHourInt < this.openHour && endHourInt > this.closeHour) return false;
|
||||
} else {
|
||||
if (endHourInt > this.closeHour) return false;
|
||||
}
|
||||
|
||||
const isOriginalTimeSlot = this.selectedDate === this.originalDate &&
|
||||
parseInt(this.selectedTableId) === parseInt(this.originalTableId) &&
|
||||
hour === this.originalStartTime.split(':')[0];
|
||||
|
||||
if (isOriginalTimeSlot) {
|
||||
if (isToday) {
|
||||
const currentHour = today.getHours();
|
||||
return hourInt > currentHour;
|
||||
}
|
||||
return true;
|
||||
const now = new Date();
|
||||
const selectedDateTime = new Date(`${this.selectedDate}T${hour}:00:00`);
|
||||
if (this.isOvernight && startHourInt < this.openHour) {
|
||||
selectedDateTime.setDate(selectedDateTime.getDate() + 1);
|
||||
}
|
||||
|
||||
return !this.bookedSchedules.some(schedule => {
|
||||
if (selectedDateTime <= now) {
|
||||
return false;
|
||||
}
|
||||
for (const schedule of this.bookedSchedules) {
|
||||
const scheduleStart = parseInt(schedule.start.split(':')[0]);
|
||||
const scheduleEnd = parseInt(schedule.end.split(':')[0]);
|
||||
|
||||
return (hourInt < scheduleEnd && endHourInt > scheduleStart);
|
||||
});
|
||||
const isOvernightBooking = scheduleEnd < scheduleStart;
|
||||
if (isOvernightBooking) {
|
||||
if ((startHourInt >= scheduleStart || startHourInt < scheduleEnd) && (endHourInt > scheduleStart || endHourInt <= scheduleEnd)) return false;
|
||||
} else {
|
||||
if (startHourInt < scheduleEnd && endHourInt > scheduleStart) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
selectStartHour(hour) {
|
||||
if (this.isTimeSlotAvailable(hour)) {
|
||||
this.selectedStartHour = hour;
|
||||
getSlotClass(hour) {
|
||||
if (!this.isSlotAvailable(hour)) {
|
||||
return 'bg-gray-200 text-gray-400 cursor-not-allowed';
|
||||
}
|
||||
if (this.selectedStartHour === hour) {
|
||||
return 'bg-blue-600 text-white shadow-md';
|
||||
}
|
||||
return 'bg-gray-100 hover:bg-gray-200 text-gray-800';
|
||||
},
|
||||
|
||||
async submitReschedule() {
|
||||
if (!this.canSubmit || this.isSubmitting) return;
|
||||
|
||||
if (!this.canSubmit) return;
|
||||
this.isSubmitting = true;
|
||||
|
||||
const startHour = parseInt(this.selectedStartHour);
|
||||
const endHour = startHour + this.bookingDuration;
|
||||
|
||||
const startTime = `${this.selectedDate} ${this.selectedStartHour}:00:00`;
|
||||
const endTime = `${this.selectedDate} ${endHour.toString().padStart(2, '0')}:00:00`;
|
||||
|
||||
const startDateTime = new Date(`${this.selectedDate}T${this.selectedStartHour}:00:00`);
|
||||
if (this.isOvernight && parseInt(this.selectedStartHour) < this.openHour) {
|
||||
startDateTime.setDate(startDateTime.getDate() + 1);
|
||||
}
|
||||
const endDateTime = new Date(startDateTime.getTime());
|
||||
endDateTime.setHours(endDateTime.getHours() + this.bookingDuration);
|
||||
const formatForServer = (date) => {
|
||||
const pad = (num) => num.toString().padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
const month = pad(date.getMonth() + 1);
|
||||
const day = pad(date.getDate());
|
||||
const hours = pad(date.getHours());
|
||||
const minutes = pad(date.getMinutes());
|
||||
const seconds = pad(date.getSeconds());
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
try {
|
||||
const response = await fetch(`/booking/${this.bookingId}/reschedule`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content },
|
||||
body: JSON.stringify({
|
||||
table_id: this.selectedTableId,
|
||||
start_time: startTime,
|
||||
end_time: endTime,
|
||||
start_time: formatForServer(startDateTime),
|
||||
end_time: formatForServer(endDateTime),
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showNotification(result.message, 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = result.redirect;
|
||||
}, 2000);
|
||||
} else {
|
||||
showNotification(result.message || 'Terjadi kesalahan saat memproses reschedule.', 'error');
|
||||
this.isSubmitting = false;
|
||||
}
|
||||
if (response.ok && result.success) {
|
||||
showNotification('Booking berhasil di-reschedule!', 'success');
|
||||
setTimeout(() => window.location.href = result.redirect, 1500);
|
||||
} else { throw new Error(result.message || 'Gagal reschedule.'); }
|
||||
} catch (error) {
|
||||
console.error('Error submitting reschedule:', error);
|
||||
showNotification('Terjadi kesalahan. Silakan coba lagi.', 'error');
|
||||
showNotification(error.message, 'error');
|
||||
} finally {
|
||||
this.isSubmitting = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,8 +26,8 @@ class="w-full h-full object-cover rounded-lg mb-4 mt-8" />
|
|||
{{-- Venue sedang buka - tampilkan jam operasional --}}
|
||||
<p class="text-sm text-gray-600 mt-1">
|
||||
<i class="fa-regular fa-clock text-green-500"></i>
|
||||
Jam Operasional: {{ date('H:i', strtotime($venue['open_time'])) }} -
|
||||
{{ date('H:i', strtotime($venue['close_time'])) }}
|
||||
Jam Operasional: {{ date('H:i A', strtotime($venue['open_time'])) }} -
|
||||
{{ date('H:i A', strtotime($venue['close_time'])) }}
|
||||
</p>
|
||||
@else
|
||||
{{-- Venue sedang tutup - tampilkan informasi penutupan --}}
|
||||
|
@ -116,7 +116,13 @@ class="bg-gray-200 text-gray-700 text-sm px-3 py-1 rounded-md hover:bg-gray-300"
|
|||
</div>
|
||||
</div>
|
||||
@foreach ($venue['tables'] as $table)
|
||||
<div x-data="booking(@json(auth()->check()), '{{ $table['id'] }}', {{ $openHour }}, {{ $closeHour }})"
|
||||
<div x-data="booking(
|
||||
@json(auth()->check()),
|
||||
'{{ $table['id'] }}',
|
||||
{{ date('G', strtotime($venue['open_time'])) }}, // Jam buka (format 24 jam tanpa leading zero)
|
||||
{{ date('G', strtotime($venue['close_time'])) }}, // Jam tutup
|
||||
{{ $venue->is_overnight ? 'true' : 'false' }} // Tambahkan flag is_overnight
|
||||
)"
|
||||
class="border rounded-lg shadow-md p-4 mb-4">
|
||||
<div class="flex items-center justify-between cursor-pointer"
|
||||
@click="open = !open; if(open) checkBookedSchedules()">
|
||||
|
@ -564,30 +570,63 @@ function formatPrice(price) {
|
|||
}));
|
||||
|
||||
// Regular booking component (updated with dynamic hours)
|
||||
Alpine.data('booking', (isLoggedIn, tableId, openHour, closeHour) => ({
|
||||
isLoggedIn,
|
||||
tableId,
|
||||
openHour, // Dynamic open hour from venue
|
||||
closeHour, // Dynamic close hour from venue
|
||||
open: false,
|
||||
selectedTime: '',
|
||||
selectedDuration: '',
|
||||
isLoading: false,
|
||||
bookedSchedules: [],
|
||||
Alpine.data('booking', (isLoggedIn, tableId, openHour, closeHour, isOvernight) => ({
|
||||
isLoggedIn,
|
||||
tableId,
|
||||
openHour,
|
||||
closeHour,
|
||||
isOvernight,
|
||||
open: false,
|
||||
selectedTime: '',
|
||||
selectedDuration: '',
|
||||
isLoading: false,
|
||||
bookedSchedules: [],
|
||||
|
||||
// Updated method to use dynamic hours from venue
|
||||
getAvailableHours() {
|
||||
let hours = [];
|
||||
for (let i = this.openHour; i <= this.closeHour; i++) {
|
||||
let hours = [];
|
||||
const currentJakartaHour = getJakartaDate().getHours();
|
||||
|
||||
if (this.isOvernight) {
|
||||
// Jam dari waktu buka sampai tengah malam (23)
|
||||
for (let i = this.openHour; i < 24; i++) {
|
||||
// Hanya tampilkan jam yang akan datang
|
||||
if (i >= currentJakartaHour) {
|
||||
hours.push(i.toString().padStart(2, '0'));
|
||||
}
|
||||
}
|
||||
// Jam dari tengah malam (00) sampai waktu tutup
|
||||
for (let i = 0; i <= this.closeHour; i++) {
|
||||
hours.push(i.toString().padStart(2, '0'));
|
||||
}
|
||||
return hours;
|
||||
},
|
||||
} else {
|
||||
// Logika standar untuk venue yang tidak overnight
|
||||
for (let i = this.openHour; i <= this.closeHour; i++) {
|
||||
// Hanya tampilkan jam yang akan datang
|
||||
if (i >= currentJakartaHour) {
|
||||
hours.push(i.toString().padStart(2, '0'));
|
||||
}
|
||||
}
|
||||
}
|
||||
return hours;
|
||||
},
|
||||
|
||||
isTimeBooked(time) {
|
||||
const timeFormatted = time.padStart(5, '0');
|
||||
return this.bookedSchedules.some(schedule => {
|
||||
return timeFormatted >= schedule.start && timeFormatted < schedule.end;
|
||||
const isOvernightBooking = schedule.end < schedule.start;
|
||||
|
||||
if (isOvernightBooking) {
|
||||
// Untuk booking overnight (misal 23:00 - 01:00)
|
||||
// Slot dianggap booked jika:
|
||||
// 1. Lebih besar atau sama dengan jam mulai (misal 23:00)
|
||||
// ATAU
|
||||
// 2. Lebih kecil dari jam selesai (misal 00:00)
|
||||
return (timeFormatted >= schedule.start || timeFormatted < schedule.end);
|
||||
} else {
|
||||
// Untuk booking normal
|
||||
return (timeFormatted >= schedule.start && timeFormatted < schedule.end);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -623,6 +662,10 @@ function formatPrice(price) {
|
|||
const [selectedHour, selectedMinute] = selectedTime.split(':').map(Number);
|
||||
selectedDateTime.setHours(selectedHour, selectedMinute, 0, 0);
|
||||
|
||||
if (this.isOvernight && selectedHour < this.openHour) {
|
||||
selectedDateTime.setDate(selectedDateTime.getDate() + 1);
|
||||
}
|
||||
|
||||
// Uncomment this for production to prevent booking past times
|
||||
if (selectedDateTime <= now) {
|
||||
showToast('Tidak bisa booking untuk waktu yang sudah berlalu', 'warning');
|
||||
|
|
Loading…
Reference in New Issue