Reschedule

This commit is contained in:
Stephen Gesityan 2025-05-15 05:01:28 +07:00
parent ff6caa6010
commit 2231c3e874
5 changed files with 418 additions and 4 deletions

View File

@ -480,4 +480,134 @@ public function deletePendingBooking($id)
], 500);
}
}
public function showReschedule($id)
{
$booking = Booking::with(['table.venue', 'table.venue.tables'])->findOrFail($id);
// 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.');
}
// Check if booking is upcoming and paid
if ($booking->start_time <= now() || $booking->status !== 'paid') {
return redirect()->route('booking.history')->with('error', 'Booking ini tidak dapat di-reschedule.');
}
// Check if already rescheduled
if ($booking->has_rescheduled) {
return redirect()->route('booking.history')->with('error', 'Booking ini sudah pernah di-reschedule sebelumnya.');
}
// 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'));
}
/**
* Process a reschedule request.
*/
public function processReschedule(Request $request, $id)
{
$request->validate([
'table_id' => 'required|exists:tables,id',
'start_time' => 'required|date_format:Y-m-d H:i:s',
'end_time' => 'required|date_format:Y-m-d H:i:s|after:start_time',
]);
$booking = Booking::findOrFail($id);
// Perform the same validation as in showReschedule
if ($booking->user_id !== auth()->id() ||
$booking->start_time <= now() ||
$booking->status !== 'paid' ||
$booking->has_rescheduled ||
now() > Carbon::parse($booking->start_time)->subHour()) {
return response()->json([
'success' => false,
'message' => 'Booking ini tidak dapat di-reschedule.'
], 422);
}
// Check if the selected time is available (except for this booking)
$existingBookings = Booking::where('table_id', $request->table_id)
->where('id', '!=', $booking->id)
->where('status', 'paid')
->where(function ($query) use ($request) {
$query->where(function ($q) use ($request) {
$q->where('start_time', '<', $request->end_time)
->where('end_time', '>', $request->start_time);
});
})->count();
if ($existingBookings > 0) {
return response()->json([
'success' => false,
'message' => 'Jam yang dipilih sudah dibooking oleh orang lain.'
], 422);
}
// Store original booking details
$originalStartTime = $booking->start_time;
$originalEndTime = $booking->end_time;
$originalTableId = $booking->table_id;
// Update the booking
$booking->original_start_time = $originalStartTime;
$booking->original_end_time = $originalEndTime;
$booking->original_table_id = $originalTableId;
$booking->start_time = $request->start_time;
$booking->end_time = $request->end_time;
$booking->table_id = $request->table_id;
$booking->has_rescheduled = true;
$booking->save();
return response()->json([
'success' => true,
'message' => 'Booking berhasil di-reschedule.',
'redirect' => route('booking.history')
]);
}
/**
* Check availability for reschedule.
*/
public function checkRescheduleAvailability(Request $request)
{
$request->validate([
'table_id' => 'required|exists:tables,id',
'date' => 'required|date_format:Y-m-d',
'booking_id' => 'required|exists:bookings,id'
]);
$date = $request->date;
$tableId = $request->table_id;
$bookingId = $request->booking_id;
// 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'),
];
});
return response()->json($bookings);
}
}

View File

@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddRescheduleColumnsToBookingsTable extends Migration
{
public function up()
{
Schema::table('bookings', function (Blueprint $table) {
$table->boolean('has_rescheduled')->default(false);
$table->timestamp('original_start_time')->nullable();
$table->timestamp('original_end_time')->nullable();
$table->unsignedBigInteger('original_table_id')->nullable();
});
}
public function down()
{
Schema::table('bookings', function (Blueprint $table) {
$table->dropColumn(['has_rescheduled', 'original_start_time', 'original_end_time', 'original_table_id']);
});
}
}

View File

@ -1,6 +1,4 @@
@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 class="min-h-96 mx-4 md:w-3/4 md:mx-auto py-8">
<h1 class="text-2xl font-bold mb-6">Riwayat Booking</h1>
@if($bookings->isEmpty())
@ -53,12 +51,30 @@ class="px-3 py-1 rounded-full text-sm {{ $booking->start_time > now() ? 'bg-gree
<p class="text-sm text-gray-500">Metode Pembayaran</p>
<p class="font-medium capitalize">{{ $booking->payment_method ?? '-' }}</p>
</div>
@if($booking->has_rescheduled)
<div class="col-span-2">
<p class="text-sm text-gray-500">Informasi Reschedule</p>
<p class="text-sm text-orange-600">
Booking ini telah di-reschedule dari tanggal
{{ \Carbon\Carbon::parse($booking->original_start_time)->format('d M Y') }} jam
{{ \Carbon\Carbon::parse($booking->original_start_time)->format('H:i') }} -
{{ \Carbon\Carbon::parse($booking->original_end_time)->format('H:i') }}
</p>
</div>
@endif
</div>
@if($booking->start_time > now() && $booking->status == 'paid')
<div class="mt-4 flex justify-end">
<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>
@if(!$booking->has_rescheduled && \Carbon\Carbon::parse($booking->start_time)->subHour() > now())
<a href="{{ route('booking.reschedule.form', $booking->id) }}"
class="text-orange-500 hover:underline">
Reschedule
</a>
@endif
</div>
@endif
</div>

View File

@ -0,0 +1,238 @@
@extends('layouts.main')
@section('content')
<div class="min-h-96 mx-4 md:w-3/4 md:mx-auto py-8">
<h1 class="text-2xl font-bold mb-6">Reschedule Booking</h1>
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<h2 class="text-lg font-semibold mb-4">Detail Booking Saat Ini</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div>
<p class="text-sm text-gray-500">Venue</p>
<p class="font-medium">{{ $booking->table->venue->name }}</p>
</div>
<div>
<p class="text-sm text-gray-500">Meja</p>
<p class="font-medium">{{ $booking->table->name }} ({{ $booking->table->brand }})</p>
</div>
<div>
<p class="text-sm text-gray-500">Tanggal & Waktu</p>
<p class="font-medium">
{{ \Carbon\Carbon::parse($booking->start_time)->format('d M Y') }},
{{ \Carbon\Carbon::parse($booking->start_time)->format('H:i') }} -
{{ \Carbon\Carbon::parse($booking->end_time)->format('H:i') }}
</p>
</div>
<div>
<p class="text-sm text-gray-500">Durasi</p>
<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>
<h3 class="font-semibold text-yellow-700">Perhatian</h3>
<p class="text-yellow-700 text-sm">
Reschedule hanya dapat dilakukan 1x untuk setiap booking<br>
Batas waktu reschedule adalah 1 jam sebelum jadwal booking<br>
Durasi booking akan tetap sama ({{ $duration }} jam)
</p>
</div>
</div>
</div>
</div>
<div x-data="rescheduleForm" class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-lg font-semibold mb-4">Pilih Jadwal Baru</h2>
<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">
</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">
<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>
</template>
</select>
</div>
</div>
<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">
<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>
</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>
</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">
<span x-show="!isSubmitting">Konfirmasi Reschedule</span>
<span x-show="isSubmitting">Memproses...</span>
</button>
</div>
</div>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('rescheduleForm', () => ({
tables: @json($venue->tables),
bookingId: {{ $booking->id }},
bookingDuration: {{ $duration }},
originalTableId: {{ $booking->table_id }},
selectedDate: '',
selectedTableId: '',
selectedStartHour: null,
bookedSchedules: [],
availableHours: Array.from({length: 16}, (_, i) => (i + 9).toString().padStart(2, '0')), // 9AM to 24PM
isSubmitting: false,
init() {
// Set today as default value
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.selectedDate = this.today;
// Set original table as default
this.selectedTableId = this.originalTableId;
// Load schedules for today and selected table
this.checkBookedSchedules();
},
get canSubmit() {
return this.selectedDate && this.selectedTableId && this.selectedStartHour !== null;
},
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() {
if (!this.selectedDate || !this.selectedTableId) return;
try {
const response = await fetch(`/booking/reschedule/check-availability?table_id=${this.selectedTableId}&date=${this.selectedDate}&booking_id=${this.bookingId}`);
this.bookedSchedules = await response.json();
} catch (error) {
console.error('Error checking booked schedules:', error);
}
},
isTimeSlotAvailable(hour) {
const hourInt = parseInt(hour);
const endHourInt = hourInt + this.bookingDuration;
// Check if slot end time exceeds midnight
if (endHourInt > 24) return false;
// Check if any existing booking overlaps with this slot
return !this.bookedSchedules.some(schedule => {
const scheduleStart = parseInt(schedule.start.split(':')[0]);
const scheduleEnd = parseInt(schedule.end.split(':')[0]);
// Check if there's overlap
return (hourInt < scheduleEnd && endHourInt > scheduleStart);
});
},
selectStartHour(hour) {
if (this.isTimeSlotAvailable(hour)) {
this.selectedStartHour = hour;
}
},
async submitReschedule() {
if (!this.canSubmit || this.isSubmitting) return;
this.isSubmitting = true;
// Calculate end time based on selected start hour and duration
const startHour = parseInt(this.selectedStartHour);
const endHour = startHour + this.bookingDuration;
// Format date strings for API
const startTime = `${this.selectedDate} ${this.selectedStartHour}:00:00`;
const endTime = `${this.selectedDate} ${endHour.toString().padStart(2, '0')}:00:00`;
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,
},
body: JSON.stringify({
table_id: this.selectedTableId,
start_time: startTime,
end_time: endTime,
}),
});
const result = await response.json();
if (result.success) {
alert(result.message);
window.location.href = result.redirect;
} else {
alert(result.message || 'Terjadi kesalahan saat memproses reschedule.');
this.isSubmitting = false;
}
} catch (error) {
console.error('Error submitting reschedule:', error);
alert('Terjadi kesalahan. Silakan coba lagi.');
this.isSubmitting = false;
}
}
}));
});
</script>
@endsection

View File

@ -57,6 +57,11 @@
Route::get('/booking/pending/{id}/resume', [BookingController::class, 'resumeBooking'])->name('booking.resume');
Route::delete('/booking/pending/{id}', [BookingController::class, 'deletePendingBooking'])->name('booking.pending.delete');
// Route Reschedule
Route::get('/booking/{id}/reschedule', [BookingController::class, 'showReschedule'])->name('booking.reschedule.form');
Route::post('/booking/{id}/reschedule', [BookingController::class, 'processReschedule'])->name('booking.reschedule.process');
Route::get('/booking/reschedule/check-availability', [BookingController::class, 'checkRescheduleAvailability'])->name('booking.reschedule.check-availability');
// Routes that require password confirmation - moved account settings out of this group
Route::middleware(['password.confirm'])->group(function () {
// Any sensitive operations that should still require password confirmation can go here