Compare commits

...

10 Commits

Author SHA1 Message Date
Stephen Gesityan f0f2d5299d Switch remote 2025-07-16 14:26:26 +07:00
Stephen Gesityan 79307fb0d6 Checkpoint sebelum revisi 2025-07-07 11:58:28 +07:00
Stephen Gesityan f5cf7a4f8a Final 2025-06-15 15:17:49 +07:00
Stephen Gesityan 2d82a9489e Checkpointt 2025-06-07 20:25:56 +07:00
Stephen Gesityan cbbd272b15 Admin bisa tutup buka venue 2025-06-05 02:48:00 +07:00
Stephen Gesityan 6873f94b83 Checkpoint 2025-06-05 01:57:25 +07:00
Stephen Gesityan 73b264107e Checkpoint 2025-06-04 19:23:37 +07:00
Stephen Gesityan 4463aef193 Jam buka tutup dinamis, booking by user bisa, booking by admin bisa, kelola venue by admin bisa 2025-06-04 17:47:55 +07:00
Stephen Gesityan 64951d2018 Kelola Venue by Admin bisa, tapi bookingnya error lg 2025-06-04 16:57:17 +07:00
Stephen Gesityan 0580940cf5 Jam buka tutup sudah dinamis, booking sbg user sudah bisa, booking sbg admin sudah bisa 2025-06-04 15:58:06 +07:00
50 changed files with 4178 additions and 1671 deletions

View File

@ -0,0 +1,55 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Venue;
use Carbon\Carbon;
class ReopenVenuesCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'venues:reopen';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Automatically reopen venues that have reached their reopen date';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('Checking for venues to reopen...');
$venuesReopened = 0;
// Get all closed venues that should be reopened today
$venues = Venue::where('status', 'close')
->whereNotNull('reopen_date')
->whereDate('reopen_date', '<=', Carbon::today())
->get();
foreach ($venues as $venue) {
if ($venue->checkAutoReopen()) {
$this->info("Venue '{$venue->name}' has been automatically reopened.");
$venuesReopened++;
}
}
if ($venuesReopened > 0) {
$this->info("Successfully reopened {$venuesReopened} venue(s).");
} else {
$this->info('No venues to reopen today.');
}
return Command::SUCCESS;
}
}

View File

@ -12,7 +12,11 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule): void
{
// $schedule->command('inspire')->hourly();
// Run venue reopen check every day at 00:01
$schedule->command('venues:reopen')
->dailyAt('00:01')
->withoutOverlapping()
->runInBackground();
}
/**

View File

@ -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!');
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers\admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;
class AdminProfileController extends Controller
{
/**
* Display the admin profile page.
*/
public function index()
{
$user = Auth::user();
return view('admin.profile.index', compact('user'));
}
/**
* Update the admin profile information.
*/
public function updateProfile(Request $request)
{
$user = Auth::user();
$request->validate([
'name' => 'required|string|max:255',
'email' => [
'required',
'string',
'email',
'max:255',
Rule::unique('users')->ignore($user->id),
],
]);
$user->update([
'name' => $request->name,
'email' => $request->email,
]);
return redirect()->route('admin.profile.index')
->with('success', 'Profile berhasil diperbarui.');
}
/**
* Update the admin password.
*/
public function updatePassword(Request $request)
{
$request->validate([
'current_password' => 'required',
'password' => 'required|string|min:8|confirmed',
]);
$user = Auth::user();
// Check if current password is correct
if (!Hash::check($request->current_password, $user->password)) {
return back()->withErrors(['current_password' => 'Password saat ini tidak sesuai.']);
}
$user->update([
'password' => Hash::make($request->password),
]);
return redirect()->route('admin.profile.index')
->with('success', 'Password berhasil diperbarui.');
}
}

View File

@ -15,7 +15,15 @@ class BookingsController extends Controller
{
public function index(Request $request)
{
$query = Booking::with(['table', 'user']);
// Ambil venue_id dari admin yang sedang login
// Sesuaikan dengan struktur database kamu:
$adminVenueId = auth()->user()->venue_id; // Asumsi admin punya kolom venue_id
// Query booking dengan filter venue terlebih dahulu
$query = Booking::with(['table', 'user'])
->whereHas('table', function ($q) use ($adminVenueId) {
$q->where('venue_id', $adminVenueId);
});
// Search functionality
if ($request->has('search') && !empty($request->search)) {
@ -63,21 +71,73 @@ public function index(Request $request)
$query->orderBy($sortColumn, $sortDirection);
}
$bookings = $query->paginate(10)->withQueryString();
$bookings = $query->paginate(20)->withQueryString();
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)
{
$booking = Booking::with(['table', 'user'])->findOrFail($id);
// Pastikan booking yang dilihat adalah milik venue admin
$adminVenueId = auth()->user()->venue_id;
$booking = Booking::with(['table', 'user'])
->whereHas('table', function ($q) use ($adminVenueId) {
$q->where('venue_id', $adminVenueId);
})
->findOrFail($id);
return view('admin.bookings.show', compact('booking'));
}
public function edit($id)
{
$booking = Booking::findOrFail($id);
$tables = Table::all();
$adminVenueId = auth()->user()->venue_id;
// Pastikan booking yang diedit adalah milik venue admin
$booking = Booking::whereHas('table', function ($q) use ($adminVenueId) {
$q->where('venue_id', $adminVenueId);
})->findOrFail($id);
// Hanya tampilkan tables dari venue admin
$tables = Table::where('venue_id', $adminVenueId)->get();
return view('admin.bookings.edit', compact('booking', 'tables'));
}
@ -89,7 +149,18 @@ public function update(Request $request, $id)
'end_time' => 'required|date|after:start_time',
]);
$booking = Booking::findOrFail($id);
$adminVenueId = auth()->user()->venue_id;
// Pastikan booking yang diupdate adalah milik venue admin
$booking = Booking::whereHas('table', function ($q) use ($adminVenueId) {
$q->where('venue_id', $adminVenueId);
})->findOrFail($id);
// Validasi tambahan: pastikan table_id yang dipilih juga milik venue admin
$table = Table::where('id', $request->table_id)
->where('venue_id', $adminVenueId)
->firstOrFail();
$booking->update($request->all());
return redirect()->route('admin.bookings.index')
@ -98,7 +169,12 @@ public function update(Request $request, $id)
public function complete($id)
{
$booking = Booking::findOrFail($id);
$adminVenueId = auth()->user()->venue_id;
$booking = Booking::whereHas('table', function ($q) use ($adminVenueId) {
$q->where('venue_id', $adminVenueId);
})->findOrFail($id);
$booking->status = 'selesai';
$booking->save();
@ -108,7 +184,12 @@ public function complete($id)
public function cancel($id)
{
$booking = Booking::findOrFail($id);
$adminVenueId = auth()->user()->venue_id;
$booking = Booking::whereHas('table', function ($q) use ($adminVenueId) {
$q->where('venue_id', $adminVenueId);
})->findOrFail($id);
$booking->status = 'cancelled';
$booking->save();
@ -118,7 +199,10 @@ public function cancel($id)
public function export(Request $request)
{
$adminVenueId = auth()->user()->venue_id;
$filename = 'bookings-' . Carbon::now()->format('Y-m-d') . '.xlsx';
return Excel::download(new BookingsExport($request), $filename);
// Pass venue_id ke export class jika diperlukan
return Excel::download(new BookingsExport($request, $adminVenueId), $filename);
}
}

View File

@ -29,7 +29,7 @@ public function index(Request $request)
$query->where('status', $request->status);
}
$tables = $query->latest()->paginate(10);
$tables = $query->orderBy('created_at', 'asc')->paginate(10);
return view('admin.tables.index', compact('tables'));
}
@ -55,7 +55,7 @@ public function store(Request $request)
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'brand' => 'required|string|max:255',
'status' => 'required|in:Available,Booked,Unavailable',
// 'status' => 'required|in:Available,Booked,Unavailable',
'price_per_hour' => 'required|numeric|min:0',
]);
@ -68,7 +68,7 @@ public function store(Request $request)
Table::create([
'name' => $request->name,
'brand' => $request->brand,
'status' => $request->status,
// 'status' => $request->status,
'price_per_hour' => $request->price_per_hour,
'venue_id' => auth()->user()->venue_id,
]);
@ -101,7 +101,7 @@ public function update(Request $request, $id)
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'brand' => 'required|string|max:255',
'status' => 'required|in:Available,Booked,Unavailable',
// 'status' => 'required|in:Available,Booked,Unavailable',
'price_per_hour' => 'required|numeric|min:0',
]);
@ -116,7 +116,7 @@ public function update(Request $request, $id)
$table->update([
'name' => $request->name,
'brand' => $request->brand,
'status' => $request->status,
// 'status' => $request->status,
'price_per_hour' => $request->price_per_hour,
]);

View File

@ -0,0 +1,222 @@
<?php
namespace App\Http\Controllers\admin;
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;
use Carbon\Carbon;
class VenueController extends Controller
{
/**
* Display venue management page for current admin
*/
public function index()
{
// Get current admin's venue
$venue = auth()->user()->venue;
if (!$venue) {
return redirect()->route('admin.dashboard')->with('error', 'Anda belum memiliki venue yang ditugaskan.');
}
// Check for auto reopen
if ($venue->checkAutoReopen()) {
session()->flash('success', 'Venue telah dibuka kembali secara otomatis!');
}
return view('admin.venues.index', compact('venue'));
}
/**
* Show the form for editing venue
*/
public function edit()
{
$venue = auth()->user()->venue()->with('images', 'operatingHours')->first(); // Tambahkan 'operatingHours'
if (!$venue) {
return redirect()->route('admin.dashboard')->with('error', 'Anda belum memiliki venue yang ditugaskan.');
}
return view('admin.venues.edit', compact('venue'));
}
/**
* Update venue information
*/
public function update(Request $request)
{
$venue = auth()->user()->venue;
if (!$venue) {
return redirect()->route('admin.dashboard')->with('error', 'Anda belum memiliki venue yang ditugaskan.');
}
// 1. Pra-proses data jam operasional sebelum validasi
$input = $request->all();
if (isset($input['hours'])) {
foreach ($input['hours'] as $day => &$data) { // Gunakan '&' untuk modifikasi langsung
// Jika 'is_closed' dicentang, atau jika input waktu kosong, paksa menjadi null
if (isset($data['is_closed']) && $data['is_closed'] == 'on') {
$data['open_time'] = null;
$data['close_time'] = null;
}
}
}
// 2. Buat aturan validasi yang lebih pintar
$validator = Validator::make($input, [
'name' => 'required|string|max:255',
'address' => 'required|string|max:500',
'latitude' => 'nullable|string|max:255',
'longitude' => 'nullable|string|max:255',
// ... (validasi lain tetap sama) ...
'image' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
'gallery_images.*' => 'image|mimes:jpeg,png,jpg,gif|max:2048',
'hours' => 'required|array|size:7',
// Aturan baru: open_time wajib diisi KECUALI jika is_closed dicentang
'hours.*.open_time' => 'required_unless:hours.*.is_closed,on|nullable|date_format:H:i',
'hours.*.close_time' => 'required_unless:hours.*.is_closed,on|nullable|date_format:H:i',
'link_instagram' => 'nullable|url',
'link_tiktok' => 'nullable|url',
'link_facebook' => 'nullable|url',
'link_x' => 'nullable|url',
]);
if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput();
}
try {
// (Sisa dari kode ini tidak perlu diubah)
$imagePath = $venue->image;
if ($request->hasFile('image')) { /* ... logika upload ... */ }
if ($request->hasFile('gallery_images')) { /* ... logika upload galeri ... */ }
$venue->update([
'name' => $request->name,
'address' => $request->address,
'latitude' => $request->latitude,
'longitude' => $request->longitude,
'phone' => $request->phone,
'link_instagram' => $request->link_instagram,
'link_tiktok' => $request->link_tiktok,
'link_facebook' => $request->link_facebook,
'link_x' => $request->link_x,
'description' => $request->description,
'image' => $imagePath,
]);
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,
]
);
}
return redirect()->route('admin.venue.index')
->with('success', 'Informasi venue berhasil diperbarui!');
} catch (\Exception $e) {
return redirect()->back()
->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)
*/
public function toggleStatus(Request $request)
{
$venue = auth()->user()->venue;
if (!$venue) {
return response()->json(['error' => 'Venue tidak ditemukan'], 404);
}
try {
if ($venue->status === 'open') {
// Closing venue - validate required fields
$validator = Validator::make($request->all(), [
'close_reason' => 'required|string|max:500',
'reopen_date' => 'required|date|after:today',
], [
'close_reason.required' => 'Alasan penutupan harus diisi.',
'reopen_date.required' => 'Tanggal buka kembali harus diisi.',
'reopen_date.date' => 'Format tanggal tidak valid.',
'reopen_date.after' => 'Tanggal buka kembali harus setelah hari ini.',
]);
if ($validator->fails()) {
return response()->json([
'error' => 'Validasi gagal',
'errors' => $validator->errors()
], 422);
}
$venue->closeVenue($request->close_reason, $request->reopen_date);
$message = 'Venue berhasil ditutup!';
} else {
// Opening venue
$venue->openVenue();
$message = 'Venue berhasil dibuka!';
}
return response()->json([
'success' => true,
'message' => $message,
'status' => $venue->status,
'venue' => [
'status' => $venue->status,
'close_reason' => $venue->close_reason,
'reopen_date' => $venue->reopen_date ? $venue->reopen_date->format('d M Y') : null,
'open_time' => $venue->open_time ? Carbon::parse($venue->open_time)->format('H:i') : null,
'close_time' => $venue->close_time ? Carbon::parse($venue->close_time)->format('H:i') : null,
]
]);
} catch (\Exception $e) {
return response()->json([
'error' => 'Terjadi kesalahan: ' . $e->getMessage()
], 500);
}
}
}

View File

@ -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,169 +24,208 @@ public function __construct(MidtransService $midtransService)
$this->midtransService = $midtransService;
}
// Tambahkan method baru untuk booking langsung oleh admin
public function adminDirectBooking(Request $request) {
try {
$request->validate([
'table_id' => 'required|exists:tables,id',
'start_time' => 'required|date',
'end_time' => 'required|date|after:start_time',
]);
$user = Auth::user();
// Validasi bahwa user adalah admin dan mengelola venue dari meja tersebut
$table = Table::findOrFail($request->table_id);
if ($user->role !== 'admin' || $user->venue_id !== $table->venue_id) {
return response()->json([
'message' => 'Unauthorized action'
], 403);
public function showQrCode(Booking $booking)
{
// Otorisasi: Pastikan user yang login adalah pemilik booking
if ($booking->user_id !== Auth::id()) {
abort(403);
}
// Cek konflik booking
$conflict = Booking::where('table_id', $request->table_id)
->where(function($query) use ($request) {
$query->whereBetween('start_time', [$request->start_time, $request->end_time])
->orWhere(function($query) use ($request) {
$query->where('start_time', '<', $request->start_time)
->where('end_time', '>', $request->start_time);
});
})
->where('status', 'paid')
// Pastikan booking memiliki token validasi
if (!$booking->validation_token) {
abort(404, 'QR Code tidak ditemukan.');
}
// Buat gambar QR Code dari token validasi
$qrCode = QrCode::size(300)->generate($booking->validation_token);
// Kembalikan sebagai respons gambar SVG
return response($qrCode)->header('Content-Type', 'image/svg+xml');
}
// Tambahkan method baru untuk booking langsung oleh admin
// Ganti seluruh fungsi adminDirectBooking dengan ini
public function adminDirectBooking($request) {
try {
$data = $request instanceof \Illuminate\Http\Request ? $request->all() : $request->toArray();
if (!isset($data['table_id']) || !isset($data['start_time']) || !isset($data['end_time'])) {
return response()->json(['message' => 'Missing required fields'], 400);
}
$user = Auth::user();
$table = Table::with(['venue.operatingHours'])->findOrFail($data['table_id']);
$venue = $table->venue;
if ($user->role !== 'admin' || $user->venue_id !== $table->venue_id) {
return response()->json(['message' => 'Unauthorized action'], 403);
}
$startDateTime = Carbon::parse($data['start_time']);
$endDateTime = Carbon::parse($data['end_time']);
// --- LOGIKA BARU: Validasi Jam Operasional Harian ---
$bookingDate = $startDateTime->copy()->setTimezone('Asia/Jakarta');
$dayOfWeek = $bookingDate->dayOfWeekIso;
$todaysHours = $venue->operatingHours->firstWhere('day_of_week', $dayOfWeek);
if (!$todaysHours || $todaysHours->is_closed) {
return response()->json(['message' => 'Venue tutup pada tanggal yang dipilih.'], 400);
}
$openTimeToday = $todaysHours->open_time;
$closeTimeToday = $todaysHours->close_time;
$isOvernightToday = strtotime($closeTimeToday) < strtotime($openTimeToday);
$operationalDayStart = Carbon::createFromFormat('Y-m-d H:i:s', $startDateTime->format('Y-m-d') . ' ' . $openTimeToday, 'Asia/Jakarta');
if ($isOvernightToday && $startDateTime < $operationalDayStart) {
$operationalDayStart->subDay();
}
$operationalDayEnd = $operationalDayStart->copy()->setTimeFromTimeString($closeTimeToday);
if ($isOvernightToday) {
$operationalDayEnd->addDay();
}
if ($startDateTime->lt($operationalDayStart) || $endDateTime->gt($operationalDayEnd)) {
return response()->json(['message' => 'Waktu booking di luar jam operasional venue.'], 400);
}
// --- AKHIR VALIDASI BARU ---
$conflict = Booking::where('table_id', $data['table_id'])
->whereIn('status', ['paid', 'pending'])
->where('start_time', '<', $endDateTime)
->where('end_time', '>', $startDateTime)
->exists();
if ($conflict) {
return response()->json(['message' => 'Meja sudah dibooking di jam tersebut'], 409);
}
// Hitung total biaya (meskipun admin tidak membayar, kita tetap catat nilainya)
$startTime = Carbon::parse($request->start_time);
$endTime = Carbon::parse($request->end_time);
$duration = $endTime->diffInHours($startTime);
// Sisa fungsi tidak berubah
$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' => $request->table_id,
'table_id' => $data['table_id'],
'user_id' => $user->id,
'start_time' => $request->start_time,
'end_time' => $request->end_time,
'status' => 'paid', // langsung set sebagai paid
'start_time' => $startDateTime,
'end_time' => $endDateTime,
'status' => 'paid',
'total_amount' => $totalAmount,
'payment_id' => null, // Admin tidak perlu payment_id
'payment_method' => 'admin_direct', // Tandai sebagai booking langsung admin
'payment_method' => 'admin_direct',
'order_id' => $adminOrderId,
'validation_token' => (string) Str::uuid(),
]);
// Update table status menjadi Booked
$table->update(['status' => 'Booked']);
return response()->json([
'message' => 'Booking created successfully',
'booking_id' => $booking->id
'success' => true,
'message' => 'Booking berhasil dibuat oleh admin',
'booking_id' => $booking->id,
// ... (detail booking lainnya)
]);
} catch (\Exception $e) {
\Log::error('Admin direct booking error:', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return response()->json([
'message' => 'Failed to create booking: ' . $e->getMessage()
], 500);
}
\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|date',
'end_time' => 'required|date|after:start_time',
'start_time' => 'required',
'duration' => 'required|integer|min:1|max:12',
'booking_date' => 'required|date_format:Y-m-d',
]);
$user = Auth::user();
$table = Table::findOrFail($request->table_id);
$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($request);
return $this->adminDirectBooking(collect([
'table_id' => $request->table_id,
'start_time' => $startDateTime->toDateTimeString(),
'end_time' => $endDateTime->toDateTimeString(),
]));
}
// Cek apakah meja sedang dibooking pada waktu tersebut (hanya yang sudah paid)
// Cek konflik booking (tidak berubah)
$conflict = Booking::where('table_id', $request->table_id)
->where(function($query) use ($request) {
$query->whereBetween('start_time', [$request->start_time, $request->end_time])
->orWhere(function($query) use ($request) {
$query->where('start_time', '<', $request->start_time)
->where('end_time', '>', $request->start_time);
});
->where('status', 'paid')
->where(function($query) use ($startDateTime, $endDateTime) {
$query->where('start_time', '<', $endDateTime)
->where('end_time', '>', $startDateTime);
})
->where('status', 'paid') // Hanya cek yang sudah paid
->exists();
if ($conflict) {
return response()->json(['message' => 'Meja sudah dibooking di jam tersebut'], 409);
return response()->json(['success' => false, 'message' => 'Meja sudah dibooking di jam tersebut'], 409);
}
// Hitung total biaya
$table = Table::findOrFail($request->table_id);
$startTime = Carbon::parse($request->start_time);
$endTime = Carbon::parse($request->end_time);
$duration = $endTime->diffInHours($startTime);
// Proses ke Midtrans (tidak berubah)
$totalAmount = $duration * $table->price_per_hour;
// Simpan data booking sementara di session untuk digunakan setelah pembayaran
Session::put('temp_booking', [
'table_id' => $request->table_id,
'user_id' => Auth::id(),
'start_time' => $request->start_time,
'end_time' => $request->end_time,
'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 untuk bisa dilanjutkan nanti
PendingBooking::updateOrCreate(
[
'user_id' => Auth::id(),
'table_id' => $request->table_id,
'start_time' => $request->start_time
],
[
'end_time' => $request->end_time,
'total_amount' => $totalAmount,
'order_id' => $tempOrderId,
'expired_at' => now()->addHours(24), // Kadaluarsa dalam 24 jam
]
['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 tanpa menyimpan booking
$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
]);
return response()->json([
'message' => 'Payment intent created, proceed to payment',
'total_amount' => $totalAmount,
'success' => true,
'snap_token' => $snapToken,
'order_id' => $tempOrderId
]);
} catch (\Exception $e) {
\Log::error('Payment intent error:', [
'message' => $e->getMessage(),
@ -192,6 +233,7 @@ public function createPaymentIntent(Request $request) {
]);
return response()->json([
'success' => false,
'message' => 'Gagal membuat transaksi: ' . $e->getMessage()
], 500);
}
@ -243,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
@ -285,19 +328,38 @@ 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')
$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);
}
@ -481,40 +543,43 @@ public function deletePendingBooking($id)
}
}
public function showReschedule($id)
{
// GANTI SELURUH FUNGSI showReschedule DENGAN YANG INI
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.');
// 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).');
}
// 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'));
// --- 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.
*/
@ -577,23 +642,36 @@ 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'
]);
$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'])
// Query untuk mengambil booking lain di meja yang sama
$query = Booking::where('table_id', $table->id)
->where('id', '!=', $request->booking_id) // Jangan ikut sertakan booking yang sedang di-reschedule
->where('status', 'paid');
// --- LOGIKA OVERNIGHT DITERAPKAN DI SINI ---
if ($venue->is_overnight) {
// Ambil booking dari jam buka di hari H sampai jam tutup di hari H+1
$startOperationalDay = $requestDate->copy()->setTimeFromTimeString($venue->open_time);
$endOperationalDay = $requestDate->copy()->addDay()->setTimeFromTimeString($venue->close_time);
$query->whereBetween('start_time', [$startOperationalDay, $endOperationalDay]);
} else {
// Logika standar untuk venue yang tidak overnight
$query->whereDate('start_time', $requestDate);
}
// --- AKHIR DARI LOGIKA OVERNIGHT ---
$bookings = $query->get(['start_time', 'end_time'])
->map(function ($booking) {
return [
'start' => Carbon::parse($booking->start_time)->format('H:i'),
@ -602,5 +680,5 @@ public function checkRescheduleAvailability(Request $request)
});
return response()->json($bookings);
}
}
}

View File

@ -12,7 +12,7 @@ public function index()
{
$bookings = Booking::where('user_id', Auth::id())
->with(['table.venue'])
->orderBy('start_time', 'desc')
->orderBy('created_at', 'desc')
->paginate(10);
return view('pages.booking-history', compact('bookings'));

View File

@ -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'));
}
}

View File

@ -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';

View File

@ -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);
}
}

34
app/Models/Review.php Normal file
View File

@ -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);
}
}

View File

@ -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.
*/

View File

@ -4,15 +4,126 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
use App\Models\VenueImage; // <-- Tambahkan ini
class Venue extends Model
{
use HasFactory;
protected $fillable = ['name', 'location', 'address', 'image'];
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',
'latitude',
'longitude',
'image',
'phone',
'link_instagram',
'link_tiktok',
'link_facebook',
'link_x',
'description',
'open_time',
'close_time',
'status',
'close_reason',
'reopen_date',
'original_open_time',
'original_close_time',
];
protected $dates = [
'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);
}
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
*/
public function checkAutoReopen()
{
if ($this->status === 'close' && $this->reopen_date && Carbon::today()->gte($this->reopen_date)) {
$this->update([
'status' => 'open',
'open_time' => $this->original_open_time,
'close_time' => $this->original_close_time,
'close_reason' => null,
'reopen_date' => null,
'original_open_time' => null,
'original_close_time' => null,
]);
return true;
}
return false;
}
/**
* Close venue with reason and reopen date
*/
public function closeVenue($reason, $reopenDate)
{
// Simpan jam operasional saat ini sebelum mengubahnya
$currentOpenTime = $this->open_time;
$currentCloseTime = $this->close_time;
$this->update([
'status' => 'close',
'close_reason' => $reason,
'reopen_date' => $reopenDate,
'original_open_time' => $currentOpenTime, // Simpan jam asli
'original_close_time' => $currentCloseTime, // Simpan jam asli
'open_time' => '00:00', // Set ke 00:00 setelah menyimpan original
'close_time' => '00:00', // Set ke 00:00 setelah menyimpan original
]);
}
/**
* Open venue manually
*/
public function openVenue()
{
$this->update([
'status' => 'open',
'open_time' => $this->original_open_time ?: $this->open_time,
'close_time' => $this->original_close_time ?: $this->close_time,
'close_reason' => null,
'reopen_date' => null,
'original_open_time' => null,
'original_close_time' => null,
]);
}
}

18
app/Models/VenueImage.php Normal file
View File

@ -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);
}
}

View File

@ -2,6 +2,7 @@
namespace App\Providers;
use Carbon\Carbon;
use Illuminate\Support\ServiceProvider;
use App\Services\MidtransService;
@ -20,8 +21,8 @@ public function register(): void
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
public function boot()
{
Carbon::setLocale('id');
}
}

View File

@ -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",

174
composer.lock generated
View File

@ -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",

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('venues', function (Blueprint $table) {
$table->enum('status', ['open', 'close'])->default('open')->after('close_time');
$table->text('close_reason')->nullable()->after('status');
$table->date('reopen_date')->nullable()->after('close_reason');
$table->time('original_open_time')->nullable()->after('reopen_date');
$table->time('original_close_time')->nullable()->after('original_open_time');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('venues', function (Blueprint $table) {
$table->dropColumn(['status', 'close_reason', 'reopen_date', 'original_open_time', 'original_close_time']);
});
}
};

View File

@ -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');
}
}

View File

@ -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');
}
};

View File

@ -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']);
});
}
};

View File

@ -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']);
});
}
};

View File

@ -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');
}
};

View File

@ -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');
});
}
};

View File

@ -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

View File

@ -8,14 +8,17 @@
</h1>
<div class="flex flex-col md:flex-row gap-3 w-full md:w-auto">
<a href="{{ route('admin.dashboard') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left mr-1"></i> Kembali
</a>
{{-- <a href="{{ route('admin.bookings.export') }}" class="btn btn-success">
<i class="fas fa-file-excel mr-1"></i> Export Excel
</a> --}}
</div>
<a href="/"
class="inline-flex items-center justify-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg shadow transition duration-300 w-full md:w-auto text-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
clip-rule="evenodd" />
</svg>
Buat Booking
</a>
</div>
</div>
<!-- Search and Filter Card -->
<div class="bg-white rounded-lg shadow-md p-4 mb-6">
@ -33,7 +36,7 @@ class="form-input w-full rounded-md border-gray-300 focus:border-blue-500 focus:
</div>
</div>
<div class="w-full md:w-1/4">
{{-- <div class="w-full md:w-1/4">
<label for="status" class="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select name="status" id="status"
class="form-select w-full rounded-md border-gray-300 focus:border-blue-500 focus:ring focus:ring-blue-200">
@ -43,7 +46,7 @@ class="form-select w-full rounded-md border-gray-300 focus:border-blue-500 focus
<option value="cancelled" {{ request('status') == 'cancelled' ? 'selected' : '' }}>Dibatalkan
</option>
</select>
</div>
</div> --}}
<div class="w-full md:w-1/4">
<label for="date_from" class="block text-sm font-medium text-gray-700 mb-1">Dari Tanggal</label>
@ -251,13 +254,13 @@ class="inline">
</div>
<div class="px-4 py-3 bg-gray-50 border-t border-gray-200">
<div class="flex items-center justify-between">
<div class="flex flex-col md:flex-row items-center justify-between space-y-2 md:space-y-0">
<div class="text-sm text-gray-500">
Menampilkan {{ $bookings->firstItem() ?? 0 }} - {{ $bookings->lastItem() ?? 0 }} dari
{{ $bookings->total() }} data
</div>
<div>
{{ $bookings->appends(request()->query())->links() }}
<div class="flex justify-center">
{{ $bookings->appends(request()->query())->onEachSide(1)->links() }}
</div>
</div>
</div>

View File

@ -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

View File

@ -4,17 +4,18 @@
<div class="bg-gray-50 min-h-screen">
<div class="p-6">
<!-- Header and Welcome -->
<div class="flex justify-between items-center mb-6">
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-6 space-y-4 sm:space-y-0">
<div>
<h1 class="text-3xl font-bold text-gray-800">Dashboard {{ $venue->name }}</h1>
<h1 class="text-2xl sm:text-3xl font-bold text-gray-800">Dashboard {{ $venue->name }}</h1>
<p class="text-gray-600 mt-1">Selamat datang, {{ auth()->user()->name }}!</p>
</div>
<div class="text-right">
<p class="text-sm text-gray-500">{{ now()->format('l, d F Y') }}</p>
<p class="text-2xl font-semibold text-gray-800">{{ now()->format('H:i') }}</p>
<div class="text-left sm:text-right">
<p class="text-sm text-gray-500">{{ now()->translatedFormat('l, d F Y') }}</p>
<p class="text-xl sm:text-2xl font-semibold text-gray-800">{{ now()->translatedFormat('H:i') }}</p>
</div>
</div>
<!-- Stats Cards - Row 1: Revenue and Booking Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<!-- Today's Revenue -->
@ -53,8 +54,8 @@
</div>
</div>
<div class="flex mt-2 space-x-4">
<p class="text-xs text-gray-500">Pending: <span
class="font-semibold text-amber-500">{{ $pendingBookings }}</span></p>
{{-- <p class="text-xs text-gray-500">Pending: <span class="font-semibold text-amber-500">{{
$pendingBookings }}</span></p> --}}
<p class="text-xs text-gray-500">Paid: <span
class="font-semibold text-green-500">{{ $paidBookings }}</span></p>
</div>

View File

@ -0,0 +1,177 @@
@extends('layouts.admin')
@section('content')
<div class="p-6">
<!-- Page Header -->
<div class="mb-8">
<h1 class="text-2xl font-bold text-gray-900">Pengaturan Profile</h1>
<p class="text-gray-600 mt-1">Kelola informasi akun dan keamanan Anda</p>
</div>
<!-- Flash Messages -->
@if(session('success'))
<div class="mb-6 bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-lg">
{{ session('success') }}
</div>
@endif
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Profile Information Card -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200">
<div class="p-6 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900">Informasi Profile</h2>
<p class="text-sm text-gray-600 mt-1">Perbarui informasi dasar akun Anda</p>
</div>
<form action="{{ route('admin.profile.update') }}" method="POST" class="p-6">
@csrf
@method('PUT')
<!-- Profile Avatar -->
<div class="flex items-center space-x-4 mb-6">
<div
class="w-16 h-16 bg-blue-600 rounded-full flex items-center justify-center text-white font-bold text-xl">
{{ substr($user->name, 0, 1) }}
</div>
<div>
<h3 class="font-medium text-gray-900">{{ $user->name }}</h3>
<p class="text-sm text-gray-500">{{ ucfirst($user->role) }}</p>
@if($user->venue)
<p class="text-sm text-blue-600">{{ $user->venue->name }}</p>
@endif
</div>
</div>
<!-- Name Field -->
<div class="mb-4">
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">
Nama Lengkap
</label>
<input type="text" id="name" name="name" value="{{ old('name', $user->name) }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent @error('name') border-red-500 @enderror">
@error('name')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<!-- Email Field -->
<div class="mb-6">
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">
Email
</label>
<input type="email" id="email" name="email" value="{{ old('email', $user->email) }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent @error('email') border-red-500 @enderror"
readonly>
@error('email')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<!-- Account Info -->
<div class="mb-6 p-4 bg-gray-50 rounded-lg">
<h4 class="font-medium text-gray-900 mb-2">Informasi Akun</h4>
<div class="text-sm text-gray-600 space-y-1">
<p><span class="font-medium">Role:</span> {{ ucfirst($user->role) }}</p>
<p><span class="font-medium">Bergabung:</span> {{ $user->created_at->format('d M Y') }}</p>
@if($user->email_verified_at)
<p><span class="font-medium">Status Email:</span>
<span class="text-green-600"> Terverifikasi</span>
</p>
@else
<p><span class="font-medium">Status Email:</span>
<span class="text-red-600"> Belum Terverifikasi</span>
</p>
@endif
</div>
</div>
<!-- Submit Button -->
<button type="submit"
class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition duration-200">
Perbarui Profile
</button>
</form>
</div>
<!-- Change Password Card -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200">
<div class="p-6 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900">Ubah Password</h2>
<p class="text-sm text-gray-600 mt-1">Pastikan akun Anda menggunakan password yang kuat</p>
</div>
<form action="{{ route('admin.profile.password') }}" method="POST" class="p-6">
@csrf
@method('PUT')
<!-- Current Password -->
<div class="mb-4">
<label for="current_password" class="block text-sm font-medium text-gray-700 mb-2">
Password Saat Ini
</label>
<input type="password" id="current_password" name="current_password"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent @error('current_password') border-red-500 @enderror">
@error('current_password')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<!-- New Password -->
<div class="mb-4">
<label for="password" class="block text-sm font-medium text-gray-700 mb-2">
Password Baru
</label>
<input type="password" id="password" name="password"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent @error('password') border-red-500 @enderror">
@error('password')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<!-- Confirm Password -->
<div class="mb-6">
<label for="password_confirmation" class="block text-sm font-medium text-gray-700 mb-2">
Konfirmasi Password Baru
</label>
<input type="password" id="password_confirmation" name="password_confirmation"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
</div>
<!-- Password Requirements -->
<div class="mb-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<h4 class="font-medium text-yellow-800 mb-2">Persyaratan Password:</h4>
<ul class="text-sm text-yellow-700 space-y-1">
<li> Minimal 8 karakter</li>
<li> Kombinasi huruf besar dan kecil</li>
<li> Minimal satu angka</li>
<li> Minimal satu karakter khusus</li>
</ul>
</div>
<!-- Submit Button -->
<button type="submit"
class="w-full bg-red-600 hover:bg-red-700 text-white font-medium py-2 px-4 rounded-lg transition duration-200">
Ubah Password
</button>
</form>
</div>
</div>
<!-- Security Notice -->
<div class="mt-8 bg-blue-50 border border-blue-200 rounded-lg p-6">
<div class="flex items-start space-x-3">
<svg class="w-5 h-5 text-blue-600 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<div>
<h3 class="font-medium text-blue-900">Tips Keamanan</h3>
<p class="text-sm text-blue-700 mt-1">
Selalu gunakan password yang unik dan kuat. Jangan bagikan informasi login Anda kepada siapa pun.
Jika Anda mencurigai adanya aktivitas yang tidak biasa, segera ubah password Anda.
</p>
</div>
</div>
</div>
</div>
@endsection

View File

@ -49,19 +49,19 @@ class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none foc
</div>
</div>
<div>
{{-- <div>
<label for="status" class="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select id="status" name="status"
class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 @error('status') border-red-500 @enderror">
<option value="Available" {{ old('status') === 'Available' ? 'selected' : '' }}>Available</option>
<option value="Booked" {{ old('status') === 'Booked' ? 'selected' : '' }}>Booked</option>
<option value="Unavailable" {{ old('status') === 'Unavailable' ? 'selected' : '' }}>Unavailable
<option value="Available" {{ old('status')==='Available' ? 'selected' : '' }}>Available</option>
<option value="Booked" {{ old('status')==='Booked' ? 'selected' : '' }}>Booked</option>
<option value="Unavailable" {{ old('status')==='Unavailable' ? 'selected' : '' }}>Unavailable
</option>
</select>
@error('status')
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
</div> --}}
<div class="flex justify-end space-x-3">
<a href="{{ route('admin.tables.index') }}"

View File

@ -50,7 +50,7 @@ class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none foc
</div>
</div>
<div>
{{-- <div>
<label for="status" class="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select id="status" name="status"
class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 @error('status') border-red-500 @enderror">
@ -58,12 +58,13 @@ class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none foc
Available</option>
<option value="Booked" {{ old('status', $table->status) === 'Booked' ? 'selected' : '' }}>Booked
</option>
<option value="Unavailable" {{ old('status', $table->status) === 'Unavailable' ? 'selected' : '' }}>Unavailable</option>
<option value="Unavailable" {{ old('status', $table->status) === 'Unavailable' ? 'selected' : ''
}}>Unavailable</option>
</select>
@error('status')
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
</div> --}}
<div class="flex justify-end space-x-3">
<a href="{{ route('admin.tables.index') }}"

View File

@ -3,10 +3,10 @@
@section('content')
<div class="p-6 bg-gray-50">
<!-- Header dengan Action Button -->
<div class="flex justify-between items-center mb-6">
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6">
<h1 class="text-3xl font-bold text-gray-800">Kelola Meja</h1>
<a href="{{ route('admin.tables.create') }}"
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center transition-colors duration-300">
class="w-full sm:w-auto bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center justify-center transition-colors duration-300">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
@ -16,6 +16,7 @@ class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-
</a>
</div>
<!-- Flash Message -->
@if(session('success'))
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 mb-6 rounded shadow-md" role="alert">
@ -38,16 +39,16 @@ class="md:flex items-center space-y-4 md:space-y-0 md:space-x-4">
<input type="text" name="search" value="{{ request('search') }}" placeholder="Cari nama meja..."
class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="md:w-1/4">
{{-- <div class="md:w-1/4">
<select name="status"
class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">Semua Status</option>
<option value="Available" {{ request('status') == 'Available' ? 'selected' : '' }}>Available</option>
<option value="Booked" {{ request('status') == 'Booked' ? 'selected' : '' }}>Booked</option>
<option value="Unavailable" {{ request('status') == 'Unavailable' ? 'selected' : '' }}>Unavailable
<option value="Available" {{ request('status')=='Available' ? 'selected' : '' }}>Available</option>
<option value="Booked" {{ request('status')=='Booked' ? 'selected' : '' }}>Booked</option>
<option value="Unavailable" {{ request('status')=='Unavailable' ? 'selected' : '' }}>Unavailable
</option>
</select>
</div>
</div> --}}
<div class="flex space-x-2">
<button type="submit"
class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors duration-300">

View File

@ -0,0 +1,205 @@
@extends('layouts.admin')
@section('content')
<div class="p-6">
<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>
</a>
<div>
<h1 class="text-2xl font-bold text-gray-900">Edit Venue</h1>
<p class="text-gray-600 mt-1">Perbarui informasi venue Anda</p>
</div>
</div>
</div>
@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>
@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">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<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-x-8 gap-y-6">
<div class="space-y-6">
<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" required>
</div>
<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" required>{{ old('address', $venue->address) }}</textarea>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<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="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>
<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 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>
<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>
<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">Simpan Perubahan</button>
</div>
</form>
</div>
</div>
<script>
function deleteImage(imageId, event) {
event.preventDefault();
if (!confirm('Apakah Anda yakin ingin menghapus gambar ini?')) return;
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

View File

@ -0,0 +1,427 @@
@extends('layouts.admin')
@section('content')
<div class="p-6">
<!-- Header -->
<div class="mb-6">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<!-- Judul -->
<div>
<h1 class="text-2xl font-bold text-gray-900">Kelola Venue</h1>
<p class="text-gray-600 mt-1">Kelola informasi venue Anda</p>
</div>
<!-- Aksi -->
<div class="flex flex-col sm:flex-row sm:items-center gap-4">
<!-- Status Venue -->
<div class="flex items-center space-x-3">
<span class="text-sm font-medium text-gray-700 whitespace-nowrap">Status Venue:</span>
<div class="relative">
<button id="statusToggle"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 {{ $venue->status === 'open' ? 'bg-green-600' : 'bg-red-600' }}"
onclick="toggleVenueStatus()">
<span class="sr-only">Toggle venue status</span>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform {{ $venue->status === 'open' ? 'translate-x-6' : 'translate-x-1' }}"></span>
</button>
</div>
<span id="statusText"
class="text-sm font-medium {{ $venue->status === 'open' ? 'text-green-600' : 'text-red-600' }}">
{{ $venue->status === 'open' ? 'Buka' : 'Tutup' }}
</span>
</div>
<!-- Tombol Edit -->
<a href="{{ route('admin.venue.edit') }}"
class="flex items-center justify-center bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium px-4 py-2 rounded-lg shadow transition duration-300 w-full sm:w-auto">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Edit Venue
</a>
</div>
</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>
@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
<!-- Venue Information Card -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200">
<div class="p-6">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Venue Image -->
<div class="lg:col-span-1">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Foto Venue</h3>
<div class="aspect-w-16 aspect-h-9 rounded-lg overflow-hidden bg-gray-100">
@if($venue->image)
<img src="{{ asset('storage/' . $venue->image) }}" alt="{{ $venue->name }}"
class="w-full h-48 object-cover rounded-lg">
@else
<div class="w-full h-48 bg-gray-200 rounded-lg flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-gray-400" 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>
</div>
<!-- Venue Details -->
<div class="lg:col-span-2">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Informasi Venue</h3>
<div class="space-y-4">
<!-- Venue Name -->
<div class="flex items-start">
<div class="flex-shrink-0 w-32">
<span class="text-sm font-medium text-gray-500">Nama Venue:</span>
</div>
<div class="flex-1">
<span class="text-sm text-gray-900 font-medium">{{ $venue->name }}</span>
</div>
</div>
<!-- Address -->
<div class="flex items-start">
<div class="flex-shrink-0 w-32">
<span class="text-sm font-medium text-gray-500">Alamat:</span>
</div>
<div class="flex-1">
<span class="text-sm text-gray-900">{{ $venue->address }}</span>
</div>
</div>
<!-- Phone -->
<div class="flex items-start">
<div class="flex-shrink-0 w-32">
<span class="text-sm font-medium text-gray-500">Telepon:</span>
</div>
<div class="flex-1">
<span class="text-sm text-gray-900">{{ $venue->phone ?: '-' }}</span>
</div>
</div>
<!-- Status -->
<div class="flex items-start">
<div class="flex-shrink-0 w-32">
<span class="text-sm font-medium text-gray-500">Status:</span>
</div>
<div class="flex-1">
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $venue->status === 'open' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}">
{{ $venue->status === 'open' ? 'Buka' : 'Tutup' }}
</span>
</div>
</div>
<!-- Operating Hours -->
<div class="flex items-start">
<div class="flex-shrink-0 w-32">
<span class="text-sm font-medium text-gray-500">Jam Operasional:</span>
</div>
<div class="flex-1">
@if($venue->status === 'open')
<span class="text-sm text-gray-900">
{{ $venue->open_time ? \Carbon\Carbon::parse($venue->open_time)->format('H:i') : '-' }}
-
{{ $venue->close_time ? \Carbon\Carbon::parse($venue->close_time)->format('H:i') : '-' }}
</span>
@else
<span class="text-sm text-red-600">Tutup Sementara</span>
@if($venue->original_open_time && $venue->original_close_time)
<div class="text-xs text-gray-500 mt-1">
Jam normal: {{ \Carbon\Carbon::parse($venue->original_open_time)->format('H:i') }} -
{{ \Carbon\Carbon::parse($venue->original_close_time)->format('H:i') }}
</div>
@endif
@endif
</div>
</div>
<!-- Description -->
<div class="flex items-start">
<div class="flex-shrink-0 w-32">
<span class="text-sm font-medium text-gray-500">Deskripsi:</span>
</div>
<div class="flex-1">
<p class="text-sm text-gray-900">{{ $venue->description ?: 'Belum ada deskripsi' }}</p>
</div>
</div>
<!-- Last Updated -->
<div class="flex items-start">
<div class="flex-shrink-0 w-32">
<span class="text-sm font-medium text-gray-500">Terakhir Diperbarui:</span>
</div>
<div class="flex-1">
<span
class="text-sm text-gray-900">{{ $venue->updated_at->format('d M Y, H:i') }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Close Venue Modal -->
<div id="closeVenueModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden">
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<div class="mt-3">
<div class="flex items-center">
<div
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
<svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
</div>
<div class="mt-2 text-center">
<h3 class="text-lg leading-6 font-medium text-gray-900">Tutup Venue</h3>
<div class="mt-4 text-left">
<form id="closeVenueForm">
<div class="mb-4">
<label for="closeReason" class="block text-sm font-medium text-gray-700 mb-2">Alasan
Penutupan *</label>
<textarea id="closeReason" name="close_reason" rows="3"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
placeholder="Masukkan alasan penutupan venue..." required></textarea>
<div id="closeReasonError" class="text-red-500 text-sm mt-1 hidden"></div>
</div>
<div class="mb-4">
<label for="reopenDate" class="block text-sm font-medium text-gray-700 mb-2">Tanggal
Buka
Kembali *</label>
<input type="date" id="reopenDate" name="reopen_date"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
min="{{ date('Y-m-d', strtotime('+1 day')) }}" required>
<div id="reopenDateError" class="text-red-500 text-sm mt-1 hidden"></div>
</div>
</form>
</div>
</div>
<div class="flex justify-end space-x-3 mt-4">
<button type="button" onclick="closeModal()"
class="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400 transition-colors">
Batal
</button>
<button type="button" onclick="confirmCloseVenue()"
class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors">
Tutup Venue
</button>
</div>
</div>
</div>
</div>
<!-- Loading Spinner -->
<div id="loadingSpinner" class="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center hidden">
<div class="bg-white p-4 rounded-lg">
<div class="flex items-center space-x-3">
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
<span class="text-gray-700">Memproses...</span>
</div>
</div>
</div>
<script>
let currentVenueStatus = '{{ $venue->status }}';
function toggleVenueStatus() {
if (currentVenueStatus === 'open') {
// Show close venue modal
document.getElementById('closeVenueModal').classList.remove('hidden');
} else {
// Open venue directly
confirmToggleStatus();
}
}
function closeModal() {
document.getElementById('closeVenueModal').classList.add('hidden');
// Clear form
document.getElementById('closeVenueForm').reset();
clearErrors();
}
function clearErrors() {
document.getElementById('closeReasonError').classList.add('hidden');
document.getElementById('reopenDateError').classList.add('hidden');
}
function confirmCloseVenue() {
const closeReason = document.getElementById('closeReason').value.trim();
const reopenDate = document.getElementById('reopenDate').value;
// Clear previous errors
clearErrors();
// Validate form
let hasError = false;
if (!closeReason) {
document.getElementById('closeReasonError').textContent = 'Alasan penutupan harus diisi.';
document.getElementById('closeReasonError').classList.remove('hidden');
hasError = true;
}
if (!reopenDate) {
document.getElementById('reopenDateError').textContent = 'Tanggal buka kembali harus diisi.';
document.getElementById('reopenDateError').classList.remove('hidden');
hasError = true;
} else {
const today = new Date();
const selectedDate = new Date(reopenDate);
if (selectedDate <= today) {
document.getElementById('reopenDateError').textContent = 'Tanggal buka kembali harus setelah hari ini.';
document.getElementById('reopenDateError').classList.remove('hidden');
hasError = true;
}
}
if (hasError) {
return;
}
// Close modal and proceed with toggle
closeModal();
confirmToggleStatus({
close_reason: closeReason,
reopen_date: reopenDate
});
}
function confirmToggleStatus(data = {}) {
// Show loading spinner
document.getElementById('loadingSpinner').classList.remove('hidden');
// Prepare request data
const requestData = {
_token: '{{ csrf_token() }}',
...data
};
fetch('{{ route("admin.venue.toggle-status") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify(requestData)
})
.then(response => response.json())
.then(data => {
// Hide loading spinner
document.getElementById('loadingSpinner').classList.add('hidden');
if (data.success) {
// Update UI
updateVenueStatusUI(data.venue);
// Show success message
showAlert(data.message, 'success');
// Update current status
currentVenueStatus = data.status;
// Reload page after 2 seconds to refresh all data
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
if (data.errors) {
// Handle validation errors
let errorMessage = 'Terjadi kesalahan validasi:\n';
Object.values(data.errors).forEach(error => {
errorMessage += '- ' + error[0] + '\n';
});
showAlert(errorMessage, 'error');
} else {
showAlert(data.error || 'Terjadi kesalahan yang tidak diketahui', 'error');
}
}
})
.catch(error => {
// Hide loading spinner
document.getElementById('loadingSpinner').classList.add('hidden');
console.error('Error:', error);
showAlert('Venue ditutup sementara!', 'error');
setTimeout(() => {
location.reload();
}, 1000);
});
}
function updateVenueStatusUI(venue) {
const toggle = document.getElementById('statusToggle');
const statusText = document.getElementById('statusText');
const toggleButton = toggle.querySelector('span:last-child');
if (venue.status === 'open') {
toggle.classList.remove('bg-red-600');
toggle.classList.add('bg-green-600');
toggleButton.classList.remove('translate-x-1');
toggleButton.classList.add('translate-x-6');
statusText.textContent = 'Buka';
statusText.classList.remove('text-red-600');
statusText.classList.add('text-green-600');
} else {
toggle.classList.remove('bg-green-600');
toggle.classList.add('bg-red-600');
toggleButton.classList.remove('translate-x-6');
toggleButton.classList.add('translate-x-1');
statusText.textContent = 'Tutup';
statusText.classList.remove('text-green-600');
statusText.classList.add('text-red-600');
}
}
function showAlert(message, type) {
// Remove existing alerts
const existingAlerts = document.querySelectorAll('.alert-message');
existingAlerts.forEach(alert => alert.remove());
// Create new alert
const alertDiv = document.createElement('div');
alertDiv.className = `alert-message mb-6 px-4 py-3 rounded-lg ${type === 'success' ? 'bg-green-100 border border-green-400 text-green-700' : 'bg-red-100 border border-red-400 text-red-700'
}`;
alertDiv.textContent = message;
// Insert alert after header
const header = document.querySelector('.mb-6');
header.parentNode.insertBefore(alertDiv, header.nextSibling);
// Auto remove after 5 seconds
setTimeout(() => {
alertDiv.remove();
}, 5000);
}
// Set minimum date for reopen date input
document.addEventListener('DOMContentLoaded', function () {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const minDate = tomorrow.toISOString().split('T')[0];
document.getElementById('reopenDate').setAttribute('min', minDate);
});
</script>
@endsection

View File

@ -46,11 +46,50 @@
</style>
</head>
<body x-data="{ sidebarOpen: true, userDropdownOpen: false }" class="bg-gray-50">
<body x-data="{
sidebarOpen: getSidebarState(),
userDropdownOpen: false,
toggleSidebar() {
this.sidebarOpen = !this.sidebarOpen;
saveSidebarState(this.sidebarOpen);
}
}" x-init="
// Watch for sidebar changes and save to localStorage
$watch('sidebarOpen', value => saveSidebarState(value))
" class="bg-gray-50">
<script>
// Function to get sidebar state from localStorage
function getSidebarState() {
const saved = localStorage.getItem('admin_sidebar_open');
// Default to true for desktop, false for mobile
if (saved === null) {
return window.innerWidth >= 1024; // lg breakpoint
}
return saved === 'true';
}
// Function to save sidebar state to localStorage
function saveSidebarState(isOpen) {
localStorage.setItem('admin_sidebar_open', isOpen.toString());
}
// Handle responsive behavior on window resize
window.addEventListener('resize', function () {
// Only auto-adjust if no explicit state has been saved
const saved = localStorage.getItem('admin_sidebar_open');
if (saved === null) {
// Auto close on mobile, open on desktop
const shouldOpen = window.innerWidth >= 1024;
Alpine.store('sidebar', { open: shouldOpen });
}
});
</script>
<div class="flex h-screen overflow-hidden">
<!-- Sidebar Overlay -->
<div x-show="sidebarOpen" @click="sidebarOpen = false"
class="fixed inset-0 z-20 bg-black bg-opacity-50 lg:hidden"></div>
<div x-show="sidebarOpen" @click="toggleSidebar()" class="fixed inset-0 z-20 bg-black bg-opacity-50 lg:hidden">
</div>
<!-- Sidebar -->
<div :class="sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0 lg:w-20'"
@ -66,7 +105,7 @@ class="fixed inset-y-0 left-0 z-30 w-64 bg-white shadow-lg transition-all durati
</a>
</div> --}}
</div>
<button @click="sidebarOpen = !sidebarOpen" class="p-1 rounded-md hover:bg-gray-100 focus:outline-none">
<button @click="toggleSidebar()" class="p-1 rounded-md hover:bg-gray-100 focus:outline-none">
<svg x-show="sidebarOpen" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-500"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@ -92,17 +131,28 @@ class="nav-item flex items-center px-3 py-2.5 rounded-lg {{ request()->routeIs('
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
d="M3 3h7v7H3V3zm11 0h7v7h-7V3zM3 14h7v7H3v-7zm11 0h7v7h-7v-7z" />
</svg>
<span x-show="sidebarOpen">Dashboard</span>
</a>
<a href="{{ route('admin.venue.index') }}"
class="nav-item flex items-center px-3 py-2.5 rounded-lg {{ request()->routeIs('admin.venue.*') ? 'active' : '' }}">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 10l9-7 9 7v10a2 2 0 01-2 2h-2a2 2 0 01-2-2V14H9v6a2 2 0 01-2 2H5a2 2 0 01-2-2V10z" />
</svg>
<span x-show="sidebarOpen">Kelola Venue</span>
</a>
<a href="{{ route('admin.tables.index') }}"
class="nav-item flex items-center px-3 py-2.5 rounded-lg {{ request()->routeIs('admin.tables.*') ? 'active' : '' }}">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
<rect width="18" height="10" x="3" y="7" rx="2" ry="2" stroke-width="2"
stroke="currentColor" fill="none" />
<circle cx="12" cy="12" r="1.5" fill="currentColor" />
</svg>
<span x-show="sidebarOpen">Kelola Meja</span>
</a>
@ -112,18 +162,26 @@ class="nav-item flex items-center px-3 py-2.5 rounded-lg {{ request()->routeIs('
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
d="M8 7V3m8 4V3M5 11h14M5 5h14a2 2 0 012 2v12a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2z" />
</svg>
<span x-show="sidebarOpen">Daftar Booking</span>
<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"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
d="M12 8c-1.5 0-3 .75-3 2s1.5 2 3 2 3 .75 3 2-1.5 2-3 2m0-10v10m-6 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span x-show="sidebarOpen">Revenues</span>
<span x-show="sidebarOpen">Laporan Pendapatan</span>
</a>
</nav>
@ -179,8 +237,10 @@ class="ml-auto h-4 w-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="cu
<!-- Dropdown -->
<div x-show="open" @click.outside="open = false"
class="absolute bottom-full left-0 mb-1 w-full bg-white rounded-lg shadow-lg border border-gray-200 py-1 dropdown-transition">
{{-- <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Profile</a>
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Settings</a> --}}
<a href="{{ route('admin.profile.index') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Profile</a>
{{-- <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Settings</a>
--}}
<div class="border-t border-gray-200 my-1"></div>
<form method="POST" action="{{ route('logout') }}">
@csrf
@ -199,7 +259,7 @@ class="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-gray-100">
<!-- Top Header -->
<header class="bg-white shadow-sm lg:hidden">
<div class="px-4 sm:px-6 lg:px-8 py-4 flex items-center justify-between">
<button @click="sidebarOpen = !sidebarOpen"
<button @click="toggleSidebar()"
class="p-1 rounded-md text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 lg:hidden">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">

View File

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Document</title>
<title>Cari Meja</title>
@vite('resources/css/app.css')
{{-- Font | Google Fonts --}}
<link rel="preconnect" href="https://fonts.googleapis.com">
@ -52,14 +52,29 @@ class="flex items-center space-x-2 text-sm font-medium text-gray-700 hover:text-
<div x-show="open" @click.away="open = false" x-transition
class="absolute right-0 mt-2 w-40 bg-white rounded-lg shadow-lg py-2 z-50">
<a href="{{ route('booking.history') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Riwayat Booking
</a>
@if (Auth::user()->role === 'user')
<a href="{{ route('account.settings') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Pengaturan Akun
</a>
@elseif (Auth::user()->role === 'admin')
<a href="{{ url('/admin') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Halaman Admin
</a>
@elseif (Auth::user()->role === 'superadmin')
<a href="{{ url('/superadmin') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Halaman Superadmin
</a>
@endif
@if (Auth::user()->email_verified_at === null)
<a href="{{ route('verification.notice') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
@ -67,6 +82,7 @@ class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Verifikasi Email
</a>
@endif
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit"
@ -92,10 +108,22 @@ class="absolute top-full left-0 right-0 bg-white shadow-md mt-1 p-4 z-50">
class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Riwayat Booking
</a>
@if (Auth::user()->role === 'user')
<a href="{{ route('account.settings') }}"
class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Pengaturan Akun
</a>
@elseif (Auth::user()->role === 'admin')
<a href="{{ url('/admin') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Halaman Admin
</a>
@elseif (Auth::user()->role === 'superadmin')
<a href="{{ url('/superadmin') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Halaman Superadmin
</a>
@endif
@if (Auth::user()->email_verified_at === null)
<a href="{{ route('verification.notice') }}"
class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
@ -243,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>

View File

@ -7,247 +7,256 @@
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Super Admin Dashboard</title>
<!-- Tailwind CSS via CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<!-- FontAwesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Alpine.js -->
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/alpinejs/3.12.0/cdn.min.js"></script>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; }
/* Custom scrollbar for sidebar */
.sidebar-scroll::-webkit-scrollbar {
width: 4px;
body {
font-family: 'Inter', sans-serif;
}
.sidebar-scroll::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
}
.sidebar-scroll::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
}
/* Hover effects */
.nav-item {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
transition: all 0.3s ease;
}
.nav-item::before {
.nav-item.active {
position: relative;
background-color: rgb(239, 246, 255);
color: rgb(37, 99, 235);
font-weight: 500;
}
.nav-item.active::before {
content: '';
position: absolute;
left: 0;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
transition: left 0.5s;
width: 4px;
background-color: rgb(37, 99, 235);
border-radius: 0 4px 4px 0;
}
.nav-item:hover::before {
left: 100%;
.nav-item:hover:not(.active) {
background-color: rgb(249, 250, 251);
color: rgb(55, 65, 81);
}
/* Active nav indicator */
.nav-active {
background: rgba(255, 255, 255, 0.15);
border-right: 3px solid white;
}
/* Profile section gradient */
.profile-section {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
.dropdown-transition {
transition: all 0.2s ease-out;
}
</style>
</head>
<body class="bg-gray-50">
<div x-data="{ sidebarOpen: true }">
<!-- Sidebar -->
<aside :class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'"
class="fixed top-0 left-0 z-40 w-72 h-screen transition-transform duration-300 ease-in-out bg-gradient-to-br from-slate-800 via-slate-700 to-slate-900 text-white shadow-2xl">
<body x-data="{
sidebarOpen: getSuperAdminSidebarState(),
userDropdownOpen: false,
toggleSidebar() {
this.sidebarOpen = !this.sidebarOpen;
saveSuperAdminSidebarState(this.sidebarOpen);
}
}" x-init="
// Watch for sidebar changes and save to localStorage
$watch('sidebarOpen', value => saveSuperAdminSidebarState(value))
" class="bg-gray-50">
<!-- Header -->
<div class="flex items-center justify-between p-6 border-b border-slate-600/30">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center shadow-lg">
<i class="fas fa-cubes text-white text-lg"></i>
</div>
<div>
<h2 class="text-xl font-bold bg-gradient-to-r from-white to-slate-300 bg-clip-text text-transparent">
VenueSystem
</h2>
<p class="text-xs text-slate-400 font-medium">Super Admin Panel</p>
</div>
</div>
<button @click="sidebarOpen = !sidebarOpen"
class="lg:hidden w-8 h-8 rounded-lg bg-slate-600/50 hover:bg-slate-600 transition-colors flex items-center justify-center">
<i class="fas fa-times text-sm"></i>
<script>
// Function to get sidebar state from localStorage (separate key for super admin)
function getSuperAdminSidebarState() {
const saved = localStorage.getItem('superadmin_sidebar_open');
// Default to true for desktop, false for mobile
if (saved === null) {
return window.innerWidth >= 1024; // lg breakpoint
}
return saved === 'true';
}
// Function to save sidebar state to localStorage (separate key for super admin)
function saveSuperAdminSidebarState(isOpen) {
localStorage.setItem('superadmin_sidebar_open', isOpen.toString());
}
// Handle responsive behavior on window resize
window.addEventListener('resize', function () {
// Only auto-adjust if no explicit state has been saved
const saved = localStorage.getItem('superadmin_sidebar_open');
if (saved === null) {
// Auto close on mobile, open on desktop
const shouldOpen = window.innerWidth >= 1024;
Alpine.store('sidebar', { open: shouldOpen });
}
});
</script>
<div class="flex h-screen overflow-hidden">
<!-- Mobile Menu Button - Always visible on mobile when sidebar is closed -->
<div x-show="!sidebarOpen" class="fixed top-4 left-4 z-50 lg:hidden">
<button @click="toggleSidebar()"
class="p-2 bg-white rounded-lg shadow-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 border border-gray-200">
<svg class="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7">
</path>
</svg>
</button>
</div>
<!-- Profile Section -->
<div class="p-6">
<div class="profile-section rounded-xl p-4 mb-6">
<div class="flex items-center space-x-4">
<div class="relative">
<div class="w-12 h-12 rounded-full bg-gradient-to-br from-emerald-400 to-cyan-500 flex items-center justify-center shadow-lg">
<i class="fas fa-user-shield text-white text-lg"></i>
</div>
<div class="absolute -bottom-1 -right-1 w-4 h-4 bg-green-400 rounded-full border-2 border-slate-800"></div>
</div>
<div class="flex-1">
<p class="font-semibold text-white">{{ auth()->user()->name ?? 'Admin' }}</p>
<p class="text-sm text-slate-300">Super Administrator</p>
<div class="mt-1">
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-emerald-500/20 text-emerald-300 border border-emerald-500/30">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400 mr-1"></span>
Online
</span>
</div>
<!-- Sidebar Overlay -->
<div x-show="sidebarOpen" @click="toggleSidebar()" class="fixed inset-0 z-20 bg-black bg-opacity-50 lg:hidden">
</div>
<!-- Sidebar -->
<div :class="sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0 lg:w-20'"
class="fixed inset-y-0 left-0 z-30 w-64 bg-white shadow-lg transition-all duration-300 transform lg:relative lg:translate-x-0">
<!-- Sidebar Header -->
<div class="flex items-center justify-between h-16 px-4 border-b">
<div class="flex items-center space-x-2">
<span class="font-bold text-lg text-gray-800" x-show="sidebarOpen">Super Admin</span>
</div>
<button @click="toggleSidebar()" class="p-1 rounded-md hover:bg-gray-100 focus:outline-none">
<svg x-show="sidebarOpen" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-500"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
</svg>
<svg x-show="!sidebarOpen" class="w-6 h-6 text-gray-500" fill="none" stroke="currentColor"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M4 12h16m-7 6h7"></path>
</svg>
</button>
</div>
<!-- Navigation -->
<nav class="space-y-2">
<div class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-4 px-3">
Main Navigation
<div class="px-2 py-4">
<div x-show="sidebarOpen"
class="text-xs font-semibold text-gray-400 uppercase tracking-wider px-3 mb-2">
Menu Utama
</div>
<nav class="space-y-1">
<a href="{{ route('superadmin.dashboard') }}"
class="nav-item flex items-center p-3 rounded-lg hover:bg-slate-600/30 {{ request()->routeIs('superadmin.dashboard') ? 'nav-active' : '' }} group">
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-500/20 to-blue-600/20 flex items-center justify-center group-hover:from-blue-500/30 group-hover:to-blue-600/30 transition-all">
<i class="fas fa-chart-line text-blue-400 group-hover:text-blue-300"></i>
</div>
<div class="ml-4">
<span class="font-medium text-slate-200 group-hover:text-white transition-colors">Dashboard</span>
<p class="text-xs text-slate-400 group-hover:text-slate-300">Analytics & Overview</p>
</div>
class="nav-item flex items-center px-3 py-2.5 rounded-lg {{ request()->routeIs('superadmin.dashboard') ? 'active' : '' }}">
<i class="fas fa-chart-line w-5 h-5 mr-2 text-sm"></i>
<span x-show="sidebarOpen">Dashboard</span>
</a>
<a href="{{ route('superadmin.venue.index') }}"
class="nav-item flex items-center p-3 rounded-lg hover:bg-slate-600/30 {{ request()->routeIs('superadmin.venue.*') ? 'nav-active' : '' }} group">
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-emerald-500/20 to-emerald-600/20 flex items-center justify-center group-hover:from-emerald-500/30 group-hover:to-emerald-600/30 transition-all">
<i class="fas fa-building text-emerald-400 group-hover:text-emerald-300"></i>
</div>
<div class="ml-4">
<span class="font-medium text-slate-200 group-hover:text-white transition-colors">Manajemen Venue</span>
<p class="text-xs text-slate-400 group-hover:text-slate-300">Kelola semua venue</p>
</div>
class="nav-item flex items-center px-3 py-2.5 rounded-lg {{ request()->routeIs('superadmin.venue.*') ? 'active' : '' }}">
<i class="fas fa-building w-5 h-5 mr-2 text-sm"></i>
<span x-show="sidebarOpen">Manajemen Venue</span>
</a>
<a href="{{ route('superadmin.admin.index') }}"
class="nav-item flex items-center p-3 rounded-lg hover:bg-slate-600/30 {{ request()->routeIs('superadmin.admin.*') ? 'nav-active' : '' }} group">
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-purple-500/20 to-purple-600/20 flex items-center justify-center group-hover:from-purple-500/30 group-hover:to-purple-600/30 transition-all">
<i class="fas fa-users-cog text-purple-400 group-hover:text-purple-300"></i>
</div>
<div class="ml-4">
<span class="font-medium text-slate-200 group-hover:text-white transition-colors">Manajemen Admin</span>
<p class="text-xs text-slate-400 group-hover:text-slate-300">Kelola admin venue</p>
</div>
class="nav-item flex items-center px-3 py-2.5 rounded-lg {{ request()->routeIs('superadmin.admin.*') ? 'active' : '' }}">
<i class="fas fa-users-cog w-5 h-5 mr-2 text-sm"></i>
<span x-show="sidebarOpen">Manajemen Admin</span>
</a>
</nav>
</div>
<a href="{{ route('logout') }}"
onclick="event.preventDefault(); document.getElementById('logout-form').submit();"
class="flex items-center p-3 rounded-lg hover:bg-red-700">
<i class="fas fa-sign-out-alt w-5"></i>
<span class="ml-3">Logout</span>
<form id="logout-form" action="{{ route('logout') }}" method="POST" class="hidden">
<!-- User Profile -->
<div class="absolute bottom-0 w-full border-t border-gray-200">
<div x-data="{ open: false }" class="relative p-4">
<button @click="open = !open" class="flex items-center w-full text-left focus:outline-none">
<div class="flex-shrink-0">
<div
class="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center text-white font-semibold">
{{ substr(auth()->user()->name ?? 'SA', 0, 1) }}
</div>
</div>
<div x-show="sidebarOpen" class="ml-3">
<p class="text-sm font-medium text-gray-800 truncate">
{{ auth()->user()->name ?? 'Super Admin' }}
</p>
<p class="text-xs text-gray-500 truncate">{{ auth()->user()->email ??
'superadmin@example.com' }}</p>
</div>
<svg x-show="sidebarOpen" xmlns="http://www.w3.org/2000/svg"
class="ml-auto h-4 w-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<!-- Dropdown -->
<div x-show="open" @click.outside="open = false"
class="absolute bottom-full left-0 mb-1 w-full bg-white rounded-lg shadow-lg border border-gray-200 py-1 dropdown-transition">
<div class="border-t border-gray-200 my-1"></div>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit"
class="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-gray-100">
Logout
</button>
</form>
</a>
</nav>
</div>
</aside>
<!-- Content -->
<div :class="sidebarOpen ? 'lg:ml-72' : ''" class="transition-all duration-300">
<!-- Top bar -->
<header class="bg-white/80 backdrop-blur-md border-b border-gray-200/50 sticky top-0 z-30 shadow-sm">
<div class="px-6 py-4 flex items-center justify-between">
<div class="flex items-center space-x-4">
<button @click="sidebarOpen = !sidebarOpen"
class="w-10 h-10 rounded-lg bg-gray-100 hover:bg-gray-200 transition-colors flex items-center justify-center text-gray-600 hover:text-gray-800">
<i class="fas fa-bars text-sm"></i>
</button>
<!-- Breadcrumb -->
<nav class="flex items-center space-x-2 text-sm text-gray-500">
<span class="font-medium text-gray-900">Super Admin</span>
<i class="fas fa-chevron-right text-xs"></i>
<span>Dashboard</span>
</nav>
</div>
</div>
</div>
<div class="flex items-center space-x-4">
<!-- Notifications -->
<button class="relative w-10 h-10 rounded-lg bg-gray-100 hover:bg-gray-200 transition-colors flex items-center justify-center text-gray-600 hover:text-gray-800">
<i class="fas fa-bell text-sm"></i>
<span class="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full"></span>
<!-- Main Content -->
<div class="flex-1 flex flex-col overflow-hidden">
<!-- Top Header with Mobile Menu Button Alternative -->
<header class="bg-white shadow-sm lg:hidden">
<div class="px-4 py-3 flex items-center justify-between">
<button @click="toggleSidebar()" x-show="!sidebarOpen"
class="p-2 rounded-md text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
<!-- Profile Dropdown -->
<h1 class="text-lg font-semibold text-gray-900">Super Admin</h1>
<!-- Mobile Profile -->
<div class="relative" x-data="{ open: false }">
<button @click="open = !open"
class="flex items-center space-x-3 p-2 rounded-lg hover:bg-gray-100 transition-colors">
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-emerald-400 to-cyan-500 flex items-center justify-center">
<i class="fas fa-user text-white text-sm"></i>
class="flex items-center p-1 rounded-full hover:bg-gray-100 transition-colors">
<div
class="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white font-semibold text-sm">
{{ substr(auth()->user()->name ?? 'SA', 0, 1) }}
</div>
<div class="text-left hidden md:block">
<p class="text-sm font-medium text-gray-900">{{ auth()->user()->name ?? 'Admin' }}</p>
<p class="text-xs text-gray-500">Super Admin</p>
</div>
<i class="fas fa-chevron-down text-xs text-gray-400"
:class="{ 'rotate-180': open }"
style="transition: transform 0.2s"></i>
</button>
<div x-show="open"
@click.away="open = false"
<div x-show="open" @click.away="open = false"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute right-0 mt-2 w-56 bg-white rounded-xl shadow-lg border border-gray-200 py-2 z-50">
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-2 z-50">
<div class="px-4 py-3 border-b border-gray-100">
<p class="text-sm font-medium text-gray-900">{{ auth()->user()->name ?? 'Admin' }}</p>
<p class="text-xs text-gray-500">{{ auth()->user()->email ?? 'admin@example.com' }}</p>
</div>
{{-- <a href="#" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">
<i class="fas fa-user mr-3 text-gray-400"></i>
Profile Settings
</a>
<a href="#" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">
<i class="fas fa-cog mr-3 text-gray-400"></i>
Account Settings
</a> --}}
<div class="border-t border-gray-100 mt-2 pt-2">
<a href="{{ route('logout') }}"
onclick="event.preventDefault(); document.getElementById('logout-form').submit();"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Logout</a>
<p class="text-sm font-medium text-gray-900">{{ auth()->user()->name ?? 'Super Admin' }}
</p>
<p class="text-xs text-gray-500">{{ auth()->user()->email ?? 'superadmin@example.com' }}
</p>
</div>
<div class="pt-2">
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit"
class="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-gray-100">
Logout
</button>
</form>
</div>
</div>
</div>
</div>
</header>
<!-- Main content -->
<main class="p-6">
<!-- Page Content -->
<main class="flex-1 overflow-x-hidden overflow-y-auto">
@yield('content')
</main>
</div>
</div>
@stack('scripts')
</body>
</html>

View File

@ -1,5 +1,46 @@
@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>
@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())
<div class="bg-white rounded-lg shadow-md p-6 text-center">
@ -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">
{{-- 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>
</div>
@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

View File

@ -8,8 +8,10 @@
TERBAIK</h1>
</div>
<div class="text-center mt-5">
<a href="#" class="text-white bg-yellow-500 py-2 px-4 rounded-lg font-semibold text-sm md:text-lg">Daftarkan
Venue</a>
<a href="https://wa.me/6285730595855?text=Halo%2C%20saya%20tertarik%20untuk%20mendaftarkan%20venue%20saya"
target="_blank" class="text-white bg-yellow-500 py-2 px-4 rounded-lg font-semibold text-sm md:text-lg">
Daftarkan Venue
</a>
</div>
</div>
@ -52,10 +54,36 @@ class="w-full py-3 md:px-6 rounded-lg text-sm bg-primary text-white font-semibol
<a href="{{ route('venue', ['venueName' => $venue->name]) }}"
class="flex flex-col h-full border border-gray-400 rounded-lg overflow-hidden">
<img src="{{ Storage::url($venue->image) }}" alt="{{ $venue->name }}" class="w-full h-48 object-cover">
<div class="flex-grow px-4 py-2">
<h3 class="text-sm text-gray-400 font-semibold mb-2">Venue</h3>
<h1 class="text-xl text-gray-800 font-semibold">{{ $venue->name }}</h1>
{{-- <p class="text-sm text-gray-500">{{ $venue->address }}</p> --}}
@if($venue['status'] === 'open')
{{-- 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 A', strtotime($venue['open_time'])) }} -
{{ date('H:i A', strtotime($venue['close_time'])) }}
</p>
@else
{{-- Venue sedang tutup - tampilkan informasi penutupan --}}
<div class="mt-1">
<p class="text-sm text-red-600 font-medium">
<i class="fa-solid fa-circle-xmark text-red-500"></i>
Tutup Sementara - {{ $venue['close_reason'] }}
</p>
@if(!empty($venue['reopen_date']))
<p class="text-xs text-gray-500 mt-1">
<i class="fa-regular fa-calendar"></i>
<strong>Buka kembali:</strong>
{{ \Carbon\Carbon::parse($venue['reopen_date'])->format('d M Y') }} - Jam
{{ date('H:i', strtotime($venue['original_open_time'])) }}
</p>
@endif
</div>
@endif
<p class="mt-10 text-gray-500 text-sm">Mulai:
<span class="font-bold text-gray-800">Rp30,000</span>
<span class="text-gray-400 font-thin text-sm">/ jam</span>

View File

@ -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">
<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) {
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 now = new Date();
const selectedDateTime = new Date(`${this.selectedDate}T${hour}:00:00`);
if (this.isOvernight && startHourInt < this.openHour) {
selectedDateTime.setDate(selectedDateTime.getDate() + 1);
}
if (selectedDateTime <= now) {
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;
}
return !this.bookedSchedules.some(schedule => {
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;
}
}

View File

@ -1,9 +1,7 @@
@extends('layouts.main')
@section('content')
<!-- Toast Notification Container -->
<div id="toast-container" class="fixed top-4 right-4 z-50 space-y-2"></div>
<!-- Loading Overlay -->
<div x-data="{ show: false }" x-show="show" x-cloak x-on:show-loading.window="show = true"
x-on:hide-loading.window="show = false"
class="fixed inset-0 bg-black bg-opacity-50 z-40 flex items-center justify-center">
@ -16,19 +14,244 @@ class="fixed inset-0 bg-black bg-opacity-50 z-40 flex items-center justify-cente
</div>
<div class="min-h-96 mx-4 md:w-3/4 md:mx-auto">
<div class="mb-6">
<img src="{{ Storage::url($venue['image']) }}" alt="{{ $venue['name'] }}"
class="w-full h-full object-cover rounded-lg mb-4 mt-8" />
@php
// Menyiapkan semua gambar (cover + galeri) untuk Alpine.js
$galleryPaths = collect();
if ($venue->image) {
// Tambahkan gambar utama sebagai gambar pertama
$galleryPaths->push(asset('storage/' . $venue->image));
}
// Tambahkan gambar-gambar dari relasi 'images'
if(isset($venue->images) && $venue->images->isNotEmpty()) {
foreach ($venue->images as $img) {
$galleryPaths->push(asset('storage/' . $img->path));
}
}
@endphp
<div x-data="galleryViewer({ images: {{ $galleryPaths->toJson() }} })" class="mb-6 mt-8">
<div class="grid grid-cols-1 md:grid-cols-3 md:gap-2">
<div class="md:col-span-2 mb-2 md:mb-0">
<div @if($galleryPaths->isNotEmpty()) @click="open(0)" @endif class="relative w-full h-64 md:h-[33rem] cursor-pointer group">
<img src="{{ $venue->image ? asset('storage/' . $venue->image) : 'https://via.placeholder.com/800x400.png?text=Venue+Image' }}" alt="{{ $venue['name'] }}"
class="w-full h-full object-cover rounded-lg shadow-lg" />
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-20 transition-all duration-300 rounded-lg"></div>
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-1 gap-2">
@if(isset($venue->images) && $venue->images->isNotEmpty())
@foreach($venue->images->take(2) as $image)
@php
// Indeks ini untuk membuka gambar yang benar di modal
// +1 karena gambar utama (cover) ada di indeks 0
$modalIndex = $loop->index + 1;
@endphp
<div @click="open({{ $modalIndex }})" class="relative w-full h-40 md:h-64 cursor-pointer group">
<img src="{{ asset('storage/' . $image->path) }}" alt="Gallery image {{ $loop->iteration }}"
class="w-full h-full object-cover rounded-lg shadow-lg" />
@if($loop->last && $galleryPaths->count() > 1)
<div class="absolute inset-0 bg-black bg-opacity-40 flex items-center justify-center rounded-lg opacity-100 group-hover:bg-opacity-50 transition-all duration-300">
<span class="text-white font-semibold text-center">
<i class="fa-regular fa-images mr-1"></i>
Lihat Semua Foto
</span>
</div>
@else
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-20 transition-all duration-300 rounded-lg"></div>
@endif
</div>
@endforeach
@endif
</div>
</div>
<div x-show="isOpen" @keydown.escape.window="close()" @keydown.arrow-right.window="next()" @keydown.arrow-left.window="prev()"
class="fixed inset-0 z-[999] flex items-center justify-center bg-black bg-opacity-80" x-cloak>
<button @click="close()" class="absolute top-4 right-5 text-white text-4xl z-50 hover:text-gray-300">×</button>
<div class="relative w-full h-full flex items-center justify-center p-4 md:p-8">
<button @click="prev()" class="absolute left-2 md:left-5 text-white text-3xl md:text-5xl opacity-70 hover:opacity-100 p-2 z-50">
</button>
<img :src="images[currentIndex]" class="w-auto h-auto object-contain" style="max-height: 90vh; max-width: 90vw;">
<button @click="next()" class="absolute right-2 md:right-5 text-white text-3xl md:text-5xl opacity-70 hover:opacity-100 p-2 z-50">
</button>
<div class="absolute bottom-4 text-white text-sm bg-black bg-opacity-50 px-2 py-1 rounded">
<span x-text="currentIndex + 1"></span> / <span x-text="images.length"></span>
</div>
</div>
</div>
<div class="mt-4">
<h1 class="text-xl text-gray-800 font-semibold">{{ $venue['name'] }}</h1>
<p class="text-sm text-gray-500">{{ $venue['location'] ?? 'Lokasi tidak tersedia' }}</p>
<p class="text-sm text-gray-600 mt-1">
<i class="fa-regular fa-clock"></i>
Jam Operasional: {{ date('H:i', strtotime($venue['open_time'])) }} -
{{ date('H:i', strtotime($venue['close_time'])) }}
{{-- <p class="text-sm text-gray-500">{{ $venue['description'] ?? 'Tidak ada deskripsi.' }}</p> --}}
<div x-data="{ isOpen: false }" class="text-sm">
@if($venue['status'] === 'open')
@php
// Siapkan data untuk ditampilkan
$days = [1 => 'Senin', 2 => 'Selasa', 3 => 'Rabu', 4 => 'Kamis', 5 => 'Jumat', 6 => 'Sabtu', 7 => 'Minggu'];
$hoursByDay = $venue->operatingHours->keyBy('day_of_week');
$todayDayNumber = now('Asia/Jakarta')->dayOfWeekIso; // 1 for Monday, 7 for Sunday
$todaysHours = $hoursByDay->get($todayDayNumber);
$isOpenNow = false;
$statusText = 'Tutup';
$statusColor = 'text-red-600';
if ($todaysHours && !$todaysHours->is_closed) {
$openTimeToday = \Carbon\Carbon::parse($todaysHours->open_time);
$closeTimeToday = \Carbon\Carbon::parse($todaysHours->close_time);
$now = now('Asia/Jakarta');
// Logika untuk 'Buka 24 Jam'
if ($openTimeToday->format('H:i') == '00:00' && $closeTimeToday->format('H:i') == '23:59') {
$isOpenNow = true;
$statusText = 'Buka 24 jam';
}
// Logika untuk jam overnight (lewat tengah malam)
elseif ($closeTimeToday->lt($openTimeToday)) {
if ($now->between($openTimeToday, $closeTimeToday->copy()->addDay())) {
$isOpenNow = true;
}
}
// Logika untuk jam normal
else {
if ($now->between($openTimeToday, $closeTimeToday)) {
$isOpenNow = true;
}
}
if($isOpenNow && $statusText == 'Tutup'){
$statusText = 'Buka sekarang';
}
$statusColor = $isOpenNow ? 'text-green-600' : 'text-red-600';
}
@endphp
<button @click="isOpen = !isOpen" class="flex items-center space-x-2 w-full text-left">
<i class="fa-regular fa-clock {{ $statusColor }}"></i>
<span class="font-medium {{ $statusColor }}">{{ $statusText }}</span>
<span class="text-gray-500">·</span>
<span class="text-gray-600">
@if($todaysHours && !$todaysHours->is_closed)
{{ \Carbon\Carbon::parse($todaysHours->open_time)->format('H:i') }} - {{ \Carbon\Carbon::parse($todaysHours->close_time)->format('H:i') }}
@else
Tutup
@endif
</span>
<i class="fa-solid fa-chevron-down text-xs text-gray-500 transition-transform" :class="{'rotate-180': isOpen}"></i>
</button>
<div x-show="isOpen" x-collapse class="mt-3 pl-6">
<table class="w-full text-left">
<tbody>
@foreach($days as $dayNumber => $dayName)
@php
$schedule = $hoursByDay->get($dayNumber);
$isToday = ($dayNumber == $todayDayNumber);
@endphp
<tr class="{{ $isToday ? 'font-bold' : '' }}">
<td class="py-1 pr-4">{{ $dayName }}</td>
<td class="py-1 text-gray-800">
@if($schedule && !$schedule->is_closed)
@if($schedule->open_time == '00:00:00' && $schedule->close_time == '23:59:00')
Buka 24 jam
@else
{{ \Carbon\Carbon::parse($schedule->open_time)->format('H:i') }} - {{ \Carbon\Carbon::parse($schedule->close_time)->format('H:i') }}
@endif
@else
Tutup
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else <div class="mt-1 flex items-center space-x-2">
<i class="fa-solid fa-circle-xmark text-red-500"></i>
<p class="text-sm text-red-600 font-medium">
Tutup Sementara - {{ $venue['close_reason'] }}
</p>
</div>
<a href="https://www.google.com/maps/search/?api=1&query={{ urlencode($venue['address']) }}" target="_blank"
@endif
</div>
<div class="mt-4 flex items-center space-x-4">
@if($venue->link_instagram)
<a href="{{ $venue->link_instagram }}" target="_blank" class="text-2xl text-orange-500 hover:text-orange-800">
<i class="fab fa-instagram fa-lg"></i>
</a>
@endif
@if($venue->link_tiktok)
<a href="{{ $venue->link_tiktok }}" target="_blank" class="text-2xl text-gray-700 hover:text-black">
<i class="fab fa-tiktok fa-lg"></i>
</a>
@endif
@if($venue->link_facebook)
<a href="{{ $venue->link_facebook }}" target="_blank" class="text-2xl text-blue-600 hover:text-blue-900">
<i class="fab fa-facebook-f fa-lg"></i>
</a>
@endif
@if($venue->link_x)
<a href="{{ $venue->link_x }}" target="_blank" class="text-2xl text-blue-500 hover:text-blue-800">
<i class="fab fa-twitter fa-lg"></i>
</a>
@endif
</div>
<div x-data="{ open: false }" class="relative inline-block text-left mt-4">
<div>
<button @click="open = !open" type="button" class="inline-flex items-center justify-center w-full rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
<i class="fa-solid fa-map-location-dot mr-2"></i>
Di Sekitar
<i class="fa-solid fa-chevron-down text-xs ml-2"></i>
</button>
</div>
<div x-show="open" @click.away="open = false" x-transition
class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-10">
<div class="py-1" role="none">
@if($venue->latitude && $venue->longitude)
@php
// PASTIKAN BARIS INI MEMILIKI "https://www."
$baseUrl = "https://www.google.com/maps/search/";
$coordinates = "@" . $venue->latitude . ',' . $venue->longitude;
$restoUrl = $baseUrl . "Restoran/" . $coordinates . ",15z";
$hotelUrl = $baseUrl . "Hotel/" . $coordinates . ",15z";
$minimarketUrl = $baseUrl . "Minimarket/" . $coordinates . ",15z";
$atmUrl = $baseUrl . "ATM/" . $coordinates . ",17z";
@endphp
<a href="{{ $restoUrl }}" target="_blank" class="text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100">
<i class="fa-solid fa-utensils w-5 mr-2"></i>Restoran
</a>
<a href="{{ $hotelUrl }}" target="_blank" class="text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100">
<i class="fa-solid fa-hotel w-5 mr-2"></i>Hotel
</a>
<a href="{{ $minimarketUrl }}" target="_blank" class="text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100">
<i class="fa-solid fa-store w-5 mr-2"></i>Minimarket
</a>
<a href="{{ $atmUrl }}" target="_blank" class="text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100">
<i class="fa-solid fa-credit-card w-5 mr-2"></i>ATM
</a>
@else
<span class="text-gray-400 block px-4 py-2 text-sm">Koordinat venue belum diatur.</span>
@endif
</div>
</div>
</div>
</div>
</div>
<a href="http://maps.google.com/?q={{ urlencode($venue['address']) }}" target="_blank"
class="flex items-center bg-[url('/public/images/map.jpg')] bg-cover bg-center p-4">
<div>
<h1 class="font-semibold">Lokasi Venue</h1>
@ -38,8 +261,10 @@ class="flex items-center bg-[url('/public/images/map.jpg')] bg-cover bg-center p
<i class="fa-solid fa-map-pin text-red-800 text-3xl"></i>
</div>
</a>
@auth
<!-- Pending Bookings Section -->
<div x-data="pendingBookingsComponent" class="mt-6">
<template x-if="pendingBookings.length > 0">
<div class="bg-orange-50 border border-orange-200 rounded-lg p-4 mb-6">
@ -105,7 +330,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($openTime)) }}, {{-- <-- Gunakan variabel baru --}}
{{ date('G', strtotime($closeTime)) }}, {{-- <-- Gunakan variabel baru --}}
{{ (strtotime($closeTime) < strtotime($openTime)) ? 'true' : 'false' }} {{-- <-- Logika is_overnight dinamis --}}
)"
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()">
@ -136,15 +367,25 @@ class="border rounded-lg shadow-md p-4 mb-4">
</select>
<h4 class="font-semibold mb-2 mt-4">Pilih Durasi Main:</h4>
@if ($venue['status'] === 'open')
<select class="w-full border p-2 rounded-lg" x-model="selectedDuration">
<option value="">-- Pilih Durasi --</option>
<option value="1">1 Jam</option>
<option value="2">2 Jam</option>
<option value="3">3 Jam</option>
</select>
@else
<select class="w-full border p-2 rounded-lg bg-gray-100 text-gray-400 cursor-not-allowed" disabled>
<option value="">Venue sedang tutup</option>
</select>
@endif
<button class="mt-3 px-4 py-2 bg-green-500 text-white rounded-lg w-full" :disabled="!selectedTime || !selectedDuration || isLoading"
<button
class="mt-3 px-4 py-2 rounded-lg w-full
{{ $venue['status'] === 'open' ? 'bg-green-500 text-white' : 'bg-gray-400 text-gray-700 cursor-not-allowed' }}"
:disabled="!selectedTime || !selectedDuration || isLoading || '{{ $venue['status'] }}' !== 'open'"
@click="initiateBooking('{{ $table['id'] }}', '{{ addslashes($table['name']) }}')">
<template x-if="isLoading">
<span>Loading...</span>
</template>
@ -155,6 +396,58 @@ class="border rounded-lg shadow-md p-4 mb-4">
</div>
</div>
@endforeach
<div class="mt-8 pt-6 border-t">
<h2 class="text-xl font-bold text-gray-800 mb-4">Ulasan Pengguna</h2>
@if($totalReviews > 0)
<div class="flex items-center mb-6 bg-gray-50 p-4 rounded-lg">
<div class="text-5xl font-bold text-gray-800">{{ number_format($averageRating, 1) }}</div>
<div class="ml-4">
<div class="flex items-center">
@for ($i = 1; $i <= 5; $i++)
@if ($i <= round($averageRating))
<i class="fa-solid fa-star text-yellow-400"></i>
@else
<i class="fa-regular fa-star text-gray-300"></i>
@endif
@endfor
</div>
<p class="text-sm text-gray-600">Berdasarkan {{ $totalReviews }} ulasan</p>
</div>
</div>
<div class="space-y-6">
@foreach($venue->reviews->sortByDesc('created_at') as $review)
<div class="flex items-start space-x-4">
<div class="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center font-bold text-gray-600">
{{-- Ambil inisial nama --}}
{{ strtoupper(substr($review->user->name, 0, 1)) }}
</div>
<div class="flex-1">
<div class="flex items-center justify-between">
<div>
<p class="font-semibold text-gray-800">{{ $review->user->name }}</p>
<p class="text-xs text-gray-500">{{ $review->created_at->diffForHumans() }}</p>
</div>
<div class="flex items-center">
<span class="text-sm font-bold mr-1">{{ $review->rating }}</span>
<i class="fa-solid fa-star text-yellow-400"></i>
</div>
</div>
<p class="mt-2 text-gray-700">
{{ $review->comment }}
</p>
</div>
</div>
@endforeach
</div>
@else
<div class="text-center py-8 bg-gray-50 rounded-lg">
<p class="text-gray-500">Belum ada ulasan untuk venue ini.</p>
</div>
@endif
</div>
</div>
</div>
@ -192,12 +485,10 @@ function showToast(message, type = 'info', duration = 5000) {
toastContainer.appendChild(toast);
// Animate in
setTimeout(() => {
toast.classList.remove('translate-x-full', 'opacity-0');
}, 100);
// Auto remove
setTimeout(() => {
toast.classList.add('translate-x-full', 'opacity-0');
setTimeout(() => toast.remove(), 300);
@ -241,7 +532,6 @@ class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 transition-
document.body.appendChild(modal);
// Close on backdrop click
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.remove();
@ -288,7 +578,6 @@ function showConfirmModal(title, message, onConfirm, onCancel = null) {
if (onCancel) onCancel();
});
// Close on backdrop click
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.remove();
@ -297,13 +586,10 @@ function showConfirmModal(title, message, onConfirm, onCancel = null) {
});
}
// Custom event for refreshing pending bookings across components
const refreshPendingBookingsEvent = new Event('refresh-pending-bookings');
// Tambahkan fungsi helper untuk mendapatkan tanggal Jakarta yang konsisten
function getJakartaDate() {
const now = new Date();
// Buat objek Date dengan timezone Jakarta
const jakartaTime = new Date(now.toLocaleString("en-US", { timeZone: "Asia/Jakarta" }));
return jakartaTime;
}
@ -328,27 +614,19 @@ function updateClock() {
document.getElementById('realTimeClock').textContent = timeFormatter.format(jakartaTime);
}
// Format functions for pending bookings
function formatDateTime(dateTimeStr) {
// Parse the datetime string
const parts = dateTimeStr.split(/[^0-9]/);
const year = parseInt(parts[0]);
const month = parseInt(parts[1]) - 1; // JS months are 0-based
const month = parseInt(parts[1]) - 1;
const day = parseInt(parts[2]);
const hour = parseInt(parts[3]);
const minute = parseInt(parts[4]);
// Gunakan zona waktu Asia/Jakarta (UTC+7)
// Tambahkan 7 jam untuk mengkonversi dari UTC ke WIB
const adjustedHour = (hour + 7) % 24;
const dateFormatter = new Intl.DateTimeFormat('id-ID', {
day: '2-digit',
month: 'short',
year: 'numeric',
});
// Format the date and time separately
const dateObj = new Date(year, month, day);
return dateFormatter.format(dateObj) + ' ' +
(adjustedHour.toString().padStart(2, '0') + ':' +
@ -356,15 +634,10 @@ function formatDateTime(dateTimeStr) {
}
function formatTime(timeStr) {
// Parse the ISO date string without timezone conversion
const parts = timeStr.split(/[^0-9]/);
const hour = parseInt(parts[3]);
const minute = parseInt(parts[4]);
// Tambahkan 7 jam untuk mengkonversi dari UTC ke WIB
const adjustedHour = (hour + 7) % 24;
// Format time manually
return adjustedHour.toString().padStart(2, '0') + ':' +
minute.toString().padStart(2, '0');
}
@ -374,6 +647,30 @@ function formatPrice(price) {
}
document.addEventListener('alpine:init', () => {
// --- START: Gallery Viewer Alpine Component (KODE BARU) ---
Alpine.data('galleryViewer', (config) => ({
isOpen: false,
currentIndex: 0,
images: config.images || [],
open(index) {
if (this.images.length === 0) return;
this.currentIndex = index;
this.isOpen = true;
document.body.style.overflow = 'hidden';
},
close() {
this.isOpen = false;
document.body.style.overflow = 'auto';
},
next() {
this.currentIndex = (this.currentIndex + 1) % this.images.length;
},
prev() {
this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length;
}
}));
// --- END: Gallery Viewer Alpine Component ---
// Pending bookings component
Alpine.data('pendingBookingsComponent', () => ({
pendingBookings: [],
@ -382,12 +679,10 @@ function formatPrice(price) {
init() {
this.fetchPendingBookings();
// Listen for the custom event to refresh pending bookings
document.addEventListener('refresh-pending-bookings', () => {
console.log('Refreshing pending bookings from event');
this.fetchPendingBookings();
this.showPendingBookings = true; // Auto-expand the section
this.showPendingBookings = true;
});
},
@ -395,7 +690,6 @@ function formatPrice(price) {
fetch('/booking/pending')
.then(response => response.json())
.then(data => {
// Filter bookings untuk venue saat ini jika diperlukan
const currentVenueId = {{ $venue['id'] ?? 'null' }};
if (currentVenueId) {
this.pendingBookings = data.filter(booking =>
@ -404,12 +698,7 @@ function formatPrice(price) {
} else {
this.pendingBookings = data;
}
// Log jumlah pending bookings yang ditemukan
console.log("Found", this.pendingBookings.length, "pending bookings");
// If we have pending bookings and this was triggered by payment cancellation,
// make sure to show them
if (this.pendingBookings.length > 0 && window.justClosedPayment) {
this.showPendingBookings = true;
window.justClosedPayment = false;
@ -421,36 +710,21 @@ function formatPrice(price) {
resumeBooking(bookingId) {
this.isLoadingPending = true;
window.dispatchEvent(new CustomEvent('show-loading'));
fetch(`/booking/pending/${bookingId}/resume`)
.then(response => response.json())
.then(data => {
window.dispatchEvent(new CustomEvent('hide-loading'));
if (data.success) {
console.log("Opening payment with snap token:", data.snap_token);
// Open Snap payment
window.snap.pay(data.snap_token, {
onSuccess: (result) => {
this.createBookingAfterPayment(data.order_id, result);
},
onPending: (result) => {
showToast('Pembayaran pending, silahkan selesaikan pembayaran', 'warning');
this.isLoadingPending = false;
},
onError: (result) => {
showToast('Pembayaran gagal', 'error');
this.isLoadingPending = false;
},
onClose: () => {
showToast('Anda menutup popup tanpa menyelesaikan pembayaran', 'warning');
this.isLoadingPending = false;
}
onSuccess: (result) => this.createBookingAfterPayment(data.order_id, result),
onPending: (result) => { showToast('Pembayaran pending...', 'warning'); this.isLoadingPending = false; },
onError: (result) => { showToast('Pembayaran gagal', 'error'); this.isLoadingPending = false; },
onClose: () => { showToast('Anda menutup popup pembayaran', 'warning'); this.isLoadingPending = false; }
});
} else {
showToast(data.message, 'error');
this.isLoadingPending = false;
// Refresh pending bookings list
this.fetchPendingBookings();
}
})
@ -470,10 +744,7 @@ function formatPrice(price) {
this.isLoadingPending = true;
fetch(`/booking/pending/${bookingId}`, {
method: 'DELETE',
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 }
})
.then(response => response.json())
.then(data => {
@ -496,163 +767,101 @@ function formatPrice(price) {
createBookingAfterPayment(orderId, paymentResult) {
window.dispatchEvent(new CustomEvent('show-loading'));
fetch('/booking', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
},
body: JSON.stringify({
order_id: orderId,
transaction_id: paymentResult.transaction_id,
payment_method: paymentResult.payment_type,
transaction_status: paymentResult.transaction_status
}),
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content },
body: JSON.stringify({ order_id: orderId, transaction_id: paymentResult.transaction_id, payment_method: paymentResult.payment_type, transaction_status: paymentResult.transaction_status }),
})
.then(res => {
if (!res.ok) {
return res.json().then(err => {
throw new Error(err.message || 'Gagal menyimpan booking');
});
}
if (!res.ok) return res.json().then(err => { throw new Error(err.message || 'Gagal menyimpan booking'); });
return res.json();
})
.then(data => {
window.dispatchEvent(new CustomEvent('hide-loading'));
showToast('Pembayaran dan booking berhasil!', 'success');
this.isLoadingPending = false;
// Refresh pending bookings list
this.fetchPendingBookings();
// Redirect to booking history
setTimeout(() => {
window.location.href = '/booking/history';
}, 2000);
setTimeout(() => { window.location.href = '/booking/history'; }, 2000);
})
.catch(err => {
window.dispatchEvent(new CustomEvent('hide-loading'));
console.error('Booking error:', err);
showToast('Pembayaran berhasil tetapi gagal menyimpan booking: ' + err.message, 'error');
showToast('Gagal menyimpan booking: ' + err.message, 'error');
this.isLoadingPending = false;
});
}
}));
// 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: [],
// Updated method to use dynamic hours from venue
// Regular booking component
Alpine.data('booking', (isLoggedIn, tableId, openHour, closeHour, isOvernight) => ({
isLoggedIn, tableId, openHour, closeHour, isOvernight, open: false, selectedTime: '', selectedDuration: '', isLoading: false, bookedSchedules: [],
getAvailableHours() {
let hours = [];
for (let i = this.openHour; i <= this.closeHour; i++) {
hours.push(i.toString().padStart(2, '0'));
const currentJakartaHour = getJakartaDate().getHours();
if (this.isOvernight) {
for (let i = this.openHour; i < 24; i++) { if (i >= currentJakartaHour) 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++) { 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) return (timeFormatted >= schedule.start || timeFormatted < schedule.end);
else return (timeFormatted >= schedule.start && timeFormatted < schedule.end);
});
},
async checkBookedSchedules() {
// Gunakan tanggal Jakarta yang konsisten
const today = getJakartaDateString();
try {
const response = await fetch(`/booking/schedules?table_id=${this.tableId}&date=${today}`);
this.bookedSchedules = await response.json();
console.log('Checking schedules for date:', today, 'Table:', this.tableId);
console.log('Booked schedules:', this.bookedSchedules);
} catch (error) {
console.error('Error checking booked schedules:', error);
}
} catch (error) { console.error('Error checking booked schedules:', error); }
},
initiateBooking(tableId, tableName) {
if (!this.isLoggedIn) {
showToast('Silahkan login terlebih dahulu untuk melakukan booking', 'warning');
return;
}
const selectedTime = this.selectedTime;
const selectedDuration = this.selectedDuration;
if (!this.isLoggedIn) { showToast('Silahkan login terlebih dahulu', 'warning'); return; }
if (!this.selectedTime || !this.selectedDuration) { showToast('Pilih jam dan durasi', 'warning'); return; }
if (!selectedTime || !selectedDuration) {
showToast('Please select both time and duration', 'warning');
return;
}
// Validasi jam menggunakan waktu Jakarta
const now = getJakartaDate();
const selectedDateTime = new Date(now);
const [selectedHour, selectedMinute] = selectedTime.split(':').map(Number);
const [selectedHour, selectedMinute] = this.selectedTime.split(':').map(Number);
selectedDateTime.setHours(selectedHour, selectedMinute, 0, 0);
// Uncomment this for production to prevent booking past times
if (selectedDateTime <= now) {
showToast('Tidak bisa booking untuk waktu yang sudah berlalu', 'warning');
return;
}
if (this.isOvernight && selectedHour < this.openHour) selectedDateTime.setDate(selectedDateTime.getDate() + 1);
if (selectedDateTime <= now) { showToast('Tidak bisa booking untuk waktu yang sudah berlalu', 'warning'); return; }
this.isLoading = true;
window.dispatchEvent(new CustomEvent('show-loading'));
// Gunakan tanggal Jakarta yang konsisten
const bookingDate = getJakartaDateString();
fetch('/booking/initiate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
},
body: JSON.stringify({
table_id: tableId,
start_time: selectedTime,
duration: selectedDuration,
booking_date: bookingDate
}),
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content },
body: JSON.stringify({ table_id: tableId, start_time: this.selectedTime, duration: this.selectedDuration, booking_date: bookingDate }),
})
.then(res => res.json())
.then(data => {
window.dispatchEvent(new CustomEvent('hide-loading'));
if (data.success) {
console.log("Opening payment with snap token:", data.snap_token);
// Open Snap payment
if (data.snap_token) {
window.snap.pay(data.snap_token, {
onSuccess: (result) => {
this.createBooking(data.order_id, result);
},
onPending: (result) => {
showToast('Pembayaran pending, silahkan selesaikan pembayaran', 'warning');
this.isLoading = false;
},
onError: (result) => {
showToast('Pembayaran gagal', 'error');
this.isLoading = false;
},
onSuccess: (result) => this.createBooking(data.order_id, result),
onPending: (result) => { showToast('Pembayaran pending...', 'warning'); this.isLoading = false; },
onError: (result) => { showToast('Pembayaran gagal', 'error'); this.isLoading = false; },
onClose: () => {
showToast('Anda menutup popup tanpa menyelesaikan pembayaran', 'warning');
showToast('Anda menutup popup pembayaran', 'warning');
this.isLoading = false;
window.justClosedPayment = true;
// Dispatch event to refresh pending bookings
document.dispatchEvent(refreshPendingBookingsEvent);
}
});
} else {
showToast(data.message || 'Booking berhasil diproses', 'success');
this.isLoading = false;
if (data.booking_id) setTimeout(() => window.location.reload(), 1000);
}
} else {
showToast(data.message, 'error');
this.isLoading = false;
@ -665,62 +874,38 @@ function formatPrice(price) {
this.isLoading = false;
});
},
createBooking(orderId, paymentResult) {
window.dispatchEvent(new CustomEvent('show-loading'));
fetch('/booking', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
},
body: JSON.stringify({
order_id: orderId,
transaction_id: paymentResult.transaction_id,
payment_method: paymentResult.payment_type,
transaction_status: paymentResult.transaction_status
}),
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content },
body: JSON.stringify({ order_id: orderId, transaction_id: paymentResult.transaction_id, payment_method: paymentResult.payment_type, transaction_status: paymentResult.transaction_status }),
})
.then(res => {
if (!res.ok) {
return res.json().then(err => {
throw new Error(err.message || 'Gagal menyimpan booking');
});
}
if (!res.ok) return res.json().then(err => { throw new Error(err.message || 'Gagal menyimpan booking'); });
return res.json();
})
.then(data => {
window.dispatchEvent(new CustomEvent('hide-loading'));
showToast('Pembayaran dan booking berhasil!', 'success');
this.isLoading = false;
// Reset form
this.selectedTime = '';
this.selectedDuration = '';
this.open = false;
// Refresh booked schedules
this.checkBookedSchedules();
// Redirect to booking history
setTimeout(() => {
window.location.href = '/booking/history';
}, 2000);
setTimeout(() => { window.location.href = '/booking/history'; }, 2000);
})
.catch(err => {
window.dispatchEvent(new CustomEvent('hide-loading'));
console.error('Booking error:', err);
showToast('Pembayaran berhasil tetapi gagal menyimpan booking: ' + err.message, 'error');
showToast('Gagal menyimpan booking: ' + err.message, 'error');
this.isLoading = false;
});
}
}));
});
// Initialize clock
updateClock();
setInterval(updateClock, 1000);
</script>
@endsection

View File

@ -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

View File

@ -1,6 +1,7 @@
@extends('layouts.super-admin')
@section('content')
<div class="p-6">
<div class="mb-6 flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold text-gray-800">Manajemen Admin</h1>
@ -185,6 +186,7 @@ class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {

View File

@ -1,114 +1,150 @@
@extends('layouts.super-admin')
@section('content')
<div class="mb-8">
<h1 class="text-4xl font-bold bg-gradient-to-r from-purple-600 to-pink-600 bg-clip-text text-transparent">
Dashboard Super Admin
</h1>
<p class="text-gray-600 text-lg">Analytics & Overview dari seluruh venue</p>
<div class="bg-gray-50 min-h-screen">
<div class="p-6">
<!-- Header and Welcome -->
<div class="flex justify-between items-center mb-6">
<div>
<h1 class="text-3xl font-bold text-gray-800">Dashboard Super Admin</h1>
<p class="text-gray-600 mt-1">Analytics & Overview dari seluruh venue</p>
</div>
<div class="text-right">
<p class="text-sm text-gray-500">{{ now()->format('l, d F Y') }}</p>
<p class="text-2xl font-semibold text-gray-800">{{ now()->format('H:i') }}</p>
</div>
</div>
<!-- Main Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl shadow-lg p-6 text-white">
<div class="flex items-center justify-between">
<!-- Total Admin Card -->
<div class="bg-white rounded-xl shadow-sm p-6 border-l-4 border-blue-500 hover:shadow-md transition">
<div class="flex justify-between items-start">
<div>
<p class="text-blue-100 text-sm font-medium">Total Admin</p>
<p class="text-3xl font-bold">{{ $adminCount ?? 0 }}</p>
<p class="text-sm font-medium text-gray-500">Total Admin</p>
<p class="text-2xl font-bold text-gray-800">{{ $adminCount ?? 0 }}</p>
</div>
<div class="bg-blue-400 bg-opacity-30 rounded-full p-3">
<i class="fas fa-users-cog text-2xl"></i>
<div class="text-blue-500 p-2 bg-blue-50 rounded-lg">
<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="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">Administrator aktif</p>
</div>
<div class="bg-gradient-to-br from-emerald-500 to-emerald-600 rounded-xl shadow-lg p-6 text-white">
<div class="flex items-center justify-between">
<!-- Total Venue Card -->
<div class="bg-white rounded-xl shadow-sm p-6 border-l-4 border-green-500 hover:shadow-md transition">
<div class="flex justify-between items-start">
<div>
<p class="text-emerald-100 text-sm font-medium">Total Venue</p>
<p class="text-3xl font-bold">{{ $venueCount ?? 0 }}</p>
<p class="text-sm font-medium text-gray-500">Total Venue</p>
<p class="text-2xl font-bold text-gray-800">{{ $venueCount ?? 0 }}</p>
</div>
<div class="bg-emerald-400 bg-opacity-30 rounded-full p-3">
<i class="fas fa-building text-2xl"></i>
<div class="text-green-500 p-2 bg-green-50 rounded-lg">
<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="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">Venue terdaftar</p>
</div>
<div class="bg-gradient-to-br from-amber-500 to-orange-500 rounded-xl shadow-lg p-6 text-white">
<div class="flex items-center justify-between">
<!-- Total User Card -->
<div class="bg-white rounded-xl shadow-sm p-6 border-l-4 border-amber-500 hover:shadow-md transition">
<div class="flex justify-between items-start">
<div>
<p class="text-amber-100 text-sm font-medium">Total User</p>
<p class="text-3xl font-bold">{{ $userCount ?? 0 }}</p>
<p class="text-sm font-medium text-gray-500">Total User</p>
<p class="text-2xl font-bold text-gray-800">{{ $userCount ?? 0 }}</p>
</div>
<div class="bg-amber-400 bg-opacity-30 rounded-full p-3">
<i class="fas fa-users text-2xl"></i>
<div class="text-amber-500 p-2 bg-amber-50 rounded-lg">
<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="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">Pengguna terdaftar</p>
</div>
<div class="bg-gradient-to-br from-purple-500 to-pink-500 rounded-xl shadow-lg p-6 text-white">
<div class="flex items-center justify-between">
<!-- Total Meja Card -->
<div class="bg-white rounded-xl shadow-sm p-6 border-l-4 border-purple-500 hover:shadow-md transition">
<div class="flex justify-between items-start">
<div>
<p class="text-purple-100 text-sm font-medium">Total Meja</p>
<p class="text-3xl font-bold">{{ $tableCount ?? 0 }}</p>
<p class="text-sm font-medium text-gray-500">Total Meja</p>
<p class="text-2xl font-bold text-gray-800">{{ $tableCount ?? 0 }}</p>
</div>
<div class="bg-purple-400 bg-opacity-30 rounded-full p-3">
<i class="fas fa-table text-2xl"></i>
<div class="text-purple-500 p-2 bg-purple-50 rounded-lg">
<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="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">Meja tersedia</p>
</div>
</div>
<!-- Charts Section -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Revenue Comparison Chart -->
<div class="bg-white rounded-xl shadow-lg overflow-hidden">
<div class="bg-gradient-to-r from-indigo-500 to-purple-600 px-6 py-4">
<h3 class="text-xl font-bold text-white flex items-center">
<i class="fas fa-chart-bar mr-3"></i>
<div class="bg-white rounded-xl shadow-sm">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-semibold text-gray-800 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-3 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Revenue Bulan Ini
</h3>
<p class="text-indigo-100 text-sm">Per venue - {{ Carbon\Carbon::now()->format('F Y') }}</p>
<p class="text-gray-500 text-sm">Per venue - {{ Carbon\Carbon::now()->format('F Y') }}</p>
</div>
<div class="p-6">
<canvas id="revenueChart" width="400" height="300"></canvas>
<div style="height: 300px;">
<canvas id="revenueChart"></canvas>
</div>
</div>
</div>
<!-- Popular Venues Ranking -->
<div class="bg-white rounded-xl shadow-lg overflow-hidden">
<div class="bg-gradient-to-r from-pink-500 to-rose-600 px-6 py-4">
<h3 class="text-xl font-bold text-white flex items-center">
<i class="fas fa-trophy mr-3"></i>
<div class="bg-white rounded-xl shadow-sm">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-semibold text-gray-800 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-3 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
</svg>
Ranking Popularitas
</h3>
<p class="text-pink-100 text-sm">Venue terpopuler bulan ini</p>
<p class="text-gray-500 text-sm">Venue terpopuler bulan ini</p>
</div>
<div class="p-6">
<!-- Filter Toggle -->
<div class="mb-4">
<div class="bg-gray-100 rounded-lg p-1 flex">
<button
id="rankingByBookings"
class="flex-1 py-2 px-4 rounded-md text-sm font-medium transition-all duration-200 bg-rose-500 text-white"
<button id="rankingByBookings"
class="flex-1 py-2 px-4 rounded-md text-sm font-medium transition-all duration-200 bg-green-600 text-white"
onclick="toggleRanking('bookings')">
<i class="fas fa-calendar-check mr-2"></i>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Total Booking
</button>
<button
id="rankingByRevenue"
<button id="rankingByRevenue"
class="flex-1 py-2 px-4 rounded-md text-sm font-medium transition-all duration-200 text-gray-600 hover:text-gray-800"
onclick="toggleRanking('revenue')">
<i class="fas fa-money-bill-wave mr-2"></i>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Total Revenue
</button>
</div>
</div>
<canvas id="popularVenuesChart" width="400" height="300"></canvas>
<div style="height: 300px;">
<canvas id="popularVenuesChart"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
@push('scripts')
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
<script>
// Revenue Chart Data
@ -117,21 +153,21 @@ class="flex-1 py-2 px-4 rounded-md text-sm font-medium transition-all duration-2
// Popular Venues Data
const popularVenuesData = @json($popularVenuesData);
// Color Palettes
// Color Palettes - More subtle and professional
const revenueColors = [
'rgba(99, 102, 241, 0.8)', // Indigo
'rgba(168, 85, 247, 0.8)', // Purple
'rgba(59, 130, 246, 0.8)', // Blue
'rgba(16, 185, 129, 0.8)', // Emerald
'rgba(245, 158, 11, 0.8)', // Amber
'rgba(139, 92, 246, 0.8)', // Violet
'rgba(236, 72, 153, 0.8)', // Pink
'rgba(34, 197, 94, 0.8)', // Emerald
'rgba(251, 146, 60, 0.8)', // Orange
];
const rankingColors = [
'rgba(239, 68, 68, 0.8)', // Red
'rgba(245, 158, 11, 0.8)', // Amber
'rgba(34, 197, 94, 0.8)', // Emerald
'rgba(34, 197, 94, 0.8)', // Green
'rgba(59, 130, 246, 0.8)', // Blue
'rgba(168, 85, 247, 0.8)', // Purple
'rgba(245, 158, 11, 0.8)', // Amber
'rgba(139, 92, 246, 0.8)', // Violet
'rgba(107, 114, 128, 0.8)', // Gray
];
// Revenue Chart
@ -144,20 +180,26 @@ class="flex-1 py-2 px-4 rounded-md text-sm font-medium transition-all duration-2
label: 'Revenue (Rp)',
data: revenueData.map(item => parseFloat(item.total_revenue)),
backgroundColor: revenueColors,
borderRadius: 8,
borderRadius: 4,
borderSkipped: false,
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: 'rgba(17, 24, 39, 0.9)',
titleColor: '#fff',
bodyColor: '#fff',
borderColor: 'rgba(156, 163, 175, 0.2)',
borderWidth: 1,
callbacks: {
label: function(context) {
label: function (context) {
return 'Revenue: Rp ' + new Intl.NumberFormat('id-ID').format(context.parsed.x);
}
}
@ -166,8 +208,12 @@ class="flex-1 py-2 px-4 rounded-md text-sm font-medium transition-all duration-2
scales: {
x: {
beginAtZero: true,
grid: {
color: 'rgba(156, 163, 175, 0.1)'
},
ticks: {
callback: function(value) {
color: '#6B7280',
callback: function (value) {
return 'Rp ' + new Intl.NumberFormat('id-ID', {
notation: 'compact',
maximumFractionDigits: 1
@ -178,6 +224,9 @@ class="flex-1 py-2 px-4 rounded-md text-sm font-medium transition-all duration-2
y: {
grid: {
display: false
},
ticks: {
color: '#6B7280'
}
}
}
@ -203,7 +252,7 @@ function createPopularChart(sortBy) {
label: sortBy === 'revenue' ? 'Revenue (Rp)' : 'Total Booking',
data: sortedData.map(item => sortBy === 'revenue' ? parseFloat(item.total_revenue) : parseInt(item.total_bookings)),
backgroundColor: rankingColors,
borderRadius: 8,
borderRadius: 4,
borderSkipped: false,
}]
};
@ -218,13 +267,19 @@ function createPopularChart(sortBy) {
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: 'rgba(17, 24, 39, 0.9)',
titleColor: '#fff',
bodyColor: '#fff',
borderColor: 'rgba(156, 163, 175, 0.2)',
borderWidth: 1,
callbacks: {
label: function(context) {
label: function (context) {
if (sortBy === 'revenue') {
return 'Revenue: Rp ' + new Intl.NumberFormat('id-ID').format(context.parsed.x);
} else {
@ -237,8 +292,12 @@ function createPopularChart(sortBy) {
scales: {
x: {
beginAtZero: true,
grid: {
color: 'rgba(156, 163, 175, 0.1)'
},
ticks: {
callback: function(value) {
color: '#6B7280',
callback: function (value) {
if (sortBy === 'revenue') {
return 'Rp ' + new Intl.NumberFormat('id-ID', {
notation: 'compact',
@ -253,6 +312,9 @@ function createPopularChart(sortBy) {
y: {
grid: {
display: false
},
ticks: {
color: '#6B7280'
}
}
}
@ -269,14 +331,15 @@ function toggleRanking(type) {
const revenueBtn = document.getElementById('rankingByRevenue');
if (type === 'bookings') {
bookingBtn.className = 'flex-1 py-2 px-4 rounded-md text-sm font-medium transition-all duration-200 bg-rose-500 text-white';
bookingBtn.className = 'flex-1 py-2 px-4 rounded-md text-sm font-medium transition-all duration-200 bg-green-600 text-white';
revenueBtn.className = 'flex-1 py-2 px-4 rounded-md text-sm font-medium transition-all duration-200 text-gray-600 hover:text-gray-800';
createPopularChart('bookings');
} else {
revenueBtn.className = 'flex-1 py-2 px-4 rounded-md text-sm font-medium transition-all duration-200 bg-rose-500 text-white';
revenueBtn.className = 'flex-1 py-2 px-4 rounded-md text-sm font-medium transition-all duration-200 bg-green-600 text-white';
bookingBtn.className = 'flex-1 py-2 px-4 rounded-md text-sm font-medium transition-all duration-200 text-gray-600 hover:text-gray-800';
createPopularChart('revenue');
}
}
</script>
@endpush
@endsection

View File

@ -7,7 +7,7 @@
style="backdrop-filter: blur(20px); background-color: rgba(255, 255, 255, 0.8);">
<div class="p-6 sm:p-10">
<h2 class="text-center text-4xl font-semibold text-gray-900 mb-8">
{{ __('Edit Venue') }}
{{ __('Detail Venue') }}
</h2>
@if ($errors->any())
@ -41,7 +41,7 @@
<input type="text" id="name" name="name" value="{{ old('name', $venue->name) }}" required
autocomplete="name" autofocus
class="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-300 ease-in-out"
placeholder="Masukkan nama venue">
placeholder="Masukkan nama venue" disabled>
</div>
{{-- Nomor Telepon --}}
@ -51,7 +51,7 @@ class="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:
</label>
<input type="tel" id="phone" name="phone" value="{{ old('phone', $venue->phone) }}" required
class="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-300 ease-in-out"
placeholder="Masukkan nomor telepon">
placeholder="Masukkan nomor telepon" disabled>
</div>
</div>
@ -62,7 +62,8 @@ class="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:
</label>
<textarea id="address" name="address" required rows="3"
class="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-300 ease-in-out"
placeholder="Masukkan alamat lengkap venue">{{ old('address', $venue->address) }}</textarea>
placeholder="Masukkan alamat lengkap venue"
disabled>{{ old('address', $venue->address) }}</textarea>
</div>
{{-- Deskripsi --}}
@ -72,7 +73,8 @@ class="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:
</label>
<textarea id="description" name="description" rows="4" required
class="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-300 ease-in-out"
placeholder="Berikan deskripsi venue">{{ old('description', $venue->description) }}</textarea>
placeholder="Berikan deskripsi venue"
disabled>{{ old('description', $venue->description) }}</textarea>
</div>
{{-- Jam Operasional --}}
@ -83,6 +85,7 @@ class="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:
</label>
<input type="time" id="open_time" name="open_time"
value="{{ old('open_time', date('H:i', strtotime($venue->open_time))) }}" required
disabled
class="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-300 ease-in-out">
</div>
<div>
@ -91,6 +94,7 @@ class="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:
</label>
<input type="time" id="close_time" name="close_time"
value="{{ old('close_time', date('H:i', strtotime($venue->close_time))) }}" required
disabled
class="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-300 ease-in-out">
</div>
</div>
@ -100,13 +104,13 @@ class="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:
<label for="status" class="block text-sm font-medium text-gray-700 mb-2">
{{ __('Status') }}
</label>
<select id="status" name="status" required
<select id="status" name="status" required disabled
class="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-300 ease-in-out">
<option value="active" {{ old('status', $venue->status) == 'active' ? 'selected' : '' }}>
{{ __('Aktif') }}
<option value="open" {{ old('status', $venue->status) == 'open' ? 'selected' : '' }}>
{{ __('Open') }}
</option>
<option value="inactive" {{ old('status', $venue->status) == 'inactive' ? 'selected' : '' }}>
{{ __('Tidak Aktif') }}
<option value="close" {{ old('status', $venue->status) == 'close' ? 'selected' : '' }}>
{{ __('Close') }}
</option>
</select>
</div>
@ -126,8 +130,8 @@ class="h-32 w-auto object-cover rounded-lg border border-gray-200 shadow-sm">
</div>
@endif
<div x-ref="dropzone" @dragover.prevent="dragover = true" @dragleave.prevent="dragover = false"
@drop.prevent="handleDrop($event)"
{{-- <div x-ref="dropzone" @dragover.prevent="dragover = true"
@dragleave.prevent="dragover = false" @drop.prevent="handleDrop($event)"
class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-lg transition duration-300 ease-in-out"
:class="dragover ? 'border-blue-500 bg-blue-50' : 'hover:border-blue-500'">
<div class="space-y-1 text-center">
@ -154,19 +158,19 @@ class="relative cursor-pointer rounded-md font-medium text-blue-600 hover:text-b
</p>
<p x-text="fileName" class="text-sm text-gray-600 mt-2"></p>
</div>
</div>
</div> --}}
</div>
{{-- Tombol Aksi --}}
<div class="flex justify-end space-x-4 pt-6">
<a href="{{ route('superadmin.venue.index') }}"
class="px-6 py-3 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg font-medium transition duration-300 ease-in-out">
{{ __('Batal') }}
{{ __('Kembali') }}
</a>
<button type="submit"
{{-- <button type="submit"
class="px-6 py-3 bg-blue-500 text-white rounded-lg font-medium hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition duration-300 ease-in-out">
{{ __('Perbarui') }}
</button>
</button> --}}
</div>
</form>
</div>

View File

@ -1,6 +1,7 @@
@extends('layouts.super-admin')
@section('content')
<div class="p-6">
<div class="mb-6 flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold text-gray-800">Manajemen Venue</h1>
@ -27,7 +28,7 @@ class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 flex item
@endif
<!-- Filter and Search -->
<div class="bg-white rounded-lg shadow mb-6 p-4">
{{-- <div class="bg-white rounded-lg shadow mb-6 p-4">
<form action="{{ route('superadmin.venue.index') }}" method="GET">
<div class="flex flex-col md:flex-row gap-4">
<div class="flex-grow">
@ -40,18 +41,20 @@ class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 flex item
class="pl-10 block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring-green-500"
placeholder="Cari nama venue atau lokasi">
</div>
</div>
<div>
</div> --}}
{{-- <div>
<label for="status" class="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select id="status" name="status"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring-green-500">
<option value="">Semua Status</option>
<option value="active" {{ request('status') == 'active' ? 'selected' : '' }}>Aktif</option>
<option value="inactive" {{ request('status') == 'inactive' ? 'selected' : '' }}>Tidak Aktif</option>
<option value="open" {{ request('status')=='open' ? 'selected' : '' }}>Open</option>
<option value="close" {{ request('status')=='close' ? 'selected' : '' }}>Close
</option>
</select>
</div>
<div class="self-end">
<button type="submit" class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 text-sm">
</div> --}}
{{-- <div class="self-end">
<button type="submit"
class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 text-sm">
Filter
</button>
<a href="{{ route('superadmin.venue.index') }}"
@ -61,7 +64,7 @@ class="px-4 py-2 border border-gray-300 rounded-md bg-white hover:bg-gray-50 tex
</div>
</div>
</form>
</div>
</div> --}}
<!-- Venue Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@ -97,10 +100,10 @@ class="px-4 py-2 border border-gray-300 rounded-md bg-white hover:bg-gray-50 tex
onerror="this.src='{{ asset('images/venue-placeholder.jpg') }}'; this.onerror=null;">
<div class="absolute top-3 right-3 flex gap-2">
<a href="{{ route('superadmin.venue.edit', $venue->id) }}"
{{-- <a href="{{ route('superadmin.venue.edit', $venue->id) }}"
class="p-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200">
<i class="fas fa-edit"></i>
</a>
</a> --}}
<button type="button" onclick="confirmDelete({{ $venue->id }})"
class="p-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors duration-200">
<i class="fas fa-trash-alt"></i>
@ -109,13 +112,17 @@ class="p-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors d
</div>
<div class="p-5">
<h3 class="text-xl font-semibold text-gray-900 mb-2">{{ $venue->name }}</h3>
<div class="flex items-center mb-2">
{{-- <div class="flex items-center mb-2">
<i class="fas fa-map-marker-alt text-gray-500 mr-2"></i>
<span class="text-gray-600 truncate" title="{{ $venue->address }}">{{ $venue->address }}</span>
</div>
<div class="flex items-center mb-2">
<i class="fas fa-phone text-gray-500 mr-2"></i>
<span class="text-gray-600">{{ $venue->phone }}</span>
</div> --}}
<div class="flex items-center mb-4">
<i
class="fas fa-circle {{ $venue->status == 'open' ? 'text-green-500' : 'text-red-500' }} mr-2"></i>
<span
class="text-sm font-medium {{ $venue->status == 'open' ? 'text-green-700' : 'text-red-700' }}">
{{ $venue->status == 'open' ? 'Open' : 'Close' }}
</span>
</div>
<div class="flex items-center mb-2">
<i class="fas fa-clock text-gray-500 mr-2"></i>
@ -123,23 +130,21 @@ class="p-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors d
{{ $venue->open_time ?? '00:00' }} - {{ $venue->close_time ?? '23:59' }}
</span>
</div>
<div class="flex items-center mb-4">
<i class="fas fa-circle {{ $venue->status == 'active' ? 'text-green-500' : 'text-red-500' }} mr-2"></i>
<span class="text-sm font-medium {{ $venue->status == 'active' ? 'text-green-700' : 'text-red-700' }}">
{{ $venue->status == 'active' ? 'Aktif' : 'Tidak Aktif' }}
</span>
<div class="flex items-center mb-2">
<i class="fas fa-phone text-gray-500 mr-2"></i>
<span class="text-gray-600">{{ $venue->phone }}</span>
</div>
@if($venue->description)
{{-- @if($venue->description)
<div class="mb-4">
<p class="text-gray-600 text-sm line-clamp-2">{{ Str::limit($venue->description, 100) }}</p>
</div>
@endif
@endif --}}
<div class="border-t pt-4">
<div class="flex justify-between items-center">
<div class="text-sm text-gray-500">
<span class="font-medium">{{ $venue->created_at->format('d M Y') }}</span>
Dibuat: <span class="font-medium">{{ $venue->created_at->format('d M Y') }}</span>
</div>
<a href="{{ route('superadmin.venue.edit', $venue->id) }}"
class="text-green-600 hover:text-green-800 flex items-center text-sm transition-colors duration-200">
@ -155,7 +160,8 @@ class="text-green-600 hover:text-green-800 flex items-center text-sm transition-
<div class="max-w-sm mx-auto">
<i class="fas fa-building text-gray-300 text-6xl mb-4"></i>
<h3 class="text-xl font-semibold text-gray-900 mb-2">Belum ada venue</h3>
<p class="text-gray-500 mb-6">Mulai tambahkan venue baru untuk mengelola bisnis Anda dengan lebih baik</p>
<p class="text-gray-500 mb-6">Mulai tambahkan venue baru untuk mengelola bisnis Anda dengan lebih baik
</p>
<a href="{{ route('superadmin.venue.create') }}"
class="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 inline-flex items-center transition-colors duration-200">
<i class="fas fa-plus mr-2"></i>
@ -195,7 +201,8 @@ class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounde
</div>
<h3 class="mb-2 text-lg font-semibold text-gray-900">Hapus Venue</h3>
<p class="mb-6 text-sm text-gray-500">
Apakah Anda yakin ingin menghapus venue ini? Semua data terkait dengan venue ini akan ikut terhapus
Apakah Anda yakin ingin menghapus venue ini? Semua data terkait dengan venue ini akan ikut
terhapus
dan tidak dapat dikembalikan.
</p>
<form id="deleteVenueForm" method="POST" action="" class="space-y-4">
@ -216,6 +223,7 @@ class="px-4 py-2 bg-white text-gray-700 text-sm font-medium rounded-lg border bo
</div>
</div>
</div>
</div>
<script>
function confirmDelete(venueId) {

View File

@ -9,12 +9,14 @@
use App\Http\Controllers\admin\TableController;
use App\Http\Controllers\admin\RevenueController;
use App\Http\Controllers\admin\AdminController;
use App\Http\Controllers\admin\VenueController as AdminVenueController; // Import admin venue controller
use App\Http\Controllers\Auth\VerificationController;
use App\Http\Controllers\superadmin\SuperAdminController;
use App\Http\Controllers\superadmin\AdminManagementController;
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]);
@ -26,7 +28,7 @@
Route::get('/venue/{venueName}', [VenueController::class, "venue"])->name('venue');
// Changed routes for the new booking flow
Route::post('/booking/payment-intent', [BookingController::class, 'createPaymentIntent'])->name('booking.payment-intent');
Route::post('/booking/initiate', [BookingController::class, 'createPaymentIntent'])->name('booking.initiate');
Route::post('/booking', [BookingController::class, 'store'])->name('booking.store');
Route::get('/booking/schedules', [BookingController::class, 'getBookedSchedules'])->name('booking.schedules');
Route::post('/payment/notification', [BookingController::class, 'handleNotification'])->name('payment.notification');
@ -67,20 +69,34 @@
// 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');
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');
// CRUD routes untuk manajemen meja
// Admin Profile Routes
Route::get('/profile', [App\Http\Controllers\admin\AdminProfileController::class, 'index'])->name('admin.profile.index');
Route::put('/profile/update', [App\Http\Controllers\admin\AdminProfileController::class, 'updateProfile'])->name('admin.profile.update');
Route::put('/profile/password', [App\Http\Controllers\admin\AdminProfileController::class, 'updatePassword'])->name('admin.profile.password');
// 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/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');
Route::get('/tables/create', [TableController::class, 'create'])->name('admin.tables.create');
Route::post('/tables', [TableController::class, 'store'])->name('admin.tables.store');
@ -88,17 +104,26 @@
Route::put('/tables/{id}', [TableController::class, 'update'])->name('admin.tables.update');
Route::delete('/tables/{id}', [TableController::class, 'destroy'])->name('admin.tables.destroy');
// CRUD routes untuk revenue
// Venue management routes
Route::get('/venue', [AdminVenueController::class, 'index'])->name('admin.venue.index');
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
Route::middleware(['auth', 'verified', 'is_superadmin'])->prefix('superadmin')->group(function () {
Route::get('/', [App\Http\Controllers\superadmin\SuperAdminController::class, 'index'])->name('superadmin.dashboard');
// Tambahkan route untuk manajemen Admin
// Admin management routes
Route::get('/admin', [AdminManagementController::class, 'index'])->name('superadmin.admin.index');
Route::get('/admin/create', [AdminManagementController::class, 'create'])->name('superadmin.admin.create');
Route::post('/admin', [AdminManagementController::class, 'store'])->name('superadmin.admin.store');
@ -106,7 +131,7 @@
Route::put('/admin/{id}', [AdminManagementController::class, 'update'])->name('superadmin.admin.update');
Route::delete('/admin/{id}', [AdminManagementController::class, 'destroy'])->name('superadmin.admin.destroy');
// Tambahkan route untuk manajemen Venue
// Venue management routes (for superadmin)
Route::get('/venue', [VenueManagementController::class, 'index'])->name('superadmin.venue.index');
Route::get('/venue/create', [VenueManagementController::class, 'create'])->name('superadmin.venue.create');
Route::post('/venue', [VenueManagementController::class, 'store'])->name('superadmin.venue.store');