Compare commits

..

No commits in common. "f0f2d5299dc9ef21b7afe97f6b81b7fa127e48aa" and "b8f70e7f6fd3ad8fd5b596970e6b3c7e1ab474b6" have entirely different histories.

50 changed files with 1666 additions and 4173 deletions

View File

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

View File

@ -1,66 +0,0 @@
<?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

@ -1,73 +0,0 @@
<?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,15 +15,7 @@ class BookingsController extends Controller
{ {
public function index(Request $request) public function index(Request $request)
{ {
// Ambil venue_id dari admin yang sedang login $query = Booking::with(['table', 'user']);
// 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 // Search functionality
if ($request->has('search') && !empty($request->search)) { if ($request->has('search') && !empty($request->search)) {
@ -71,73 +63,21 @@ public function index(Request $request)
$query->orderBy($sortColumn, $sortDirection); $query->orderBy($sortColumn, $sortDirection);
} }
$bookings = $query->paginate(20)->withQueryString(); $bookings = $query->paginate(10)->withQueryString();
return view('admin.bookings.index', compact('bookings')); 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) public function show($id)
{ {
// Pastikan booking yang dilihat adalah milik venue admin $booking = Booking::with(['table', 'user'])->findOrFail($id);
$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')); return view('admin.bookings.show', compact('booking'));
} }
public function edit($id) public function edit($id)
{ {
$adminVenueId = auth()->user()->venue_id; $booking = Booking::findOrFail($id);
$tables = Table::all();
// 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')); return view('admin.bookings.edit', compact('booking', 'tables'));
} }
@ -149,18 +89,7 @@ public function update(Request $request, $id)
'end_time' => 'required|date|after:start_time', 'end_time' => 'required|date|after:start_time',
]); ]);
$adminVenueId = auth()->user()->venue_id; $booking = Booking::findOrFail($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()); $booking->update($request->all());
return redirect()->route('admin.bookings.index') return redirect()->route('admin.bookings.index')
@ -169,12 +98,7 @@ public function update(Request $request, $id)
public function complete($id) public function complete($id)
{ {
$adminVenueId = auth()->user()->venue_id; $booking = Booking::findOrFail($id);
$booking = Booking::whereHas('table', function ($q) use ($adminVenueId) {
$q->where('venue_id', $adminVenueId);
})->findOrFail($id);
$booking->status = 'selesai'; $booking->status = 'selesai';
$booking->save(); $booking->save();
@ -184,12 +108,7 @@ public function complete($id)
public function cancel($id) public function cancel($id)
{ {
$adminVenueId = auth()->user()->venue_id; $booking = Booking::findOrFail($id);
$booking = Booking::whereHas('table', function ($q) use ($adminVenueId) {
$q->where('venue_id', $adminVenueId);
})->findOrFail($id);
$booking->status = 'cancelled'; $booking->status = 'cancelled';
$booking->save(); $booking->save();
@ -199,10 +118,7 @@ public function cancel($id)
public function export(Request $request) public function export(Request $request)
{ {
$adminVenueId = auth()->user()->venue_id;
$filename = 'bookings-' . Carbon::now()->format('Y-m-d') . '.xlsx'; $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); $query->where('status', $request->status);
} }
$tables = $query->orderBy('created_at', 'asc')->paginate(10); $tables = $query->latest()->paginate(10);
return view('admin.tables.index', compact('tables')); return view('admin.tables.index', compact('tables'));
} }
@ -55,7 +55,7 @@ public function store(Request $request)
$validator = Validator::make($request->all(), [ $validator = Validator::make($request->all(), [
'name' => 'required|string|max:255', 'name' => 'required|string|max:255',
'brand' => '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', 'price_per_hour' => 'required|numeric|min:0',
]); ]);
@ -68,7 +68,7 @@ public function store(Request $request)
Table::create([ Table::create([
'name' => $request->name, 'name' => $request->name,
'brand' => $request->brand, 'brand' => $request->brand,
// 'status' => $request->status, 'status' => $request->status,
'price_per_hour' => $request->price_per_hour, 'price_per_hour' => $request->price_per_hour,
'venue_id' => auth()->user()->venue_id, 'venue_id' => auth()->user()->venue_id,
]); ]);
@ -101,7 +101,7 @@ public function update(Request $request, $id)
$validator = Validator::make($request->all(), [ $validator = Validator::make($request->all(), [
'name' => 'required|string|max:255', 'name' => 'required|string|max:255',
'brand' => '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', 'price_per_hour' => 'required|numeric|min:0',
]); ]);
@ -116,7 +116,7 @@ public function update(Request $request, $id)
$table->update([ $table->update([
'name' => $request->name, 'name' => $request->name,
'brand' => $request->brand, 'brand' => $request->brand,
// 'status' => $request->status, 'status' => $request->status,
'price_per_hour' => $request->price_per_hour, 'price_per_hour' => $request->price_per_hour,
]); ]);

View File

@ -1,222 +0,0 @@
<?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,8 +12,6 @@
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session; 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 class BookingController extends Controller
{ {
@ -24,208 +22,169 @@ public function __construct(MidtransService $midtransService)
$this->midtransService = $midtransService; $this->midtransService = $midtransService;
} }
public function showQrCode(Booking $booking)
{
// Otorisasi: Pastikan user yang login adalah pemilik booking
if ($booking->user_id !== Auth::id()) {
abort(403);
}
// Pastikan booking memiliki token validasi
if (!$booking->validation_token) {
abort(404, 'QR Code tidak ditemukan.');
}
// Buat gambar QR Code dari token validasi
$qrCode = QrCode::size(300)->generate($booking->validation_token);
// Kembalikan sebagai respons gambar SVG
return response($qrCode)->header('Content-Type', 'image/svg+xml');
}
// Tambahkan method baru untuk booking langsung oleh admin // Tambahkan method baru untuk booking langsung oleh admin
// Ganti seluruh fungsi adminDirectBooking dengan ini public function adminDirectBooking(Request $request) {
public function adminDirectBooking($request) {
try { try {
$data = $request instanceof \Illuminate\Http\Request ? $request->all() : $request->toArray(); $request->validate([
'table_id' => 'required|exists:tables,id',
if (!isset($data['table_id']) || !isset($data['start_time']) || !isset($data['end_time'])) { 'start_time' => 'required|date',
return response()->json(['message' => 'Missing required fields'], 400); 'end_time' => 'required|date|after:start_time',
} ]);
$user = Auth::user(); $user = Auth::user();
$table = Table::with(['venue.operatingHours'])->findOrFail($data['table_id']);
$venue = $table->venue;
// 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) { if ($user->role !== 'admin' || $user->venue_id !== $table->venue_id) {
return response()->json(['message' => 'Unauthorized action'], 403); return response()->json([
'message' => 'Unauthorized action'
], 403);
} }
$startDateTime = Carbon::parse($data['start_time']); // Cek konflik booking
$endDateTime = Carbon::parse($data['end_time']); $conflict = Booking::where('table_id', $request->table_id)
->where(function($query) use ($request) {
// --- LOGIKA BARU: Validasi Jam Operasional Harian --- $query->whereBetween('start_time', [$request->start_time, $request->end_time])
$bookingDate = $startDateTime->copy()->setTimezone('Asia/Jakarta'); ->orWhere(function($query) use ($request) {
$dayOfWeek = $bookingDate->dayOfWeekIso; $query->where('start_time', '<', $request->start_time)
$todaysHours = $venue->operatingHours->firstWhere('day_of_week', $dayOfWeek); ->where('end_time', '>', $request->start_time);
});
if (!$todaysHours || $todaysHours->is_closed) { })
return response()->json(['message' => 'Venue tutup pada tanggal yang dipilih.'], 400); ->where('status', 'paid')
}
$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(); ->exists();
if ($conflict) { if ($conflict) {
return response()->json(['message' => 'Meja sudah dibooking di jam tersebut'], 409); return response()->json(['message' => 'Meja sudah dibooking di jam tersebut'], 409);
} }
// Sisa fungsi tidak berubah // Hitung total biaya (meskipun admin tidak membayar, kita tetap catat nilainya)
$duration = $endDateTime->diffInHours($startDateTime); $startTime = Carbon::parse($request->start_time);
$endTime = Carbon::parse($request->end_time);
$duration = $endTime->diffInHours($startTime);
$totalAmount = $duration * $table->price_per_hour; $totalAmount = $duration * $table->price_per_hour;
// Generate order ID unik untuk admin
$adminOrderId = 'ADMIN-' . $user->id . '-' . time(); $adminOrderId = 'ADMIN-' . $user->id . '-' . time();
// Buat booking langsung dengan status paid
$booking = Booking::create([ $booking = Booking::create([
'table_id' => $data['table_id'], 'table_id' => $request->table_id,
'user_id' => $user->id, 'user_id' => $user->id,
'start_time' => $startDateTime, 'start_time' => $request->start_time,
'end_time' => $endDateTime, 'end_time' => $request->end_time,
'status' => 'paid', 'status' => 'paid', // langsung set sebagai paid
'total_amount' => $totalAmount, 'total_amount' => $totalAmount,
'payment_method' => 'admin_direct', 'payment_id' => null, // Admin tidak perlu payment_id
'payment_method' => 'admin_direct', // Tandai sebagai booking langsung admin
'order_id' => $adminOrderId, 'order_id' => $adminOrderId,
'validation_token' => (string) Str::uuid(),
]); ]);
// Update table status menjadi Booked
$table->update(['status' => 'Booked']);
return response()->json([ return response()->json([
'success' => true, 'message' => 'Booking created successfully',
'message' => 'Booking berhasil dibuat oleh admin', 'booking_id' => $booking->id
'booking_id' => $booking->id,
// ... (detail booking lainnya)
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
\Log::error('Admin direct booking error:', ['message' => $e->getMessage()]); \Log::error('Admin direct booking error:', [
return response()->json(['success' => false, 'message' => 'Gagal membuat booking: ' . $e->getMessage()], 500); 'message' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return response()->json([
'message' => 'Failed to create booking: ' . $e->getMessage()
], 500);
}
} }
}
public function createPaymentIntent(Request $request) { public function createPaymentIntent(Request $request) {
try { try {
$request->validate([ $request->validate([
'table_id' => 'required|exists:tables,id', 'table_id' => 'required|exists:tables,id',
'start_time' => 'required', 'start_time' => 'required|date',
'duration' => 'required|integer|min:1|max:12', 'end_time' => 'required|date|after:start_time',
'booking_date' => 'required|date_format:Y-m-d',
]); ]);
$user = Auth::user(); $user = Auth::user();
$table = Table::with(['venue.operatingHours'])->findOrFail($request->table_id); // Muat relasi operatingHours $table = Table::findOrFail($request->table_id);
$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) { if ($user->role === 'admin' && $user->venue_id === $table->venue_id) {
return $this->adminDirectBooking(collect([ return $this->adminDirectBooking($request);
'table_id' => $request->table_id,
'start_time' => $startDateTime->toDateTimeString(),
'end_time' => $endDateTime->toDateTimeString(),
]));
} }
// Cek konflik booking (tidak berubah) // Cek apakah meja sedang dibooking pada waktu tersebut (hanya yang sudah paid)
$conflict = Booking::where('table_id', $request->table_id) $conflict = Booking::where('table_id', $request->table_id)
->where('status', 'paid') ->where(function($query) use ($request) {
->where(function($query) use ($startDateTime, $endDateTime) { $query->whereBetween('start_time', [$request->start_time, $request->end_time])
$query->where('start_time', '<', $endDateTime) ->orWhere(function($query) use ($request) {
->where('end_time', '>', $startDateTime); $query->where('start_time', '<', $request->start_time)
->where('end_time', '>', $request->start_time);
});
}) })
->where('status', 'paid') // Hanya cek yang sudah paid
->exists(); ->exists();
if ($conflict) { if ($conflict) {
return response()->json(['success' => false, 'message' => 'Meja sudah dibooking di jam tersebut'], 409); return response()->json(['message' => 'Meja sudah dibooking di jam tersebut'], 409);
} }
// Proses ke Midtrans (tidak berubah) // 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);
$totalAmount = $duration * $table->price_per_hour; $totalAmount = $duration * $table->price_per_hour;
$tempOrderId = 'TEMP-' . Auth::id() . '-' . time();
// 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( PendingBooking::updateOrCreate(
['user_id' => Auth::id(), 'table_id' => $request->table_id, 'start_time' => $startDateTime->toDateTimeString()], [
['end_time' => $endDateTime->toDateTimeString(), 'total_amount' => $totalAmount, 'order_id' => $tempOrderId, 'expired_at' => now()->addHours(24) ] 'user_id' => Auth::id(),
'table_id' => $request->table_id,
'start_time' => $request->start_time
],
[
'end_time' => $request->end_time,
'total_amount' => $totalAmount,
'order_id' => $tempOrderId,
'expired_at' => now()->addHours(24), // Kadaluarsa dalam 24 jam
]
); );
// Dapatkan snap token dari Midtrans tanpa menyimpan booking
$snapToken = $this->midtransService->createTemporaryTransaction($table, $totalAmount, $tempOrderId, Auth::user()); $snapToken = $this->midtransService->createTemporaryTransaction($table, $totalAmount, $tempOrderId, Auth::user());
if (!$snapToken) { if (!$snapToken) {
throw new \Exception('Failed to get snap token from Midtrans'); 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([ return response()->json([
'success' => true, 'message' => 'Payment intent created, proceed to payment',
'total_amount' => $totalAmount,
'snap_token' => $snapToken, 'snap_token' => $snapToken,
'order_id' => $tempOrderId 'order_id' => $tempOrderId
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
\Log::error('Payment intent error:', [ \Log::error('Payment intent error:', [
'message' => $e->getMessage(), 'message' => $e->getMessage(),
@ -233,7 +192,6 @@ public function createPaymentIntent(Request $request) {
]); ]);
return response()->json([ return response()->json([
'success' => false,
'message' => 'Gagal membuat transaksi: ' . $e->getMessage() 'message' => 'Gagal membuat transaksi: ' . $e->getMessage()
], 500); ], 500);
} }
@ -285,7 +243,6 @@ public function store(Request $request) {
'payment_id' => $request->transaction_id, 'payment_id' => $request->transaction_id,
'payment_method' => $request->payment_method, 'payment_method' => $request->payment_method,
'order_id' => $request->order_id, 'order_id' => $request->order_id,
'validation_token' => (string) Str::uuid(),
]); ]);
// Update table status to booked // Update table status to booked
@ -328,38 +285,19 @@ public function getBookedSchedules(Request $request) {
'date' => 'required|date', '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 // Only get bookings with paid status
$query = Booking::where('table_id', $request->table_id) $bookings = Booking::where('table_id', $request->table_id)
->where('status', 'paid'); ->whereDate('start_time', $request->date)
->where('status', 'paid') // Only include paid bookings
if ($venue->is_overnight) { ->select('start_time', 'end_time')
// 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() ->get()
->map(function ($booking) { ->map(function ($booking) {
return [ return [
// Format H:i tetap sama, karena frontend hanya butuh jamnya
'start' => Carbon::parse($booking->start_time)->format('H:i'), 'start' => Carbon::parse($booking->start_time)->format('H:i'),
'end' => Carbon::parse($booking->end_time)->format('H:i') 'end' => Carbon::parse($booking->end_time)->format('H:i')
]; ];
}); });
return response()->json($bookings); return response()->json($bookings);
} }
@ -543,43 +481,40 @@ public function deletePendingBooking($id)
} }
} }
// GANTI SELURUH FUNGSI showReschedule DENGAN YANG INI public function showReschedule($id)
{
public function showReschedule($id)
{
$booking = Booking::with(['table.venue', 'table.venue.tables'])->findOrFail($id); $booking = Booking::with(['table.venue', 'table.venue.tables'])->findOrFail($id);
// Validasi kepemilikan dan status booking (tidak ada perubahan) // Check if user owns this booking
if ($booking->user_id !== auth()->id() || $booking->status !== 'paid' || $booking->reschedule_count >= 1) { if ($booking->user_id !== auth()->id()) {
return redirect()->route('booking.history')->with('error', 'Batas maksimal reschedule telah digunakan (1x).'); return redirect()->route('booking.history')->with('error', 'Anda tidak memiliki akses ke booking ini.');
} }
// 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(); $rescheduleDeadline = Carbon::parse($booking->start_time)->subHour();
if (now() > $rescheduleDeadline) { if (now() > $rescheduleDeadline) {
return redirect()->route('booking.history')->with('error', 'Batas waktu reschedule telah berakhir (1 jam sebelum mulai).'); 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; $venue = $booking->table->venue;
// Duration in hours
$duration = Carbon::parse($booking->start_time)->diffInHours($booking->end_time); $duration = Carbon::parse($booking->start_time)->diffInHours($booking->end_time);
// --- AWAL LOGIKA BARU UNTUK MENENTUKAN TANGGAL OPERASIONAL --- return view('pages.reschedule', compact('booking', 'venue', 'duration'));
$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. * Process a reschedule request.
*/ */
@ -642,36 +577,23 @@ public function processReschedule(Request $request, $id)
* Check availability for reschedule. * Check availability for reschedule.
*/ */
public function checkRescheduleAvailability(Request $request) public function checkRescheduleAvailability(Request $request)
{ {
$request->validate([ $request->validate([
'table_id' => 'required|exists:tables,id', 'table_id' => 'required|exists:tables,id',
'date' => 'required|date_format:Y-m-d', 'date' => 'required|date_format:Y-m-d',
'booking_id' => 'required|exists:bookings,id' 'booking_id' => 'required|exists:bookings,id'
]); ]);
$table = Table::with('venue')->findOrFail($request->table_id); $date = $request->date;
$venue = $table->venue; $tableId = $request->table_id;
$requestDate = Carbon::parse($request->date); $bookingId = $request->booking_id;
// Query untuk mengambil booking lain di meja yang sama // Get all bookings for this table on this date (excluding the current booking)
$query = Booking::where('table_id', $table->id) $bookings = Booking::where('table_id', $tableId)
->where('id', '!=', $request->booking_id) // Jangan ikut sertakan booking yang sedang di-reschedule ->where('id', '!=', $bookingId)
->where('status', 'paid'); ->where('status', 'paid')
->whereDate('start_time', $date)
// --- LOGIKA OVERNIGHT DITERAPKAN DI SINI --- ->get(['start_time', 'end_time'])
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) { ->map(function ($booking) {
return [ return [
'start' => Carbon::parse($booking->start_time)->format('H:i'), 'start' => Carbon::parse($booking->start_time)->format('H:i'),
@ -680,5 +602,5 @@ public function checkRescheduleAvailability(Request $request)
}); });
return response()->json($bookings); return response()->json($bookings);
} }
} }

View File

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

View File

@ -1,42 +1,34 @@
<?php <?php
// app/Http/Controllers/pages/VenueController.php
namespace App\Http\Controllers\pages; namespace App\Http\Controllers\pages;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Venue; use App\Models\Venue; // Pastikan model Venue di-import
use Carbon\Carbon; // <-- Pastikan Carbon di-import use Illuminate\Http\Request;
class VenueController extends Controller class VenueController extends Controller
{ {
public function venue($venueName) public function venue($venueName) {
{ // Mengambil venue berdasarkan nama yang diberikan
$venue = Venue::where('name', $venueName) $venue = Venue::where('name', 'like', '%' . ucfirst($venueName) . '%')->first();
->with(['tables', 'images', 'operatingHours', 'reviews.user'])
->firstOrFail();
// --- LOGIKA BARU UNTUK JAM OPERASIONAL HARI INI --- // Jika venue tidak ditemukan, tampilkan error 404
// Day of week: 1 (Senin) - 7 (Minggu). Carbon menggunakan 0 (Minggu) - 6 (Sabtu) if (!$venue) {
$dayOfWeek = Carbon::now('Asia/Jakarta')->dayOfWeekIso; // ISO standard: 1=Senin, 7=Minggu abort(404);
// 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();
// Kirim semua data ke view // Ambil tabel-tabel terkait dengan venue
return view('pages.venue', compact('venue', 'openTime', 'closeTime', 'averageRating', 'totalReviews')); $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
]);
} }
} }

View File

@ -18,9 +18,7 @@ class Booking extends Model
'payment_id', 'payment_id',
'payment_method', 'payment_method',
'total_amount', '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 = [ protected $casts = [
@ -40,11 +38,6 @@ public function user()
return $this->belongsTo(User::class); return $this->belongsTo(User::class);
} }
public function review()
{
return $this->hasOne(Review::class);
}
public function isPending() public function isPending()
{ {
return $this->status === 'pending'; return $this->status === 'pending';

View File

@ -1,24 +0,0 @@
<?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);
}
}

View File

@ -1,34 +0,0 @@
<?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,11 +66,6 @@ public function hasRole($role)
return $this->role === $role; return $this->role === $role;
} }
public function reviews()
{
return $this->hasMany(Review::class);
}
/** /**
* Get the venue that the admin belongs to. * Get the venue that the admin belongs to.
*/ */

View File

@ -4,126 +4,15 @@
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
use App\Models\VenueImage; // <-- Tambahkan ini
class Venue extends Model class Venue extends Model
{ {
use HasFactory; use HasFactory;
public function getIsOvernightAttribute() protected $fillable = ['name', 'location', 'address', 'image'];
{
// 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() public function tables()
{ {
return $this->hasMany(Table::class); 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,
]);
}
} }

View File

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

View File

@ -13,8 +13,7 @@
"laravel/tinker": "^2.8", "laravel/tinker": "^2.8",
"laravel/ui": "^4.6", "laravel/ui": "^4.6",
"maatwebsite/excel": "^1.1", "maatwebsite/excel": "^1.1",
"midtrans/midtrans-php": "^2.6", "midtrans/midtrans-php": "^2.6"
"simplesoftwareio/simple-qrcode": "^4.2"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.9.1", "fakerphp/faker": "^1.9.1",

174
composer.lock generated
View File

@ -4,62 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "66130e103a2b9c249e50df9d5700a24f", "content-hash": "bce8ddce84cf8a2d572b421f6992903e",
"packages": [ "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", "name": "brick/math",
"version": "0.12.1", "version": "0.12.1",
@ -189,56 +135,6 @@
], ],
"time": "2023-12-11T17:09:12+00:00" "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", "name": "dflydev/dot-access-data",
"version": "v3.0.3", "version": "v3.0.3",
@ -3916,74 +3812,6 @@
], ],
"time": "2024-04-27T21:32:50+00:00" "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", "name": "symfony/console",
"version": "v6.4.12", "version": "v6.4.12",

View File

@ -1,32 +0,0 @@
<?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

@ -1,23 +0,0 @@
<?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

@ -1,26 +0,0 @@
<?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

@ -1,25 +0,0 @@
<?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

@ -1,23 +0,0 @@
<?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

@ -1,26 +0,0 @@
<?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

@ -1,27 +0,0 @@
<?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) }}" <input id="email" type="email" name="email" value="{{ old('email', $user->email) }}"
required 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" 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" readonly> placeholder="Masukkan email">
</div> </div>
@error('email') @error('email')
<div class="text-red-500 mt-1 text-sm"><i <div class="text-red-500 mt-1 text-sm"><i

View File

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

View File

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

View File

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

View File

@ -3,10 +3,10 @@
@section('content') @section('content')
<div class="p-6 bg-gray-50"> <div class="p-6 bg-gray-50">
<!-- Header dengan Action Button --> <!-- Header dengan Action Button -->
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6"> <div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold text-gray-800">Kelola Meja</h1> <h1 class="text-3xl font-bold text-gray-800">Kelola Meja</h1>
<a href="{{ route('admin.tables.create') }}" <a href="{{ route('admin.tables.create') }}"
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"> class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-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"> <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" <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" 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,7 +16,6 @@ class="w-full sm:w-auto bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 round
</a> </a>
</div> </div>
<!-- Flash Message --> <!-- Flash Message -->
@if(session('success')) @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"> <div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 mb-6 rounded shadow-md" role="alert">
@ -39,16 +38,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..." <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"> 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>
{{-- <div class="md:w-1/4"> <div class="md:w-1/4">
<select name="status" <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"> 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="">Semua Status</option>
<option value="Available" {{ request('status')=='Available' ? 'selected' : '' }}>Available</option> <option value="Available" {{ request('status') == 'Available' ? 'selected' : '' }}>Available</option>
<option value="Booked" {{ request('status')=='Booked' ? 'selected' : '' }}>Booked</option> <option value="Booked" {{ request('status') == 'Booked' ? 'selected' : '' }}>Booked</option>
<option value="Unavailable" {{ request('status')=='Unavailable' ? 'selected' : '' }}>Unavailable <option value="Unavailable" {{ request('status') == 'Unavailable' ? 'selected' : '' }}>Unavailable
</option> </option>
</select> </select>
</div> --}} </div>
<div class="flex space-x-2"> <div class="flex space-x-2">
<button type="submit" <button type="submit"
class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors duration-300"> class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors duration-300">

View File

@ -1,205 +0,0 @@
@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

@ -1,427 +0,0 @@
@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,50 +46,11 @@
</style> </style>
</head> </head>
<body x-data="{ <body x-data="{ sidebarOpen: true, userDropdownOpen: false }" class="bg-gray-50">
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"> <div class="flex h-screen overflow-hidden">
<!-- Sidebar Overlay --> <!-- Sidebar Overlay -->
<div x-show="sidebarOpen" @click="toggleSidebar()" class="fixed inset-0 z-20 bg-black bg-opacity-50 lg:hidden"> <div x-show="sidebarOpen" @click="sidebarOpen = false"
</div> class="fixed inset-0 z-20 bg-black bg-opacity-50 lg:hidden"></div>
<!-- Sidebar --> <!-- Sidebar -->
<div :class="sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0 lg:w-20'" <div :class="sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0 lg:w-20'"
@ -105,7 +66,7 @@ class="fixed inset-y-0 left-0 z-30 w-64 bg-white shadow-lg transition-all durati
</a> </a>
</div> --}} </div> --}}
</div> </div>
<button @click="toggleSidebar()" class="p-1 rounded-md hover:bg-gray-100 focus:outline-none"> <button @click="sidebarOpen = !sidebarOpen" 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" <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"> fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@ -131,28 +92,17 @@ 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" <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24"
stroke="currentColor"> stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 3h7v7H3V3zm11 0h7v7h-7V3zM3 14h7v7H3v-7zm11 0h7v7h-7v-7z" /> 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" />
</svg> </svg>
<span x-show="sidebarOpen">Dashboard</span> <span x-show="sidebarOpen">Dashboard</span>
</a> </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') }}" <a href="{{ route('admin.tables.index') }}"
class="nav-item flex items-center px-3 py-2.5 rounded-lg {{ request()->routeIs('admin.tables.*') ? 'active' : '' }}"> 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" <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24"
stroke="currentColor"> stroke="currentColor">
<rect width="18" height="10" x="3" y="7" rx="2" ry="2" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
stroke="currentColor" fill="none" /> 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" />
<circle cx="12" cy="12" r="1.5" fill="currentColor" />
</svg> </svg>
<span x-show="sidebarOpen">Kelola Meja</span> <span x-show="sidebarOpen">Kelola Meja</span>
</a> </a>
@ -162,26 +112,18 @@ 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" <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24"
stroke="currentColor"> stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7V3m8 4V3M5 11h14M5 5h14a2 2 0 012 2v12a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2z" /> 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> </svg>
<span x-show="sidebarOpen">Kelola Booking</span> <span x-show="sidebarOpen">Daftar 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>
<a href="{{ route('admin.revenues.index') }}" <a href="{{ route('admin.revenues.index') }}"
class="nav-item flex items-center px-3 py-2.5 rounded-lg {{ request()->routeIs('admin.revenues.*') ? 'active' : '' }}"> 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" <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24"
stroke="currentColor"> stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
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" /> 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> </svg>
<span x-show="sidebarOpen">Laporan Pendapatan</span> <span x-show="sidebarOpen">Revenues</span>
</a> </a>
</nav> </nav>
@ -237,10 +179,8 @@ class="ml-auto h-4 w-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="cu
<!-- Dropdown --> <!-- Dropdown -->
<div x-show="open" @click.outside="open = false" <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"> 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="{{ route('admin.profile.index') }}" {{-- <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Profile</a>
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="#" 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> <div class="border-t border-gray-200 my-1"></div>
<form method="POST" action="{{ route('logout') }}"> <form method="POST" action="{{ route('logout') }}">
@csrf @csrf
@ -259,7 +199,7 @@ class="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-gray-100">
<!-- Top Header --> <!-- Top Header -->
<header class="bg-white shadow-sm lg:hidden"> <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"> <div class="px-4 sm:px-6 lg:px-8 py-4 flex items-center justify-between">
<button @click="toggleSidebar()" <button @click="sidebarOpen = !sidebarOpen"
class="p-1 rounded-md text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 lg:hidden"> 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" <svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"> 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 name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="csrf-token" content="{{ csrf_token() }}"> <meta name="csrf-token" content="{{ csrf_token() }}">
<title>Cari Meja</title> <title>Document</title>
@vite('resources/css/app.css') @vite('resources/css/app.css')
{{-- Font | Google Fonts --}} {{-- Font | Google Fonts --}}
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
@ -52,29 +52,14 @@ 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 <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"> class="absolute right-0 mt-2 w-40 bg-white rounded-lg shadow-lg py-2 z-50">
<a href="{{ route('booking.history') }}" <a href="{{ route('booking.history') }}"
class="block 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">
Riwayat Booking Riwayat Booking
</a> </a>
@if (Auth::user()->role === 'user')
<a href="{{ route('account.settings') }}" <a href="{{ route('account.settings') }}"
class="block 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 Pengaturan Akun
</a> </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) @if (Auth::user()->email_verified_at === null)
<a href="{{ route('verification.notice') }}" <a href="{{ route('verification.notice') }}"
class="block 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">
@ -82,7 +67,6 @@ class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Verifikasi Email Verifikasi Email
</a> </a>
@endif @endif
<form method="POST" action="{{ route('logout') }}"> <form method="POST" action="{{ route('logout') }}">
@csrf @csrf
<button type="submit" <button type="submit"
@ -108,22 +92,10 @@ 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"> class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Riwayat Booking Riwayat Booking
</a> </a>
@if (Auth::user()->role === 'user')
<a href="{{ route('account.settings') }}" <a href="{{ route('account.settings') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"> class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Pengaturan Akun Pengaturan Akun
</a> </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) @if (Auth::user()->email_verified_at === null)
<a href="{{ route('verification.notice') }}" <a href="{{ route('verification.notice') }}"
class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"> class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
@ -271,7 +243,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"> <div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-4" role="alert">
<ul> <ul>
@foreach($errors->get('email') as $error) @foreach($errors->get('email') as $error)
<li>Email anda belum terdaftar.</li> <li>{{ $error }}</li>
@endforeach @endforeach
</ul> </ul>
</div> </div>

View File

@ -7,256 +7,247 @@
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Super Admin Dashboard</title> <title>Super Admin Dashboard</title>
<!-- Tailwind CSS via CDN --> <!-- Tailwind CSS via CDN -->
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script> <script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<!-- FontAwesome Icons --> <!-- FontAwesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <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 --> <!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style> <style>
body { body { font-family: 'Inter', sans-serif; }
font-family: 'Inter', sans-serif;
/* Custom scrollbar for sidebar */
.sidebar-scroll::-webkit-scrollbar {
width: 4px;
} }
.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 { .nav-item {
transition: all 0.3s ease; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.nav-item.active {
position: relative; position: relative;
background-color: rgb(239, 246, 255); overflow: hidden;
color: rgb(37, 99, 235);
font-weight: 500;
} }
.nav-item.active::before { .nav-item::before {
content: ''; content: '';
position: absolute; position: absolute;
left: 0;
top: 0; top: 0;
left: -100%;
width: 100%;
height: 100%; height: 100%;
width: 4px; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
background-color: rgb(37, 99, 235); transition: left 0.5s;
border-radius: 0 4px 4px 0;
} }
.nav-item:hover:not(.active) { .nav-item:hover::before {
background-color: rgb(249, 250, 251); left: 100%;
color: rgb(55, 65, 81);
} }
.dropdown-transition { /* Active nav indicator */
transition: all 0.2s ease-out; .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);
} }
</style> </style>
</head> </head>
<body x-data="{ <body class="bg-gray-50">
sidebarOpen: getSuperAdminSidebarState(), <div x-data="{ sidebarOpen: true }">
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">
<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>
<!-- Sidebar Overlay -->
<div x-show="sidebarOpen" @click="toggleSidebar()" class="fixed inset-0 z-20 bg-black bg-opacity-50 lg:hidden">
</div>
<!-- Sidebar --> <!-- Sidebar -->
<div :class="sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0 lg:w-20'" <aside :class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'"
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"> 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">
<!-- Sidebar Header --> <!-- Header -->
<div class="flex items-center justify-between h-16 px-4 border-b"> <div class="flex items-center justify-between p-6 border-b border-slate-600/30">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-3">
<span class="font-bold text-lg text-gray-800" x-show="sidebarOpen">Super Admin</span> <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>
<button @click="toggleSidebar()" class="p-1 rounded-md hover:bg-gray-100 focus:outline-none"> <div>
<svg x-show="sidebarOpen" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-500" <h2 class="text-xl font-bold bg-gradient-to-r from-white to-slate-300 bg-clip-text text-transparent">
fill="none" viewBox="0 0 24 24" stroke="currentColor"> VenueSystem
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" </h2>
d="M11 19l-7-7 7-7m8 14l-7-7 7-7" /> <p class="text-xs text-slate-400 font-medium">Super Admin Panel</p>
</svg> </div>
<svg x-show="!sidebarOpen" class="w-6 h-6 text-gray-500" fill="none" stroke="currentColor" </div>
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <button @click="sidebarOpen = !sidebarOpen"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="lg:hidden w-8 h-8 rounded-lg bg-slate-600/50 hover:bg-slate-600 transition-colors flex items-center justify-center">
d="M4 6h16M4 12h16m-7 6h7"></path> <i class="fas fa-times text-sm"></i>
</svg>
</button> </button>
</div> </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>
</div>
</div>
</div>
<!-- Navigation --> <!-- Navigation -->
<div class="px-2 py-4"> <nav class="space-y-2">
<div x-show="sidebarOpen" <div class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-4 px-3">
class="text-xs font-semibold text-gray-400 uppercase tracking-wider px-3 mb-2"> Main Navigation
Menu Utama
</div> </div>
<nav class="space-y-1">
<a href="{{ route('superadmin.dashboard') }}" <a href="{{ route('superadmin.dashboard') }}"
class="nav-item flex items-center px-3 py-2.5 rounded-lg {{ request()->routeIs('superadmin.dashboard') ? 'active' : '' }}"> class="nav-item flex items-center p-3 rounded-lg hover:bg-slate-600/30 {{ request()->routeIs('superadmin.dashboard') ? 'nav-active' : '' }} group">
<i class="fas fa-chart-line w-5 h-5 mr-2 text-sm"></i> <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">
<span x-show="sidebarOpen">Dashboard</span> <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>
</a> </a>
<a href="{{ route('superadmin.venue.index') }}" <a href="{{ route('superadmin.venue.index') }}"
class="nav-item flex items-center px-3 py-2.5 rounded-lg {{ request()->routeIs('superadmin.venue.*') ? 'active' : '' }}"> class="nav-item flex items-center p-3 rounded-lg hover:bg-slate-600/30 {{ request()->routeIs('superadmin.venue.*') ? 'nav-active' : '' }} group">
<i class="fas fa-building w-5 h-5 mr-2 text-sm"></i> <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">
<span x-show="sidebarOpen">Manajemen Venue</span> <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>
</a> </a>
<a href="{{ route('superadmin.admin.index') }}" <a href="{{ route('superadmin.admin.index') }}"
class="nav-item flex items-center px-3 py-2.5 rounded-lg {{ request()->routeIs('superadmin.admin.*') ? 'active' : '' }}"> class="nav-item flex items-center p-3 rounded-lg hover:bg-slate-600/30 {{ request()->routeIs('superadmin.admin.*') ? 'nav-active' : '' }} group">
<i class="fas fa-users-cog w-5 h-5 mr-2 text-sm"></i> <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">
<span x-show="sidebarOpen">Manajemen Admin</span> <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>
</a>
<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">
@csrf
</form>
</a> </a>
</nav> </nav>
</div> </div>
</aside>
<!-- User Profile --> <!-- Content -->
<div class="absolute bottom-0 w-full border-t border-gray-200"> <div :class="sidebarOpen ? 'lg:ml-72' : ''" class="transition-all duration-300">
<div x-data="{ open: false }" class="relative p-4"> <!-- Top bar -->
<button @click="open = !open" class="flex items-center w-full text-left focus:outline-none"> <header class="bg-white/80 backdrop-blur-md border-b border-gray-200/50 sticky top-0 z-30 shadow-sm">
<div class="flex-shrink-0"> <div class="px-6 py-4 flex items-center justify-between">
<div <div class="flex items-center space-x-4">
class="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center text-white font-semibold"> <button @click="sidebarOpen = !sidebarOpen"
{{ substr(auth()->user()->name ?? 'SA', 0, 1) }} 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">
</div> <i class="fas fa-bars text-sm"></i>
</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> </button>
<!-- Dropdown --> <!-- Breadcrumb -->
<div x-show="open" @click.outside="open = false" <nav class="flex items-center space-x-2 text-sm text-gray-500">
class="absolute bottom-full left-0 mb-1 w-full bg-white rounded-lg shadow-lg border border-gray-200 py-1 dropdown-transition"> <span class="font-medium text-gray-900">Super Admin</span>
<div class="border-t border-gray-200 my-1"></div> <i class="fas fa-chevron-right text-xs"></i>
<form method="POST" action="{{ route('logout') }}"> <span>Dashboard</span>
@csrf </nav>
<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> </div>
<!-- Main Content --> <div class="flex items-center space-x-4">
<div class="flex-1 flex flex-col overflow-hidden"> <!-- Notifications -->
<!-- Top Header with Mobile Menu Button Alternative --> <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">
<header class="bg-white shadow-sm lg:hidden"> <i class="fas fa-bell text-sm"></i>
<div class="px-4 py-3 flex items-center justify-between"> <span class="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full"></span>
<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> </button>
<h1 class="text-lg font-semibold text-gray-900">Super Admin</h1> <!-- Profile Dropdown -->
<!-- Mobile Profile -->
<div class="relative" x-data="{ open: false }"> <div class="relative" x-data="{ open: false }">
<button @click="open = !open" <button @click="open = !open"
class="flex items-center p-1 rounded-full hover:bg-gray-100 transition-colors"> class="flex items-center space-x-3 p-2 rounded-lg hover:bg-gray-100 transition-colors">
<div <div class="w-8 h-8 rounded-full bg-gradient-to-br from-emerald-400 to-cyan-500 flex items-center justify-center">
class="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white font-semibold text-sm"> <i class="fas fa-user text-white text-sm"></i>
{{ substr(auth()->user()->name ?? 'SA', 0, 1) }}
</div> </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> </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="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="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95" x-transition:leave-start="opacity-100 scale-100"
class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-2 z-50"> 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">
<div class="px-4 py-3 border-b border-gray-100"> <div class="px-4 py-3 border-b border-gray-100">
<p class="text-sm font-medium text-gray-900">{{ auth()->user()->name ?? 'Super Admin' }} <p class="text-sm font-medium text-gray-900">{{ auth()->user()->name ?? 'Admin' }}</p>
</p> <p class="text-xs text-gray-500">{{ auth()->user()->email ?? 'admin@example.com' }}</p>
<p class="text-xs text-gray-500">{{ auth()->user()->email ?? 'superadmin@example.com' }} </div>
</p> {{-- <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>
</div> </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>
</div> </div>
</div> </div>
</header> </header>
<!-- Page Content --> <!-- Main content -->
<main class="flex-1 overflow-x-hidden overflow-y-auto"> <main class="p-6">
@yield('content') @yield('content')
</main> </main>
</div> </div>
</div> </div>
@stack('scripts')
</body> </body>
</html> </html>

View File

@ -1,46 +1,5 @@
@extends('layouts.main') @extends('layouts.main') @section('content') <div class="min-h-96 mx-4 md:w-3/4 md:mx-auto py-8">
@section('content') <h1 class="text-2xl font-bold mb-6">Riwayat Booking</h1>
<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()) @if($bookings->isEmpty())
<div class="bg-white rounded-lg shadow-md p-6 text-center"> <div class="bg-white rounded-lg shadow-md p-6 text-center">
@ -105,52 +64,17 @@ class="px-3 py-1 rounded-full text-sm {{ $booking->start_time > now() ? 'bg-gree
@endif @endif
</div> </div>
<div class="border-t mt-4 pt-4 flex justify-end items-center space-x-4"> @if($booking->start_time > now() && $booking->status == 'paid')
<a href="{{ route('venue', $booking->table->venue->name) }}" class="text-sm text-blue-600 hover:underline font-medium"> <div class="mt-4 flex justify-end space-x-4">
Lihat Venue <a href="{{ route('venue', $booking->table->venue->name) }}"
</a> class="text-blue-500 hover:underline">Lihat Venue</a>
{{-- Hanya tampilkan tombol jika status lunas --}} <a href="{{ route('booking.reschedule.form', $booking->id) }}"
@if($booking->status == 'paid') class="text-orange-500 hover:underline">
{{-- 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 Reschedule
</a> </a>
</div>
@endif @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>
</div> </div>
@endforeach @endforeach

View File

@ -8,10 +8,8 @@
TERBAIK</h1> TERBAIK</h1>
</div> </div>
<div class="text-center mt-5"> <div class="text-center mt-5">
<a href="https://wa.me/6285730595855?text=Halo%2C%20saya%20tertarik%20untuk%20mendaftarkan%20venue%20saya" <a href="#" class="text-white bg-yellow-500 py-2 px-4 rounded-lg font-semibold text-sm md:text-lg">Daftarkan
target="_blank" class="text-white bg-yellow-500 py-2 px-4 rounded-lg font-semibold text-sm md:text-lg"> Venue</a>
Daftarkan Venue
</a>
</div> </div>
</div> </div>
@ -54,36 +52,10 @@ 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]) }}" <a href="{{ route('venue', ['venueName' => $venue->name]) }}"
class="flex flex-col h-full border border-gray-400 rounded-lg overflow-hidden"> 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"> <img src="{{ Storage::url($venue->image) }}" alt="{{ $venue->name }}" class="w-full h-48 object-cover">
<div class="flex-grow px-4 py-2"> <div class="flex-grow px-4 py-2">
<h3 class="text-sm text-gray-400 font-semibold mb-2">Venue</h3> <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> <h1 class="text-xl text-gray-800 font-semibold">{{ $venue->name }}</h1>
@if($venue['status'] === 'open') {{-- <p class="text-sm text-gray-500">{{ $venue->address }}</p> --}}
{{-- 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: <p class="mt-10 text-gray-500 text-sm">Mulai:
<span class="font-bold text-gray-800">Rp30,000</span> <span class="font-bold text-gray-800">Rp30,000</span>
<span class="text-gray-400 font-thin text-sm">/ jam</span> <span class="text-gray-400 font-thin text-sm">/ jam</span>

View File

@ -1,7 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('content') @section('content')
<div class="min-h-96 mx-4 md:w-3/4 md:mx-auto py-8"> <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> <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> <h1 class="text-2xl font-bold mb-6">Reschedule Booking</h1>
@ -30,9 +30,12 @@
<p class="font-medium">{{ $duration }} Jam</p> <p class="font-medium">{{ $duration }} Jam</p>
</div> </div>
</div> </div>
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6"> <div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<div class="flex items-start"> <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> <div>
<h3 class="font-semibold text-yellow-700">Perhatian</h3> <h3 class="font-semibold text-yellow-700">Perhatian</h3>
<p class="text-yellow-700 text-sm"> <p class="text-yellow-700 text-sm">
@ -51,13 +54,14 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<label for="date-picker" class="block text-sm font-medium text-gray-700 mb-1">Tanggal Operasional:</label> <label class="block text-sm font-medium text-gray-700 mb-1">Pilih Tanggal:</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> <input type="date" x-model="selectedDate" class="w-full border p-2 rounded-lg"
:min="today" @change="dateChanged">
</div> </div>
<div> <div>
<label for="table-picker" class="block text-sm font-medium text-gray-700 mb-1">Pilih Meja:</label> <label 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"> <select x-model="selectedTableId" class="w-full border p-2 rounded-lg" @change="tableChanged">
<option value="">-- Pilih Meja --</option> <option value="">-- Pilih Meja --</option>
<template x-for="table in tables" :key="table.id"> <template x-for="table in tables" :key="table.id">
<option :value="table.id" x-text="table.name + ' (' + table.brand + ')'"></option> <option :value="table.id" x-text="table.name + ' (' + table.brand + ')'"></option>
@ -68,168 +72,264 @@
<div class="mt-6" x-show="selectedDate && selectedTableId"> <div class="mt-6" x-show="selectedDate && selectedTableId">
<label class="block text-sm font-medium text-gray-700 mb-1">Pilih Jam Mulai:</label> <label class="block text-sm font-medium text-gray-700 mb-1">Pilih Jam Mulai:</label>
<div x-show="!isLoadingSchedules" class="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-2"> <div class="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-2">
<template x-for="hour in availableHours" :key="hour"> <template x-for="hour in availableHours" :key="hour">
<button class="py-2 px-1 rounded-lg text-sm font-medium transition duration-150" <button
:class="getSlotClass(hour)" :disabled="!isSlotAvailable(hour)" @click="selectedStartHour = hour" class="py-2 px-1 rounded-lg text-sm font-medium transition duration-150"
x-text="hour + ':00'"></button> :class="isTimeSlotAvailable(hour) ?
(selectedStartHour === hour ? 'bg-blue-500 text-white' : 'bg-gray-100 hover:bg-gray-200 text-gray-800') :
'bg-gray-200 text-gray-400 cursor-not-allowed opacity-70'"
:disabled="!isTimeSlotAvailable(hour)"
@click="selectStartHour(hour)"
x-text="hour + ':00'">
</button>
</template> </template>
</div> </div>
<div x-show="isLoadingSchedules" class="text-center text-gray-500 py-4">
Memeriksa jadwal... <div class="mt-4" x-show="selectedStartHour">
<p class="text-sm text-gray-700 mb-2">
Jadwal reschedule: <span class="font-medium" x-text="formattedSchedule"></span>
</p>
</div> </div>
</div> </div>
<div class="mt-8 flex justify-end"> <div class="mt-8 flex justify-end">
<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> <a href="{{ route('booking.history') }}" class="bg-gray-300 text-gray-700 px-4 py-2 rounded-lg mr-3">
<button @click="submitReschedule" :disabled="!canSubmit || isSubmitting" Batal
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'"> </a>
<button @click="submitReschedule"
:disabled="!canSubmit || isSubmitting"
:class="canSubmit ? 'bg-green-500 hover:bg-green-600' : 'bg-green-300 cursor-not-allowed'"
class="text-white px-4 py-2 rounded-lg">
<span x-show="!isSubmitting">Konfirmasi Reschedule</span> <span x-show="!isSubmitting">Konfirmasi Reschedule</span>
<span x-show="isSubmitting"> <span x-show="isSubmitting">Memproses...</span>
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div> Memproses...
</span>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<script> <script>
function showNotification(message, type = 'info') { /* ... fungsi notifikasi tidak berubah ... */ } // 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);
}
document.addEventListener('alpine:init', () => { document.addEventListener('alpine:init', () => {
Alpine.data('rescheduleForm', () => ({ Alpine.data('rescheduleForm', () => ({
// PERUBAHAN 2: Inisialisasi data yang bersih & benar tables: @json($venue->tables),
venue: @json($venue),
bookingId: {{ $booking->id }}, bookingId: {{ $booking->id }},
bookingDuration: {{ $duration }}, bookingDuration: {{ $duration }},
tables: @json($venue->tables), originalTableId: {{ $booking->table_id }},
originalStartTime: "{{ \Carbon\Carbon::parse($booking->start_time)->format('H:i') }}",
selectedDate: '{{ $operational_date_string }}', originalDate: "{{ \Carbon\Carbon::parse($booking->start_time)->format('Y-m-d') }}",
selectedTableId: {{ $booking->table_id }}, selectedDate: '',
selectedTableId: '',
selectedStartHour: null, selectedStartHour: null,
bookedSchedules: [], bookedSchedules: [],
isLoadingSchedules: true, availableHours: Array.from({length: 16}, (_, i) => (i + 9).toString().padStart(2, '0')),
isSubmitting: false, isSubmitting: false,
// PERUBAHAN 3: Fungsi init() disederhanakan
init() { init() {
this.fetchSchedules(); 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}`;
// Semua fungsi di bawah ini sudah benar & tidak perlu diubah lagi this.selectedDate = this.originalDate;
get openHour() { return parseInt(this.venue.open_time.split(':')[0]); }, this.selectedTableId = this.originalTableId;
get closeHour() { return parseInt(this.venue.close_time.split(':')[0]); },
get isOvernight() { return this.venue.is_overnight; },
get availableHours() { this.checkBookedSchedules();
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() { get canSubmit() {
return this.selectedStartHour !== null && !this.isLoadingSchedules; return this.selectedDate &&
this.selectedTableId &&
this.selectedStartHour !== null &&
(this.selectedDate !== this.originalDate ||
this.selectedTableId != this.originalTableId ||
this.selectedStartHour !== this.originalStartTime.split(':')[0]);
}, },
async fetchSchedules() { get formattedSchedule() {
if (!this.selectedDate || !this.selectedTableId) return; if (!this.selectedStartHour) return '';
this.isLoadingSchedules = true;
this.bookedSchedules = []; 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; this.selectedStartHour = null;
await this.checkBookedSchedules();
},
async tableChanged() {
this.selectedStartHour = null;
await this.checkBookedSchedules();
},
async checkBookedSchedules() {
if (!this.selectedDate || !this.selectedTableId) return;
try { try {
const response = await fetch(`/booking/reschedule/check-availability?table_id=${this.selectedTableId}&date=${this.selectedDate}&booking_id=${this.bookingId}`); 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('Failed to fetch schedules'); if (!response.ok) {
throw new Error('Network response was not ok');
}
this.bookedSchedules = await response.json(); 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) { } catch (error) {
console.error(error); console.error('Error checking booked schedules:', error);
} finally { showNotification('Terjadi kesalahan saat memeriksa jadwal. Silakan coba lagi.', 'error');
this.isLoadingSchedules = false;
} }
}, },
isSlotAvailable(hour) { isTimeSlotAvailable(hour) {
const startHourInt = parseInt(hour); const hourInt = parseInt(hour);
const endHourInt = startHourInt + this.bookingDuration; const endHourInt = hourInt + this.bookingDuration;
if (this.isOvernight) {
if (startHourInt >= this.openHour && endHourInt > (24 + this.closeHour)) return false; if (endHourInt > 24) return false;
if (startHourInt < this.openHour && endHourInt > this.closeHour) return false;
} else { const selectedDate = new Date(this.selectedDate);
if (endHourInt > this.closeHour) return false; const today = new Date();
} const isToday = selectedDate.toDateString() === today.toDateString();
const now = new Date();
const selectedDateTime = new Date(`${this.selectedDate}T${hour}:00:00`); if (isToday) {
if (this.isOvernight && startHourInt < this.openHour) { const currentHour = today.getHours();
selectedDateTime.setDate(selectedDateTime.getDate() + 1); if (hourInt <= currentHour) {
}
if (selectedDateTime <= now) {
return false; return false;
} }
for (const schedule of this.bookedSchedules) {
const scheduleStart = parseInt(schedule.start.split(':')[0]);
const scheduleEnd = parseInt(schedule.end.split(':')[0]);
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;
} }
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 true;
}
return !this.bookedSchedules.some(schedule => {
const scheduleStart = parseInt(schedule.start.split(':')[0]);
const scheduleEnd = parseInt(schedule.end.split(':')[0]);
return (hourInt < scheduleEnd && endHourInt > scheduleStart);
});
}, },
getSlotClass(hour) { selectStartHour(hour) {
if (!this.isSlotAvailable(hour)) { if (this.isTimeSlotAvailable(hour)) {
return 'bg-gray-200 text-gray-400 cursor-not-allowed'; this.selectedStartHour = hour;
} }
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() { async submitReschedule() {
if (!this.canSubmit) return; if (!this.canSubmit || this.isSubmitting) return;
this.isSubmitting = true; this.isSubmitting = true;
const startDateTime = new Date(`${this.selectedDate}T${this.selectedStartHour}:00:00`);
if (this.isOvernight && parseInt(this.selectedStartHour) < this.openHour) { const startHour = parseInt(this.selectedStartHour);
startDateTime.setDate(startDateTime.getDate() + 1); const endHour = startHour + this.bookingDuration;
}
const endDateTime = new Date(startDateTime.getTime()); const startTime = `${this.selectedDate} ${this.selectedStartHour}:00:00`;
endDateTime.setHours(endDateTime.getHours() + this.bookingDuration); const endTime = `${this.selectedDate} ${endHour.toString().padStart(2, '0')}:00:00`;
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 { try {
const response = await fetch(`/booking/${this.bookingId}/reschedule`, { const response = await fetch(`/booking/${this.bookingId}/reschedule`, {
method: 'POST', 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({ body: JSON.stringify({
table_id: this.selectedTableId, table_id: this.selectedTableId,
start_time: formatForServer(startDateTime), start_time: startTime,
end_time: formatForServer(endDateTime), end_time: endTime,
}), }),
}); });
const result = await response.json(); const result = await response.json();
if (response.ok && result.success) {
showNotification('Booking berhasil di-reschedule!', 'success'); if (result.success) {
setTimeout(() => window.location.href = result.redirect, 1500); showNotification(result.message, 'success');
} else { throw new Error(result.message || 'Gagal reschedule.'); } setTimeout(() => {
window.location.href = result.redirect;
}, 2000);
} else {
showNotification(result.message || 'Terjadi kesalahan saat memproses reschedule.', 'error');
this.isSubmitting = false;
}
} catch (error) { } catch (error) {
showNotification(error.message, 'error'); console.error('Error submitting reschedule:', error);
} finally { showNotification('Terjadi kesalahan. Silakan coba lagi.', 'error');
this.isSubmitting = false; this.isSubmitting = false;
} }
} }

View File

@ -1,7 +1,9 @@
@extends('layouts.main') @extends('layouts.main')
@section('content') @section('content')
<!-- Toast Notification Container -->
<div id="toast-container" class="fixed top-4 right-4 z-50 space-y-2"></div> <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" <div x-data="{ show: false }" x-show="show" x-cloak x-on:show-loading.window="show = true"
x-on:hide-loading.window="show = false" x-on:hide-loading.window="show = false"
class="fixed inset-0 bg-black bg-opacity-50 z-40 flex items-center justify-center"> class="fixed inset-0 bg-black bg-opacity-50 z-40 flex items-center justify-center">
@ -14,244 +16,19 @@ class="fixed inset-0 bg-black bg-opacity-50 z-40 flex items-center justify-cente
</div> </div>
<div class="min-h-96 mx-4 md:w-3/4 md:mx-auto"> <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> <h1 class="text-xl text-gray-800 font-semibold">{{ $venue['name'] }}</h1>
{{-- <p class="text-sm text-gray-500">{{ $venue['description'] ?? 'Tidak ada deskripsi.' }}</p> --}} <p class="text-sm text-gray-500">{{ $venue['location'] ?? 'Lokasi tidak tersedia' }}</p>
<div x-data="{ isOpen: false }" class="text-sm"> <p class="text-sm text-gray-600 mt-1">
@if($venue['status'] === 'open') <i class="fa-regular fa-clock"></i>
@php Jam Operasional: {{ date('H:i', strtotime($venue['open_time'])) }} -
// Siapkan data untuk ditampilkan {{ date('H:i', strtotime($venue['close_time'])) }}
$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> </p>
</div> </div>
@endif <a href="https://www.google.com/maps/search/?api=1&query={{ urlencode($venue['address']) }}" target="_blank"
</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"> class="flex items-center bg-[url('/public/images/map.jpg')] bg-cover bg-center p-4">
<div> <div>
<h1 class="font-semibold">Lokasi Venue</h1> <h1 class="font-semibold">Lokasi Venue</h1>
@ -261,10 +38,8 @@ 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> <i class="fa-solid fa-map-pin text-red-800 text-3xl"></i>
</div> </div>
</a> </a>
@auth @auth
<!-- Pending Bookings Section -->
<div x-data="pendingBookingsComponent" class="mt-6"> <div x-data="pendingBookingsComponent" class="mt-6">
<template x-if="pendingBookings.length > 0"> <template x-if="pendingBookings.length > 0">
<div class="bg-orange-50 border border-orange-200 rounded-lg p-4 mb-6"> <div class="bg-orange-50 border border-orange-200 rounded-lg p-4 mb-6">
@ -330,13 +105,7 @@ class="bg-gray-200 text-gray-700 text-sm px-3 py-1 rounded-md hover:bg-gray-300"
</div> </div>
</div> </div>
@foreach ($venue['tables'] as $table) @foreach ($venue['tables'] as $table)
<div x-data="booking( <div x-data="booking(@json(auth()->check()), '{{ $table['id'] }}', {{ $openHour }}, {{ $closeHour }})"
@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"> class="border rounded-lg shadow-md p-4 mb-4">
<div class="flex items-center justify-between cursor-pointer" <div class="flex items-center justify-between cursor-pointer"
@click="open = !open; if(open) checkBookedSchedules()"> @click="open = !open; if(open) checkBookedSchedules()">
@ -367,25 +136,15 @@ class="border rounded-lg shadow-md p-4 mb-4">
</select> </select>
<h4 class="font-semibold mb-2 mt-4">Pilih Durasi Main:</h4> <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"> <select class="w-full border p-2 rounded-lg" x-model="selectedDuration">
<option value="">-- Pilih Durasi --</option> <option value="">-- Pilih Durasi --</option>
<option value="1">1 Jam</option> <option value="1">1 Jam</option>
<option value="2">2 Jam</option> <option value="2">2 Jam</option>
<option value="3">3 Jam</option> <option value="3">3 Jam</option>
</select> </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 <button class="mt-3 px-4 py-2 bg-green-500 text-white rounded-lg w-full" :disabled="!selectedTime || !selectedDuration || isLoading"
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']) }}')"> @click="initiateBooking('{{ $table['id'] }}', '{{ addslashes($table['name']) }}')">
<template x-if="isLoading"> <template x-if="isLoading">
<span>Loading...</span> <span>Loading...</span>
</template> </template>
@ -396,58 +155,6 @@ class="mt-3 px-4 py-2 rounded-lg w-full
</div> </div>
</div> </div>
@endforeach @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>
</div> </div>
@ -485,10 +192,12 @@ function showToast(message, type = 'info', duration = 5000) {
toastContainer.appendChild(toast); toastContainer.appendChild(toast);
// Animate in
setTimeout(() => { setTimeout(() => {
toast.classList.remove('translate-x-full', 'opacity-0'); toast.classList.remove('translate-x-full', 'opacity-0');
}, 100); }, 100);
// Auto remove
setTimeout(() => { setTimeout(() => {
toast.classList.add('translate-x-full', 'opacity-0'); toast.classList.add('translate-x-full', 'opacity-0');
setTimeout(() => toast.remove(), 300); setTimeout(() => toast.remove(), 300);
@ -532,6 +241,7 @@ class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 transition-
document.body.appendChild(modal); document.body.appendChild(modal);
// Close on backdrop click
modal.addEventListener('click', (e) => { modal.addEventListener('click', (e) => {
if (e.target === modal) { if (e.target === modal) {
modal.remove(); modal.remove();
@ -578,6 +288,7 @@ function showConfirmModal(title, message, onConfirm, onCancel = null) {
if (onCancel) onCancel(); if (onCancel) onCancel();
}); });
// Close on backdrop click
modal.addEventListener('click', (e) => { modal.addEventListener('click', (e) => {
if (e.target === modal) { if (e.target === modal) {
modal.remove(); modal.remove();
@ -586,10 +297,13 @@ function showConfirmModal(title, message, onConfirm, onCancel = null) {
}); });
} }
// Custom event for refreshing pending bookings across components
const refreshPendingBookingsEvent = new Event('refresh-pending-bookings'); const refreshPendingBookingsEvent = new Event('refresh-pending-bookings');
// Tambahkan fungsi helper untuk mendapatkan tanggal Jakarta yang konsisten
function getJakartaDate() { function getJakartaDate() {
const now = new Date(); const now = new Date();
// Buat objek Date dengan timezone Jakarta
const jakartaTime = new Date(now.toLocaleString("en-US", { timeZone: "Asia/Jakarta" })); const jakartaTime = new Date(now.toLocaleString("en-US", { timeZone: "Asia/Jakarta" }));
return jakartaTime; return jakartaTime;
} }
@ -614,19 +328,27 @@ function updateClock() {
document.getElementById('realTimeClock').textContent = timeFormatter.format(jakartaTime); document.getElementById('realTimeClock').textContent = timeFormatter.format(jakartaTime);
} }
// Format functions for pending bookings
function formatDateTime(dateTimeStr) { function formatDateTime(dateTimeStr) {
// Parse the datetime string
const parts = dateTimeStr.split(/[^0-9]/); const parts = dateTimeStr.split(/[^0-9]/);
const year = parseInt(parts[0]); const year = parseInt(parts[0]);
const month = parseInt(parts[1]) - 1; const month = parseInt(parts[1]) - 1; // JS months are 0-based
const day = parseInt(parts[2]); const day = parseInt(parts[2]);
const hour = parseInt(parts[3]); const hour = parseInt(parts[3]);
const minute = parseInt(parts[4]); 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 adjustedHour = (hour + 7) % 24;
const dateFormatter = new Intl.DateTimeFormat('id-ID', { const dateFormatter = new Intl.DateTimeFormat('id-ID', {
day: '2-digit', day: '2-digit',
month: 'short', month: 'short',
year: 'numeric', year: 'numeric',
}); });
// Format the date and time separately
const dateObj = new Date(year, month, day); const dateObj = new Date(year, month, day);
return dateFormatter.format(dateObj) + ' ' + return dateFormatter.format(dateObj) + ' ' +
(adjustedHour.toString().padStart(2, '0') + ':' + (adjustedHour.toString().padStart(2, '0') + ':' +
@ -634,10 +356,15 @@ function formatDateTime(dateTimeStr) {
} }
function formatTime(timeStr) { function formatTime(timeStr) {
// Parse the ISO date string without timezone conversion
const parts = timeStr.split(/[^0-9]/); const parts = timeStr.split(/[^0-9]/);
const hour = parseInt(parts[3]); const hour = parseInt(parts[3]);
const minute = parseInt(parts[4]); const minute = parseInt(parts[4]);
// Tambahkan 7 jam untuk mengkonversi dari UTC ke WIB
const adjustedHour = (hour + 7) % 24; const adjustedHour = (hour + 7) % 24;
// Format time manually
return adjustedHour.toString().padStart(2, '0') + ':' + return adjustedHour.toString().padStart(2, '0') + ':' +
minute.toString().padStart(2, '0'); minute.toString().padStart(2, '0');
} }
@ -647,30 +374,6 @@ function formatPrice(price) {
} }
document.addEventListener('alpine:init', () => { 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 // Pending bookings component
Alpine.data('pendingBookingsComponent', () => ({ Alpine.data('pendingBookingsComponent', () => ({
pendingBookings: [], pendingBookings: [],
@ -679,10 +382,12 @@ function formatPrice(price) {
init() { init() {
this.fetchPendingBookings(); this.fetchPendingBookings();
// Listen for the custom event to refresh pending bookings
document.addEventListener('refresh-pending-bookings', () => { document.addEventListener('refresh-pending-bookings', () => {
console.log('Refreshing pending bookings from event'); console.log('Refreshing pending bookings from event');
this.fetchPendingBookings(); this.fetchPendingBookings();
this.showPendingBookings = true; this.showPendingBookings = true; // Auto-expand the section
}); });
}, },
@ -690,6 +395,7 @@ function formatPrice(price) {
fetch('/booking/pending') fetch('/booking/pending')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
// Filter bookings untuk venue saat ini jika diperlukan
const currentVenueId = {{ $venue['id'] ?? 'null' }}; const currentVenueId = {{ $venue['id'] ?? 'null' }};
if (currentVenueId) { if (currentVenueId) {
this.pendingBookings = data.filter(booking => this.pendingBookings = data.filter(booking =>
@ -698,7 +404,12 @@ function formatPrice(price) {
} else { } else {
this.pendingBookings = data; this.pendingBookings = data;
} }
// Log jumlah pending bookings yang ditemukan
console.log("Found", this.pendingBookings.length, "pending bookings"); 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) { if (this.pendingBookings.length > 0 && window.justClosedPayment) {
this.showPendingBookings = true; this.showPendingBookings = true;
window.justClosedPayment = false; window.justClosedPayment = false;
@ -710,21 +421,36 @@ function formatPrice(price) {
resumeBooking(bookingId) { resumeBooking(bookingId) {
this.isLoadingPending = true; this.isLoadingPending = true;
window.dispatchEvent(new CustomEvent('show-loading')); window.dispatchEvent(new CustomEvent('show-loading'));
fetch(`/booking/pending/${bookingId}/resume`) fetch(`/booking/pending/${bookingId}/resume`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
window.dispatchEvent(new CustomEvent('hide-loading')); window.dispatchEvent(new CustomEvent('hide-loading'));
if (data.success) { if (data.success) {
console.log("Opening payment with snap token:", data.snap_token); console.log("Opening payment with snap token:", data.snap_token);
// Open Snap payment
window.snap.pay(data.snap_token, { window.snap.pay(data.snap_token, {
onSuccess: (result) => this.createBookingAfterPayment(data.order_id, result), onSuccess: (result) => {
onPending: (result) => { showToast('Pembayaran pending...', 'warning'); this.isLoadingPending = false; }, this.createBookingAfterPayment(data.order_id, result);
onError: (result) => { showToast('Pembayaran gagal', 'error'); this.isLoadingPending = false; }, },
onClose: () => { showToast('Anda menutup popup pembayaran', 'warning'); this.isLoadingPending = false; } 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;
}
}); });
} else { } else {
showToast(data.message, 'error'); showToast(data.message, 'error');
this.isLoadingPending = false; this.isLoadingPending = false;
// Refresh pending bookings list
this.fetchPendingBookings(); this.fetchPendingBookings();
} }
}) })
@ -744,7 +470,10 @@ function formatPrice(price) {
this.isLoadingPending = true; this.isLoadingPending = true;
fetch(`/booking/pending/${bookingId}`, { fetch(`/booking/pending/${bookingId}`, {
method: 'DELETE', 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(response => response.json())
.then(data => { .then(data => {
@ -767,101 +496,163 @@ function formatPrice(price) {
createBookingAfterPayment(orderId, paymentResult) { createBookingAfterPayment(orderId, paymentResult) {
window.dispatchEvent(new CustomEvent('show-loading')); window.dispatchEvent(new CustomEvent('show-loading'));
fetch('/booking', { fetch('/booking', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content }, headers: {
body: JSON.stringify({ order_id: orderId, transaction_id: paymentResult.transaction_id, payment_method: paymentResult.payment_type, transaction_status: paymentResult.transaction_status }), '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 => { .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(); return res.json();
}) })
.then(data => { .then(data => {
window.dispatchEvent(new CustomEvent('hide-loading')); window.dispatchEvent(new CustomEvent('hide-loading'));
showToast('Pembayaran dan booking berhasil!', 'success'); showToast('Pembayaran dan booking berhasil!', 'success');
this.isLoadingPending = false; this.isLoadingPending = false;
// Refresh pending bookings list
this.fetchPendingBookings(); this.fetchPendingBookings();
setTimeout(() => { window.location.href = '/booking/history'; }, 2000);
// Redirect to booking history
setTimeout(() => {
window.location.href = '/booking/history';
}, 2000);
}) })
.catch(err => { .catch(err => {
window.dispatchEvent(new CustomEvent('hide-loading')); window.dispatchEvent(new CustomEvent('hide-loading'));
console.error('Booking error:', err); console.error('Booking error:', err);
showToast('Gagal menyimpan booking: ' + err.message, 'error'); showToast('Pembayaran berhasil tetapi gagal menyimpan booking: ' + err.message, 'error');
this.isLoadingPending = false; this.isLoadingPending = false;
}); });
} }
})); }));
// Regular booking component // Regular booking component (updated with dynamic hours)
Alpine.data('booking', (isLoggedIn, tableId, openHour, closeHour, isOvernight) => ({ Alpine.data('booking', (isLoggedIn, tableId, openHour, closeHour) => ({
isLoggedIn, tableId, openHour, closeHour, isOvernight, open: false, selectedTime: '', selectedDuration: '', isLoading: false, bookedSchedules: [], 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
getAvailableHours() { getAvailableHours() {
let hours = []; let hours = [];
const currentJakartaHour = getJakartaDate().getHours(); for (let i = this.openHour; i <= this.closeHour; i++) {
if (this.isOvernight) { hours.push(i.toString().padStart(2, '0'));
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; return hours;
}, },
isTimeBooked(time) { isTimeBooked(time) {
const timeFormatted = time.padStart(5, '0'); const timeFormatted = time.padStart(5, '0');
return this.bookedSchedules.some(schedule => { return this.bookedSchedules.some(schedule => {
const isOvernightBooking = schedule.end < schedule.start; return timeFormatted >= schedule.start && timeFormatted < schedule.end;
if (isOvernightBooking) return (timeFormatted >= schedule.start || timeFormatted < schedule.end);
else return (timeFormatted >= schedule.start && timeFormatted < schedule.end);
}); });
}, },
async checkBookedSchedules() { async checkBookedSchedules() {
// Gunakan tanggal Jakarta yang konsisten
const today = getJakartaDateString(); const today = getJakartaDateString();
try { try {
const response = await fetch(`/booking/schedules?table_id=${this.tableId}&date=${today}`); const response = await fetch(`/booking/schedules?table_id=${this.tableId}&date=${today}`);
this.bookedSchedules = await response.json(); this.bookedSchedules = await response.json();
} catch (error) { console.error('Error checking booked schedules:', error); } 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);
}
}, },
initiateBooking(tableId, tableName) {
if (!this.isLoggedIn) { showToast('Silahkan login terlebih dahulu', 'warning'); return; }
if (!this.selectedTime || !this.selectedDuration) { showToast('Pilih jam dan durasi', 'warning'); return; }
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 (!selectedTime || !selectedDuration) {
showToast('Please select both time and duration', 'warning');
return;
}
// Validasi jam menggunakan waktu Jakarta
const now = getJakartaDate(); const now = getJakartaDate();
const selectedDateTime = new Date(now); const selectedDateTime = new Date(now);
const [selectedHour, selectedMinute] = this.selectedTime.split(':').map(Number); const [selectedHour, selectedMinute] = selectedTime.split(':').map(Number);
selectedDateTime.setHours(selectedHour, selectedMinute, 0, 0); selectedDateTime.setHours(selectedHour, selectedMinute, 0, 0);
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; } // Uncomment this for production to prevent booking past times
if (selectedDateTime <= now) {
showToast('Tidak bisa booking untuk waktu yang sudah berlalu', 'warning');
return;
}
this.isLoading = true; this.isLoading = true;
window.dispatchEvent(new CustomEvent('show-loading')); window.dispatchEvent(new CustomEvent('show-loading'));
// Gunakan tanggal Jakarta yang konsisten
const bookingDate = getJakartaDateString(); const bookingDate = getJakartaDateString();
fetch('/booking/initiate', { fetch('/booking/initiate', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content }, headers: {
body: JSON.stringify({ table_id: tableId, start_time: this.selectedTime, duration: this.selectedDuration, booking_date: bookingDate }), '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
}),
}) })
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
window.dispatchEvent(new CustomEvent('hide-loading')); window.dispatchEvent(new CustomEvent('hide-loading'));
if (data.success) { if (data.success) {
if (data.snap_token) { console.log("Opening payment with snap token:", data.snap_token);
// Open Snap payment
window.snap.pay(data.snap_token, { window.snap.pay(data.snap_token, {
onSuccess: (result) => this.createBooking(data.order_id, result), onSuccess: (result) => {
onPending: (result) => { showToast('Pembayaran pending...', 'warning'); this.isLoading = false; }, this.createBooking(data.order_id, result);
onError: (result) => { showToast('Pembayaran gagal', 'error'); this.isLoading = false; }, },
onPending: (result) => {
showToast('Pembayaran pending, silahkan selesaikan pembayaran', 'warning');
this.isLoading = false;
},
onError: (result) => {
showToast('Pembayaran gagal', 'error');
this.isLoading = false;
},
onClose: () => { onClose: () => {
showToast('Anda menutup popup pembayaran', 'warning'); showToast('Anda menutup popup tanpa menyelesaikan pembayaran', 'warning');
this.isLoading = false; this.isLoading = false;
window.justClosedPayment = true; window.justClosedPayment = true;
// Dispatch event to refresh pending bookings
document.dispatchEvent(refreshPendingBookingsEvent); document.dispatchEvent(refreshPendingBookingsEvent);
} }
}); });
} else {
showToast(data.message || 'Booking berhasil diproses', 'success');
this.isLoading = false;
if (data.booking_id) setTimeout(() => window.location.reload(), 1000);
}
} else { } else {
showToast(data.message, 'error'); showToast(data.message, 'error');
this.isLoading = false; this.isLoading = false;
@ -874,38 +665,62 @@ function formatPrice(price) {
this.isLoading = false; this.isLoading = false;
}); });
}, },
createBooking(orderId, paymentResult) { createBooking(orderId, paymentResult) {
window.dispatchEvent(new CustomEvent('show-loading')); window.dispatchEvent(new CustomEvent('show-loading'));
fetch('/booking', { fetch('/booking', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content }, headers: {
body: JSON.stringify({ order_id: orderId, transaction_id: paymentResult.transaction_id, payment_method: paymentResult.payment_type, transaction_status: paymentResult.transaction_status }), '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 => { .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(); return res.json();
}) })
.then(data => { .then(data => {
window.dispatchEvent(new CustomEvent('hide-loading')); window.dispatchEvent(new CustomEvent('hide-loading'));
showToast('Pembayaran dan booking berhasil!', 'success'); showToast('Pembayaran dan booking berhasil!', 'success');
this.isLoading = false; this.isLoading = false;
// Reset form
this.selectedTime = ''; this.selectedTime = '';
this.selectedDuration = ''; this.selectedDuration = '';
this.open = false; this.open = false;
// Refresh booked schedules
this.checkBookedSchedules(); this.checkBookedSchedules();
setTimeout(() => { window.location.href = '/booking/history'; }, 2000);
// Redirect to booking history
setTimeout(() => {
window.location.href = '/booking/history';
}, 2000);
}) })
.catch(err => { .catch(err => {
window.dispatchEvent(new CustomEvent('hide-loading')); window.dispatchEvent(new CustomEvent('hide-loading'));
console.error('Booking error:', err); console.error('Booking error:', err);
showToast('Gagal menyimpan booking: ' + err.message, 'error'); showToast('Pembayaran berhasil tetapi gagal menyimpan booking: ' + err.message, 'error');
this.isLoading = false; this.isLoading = false;
}); });
} }
})); }));
}); });
// Initialize clock
updateClock(); updateClock();
setInterval(updateClock, 1000); setInterval(updateClock, 1000);
</script> </script>
@endsection @endsection

View File

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

View File

@ -1,150 +1,114 @@
@extends('layouts.super-admin') @extends('layouts.super-admin')
@section('content') @section('content')
<div class="bg-gray-50 min-h-screen"> <div class="mb-8">
<div class="p-6"> <h1 class="text-4xl font-bold bg-gradient-to-r from-purple-600 to-pink-600 bg-clip-text text-transparent">
<!-- Header and Welcome --> Dashboard Super Admin
<div class="flex justify-between items-center mb-6"> </h1>
<div> <p class="text-gray-600 text-lg">Analytics & Overview dari seluruh venue</p>
<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> </div>
<!-- Main Stats Cards --> <!-- Main Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<!-- Total Admin Card --> <div class="bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl shadow-lg p-6 text-white">
<div class="bg-white rounded-xl shadow-sm p-6 border-l-4 border-blue-500 hover:shadow-md transition"> <div class="flex items-center justify-between">
<div class="flex justify-between items-start">
<div> <div>
<p class="text-sm font-medium text-gray-500">Total Admin</p> <p class="text-blue-100 text-sm font-medium">Total Admin</p>
<p class="text-2xl font-bold text-gray-800">{{ $adminCount ?? 0 }}</p> <p class="text-3xl font-bold">{{ $adminCount ?? 0 }}</p>
</div> </div>
<div class="text-blue-500 p-2 bg-blue-50 rounded-lg"> <div class="bg-blue-400 bg-opacity-30 rounded-full p-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <i class="fas fa-users-cog text-2xl"></i>
<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>
</div> </div>
<p class="text-xs text-gray-500 mt-2">Administrator aktif</p>
</div> </div>
<!-- Total Venue Card --> <div class="bg-gradient-to-br from-emerald-500 to-emerald-600 rounded-xl shadow-lg p-6 text-white">
<div class="bg-white rounded-xl shadow-sm p-6 border-l-4 border-green-500 hover:shadow-md transition"> <div class="flex items-center justify-between">
<div class="flex justify-between items-start">
<div> <div>
<p class="text-sm font-medium text-gray-500">Total Venue</p> <p class="text-emerald-100 text-sm font-medium">Total Venue</p>
<p class="text-2xl font-bold text-gray-800">{{ $venueCount ?? 0 }}</p> <p class="text-3xl font-bold">{{ $venueCount ?? 0 }}</p>
</div> </div>
<div class="text-green-500 p-2 bg-green-50 rounded-lg"> <div class="bg-emerald-400 bg-opacity-30 rounded-full p-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <i class="fas fa-building text-2xl"></i>
<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>
</div> </div>
<p class="text-xs text-gray-500 mt-2">Venue terdaftar</p>
</div> </div>
<!-- Total User Card --> <div class="bg-gradient-to-br from-amber-500 to-orange-500 rounded-xl shadow-lg p-6 text-white">
<div class="bg-white rounded-xl shadow-sm p-6 border-l-4 border-amber-500 hover:shadow-md transition"> <div class="flex items-center justify-between">
<div class="flex justify-between items-start">
<div> <div>
<p class="text-sm font-medium text-gray-500">Total User</p> <p class="text-amber-100 text-sm font-medium">Total User</p>
<p class="text-2xl font-bold text-gray-800">{{ $userCount ?? 0 }}</p> <p class="text-3xl font-bold">{{ $userCount ?? 0 }}</p>
</div> </div>
<div class="text-amber-500 p-2 bg-amber-50 rounded-lg"> <div class="bg-amber-400 bg-opacity-30 rounded-full p-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <i class="fas fa-users text-2xl"></i>
<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>
</div> </div>
<p class="text-xs text-gray-500 mt-2">Pengguna terdaftar</p>
</div> </div>
<!-- Total Meja Card --> <div class="bg-gradient-to-br from-purple-500 to-pink-500 rounded-xl shadow-lg p-6 text-white">
<div class="bg-white rounded-xl shadow-sm p-6 border-l-4 border-purple-500 hover:shadow-md transition"> <div class="flex items-center justify-between">
<div class="flex justify-between items-start">
<div> <div>
<p class="text-sm font-medium text-gray-500">Total Meja</p> <p class="text-purple-100 text-sm font-medium">Total Meja</p>
<p class="text-2xl font-bold text-gray-800">{{ $tableCount ?? 0 }}</p> <p class="text-3xl font-bold">{{ $tableCount ?? 0 }}</p>
</div> </div>
<div class="text-purple-500 p-2 bg-purple-50 rounded-lg"> <div class="bg-purple-400 bg-opacity-30 rounded-full p-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <i class="fas fa-table text-2xl"></i>
<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>
</div> </div>
<p class="text-xs text-gray-500 mt-2">Meja tersedia</p>
</div> </div>
</div> </div>
<!-- Charts Section --> <!-- Charts Section -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Revenue Comparison Chart --> <!-- Revenue Comparison Chart -->
<div class="bg-white rounded-xl shadow-sm"> <div class="bg-white rounded-xl shadow-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200"> <div class="bg-gradient-to-r from-indigo-500 to-purple-600 px-6 py-4">
<h3 class="text-lg font-semibold text-gray-800 flex items-center"> <h3 class="text-xl font-bold text-white 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"> <i class="fas fa-chart-bar mr-3"></i>
<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 Revenue Bulan Ini
</h3> </h3>
<p class="text-gray-500 text-sm">Per venue - {{ Carbon\Carbon::now()->format('F Y') }}</p> <p class="text-indigo-100 text-sm">Per venue - {{ Carbon\Carbon::now()->format('F Y') }}</p>
</div> </div>
<div class="p-6"> <div class="p-6">
<div style="height: 300px;"> <canvas id="revenueChart" width="400" height="300"></canvas>
<canvas id="revenueChart"></canvas>
</div>
</div> </div>
</div> </div>
<!-- Popular Venues Ranking --> <!-- Popular Venues Ranking -->
<div class="bg-white rounded-xl shadow-sm"> <div class="bg-white rounded-xl shadow-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200"> <div class="bg-gradient-to-r from-pink-500 to-rose-600 px-6 py-4">
<h3 class="text-lg font-semibold text-gray-800 flex items-center"> <h3 class="text-xl font-bold text-white 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"> <i class="fas fa-trophy mr-3"></i>
<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 Ranking Popularitas
</h3> </h3>
<p class="text-gray-500 text-sm">Venue terpopuler bulan ini</p> <p class="text-pink-100 text-sm">Venue terpopuler bulan ini</p>
</div> </div>
<div class="p-6"> <div class="p-6">
<!-- Filter Toggle --> <!-- Filter Toggle -->
<div class="mb-4"> <div class="mb-4">
<div class="bg-gray-100 rounded-lg p-1 flex"> <div class="bg-gray-100 rounded-lg p-1 flex">
<button id="rankingByBookings" <button
class="flex-1 py-2 px-4 rounded-md text-sm font-medium transition-all duration-200 bg-green-600 text-white" id="rankingByBookings"
class="flex-1 py-2 px-4 rounded-md text-sm font-medium transition-all duration-200 bg-rose-500 text-white"
onclick="toggleRanking('bookings')"> onclick="toggleRanking('bookings')">
<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"> <i class="fas fa-calendar-check mr-2"></i>
<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 Total Booking
</button> </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" 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')"> onclick="toggleRanking('revenue')">
<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"> <i class="fas fa-money-bill-wave mr-2"></i>
<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 Total Revenue
</button> </button>
</div> </div>
</div> </div>
<div style="height: 300px;"> <canvas id="popularVenuesChart" width="400" height="300"></canvas>
<canvas id="popularVenuesChart"></canvas>
</div>
</div>
</div>
</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 src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
<script> <script>
// Revenue Chart Data // Revenue Chart Data
@ -153,21 +117,21 @@ class="flex-1 py-2 px-4 rounded-md text-sm font-medium transition-all duration-2
// Popular Venues Data // Popular Venues Data
const popularVenuesData = @json($popularVenuesData); const popularVenuesData = @json($popularVenuesData);
// Color Palettes - More subtle and professional // Color Palettes
const revenueColors = [ const revenueColors = [
'rgba(59, 130, 246, 0.8)', // Blue 'rgba(99, 102, 241, 0.8)', // Indigo
'rgba(16, 185, 129, 0.8)', // Emerald 'rgba(168, 85, 247, 0.8)', // Purple
'rgba(245, 158, 11, 0.8)', // Amber
'rgba(139, 92, 246, 0.8)', // Violet
'rgba(236, 72, 153, 0.8)', // Pink 'rgba(236, 72, 153, 0.8)', // Pink
'rgba(34, 197, 94, 0.8)', // Emerald
'rgba(251, 146, 60, 0.8)', // Orange
]; ];
const rankingColors = [ const rankingColors = [
'rgba(34, 197, 94, 0.8)', // Green 'rgba(239, 68, 68, 0.8)', // Red
'rgba(59, 130, 246, 0.8)', // Blue
'rgba(245, 158, 11, 0.8)', // Amber 'rgba(245, 158, 11, 0.8)', // Amber
'rgba(139, 92, 246, 0.8)', // Violet 'rgba(34, 197, 94, 0.8)', // Emerald
'rgba(107, 114, 128, 0.8)', // Gray 'rgba(59, 130, 246, 0.8)', // Blue
'rgba(168, 85, 247, 0.8)', // Purple
]; ];
// Revenue Chart // Revenue Chart
@ -180,26 +144,20 @@ class="flex-1 py-2 px-4 rounded-md text-sm font-medium transition-all duration-2
label: 'Revenue (Rp)', label: 'Revenue (Rp)',
data: revenueData.map(item => parseFloat(item.total_revenue)), data: revenueData.map(item => parseFloat(item.total_revenue)),
backgroundColor: revenueColors, backgroundColor: revenueColors,
borderRadius: 4, borderRadius: 8,
borderSkipped: false, borderSkipped: false,
}] }]
}, },
options: { options: {
indexAxis: 'y', indexAxis: 'y',
responsive: true, responsive: true,
maintainAspectRatio: false,
plugins: { plugins: {
legend: { legend: {
display: false display: false
}, },
tooltip: { tooltip: {
backgroundColor: 'rgba(17, 24, 39, 0.9)',
titleColor: '#fff',
bodyColor: '#fff',
borderColor: 'rgba(156, 163, 175, 0.2)',
borderWidth: 1,
callbacks: { callbacks: {
label: function (context) { label: function(context) {
return 'Revenue: Rp ' + new Intl.NumberFormat('id-ID').format(context.parsed.x); return 'Revenue: Rp ' + new Intl.NumberFormat('id-ID').format(context.parsed.x);
} }
} }
@ -208,12 +166,8 @@ class="flex-1 py-2 px-4 rounded-md text-sm font-medium transition-all duration-2
scales: { scales: {
x: { x: {
beginAtZero: true, beginAtZero: true,
grid: {
color: 'rgba(156, 163, 175, 0.1)'
},
ticks: { ticks: {
color: '#6B7280', callback: function(value) {
callback: function (value) {
return 'Rp ' + new Intl.NumberFormat('id-ID', { return 'Rp ' + new Intl.NumberFormat('id-ID', {
notation: 'compact', notation: 'compact',
maximumFractionDigits: 1 maximumFractionDigits: 1
@ -224,9 +178,6 @@ class="flex-1 py-2 px-4 rounded-md text-sm font-medium transition-all duration-2
y: { y: {
grid: { grid: {
display: false display: false
},
ticks: {
color: '#6B7280'
} }
} }
} }
@ -252,7 +203,7 @@ function createPopularChart(sortBy) {
label: sortBy === 'revenue' ? 'Revenue (Rp)' : 'Total Booking', label: sortBy === 'revenue' ? 'Revenue (Rp)' : 'Total Booking',
data: sortedData.map(item => sortBy === 'revenue' ? parseFloat(item.total_revenue) : parseInt(item.total_bookings)), data: sortedData.map(item => sortBy === 'revenue' ? parseFloat(item.total_revenue) : parseInt(item.total_bookings)),
backgroundColor: rankingColors, backgroundColor: rankingColors,
borderRadius: 4, borderRadius: 8,
borderSkipped: false, borderSkipped: false,
}] }]
}; };
@ -267,19 +218,13 @@ function createPopularChart(sortBy) {
options: { options: {
indexAxis: 'y', indexAxis: 'y',
responsive: true, responsive: true,
maintainAspectRatio: false,
plugins: { plugins: {
legend: { legend: {
display: false display: false
}, },
tooltip: { tooltip: {
backgroundColor: 'rgba(17, 24, 39, 0.9)',
titleColor: '#fff',
bodyColor: '#fff',
borderColor: 'rgba(156, 163, 175, 0.2)',
borderWidth: 1,
callbacks: { callbacks: {
label: function (context) { label: function(context) {
if (sortBy === 'revenue') { if (sortBy === 'revenue') {
return 'Revenue: Rp ' + new Intl.NumberFormat('id-ID').format(context.parsed.x); return 'Revenue: Rp ' + new Intl.NumberFormat('id-ID').format(context.parsed.x);
} else { } else {
@ -292,12 +237,8 @@ function createPopularChart(sortBy) {
scales: { scales: {
x: { x: {
beginAtZero: true, beginAtZero: true,
grid: {
color: 'rgba(156, 163, 175, 0.1)'
},
ticks: { ticks: {
color: '#6B7280', callback: function(value) {
callback: function (value) {
if (sortBy === 'revenue') { if (sortBy === 'revenue') {
return 'Rp ' + new Intl.NumberFormat('id-ID', { return 'Rp ' + new Intl.NumberFormat('id-ID', {
notation: 'compact', notation: 'compact',
@ -312,9 +253,6 @@ function createPopularChart(sortBy) {
y: { y: {
grid: { grid: {
display: false display: false
},
ticks: {
color: '#6B7280'
} }
} }
} }
@ -331,15 +269,14 @@ function toggleRanking(type) {
const revenueBtn = document.getElementById('rankingByRevenue'); const revenueBtn = document.getElementById('rankingByRevenue');
if (type === 'bookings') { if (type === 'bookings') {
bookingBtn.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 bg-rose-500 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'; 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'); createPopularChart('bookings');
} else { } else {
revenueBtn.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 bg-rose-500 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'; 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'); createPopularChart('revenue');
} }
} }
</script> </script>
@endpush
@endsection @endsection

View File

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

View File

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

View File

@ -9,14 +9,12 @@
use App\Http\Controllers\admin\TableController; use App\Http\Controllers\admin\TableController;
use App\Http\Controllers\admin\RevenueController; use App\Http\Controllers\admin\RevenueController;
use App\Http\Controllers\admin\AdminController; 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\Auth\VerificationController;
use App\Http\Controllers\superadmin\SuperAdminController; use App\Http\Controllers\superadmin\SuperAdminController;
use App\Http\Controllers\superadmin\AdminManagementController; use App\Http\Controllers\superadmin\AdminManagementController;
use App\Http\Controllers\superadmin\VenueManagementController; use App\Http\Controllers\superadmin\VenueManagementController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\ReviewController;
// Authentication Routes (dengan verifikasi email aktif) // Authentication Routes (dengan verifikasi email aktif)
Auth::routes(['verify' => true]); Auth::routes(['verify' => true]);
@ -28,7 +26,7 @@
Route::get('/venue/{venueName}', [VenueController::class, "venue"])->name('venue'); Route::get('/venue/{venueName}', [VenueController::class, "venue"])->name('venue');
// Changed routes for the new booking flow // Changed routes for the new booking flow
Route::post('/booking/initiate', [BookingController::class, 'createPaymentIntent'])->name('booking.initiate'); Route::post('/booking/payment-intent', [BookingController::class, 'createPaymentIntent'])->name('booking.payment-intent');
Route::post('/booking', [BookingController::class, 'store'])->name('booking.store'); Route::post('/booking', [BookingController::class, 'store'])->name('booking.store');
Route::get('/booking/schedules', [BookingController::class, 'getBookedSchedules'])->name('booking.schedules'); Route::get('/booking/schedules', [BookingController::class, 'getBookedSchedules'])->name('booking.schedules');
Route::post('/payment/notification', [BookingController::class, 'handleNotification'])->name('payment.notification'); Route::post('/payment/notification', [BookingController::class, 'handleNotification'])->name('payment.notification');
@ -69,34 +67,20 @@
// Any sensitive operations that should still require password confirmation can go here // 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) // 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('/', [AdminController::class, 'index'])->name('admin.dashboard');
// 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', [BookingsController::class, 'index'])->name('admin.bookings.index');
Route::get('/bookings/export', [BookingsController::class, 'export'])->name('admin.bookings.export'); 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/{id}', [BookingsController::class, 'show'])->name('admin.bookings.show');
Route::get('/bookings/validate/{token}', [BookingsController::class, 'validateBooking'])->name('admin.bookings.validate'); // <-- Pindahkan ke atas Route::get('/bookings/{id}/edit', [BookingsController::class, 'edit'])->name('admin.bookings.edit');
Route::get('/bookings/{id}', [BookingsController::class, 'show'])->name('admin.bookings.show'); // <-- Route dengan {id} sekarang di bawah Route::put('/bookings/{id}', [BookingsController::class, 'update'])->name('admin.bookings.update');
Route::get('/bookings/{id}/edit', [BookingsController::class, 'edit'])->name('admin.bookings.edit'); Route::patch('/bookings/{id}/complete', [BookingsController::class, 'complete'])->name('admin.bookings.complete');
Route::put('/bookings/{id}', [BookingsController::class, 'update'])->name('admin.bookings.update'); Route::patch('/bookings/{id}/cancel', [BookingsController::class, 'cancel'])->name('admin.bookings.cancel');
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 // CRUD routes untuk manajemen meja
Route::get('/tables', [TableController::class, 'index'])->name('admin.tables.index'); Route::get('/tables', [TableController::class, 'index'])->name('admin.tables.index');
Route::get('/tables/create', [TableController::class, 'create'])->name('admin.tables.create'); Route::get('/tables/create', [TableController::class, 'create'])->name('admin.tables.create');
Route::post('/tables', [TableController::class, 'store'])->name('admin.tables.store'); Route::post('/tables', [TableController::class, 'store'])->name('admin.tables.store');
@ -104,26 +88,17 @@
Route::put('/tables/{id}', [TableController::class, 'update'])->name('admin.tables.update'); Route::put('/tables/{id}', [TableController::class, 'update'])->name('admin.tables.update');
Route::delete('/tables/{id}', [TableController::class, 'destroy'])->name('admin.tables.destroy'); Route::delete('/tables/{id}', [TableController::class, 'destroy'])->name('admin.tables.destroy');
// Venue management routes // CRUD routes untuk revenue
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', [RevenueController::class, 'index'])->name('admin.revenues.index');
Route::get('/revenues/detail/{tableId}', [RevenueController::class, 'detail'])->name('admin.revenues.detail'); Route::get('/revenues/detail/{tableId}', [RevenueController::class, 'detail'])->name('admin.revenues.detail');
Route::get('/revenues/export', [RevenueController::class, 'export'])->name('admin.revenues.export'); Route::get('/revenues/export', [RevenueController::class, 'export'])->name('admin.revenues.export');
}); });
// Superadmin routes // Superadmin routes
Route::middleware(['auth', 'verified', 'is_superadmin'])->prefix('superadmin')->group(function () { Route::middleware(['auth', 'verified', 'is_superadmin'])->prefix('superadmin')->group(function () {
Route::get('/', [App\Http\Controllers\superadmin\SuperAdminController::class, 'index'])->name('superadmin.dashboard'); Route::get('/', [App\Http\Controllers\superadmin\SuperAdminController::class, 'index'])->name('superadmin.dashboard');
// Admin management routes // Tambahkan route untuk manajemen Admin
Route::get('/admin', [AdminManagementController::class, 'index'])->name('superadmin.admin.index'); Route::get('/admin', [AdminManagementController::class, 'index'])->name('superadmin.admin.index');
Route::get('/admin/create', [AdminManagementController::class, 'create'])->name('superadmin.admin.create'); Route::get('/admin/create', [AdminManagementController::class, 'create'])->name('superadmin.admin.create');
Route::post('/admin', [AdminManagementController::class, 'store'])->name('superadmin.admin.store'); Route::post('/admin', [AdminManagementController::class, 'store'])->name('superadmin.admin.store');
@ -131,7 +106,7 @@
Route::put('/admin/{id}', [AdminManagementController::class, 'update'])->name('superadmin.admin.update'); Route::put('/admin/{id}', [AdminManagementController::class, 'update'])->name('superadmin.admin.update');
Route::delete('/admin/{id}', [AdminManagementController::class, 'destroy'])->name('superadmin.admin.destroy'); Route::delete('/admin/{id}', [AdminManagementController::class, 'destroy'])->name('superadmin.admin.destroy');
// Venue management routes (for superadmin) // Tambahkan route untuk manajemen Venue
Route::get('/venue', [VenueManagementController::class, 'index'])->name('superadmin.venue.index'); Route::get('/venue', [VenueManagementController::class, 'index'])->name('superadmin.venue.index');
Route::get('/venue/create', [VenueManagementController::class, 'create'])->name('superadmin.venue.create'); Route::get('/venue/create', [VenueManagementController::class, 'create'])->name('superadmin.venue.create');
Route::post('/venue', [VenueManagementController::class, 'store'])->name('superadmin.venue.store'); Route::post('/venue', [VenueManagementController::class, 'store'])->name('superadmin.venue.store');