Compare commits

...

10 Commits

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

View File

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

View File

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

View File

@ -0,0 +1,66 @@
<?php
namespace App\Http\Controllers;
use App\Models\Review;
use App\Models\Booking;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ReviewController extends Controller
{
/**
* Menyimpan ulasan baru ke dalam database.
*/
public function create(Booking $booking)
{
// Otorisasi: Pastikan user yang login adalah pemilik booking
if ($booking->user_id !== Auth::id()) {
abort(403, 'Akses tidak diizinkan.');
}
// Otorisasi: Pastikan booking sudah pernah diulas atau belum
if ($booking->review) {
return redirect()->route('booking.history')->with('error', 'Anda sudah pernah memberikan ulasan untuk booking ini.');
}
// Muat relasi yang diperlukan untuk ditampilkan di view
$booking->load('table.venue');
return view('reviews.create', compact('booking'));
}
public function store(Request $request)
{
// 1. Validasi Input
$request->validate([
'booking_id' => 'required|exists:bookings,id|unique:reviews,booking_id',
'rating' => 'required|integer|min:1|max:5',
'comment' => 'required|string|max:2000',
], [
'booking_id.unique' => 'Anda sudah pernah memberikan ulasan untuk booking ini.',
'rating.required' => 'Rating bintang wajib diisi.',
'comment.required' => 'Komentar ulasan wajib diisi.',
]);
// 2. Cek Otorisasi
$booking = Booking::find($request->booking_id);
// Pastikan user yang login adalah pemilik booking tersebut
if ($booking->user_id !== Auth::id()) {
return redirect()->back()->with('error', 'Akses tidak diizinkan.');
}
// 3. Simpan Ulasan
Review::create([
'user_id' => Auth::id(),
'venue_id' => $booking->table->venue_id, // Ambil venue_id dari relasi
'booking_id' => $booking->id,
'rating' => $request->rating,
'comment' => $request->comment,
]);
// 4. Redirect dengan Pesan Sukses
return redirect()->route('booking.history')->with('success', 'Terima kasih atas ulasan Anda!');
}
}

View File

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

View File

@ -15,7 +15,15 @@ class BookingsController extends Controller
{
public function index(Request $request)
{
$query = Booking::with(['table', 'user']);
// Ambil venue_id dari admin yang sedang login
// Sesuaikan dengan struktur database kamu:
$adminVenueId = auth()->user()->venue_id; // Asumsi admin punya kolom venue_id
// Query booking dengan filter venue terlebih dahulu
$query = Booking::with(['table', 'user'])
->whereHas('table', function ($q) use ($adminVenueId) {
$q->where('venue_id', $adminVenueId);
});
// Search functionality
if ($request->has('search') && !empty($request->search)) {
@ -63,21 +71,73 @@ public function index(Request $request)
$query->orderBy($sortColumn, $sortDirection);
}
$bookings = $query->paginate(10)->withQueryString();
$bookings = $query->paginate(20)->withQueryString();
return view('admin.bookings.index', compact('bookings'));
}
public function showScanner()
{
return view('admin.bookings.scan');
}
public function validateBooking($token)
{
$booking = Booking::where('validation_token', $token)
->with(['user', 'table.venue'])
->first();
if (!$booking) {
return response()->json(['error' => 'Booking tidak ditemukan atau tidak valid.'], 404);
}
// Otorisasi: Pastikan admin hanya bisa memvalidasi booking di venue miliknya
if ($booking->table->venue_id !== auth()->user()->venue_id) {
return response()->json(['error' => 'Akses tidak diizinkan.'], 403);
}
// Mengembalikan data yang diformat dengan baik
return response()->json([
'success' => true,
'data' => [
'venue_name' => $booking->table->venue->name,
'booking_id' => $booking->id,
'user_name' => $booking->user->name,
'table_name' => $booking->table->name,
'start_time' => $booking->start_time->format('d M Y, H:i'),
'end_time' => $booking->end_time->format('H:i'),
'duration' => $booking->start_time->diffInHours($booking->end_time) . ' Jam',
'status' => $booking->status,
]
]);
}
public function show($id)
{
$booking = Booking::with(['table', 'user'])->findOrFail($id);
// Pastikan booking yang dilihat adalah milik venue admin
$adminVenueId = auth()->user()->venue_id;
$booking = Booking::with(['table', 'user'])
->whereHas('table', function ($q) use ($adminVenueId) {
$q->where('venue_id', $adminVenueId);
})
->findOrFail($id);
return view('admin.bookings.show', compact('booking'));
}
public function edit($id)
{
$booking = Booking::findOrFail($id);
$tables = Table::all();
$adminVenueId = auth()->user()->venue_id;
// Pastikan booking yang diedit adalah milik venue admin
$booking = Booking::whereHas('table', function ($q) use ($adminVenueId) {
$q->where('venue_id', $adminVenueId);
})->findOrFail($id);
// Hanya tampilkan tables dari venue admin
$tables = Table::where('venue_id', $adminVenueId)->get();
return view('admin.bookings.edit', compact('booking', 'tables'));
}
@ -89,7 +149,18 @@ public function update(Request $request, $id)
'end_time' => 'required|date|after:start_time',
]);
$booking = Booking::findOrFail($id);
$adminVenueId = auth()->user()->venue_id;
// Pastikan booking yang diupdate adalah milik venue admin
$booking = Booking::whereHas('table', function ($q) use ($adminVenueId) {
$q->where('venue_id', $adminVenueId);
})->findOrFail($id);
// Validasi tambahan: pastikan table_id yang dipilih juga milik venue admin
$table = Table::where('id', $request->table_id)
->where('venue_id', $adminVenueId)
->firstOrFail();
$booking->update($request->all());
return redirect()->route('admin.bookings.index')
@ -98,7 +169,12 @@ public function update(Request $request, $id)
public function complete($id)
{
$booking = Booking::findOrFail($id);
$adminVenueId = auth()->user()->venue_id;
$booking = Booking::whereHas('table', function ($q) use ($adminVenueId) {
$q->where('venue_id', $adminVenueId);
})->findOrFail($id);
$booking->status = 'selesai';
$booking->save();
@ -108,7 +184,12 @@ public function complete($id)
public function cancel($id)
{
$booking = Booking::findOrFail($id);
$adminVenueId = auth()->user()->venue_id;
$booking = Booking::whereHas('table', function ($q) use ($adminVenueId) {
$q->where('venue_id', $adminVenueId);
})->findOrFail($id);
$booking->status = 'cancelled';
$booking->save();
@ -118,7 +199,10 @@ public function cancel($id)
public function export(Request $request)
{
$adminVenueId = auth()->user()->venue_id;
$filename = 'bookings-' . Carbon::now()->format('Y-m-d') . '.xlsx';
return Excel::download(new BookingsExport($request), $filename);
// Pass venue_id ke export class jika diperlukan
return Excel::download(new BookingsExport($request, $adminVenueId), $filename);
}
}

View File

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

View File

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

View File

@ -12,6 +12,8 @@
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str; // <-- 1. PASTIKAN INI ADA
use SimpleSoftwareIO\QrCode\Facades\QrCode; // <-- 1. PASTIKAN INI JUGA ADA
class BookingController extends Controller
{
@ -22,169 +24,208 @@ public function __construct(MidtransService $midtransService)
$this->midtransService = $midtransService;
}
// Tambahkan method baru untuk booking langsung oleh admin
public function adminDirectBooking(Request $request) {
try {
$request->validate([
'table_id' => 'required|exists:tables,id',
'start_time' => 'required|date',
'end_time' => 'required|date|after:start_time',
]);
$user = Auth::user();
// Validasi bahwa user adalah admin dan mengelola venue dari meja tersebut
$table = Table::findOrFail($request->table_id);
if ($user->role !== 'admin' || $user->venue_id !== $table->venue_id) {
return response()->json([
'message' => 'Unauthorized action'
], 403);
}
// Cek konflik booking
$conflict = Booking::where('table_id', $request->table_id)
->where(function($query) use ($request) {
$query->whereBetween('start_time', [$request->start_time, $request->end_time])
->orWhere(function($query) use ($request) {
$query->where('start_time', '<', $request->start_time)
->where('end_time', '>', $request->start_time);
});
})
->where('status', 'paid')
->exists();
if ($conflict) {
return response()->json(['message' => 'Meja sudah dibooking di jam tersebut'], 409);
}
// Hitung total biaya (meskipun admin tidak membayar, kita tetap catat nilainya)
$startTime = Carbon::parse($request->start_time);
$endTime = Carbon::parse($request->end_time);
$duration = $endTime->diffInHours($startTime);
$totalAmount = $duration * $table->price_per_hour;
// Generate order ID unik untuk admin
$adminOrderId = 'ADMIN-' . $user->id . '-' . time();
// Buat booking langsung dengan status paid
$booking = Booking::create([
'table_id' => $request->table_id,
'user_id' => $user->id,
'start_time' => $request->start_time,
'end_time' => $request->end_time,
'status' => 'paid', // langsung set sebagai paid
'total_amount' => $totalAmount,
'payment_id' => null, // Admin tidak perlu payment_id
'payment_method' => 'admin_direct', // Tandai sebagai booking langsung admin
'order_id' => $adminOrderId,
]);
// Update table status menjadi Booked
$table->update(['status' => 'Booked']);
return response()->json([
'message' => 'Booking created successfully',
'booking_id' => $booking->id
]);
} catch (\Exception $e) {
\Log::error('Admin direct booking error:', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return response()->json([
'message' => 'Failed to create booking: ' . $e->getMessage()
], 500);
public function showQrCode(Booking $booking)
{
// Otorisasi: Pastikan user yang login adalah pemilik booking
if ($booking->user_id !== Auth::id()) {
abort(403);
}
// Pastikan booking memiliki token validasi
if (!$booking->validation_token) {
abort(404, 'QR Code tidak ditemukan.');
}
// Buat gambar QR Code dari token validasi
$qrCode = QrCode::size(300)->generate($booking->validation_token);
// Kembalikan sebagai respons gambar SVG
return response($qrCode)->header('Content-Type', 'image/svg+xml');
}
// Tambahkan method baru untuk booking langsung oleh admin
// Ganti seluruh fungsi adminDirectBooking dengan ini
public function adminDirectBooking($request) {
try {
$data = $request instanceof \Illuminate\Http\Request ? $request->all() : $request->toArray();
if (!isset($data['table_id']) || !isset($data['start_time']) || !isset($data['end_time'])) {
return response()->json(['message' => 'Missing required fields'], 400);
}
$user = Auth::user();
$table = Table::with(['venue.operatingHours'])->findOrFail($data['table_id']);
$venue = $table->venue;
if ($user->role !== 'admin' || $user->venue_id !== $table->venue_id) {
return response()->json(['message' => 'Unauthorized action'], 403);
}
$startDateTime = Carbon::parse($data['start_time']);
$endDateTime = Carbon::parse($data['end_time']);
// --- LOGIKA BARU: Validasi Jam Operasional Harian ---
$bookingDate = $startDateTime->copy()->setTimezone('Asia/Jakarta');
$dayOfWeek = $bookingDate->dayOfWeekIso;
$todaysHours = $venue->operatingHours->firstWhere('day_of_week', $dayOfWeek);
if (!$todaysHours || $todaysHours->is_closed) {
return response()->json(['message' => 'Venue tutup pada tanggal yang dipilih.'], 400);
}
$openTimeToday = $todaysHours->open_time;
$closeTimeToday = $todaysHours->close_time;
$isOvernightToday = strtotime($closeTimeToday) < strtotime($openTimeToday);
$operationalDayStart = Carbon::createFromFormat('Y-m-d H:i:s', $startDateTime->format('Y-m-d') . ' ' . $openTimeToday, 'Asia/Jakarta');
if ($isOvernightToday && $startDateTime < $operationalDayStart) {
$operationalDayStart->subDay();
}
$operationalDayEnd = $operationalDayStart->copy()->setTimeFromTimeString($closeTimeToday);
if ($isOvernightToday) {
$operationalDayEnd->addDay();
}
if ($startDateTime->lt($operationalDayStart) || $endDateTime->gt($operationalDayEnd)) {
return response()->json(['message' => 'Waktu booking di luar jam operasional venue.'], 400);
}
// --- AKHIR VALIDASI BARU ---
$conflict = Booking::where('table_id', $data['table_id'])
->whereIn('status', ['paid', 'pending'])
->where('start_time', '<', $endDateTime)
->where('end_time', '>', $startDateTime)
->exists();
if ($conflict) {
return response()->json(['message' => 'Meja sudah dibooking di jam tersebut'], 409);
}
// Sisa fungsi tidak berubah
$duration = $endDateTime->diffInHours($startDateTime);
$totalAmount = $duration * $table->price_per_hour;
$adminOrderId = 'ADMIN-' . $user->id . '-' . time();
$booking = Booking::create([
'table_id' => $data['table_id'],
'user_id' => $user->id,
'start_time' => $startDateTime,
'end_time' => $endDateTime,
'status' => 'paid',
'total_amount' => $totalAmount,
'payment_method' => 'admin_direct',
'order_id' => $adminOrderId,
'validation_token' => (string) Str::uuid(),
]);
return response()->json([
'success' => true,
'message' => 'Booking berhasil dibuat oleh admin',
'booking_id' => $booking->id,
// ... (detail booking lainnya)
]);
} catch (\Exception $e) {
\Log::error('Admin direct booking error:', ['message' => $e->getMessage()]);
return response()->json(['success' => false, 'message' => 'Gagal membuat booking: ' . $e->getMessage()], 500);
}
}
public function createPaymentIntent(Request $request) {
try {
$request->validate([
'table_id' => 'required|exists:tables,id',
'start_time' => 'required|date',
'end_time' => 'required|date|after:start_time',
'start_time' => 'required',
'duration' => 'required|integer|min:1|max:12',
'booking_date' => 'required|date_format:Y-m-d',
]);
$user = Auth::user();
$table = Table::findOrFail($request->table_id);
$table = Table::with(['venue.operatingHours'])->findOrFail($request->table_id); // Muat relasi operatingHours
$venue = $table->venue;
if ($user->role === 'admin' && $user->venue_id === $table->venue_id) {
return $this->adminDirectBooking($request);
$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);
}
// Cek apakah meja sedang dibooking pada waktu tersebut (hanya yang sudah paid)
$conflict = Booking::where('table_id', $request->table_id)
->where(function($query) use ($request) {
$query->whereBetween('start_time', [$request->start_time, $request->end_time])
->orWhere(function($query) use ($request) {
$query->where('start_time', '<', $request->start_time)
->where('end_time', '>', $request->start_time);
});
})
->where('status', 'paid') // Hanya cek yang sudah paid
->exists();
// 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 ---
if ($conflict) {
return response()->json(['message' => 'Meja sudah dibooking di jam tersebut'], 409);
$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();
}
// 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;
$operationalDayEnd = $operationalDayStart->copy()->setTimeFromTimeString($closeTimeToday);
if ($isOvernightToday) {
$operationalDayEnd->addDay();
}
// Simpan data booking sementara di session untuk digunakan setelah pembayaran
Session::put('temp_booking', [
if ($startDateTime->lt($operationalDayStart) || $endDateTime->gt($operationalDayEnd)) {
return response()->json(['success' => false, 'message' => 'Durasi booking di luar jam operasional venue.'], 422);
}
// Sisa dari fungsi ini (cek admin, cek konflik, proses Midtrans) tidak perlu diubah.
// ... (kode lama Anda untuk cek admin, cek konflik, dll, tetap di sini) ...
if ($user->role === 'admin' && $user->venue_id === $table->venue_id) {
return $this->adminDirectBooking(collect([
'table_id' => $request->table_id,
'user_id' => Auth::id(),
'start_time' => $request->start_time,
'end_time' => $request->end_time,
'total_amount' => $totalAmount,
'created_at' => now(),
]);
'start_time' => $startDateTime->toDateTimeString(),
'end_time' => $endDateTime->toDateTimeString(),
]));
}
// Generate unique order ID
$tempOrderId = 'TEMP-' . Auth::id() . '-' . time();
Session::put('temp_order_id', $tempOrderId);
// Cek konflik booking (tidak berubah)
$conflict = Booking::where('table_id', $request->table_id)
->where('status', 'paid')
->where(function($query) use ($startDateTime, $endDateTime) {
$query->where('start_time', '<', $endDateTime)
->where('end_time', '>', $startDateTime);
})
->exists();
if ($conflict) {
return response()->json(['success' => false, 'message' => 'Meja sudah dibooking di jam tersebut'], 409);
}
// Simpan booking sementara ke database untuk bisa dilanjutkan nanti
PendingBooking::updateOrCreate(
[
'user_id' => Auth::id(),
'table_id' => $request->table_id,
'start_time' => $request->start_time
],
[
'end_time' => $request->end_time,
'total_amount' => $totalAmount,
'order_id' => $tempOrderId,
'expired_at' => now()->addHours(24), // Kadaluarsa dalam 24 jam
]
);
// Proses ke Midtrans (tidak berubah)
$totalAmount = $duration * $table->price_per_hour;
$tempOrderId = 'TEMP-' . Auth::id() . '-' . time();
// Dapatkan snap token dari Midtrans tanpa menyimpan booking
$snapToken = $this->midtransService->createTemporaryTransaction($table, $totalAmount, $tempOrderId, Auth::user());
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) ]
);
if (!$snapToken) {
throw new \Exception('Failed to get snap token from Midtrans');
}
$snapToken = $this->midtransService->createTemporaryTransaction($table, $totalAmount, $tempOrderId, Auth::user());
if (!$snapToken) {
throw new \Exception('Failed to get snap token from Midtrans');
}
\Log::info('Payment intent created successfully:', [
'order_id' => $tempOrderId,
'snap_token' => $snapToken
]);
return response()->json([
'success' => true,
'snap_token' => $snapToken,
'order_id' => $tempOrderId
]);
return response()->json([
'message' => 'Payment intent created, proceed to payment',
'total_amount' => $totalAmount,
'snap_token' => $snapToken,
'order_id' => $tempOrderId
]);
} catch (\Exception $e) {
\Log::error('Payment intent error:', [
'message' => $e->getMessage(),
@ -192,6 +233,7 @@ public function createPaymentIntent(Request $request) {
]);
return response()->json([
'success' => false,
'message' => 'Gagal membuat transaksi: ' . $e->getMessage()
], 500);
}
@ -243,6 +285,7 @@ public function store(Request $request) {
'payment_id' => $request->transaction_id,
'payment_method' => $request->payment_method,
'order_id' => $request->order_id,
'validation_token' => (string) Str::uuid(),
]);
// Update table status to booked
@ -285,18 +328,37 @@ public function getBookedSchedules(Request $request) {
'date' => 'required|date',
]);
$table = Table::with('venue')->findOrFail($request->table_id);
$venue = $table->venue;
$requestDate = Carbon::parse($request->date);
// Only get bookings with paid status
$bookings = Booking::where('table_id', $request->table_id)
->whereDate('start_time', $request->date)
->where('status', 'paid') // Only include paid bookings
->select('start_time', 'end_time')
->get()
->map(function ($booking) {
return [
'start' => Carbon::parse($booking->start_time)->format('H:i'),
'end' => Carbon::parse($booking->end_time)->format('H:i')
];
});
$query = Booking::where('table_id', $request->table_id)
->where('status', 'paid');
if ($venue->is_overnight) {
// Jika overnight, ambil booking dari jam buka di hari H
// sampai jam tutup di hari H+1
$startOperationalDay = $requestDate->copy()->setTimeFromTimeString($venue->open_time);
$endOperationalDay = $requestDate->copy()->addDay()->setTimeFromTimeString($venue->close_time);
$query->whereBetween('start_time', [$startOperationalDay, $endOperationalDay]);
} else {
// Jika tidak overnight, ambil booking hanya di hari H
$query->whereDate('start_time', $requestDate);
}
$bookings = $query->select('start_time', 'end_time')
->get()
->map(function ($booking) {
return [
// Format H:i tetap sama, karena frontend hanya butuh jamnya
'start' => Carbon::parse($booking->start_time)->format('H:i'),
'end' => Carbon::parse($booking->end_time)->format('H:i')
];
});
return response()->json($bookings);
}
@ -481,40 +543,43 @@ public function deletePendingBooking($id)
}
}
public function showReschedule($id)
{
$booking = Booking::with(['table.venue', 'table.venue.tables'])->findOrFail($id);
// GANTI SELURUH FUNGSI showReschedule DENGAN YANG INI
// Check if user owns this booking
if ($booking->user_id !== auth()->id()) {
return redirect()->route('booking.history')->with('error', 'Anda tidak memiliki akses ke booking ini.');
}
public function showReschedule($id)
{
$booking = Booking::with(['table.venue', 'table.venue.tables'])->findOrFail($id);
// Check if booking is upcoming
if ($booking->start_time <= now() || $booking->status !== 'paid') {
return redirect()->route('booking.history')->with('error', 'Booking ini tidak dapat di-reschedule.');
}
// Check if booking has reached reschedule limit
if ($booking->reschedule_count >= 1) {
return redirect()->route('booking.history')->with('error', 'Booking ini sudah pernah di-reschedule sebelumnya dan tidak dapat di-reschedule lagi.');
}
// Check if it's within the time limit (at least 1 hour before start)
$rescheduleDeadline = Carbon::parse($booking->start_time)->subHour();
if (now() > $rescheduleDeadline) {
return redirect()->route('booking.history')->with('error', 'Batas waktu reschedule telah berakhir (1 jam sebelum mulai).');
}
// Get venue and tables data
$venue = $booking->table->venue;
// Duration in hours
$duration = Carbon::parse($booking->start_time)->diffInHours($booking->end_time);
return view('pages.reschedule', compact('booking', 'venue', 'duration'));
// Validasi kepemilikan dan status booking (tidak ada perubahan)
if ($booking->user_id !== auth()->id() || $booking->status !== 'paid' || $booking->reschedule_count >= 1) {
return redirect()->route('booking.history')->with('error', 'Batas maksimal reschedule telah digunakan (1x).');
}
$rescheduleDeadline = Carbon::parse($booking->start_time)->subHour();
if (now() > $rescheduleDeadline) {
return redirect()->route('booking.history')->with('error', 'Batas waktu reschedule telah berakhir (1 jam sebelum mulai).');
}
$venue = $booking->table->venue;
$duration = Carbon::parse($booking->start_time)->diffInHours($booking->end_time);
// --- AWAL LOGIKA BARU UNTUK MENENTUKAN TANGGAL OPERASIONAL ---
$startTime = Carbon::parse($booking->start_time);
$operational_date = $startTime->copy(); // Mulai dengan tanggal kalender
// Jika venue-nya overnight DAN jam booking lebih pagi dari jam buka,
// maka tanggal operasionalnya adalah H-1 dari tanggal kalender.
if ($venue->is_overnight && $startTime->format('H:i:s') < $venue->open_time) {
$operational_date->subDay();
}
// Ubah ke format Y-m-d untuk dikirim ke view
$operational_date_string = $operational_date->format('Y-m-d');
// --- AKHIR DARI LOGIKA BARU ---
// Kirim $operational_date_string ke view, bukan lagi tanggal dari $booking
return view('pages.reschedule', compact('booking', 'venue', 'duration', 'operational_date_string'));
}
/**
* Process a reschedule request.
*/
@ -577,30 +642,43 @@ public function processReschedule(Request $request, $id)
* Check availability for reschedule.
*/
public function checkRescheduleAvailability(Request $request)
{
$request->validate([
'table_id' => 'required|exists:tables,id',
'date' => 'required|date_format:Y-m-d',
'booking_id' => 'required|exists:bookings,id'
]);
{
$request->validate([
'table_id' => 'required|exists:tables,id',
'date' => 'required|date_format:Y-m-d',
'booking_id' => 'required|exists:bookings,id'
]);
$date = $request->date;
$tableId = $request->table_id;
$bookingId = $request->booking_id;
$table = Table::with('venue')->findOrFail($request->table_id);
$venue = $table->venue;
$requestDate = Carbon::parse($request->date);
// Get all bookings for this table on this date (excluding the current booking)
$bookings = Booking::where('table_id', $tableId)
->where('id', '!=', $bookingId)
->where('status', 'paid')
->whereDate('start_time', $date)
->get(['start_time', 'end_time'])
->map(function ($booking) {
return [
'start' => Carbon::parse($booking->start_time)->format('H:i'),
'end' => Carbon::parse($booking->end_time)->format('H:i'),
];
});
// Query untuk mengambil booking lain di meja yang sama
$query = Booking::where('table_id', $table->id)
->where('id', '!=', $request->booking_id) // Jangan ikut sertakan booking yang sedang di-reschedule
->where('status', 'paid');
return response()->json($bookings);
// --- LOGIKA OVERNIGHT DITERAPKAN DI SINI ---
if ($venue->is_overnight) {
// Ambil booking dari jam buka di hari H sampai jam tutup di hari H+1
$startOperationalDay = $requestDate->copy()->setTimeFromTimeString($venue->open_time);
$endOperationalDay = $requestDate->copy()->addDay()->setTimeFromTimeString($venue->close_time);
$query->whereBetween('start_time', [$startOperationalDay, $endOperationalDay]);
} else {
// Logika standar untuk venue yang tidak overnight
$query->whereDate('start_time', $requestDate);
}
// --- AKHIR DARI LOGIKA OVERNIGHT ---
$bookings = $query->get(['start_time', 'end_time'])
->map(function ($booking) {
return [
'start' => Carbon::parse($booking->start_time)->format('H:i'),
'end' => Carbon::parse($booking->end_time)->format('H:i'),
];
});
return response()->json($bookings);
}
}

View File

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

View File

@ -1,34 +1,42 @@
<?php
// app/Http/Controllers/pages/VenueController.php
namespace App\Http\Controllers\pages;
use App\Http\Controllers\Controller;
use App\Models\Venue; // Pastikan model Venue di-import
use Illuminate\Http\Request;
use App\Models\Venue;
use Carbon\Carbon; // <-- Pastikan Carbon di-import
class VenueController extends Controller
{
public function venue($venueName) {
// Mengambil venue berdasarkan nama yang diberikan
$venue = Venue::where('name', 'like', '%' . ucfirst($venueName) . '%')->first();
public function venue($venueName)
{
$venue = Venue::where('name', $venueName)
->with(['tables', 'images', 'operatingHours', 'reviews.user'])
->firstOrFail();
// Jika venue tidak ditemukan, tampilkan error 404
if (!$venue) {
abort(404);
// --- LOGIKA BARU UNTUK JAM OPERASIONAL HARI INI ---
// Day of week: 1 (Senin) - 7 (Minggu). Carbon menggunakan 0 (Minggu) - 6 (Sabtu)
$dayOfWeek = Carbon::now('Asia/Jakarta')->dayOfWeekIso; // ISO standard: 1=Senin, 7=Minggu
// Cari jadwal untuk hari ini dari data yang sudah dimuat
$todaysHours = $venue->operatingHours->firstWhere('day_of_week', $dayOfWeek);
// Siapkan data jam buka dan tutup untuk hari ini
// Jika tidak ada jadwal spesifik, atau jika hari ini libur, maka venue dianggap tutup
if ($todaysHours && !$todaysHours->is_closed) {
$openTime = $todaysHours->open_time;
$closeTime = $todaysHours->close_time;
} else {
// Set default ke 'tutup' jika tidak ada jadwal atau is_closed = true
$openTime = '00:00';
$closeTime = '00:00';
}
// --- AKHIR LOGIKA BARU ---
$averageRating = $venue->reviews->avg('rating');
$totalReviews = $venue->reviews->count();
// Ambil tabel-tabel terkait dengan venue
$venue->load('tables'); // Eager loading untuk optimasi
// Parsing jam operasional dari format H:i:s menjadi integer
$openHour = (int) date('H', strtotime($venue->open_time));
$closeHour = (int) date('H', strtotime($venue->close_time));
// Mengirim data venue dengan jam operasional ke view
return view('pages.venue', [
'venue' => $venue,
'openHour' => $openHour,
'closeHour' => $closeHour
]);
// Kirim semua data ke view
return view('pages.venue', compact('venue', 'openTime', 'closeTime', 'averageRating', 'totalReviews'));
}
}

View File

@ -18,7 +18,9 @@ class Booking extends Model
'payment_id',
'payment_method',
'total_amount',
'payment_expired_at'
'payment_expired_at',
'order_id', // Pastikan order_id juga ada di sini jika belum
'validation_token', // <-- TAMBAHKAN INI
];
protected $casts = [
@ -38,6 +40,11 @@ public function user()
return $this->belongsTo(User::class);
}
public function review()
{
return $this->hasOne(Review::class);
}
public function isPending()
{
return $this->status === 'pending';

View File

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class OperatingHour extends Model
{
use HasFactory;
protected $fillable = [
'venue_id',
'day_of_week',
'open_time',
'close_time',
'is_closed',
];
public function venue()
{
return $this->belongsTo(Venue::class);
}
}

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

@ -0,0 +1,34 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Review extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'venue_id',
'booking_id',
'rating',
'comment',
];
public function user()
{
return $this->belongsTo(User::class);
}
public function venue()
{
return $this->belongsTo(Venue::class);
}
public function booking()
{
return $this->belongsTo(Booking::class);
}
}

View File

@ -66,6 +66,11 @@ public function hasRole($role)
return $this->role === $role;
}
public function reviews()
{
return $this->hasMany(Review::class);
}
/**
* Get the venue that the admin belongs to.
*/

View File

@ -4,15 +4,126 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
use App\Models\VenueImage; // <-- Tambahkan ini
class Venue extends Model
{
use HasFactory;
protected $fillable = ['name', 'location', 'address', 'image'];
public function getIsOvernightAttribute()
{
// Jika jam tutup lebih kecil dari jam buka, berarti melewati tengah malam
return $this->close_time && $this->open_time && $this->close_time < $this->open_time;
}
protected $fillable = [
'name',
'address',
'latitude',
'longitude',
'image',
'phone',
'link_instagram',
'link_tiktok',
'link_facebook',
'link_x',
'description',
'open_time',
'close_time',
'status',
'close_reason',
'reopen_date',
'original_open_time',
'original_close_time',
];
protected $dates = [
'reopen_date',
];
// --- TAMBAHKAN PROPERTI INI ---
/**
* The accessors to append to the model's array form.
*
* @var array
*/
protected $appends = ['is_overnight'];
// --- AKHIR DARI PENAMBAHAN ---
public function tables()
{
return $this->hasMany(Table::class);
}
public function images()
{
return $this->hasMany(VenueImage::class);
}
public function operatingHours()
{
return $this->hasMany(OperatingHour::class);
}
public function reviews()
{
return $this->hasMany(Review::class);
}
/**
* Check if venue should automatically reopen
*/
public function checkAutoReopen()
{
if ($this->status === 'close' && $this->reopen_date && Carbon::today()->gte($this->reopen_date)) {
$this->update([
'status' => 'open',
'open_time' => $this->original_open_time,
'close_time' => $this->original_close_time,
'close_reason' => null,
'reopen_date' => null,
'original_open_time' => null,
'original_close_time' => null,
]);
return true;
}
return false;
}
/**
* Close venue with reason and reopen date
*/
public function closeVenue($reason, $reopenDate)
{
// Simpan jam operasional saat ini sebelum mengubahnya
$currentOpenTime = $this->open_time;
$currentCloseTime = $this->close_time;
$this->update([
'status' => 'close',
'close_reason' => $reason,
'reopen_date' => $reopenDate,
'original_open_time' => $currentOpenTime, // Simpan jam asli
'original_close_time' => $currentCloseTime, // Simpan jam asli
'open_time' => '00:00', // Set ke 00:00 setelah menyimpan original
'close_time' => '00:00', // Set ke 00:00 setelah menyimpan original
]);
}
/**
* Open venue manually
*/
public function openVenue()
{
$this->update([
'status' => 'open',
'open_time' => $this->original_open_time ?: $this->open_time,
'close_time' => $this->original_close_time ?: $this->close_time,
'close_reason' => null,
'reopen_date' => null,
'original_open_time' => null,
'original_close_time' => null,
]);
}
}

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

@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class VenueImage extends Model
{
use HasFactory;
protected $fillable = ['venue_id', 'path'];
public function venue()
{
return $this->belongsTo(Venue::class);
}
}

View File

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

View File

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

174
composer.lock generated
View File

@ -4,8 +4,62 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "bce8ddce84cf8a2d572b421f6992903e",
"content-hash": "66130e103a2b9c249e50df9d5700a24f",
"packages": [
{
"name": "bacon/bacon-qr-code",
"version": "2.0.8",
"source": {
"type": "git",
"url": "https://github.com/Bacon/BaconQrCode.git",
"reference": "8674e51bb65af933a5ffaf1c308a660387c35c22"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22",
"reference": "8674e51bb65af933a5ffaf1c308a660387c35c22",
"shasum": ""
},
"require": {
"dasprid/enum": "^1.0.3",
"ext-iconv": "*",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"phly/keep-a-changelog": "^2.1",
"phpunit/phpunit": "^7 | ^8 | ^9",
"spatie/phpunit-snapshot-assertions": "^4.2.9",
"squizlabs/php_codesniffer": "^3.4"
},
"suggest": {
"ext-imagick": "to generate QR code images"
},
"type": "library",
"autoload": {
"psr-4": {
"BaconQrCode\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"description": "BaconQrCode is a QR code generator for PHP.",
"homepage": "https://github.com/Bacon/BaconQrCode",
"support": {
"issues": "https://github.com/Bacon/BaconQrCode/issues",
"source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8"
},
"time": "2022-12-07T17:46:57+00:00"
},
{
"name": "brick/math",
"version": "0.12.1",
@ -135,6 +189,56 @@
],
"time": "2023-12-11T17:09:12+00:00"
},
{
"name": "dasprid/enum",
"version": "1.0.6",
"source": {
"type": "git",
"url": "https://github.com/DASPRiD/Enum.git",
"reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8dfd07c6d2cf31c8da90c53b83c026c7696dda90",
"reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90",
"shasum": ""
},
"require": {
"php": ">=7.1 <9.0"
},
"require-dev": {
"phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11",
"squizlabs/php_codesniffer": "*"
},
"type": "library",
"autoload": {
"psr-4": {
"DASPRiD\\Enum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"description": "PHP 7.1 enum implementation",
"keywords": [
"enum",
"map"
],
"support": {
"issues": "https://github.com/DASPRiD/Enum/issues",
"source": "https://github.com/DASPRiD/Enum/tree/1.0.6"
},
"time": "2024-08-09T14:30:48+00:00"
},
{
"name": "dflydev/dot-access-data",
"version": "v3.0.3",
@ -3812,6 +3916,74 @@
],
"time": "2024-04-27T21:32:50+00:00"
},
{
"name": "simplesoftwareio/simple-qrcode",
"version": "4.2.0",
"source": {
"type": "git",
"url": "https://github.com/SimpleSoftwareIO/simple-qrcode.git",
"reference": "916db7948ca6772d54bb617259c768c9cdc8d537"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SimpleSoftwareIO/simple-qrcode/zipball/916db7948ca6772d54bb617259c768c9cdc8d537",
"reference": "916db7948ca6772d54bb617259c768c9cdc8d537",
"shasum": ""
},
"require": {
"bacon/bacon-qr-code": "^2.0",
"ext-gd": "*",
"php": ">=7.2|^8.0"
},
"require-dev": {
"mockery/mockery": "~1",
"phpunit/phpunit": "~9"
},
"suggest": {
"ext-imagick": "Allows the generation of PNG QrCodes.",
"illuminate/support": "Allows for use within Laravel."
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"QrCode": "SimpleSoftwareIO\\QrCode\\Facades\\QrCode"
},
"providers": [
"SimpleSoftwareIO\\QrCode\\QrCodeServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"SimpleSoftwareIO\\QrCode\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Simple Software LLC",
"email": "support@simplesoftware.io"
}
],
"description": "Simple QrCode is a QR code generator made for Laravel.",
"homepage": "https://www.simplesoftware.io/#/docs/simple-qrcode",
"keywords": [
"Simple",
"generator",
"laravel",
"qrcode",
"wrapper"
],
"support": {
"issues": "https://github.com/SimpleSoftwareIO/simple-qrcode/issues",
"source": "https://github.com/SimpleSoftwareIO/simple-qrcode/tree/4.2.0"
},
"time": "2021-02-08T20:43:55+00:00"
},
{
"name": "symfony/console",
"version": "v6.4.12",

View File

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

View File

@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateVenueImagesTable extends Migration
{
public function up()
{
Schema::create('venue_images', function (Blueprint $table) {
$table->id();
$table->foreignId('venue_id')->constrained()->onDelete('cascade');
$table->string('path');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('venue_images');
}
}

View File

@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateOperatingHoursTable extends Migration
{
public function up()
{
Schema::create('operating_hours', function (Blueprint $table) {
$table->id();
$table->foreignId('venue_id')->constrained()->onDelete('cascade');
$table->tinyInteger('day_of_week'); // 1 untuk Senin, 2 Selasa, ..., 7 Minggu
$table->time('open_time')->nullable();
$table->time('close_time')->nullable();
$table->boolean('is_closed')->default(false);
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('operating_hours');
}
};

View File

@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddSocialMediaLinksToVenuesTable extends Migration
{
public function up()
{
Schema::table('venues', function (Blueprint $table) {
$table->string('link_instagram')->nullable()->after('phone');
$table->string('link_tiktok')->nullable()->after('link_instagram');
$table->string('link_facebook')->nullable()->after('link_tiktok');
$table->string('link_x')->nullable()->after('link_facebook'); // Untuk Twitter/X
});
}
public function down()
{
Schema::table('venues', function (Blueprint $table) {
$table->dropColumn(['link_instagram', 'link_tiktok', 'link_facebook', 'link_x']);
});
}
};

View File

@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::table('venues', function (Blueprint $table) {
$table->string('latitude')->nullable()->after('address');
$table->string('longitude')->nullable()->after('latitude');
});
}
public function down()
{
Schema::table('venues', function (Blueprint $table) {
$table->dropColumn(['latitude', 'longitude']);
});
}
};

View File

@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('reviews', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('venue_id')->constrained()->onDelete('cascade');
$table->foreignId('booking_id')->constrained()->onDelete('cascade')->unique();
$table->tinyInteger('rating'); // 1-5 stars
$table->text('comment')->nullable();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('reviews');
}
};

View File

@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::table('bookings', function (Blueprint $table) {
// Tambahkan kolom 'order_id' yang hilang terlebih dahulu
$table->string('order_id')->nullable()->unique()->after('payment_method');
// Kemudian, tambahkan kolom 'validation_token' setelah 'order_id'
$table->string('validation_token')->nullable()->unique()->after('order_id');
});
}
public function down()
{
Schema::table('bookings', function (Blueprint $table) {
$table->dropColumn('validation_token');
$table->dropColumn('order_id');
});
}
};

View File

@ -63,7 +63,7 @@ class="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-
<input id="email" type="email" name="email" value="{{ old('email', $user->email) }}"
required
class="focus:ring-blue-500 focus:border-blue-500 flex-1 block w-full rounded-none rounded-r-md border-gray-300 p-1"
placeholder="Masukkan email">
placeholder="Masukkan email" readonly>
</div>
@error('email')
<div class="text-red-500 mt-1 text-sm"><i

View File

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

View File

@ -0,0 +1,84 @@
@extends('layouts.admin')
@section('content')
<div class="p-6">
<h1 class="text-2xl font-bold text-gray-900 mb-6">Scan QR Code Validasi</h1>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="bg-white p-6 rounded-lg shadow-sm border">
<div id="qr-reader" class="w-full"></div>
<p id="qr-reader-status" class="text-center text-sm text-gray-500 mt-2">Arahkan kamera ke QR Code...</p>
</div>
<div class="bg-white p-6 rounded-lg shadow-sm border">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Hasil Scan</h3>
<div id="qr-scan-result">
<div class="text-center py-10">
<p class="text-gray-400">Menunggu hasil scan...</p>
</div>
</div>
</div>
</div>
</div>
{{-- Script untuk library QR Scanner --}}
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const resultContainer = document.getElementById('qr-scan-result');
const statusContainer = document.getElementById('qr-reader-status');
function onScanSuccess(decodedText, decodedResult) {
// Hentikan scanner setelah berhasil
html5QrcodeScanner.clear();
statusContainer.innerHTML = `<span class="font-bold text-green-600">Scan Berhasil!</span> Memuat data...`;
// Kirim token ke server untuk divalidasi
fetch(`/admin/bookings/validate/${decodedText}`)
.then(response => {
if (!response.ok) {
// Jika booking tidak ditemukan (error 404) atau error lainnya
throw new Error(`Booking tidak valid atau terjadi error (Status: ${response.status})`);
}
return response.json();
})
.then(data => {
if (data.success) {
const booking = data.data;
// Tampilkan hasil yang valid
resultContainer.innerHTML = `
<div class="p-4 bg-green-50 border-l-4 border-green-500">
<h4 class="text-xl font-bold text-green-800"> Valid!</h4>
<div class="mt-4 space-y-2 text-sm">
<p><strong class="w-24 inline-block">Nama User:</strong> ${booking.user_name}</p>
<p><strong class="w-24 inline-block">Venue:</strong> ${booking.venue_name}</p>
<p><strong class="w-24 inline-block">Meja:</strong> ${booking.table_name}</p>
<p><strong class="w-24 inline-block">Waktu:</strong> ${booking.start_time} - ${booking.end_time}</p>
<p><strong class="w-24 inline-block">Durasi:</strong> ${booking.duration}</p>
<p><strong class="w-24 inline-block">Status:</strong> <span class="font-bold capitalize">${booking.status}</span></p>
</div>
</div>
`;
} else {
// Jika ada pesan error dari server
resultContainer.innerHTML = `<div class="p-4 bg-red-50 border-l-4 border-red-500"><h4 class="text-xl font-bold text-red-800">❌ Tidak Valid!</h4><p class="mt-2 text-red-700">${data.error || 'Data booking tidak bisa diambil.'}</p></div>`;
}
})
.catch(error => {
console.error("Error:", error);
// Tampilkan error jika fetch gagal
resultContainer.innerHTML = `<div class="p-4 bg-red-50 border-l-4 border-red-500"><h4 class="text-xl font-bold text-red-800">❌ Error!</h4><p class="mt-2 text-red-700">${error.message}</p></div>`;
});
}
// Inisialisasi scanner
var html5QrcodeScanner = new Html5QrcodeScanner(
"qr-reader", { fps: 10, qrbox: 250 }
);
// Render scanner
html5QrcodeScanner.render(onScanSuccess);
});
</script>
@endsection

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,205 @@
@extends('layouts.admin')
@section('content')
<div class="p-6">
<div class="mb-6">
<div class="flex items-center space-x-4">
<a href="{{ route('admin.venue.index') }}" class="text-gray-500 hover:text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg>
</a>
<div>
<h1 class="text-2xl font-bold text-gray-900">Edit Venue</h1>
<p class="text-gray-600 mt-1">Perbarui informasi venue Anda</p>
</div>
</div>
</div>
@if(session('success'))
<div class="mb-6 bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded-lg">{{ session('success') }}</div>
@endif
@if(session('error'))
<div class="mb-6 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-lg">{{ session('error') }}</div>
@endif
@if ($errors->any())
<div class="mb-6 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-lg">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="bg-white rounded-lg shadow-sm border border-gray-200">
<form action="{{ route('admin.venue.update') }}" method="POST" enctype="multipart/form-data" class="p-6">
@csrf
@method('PUT')
<div class="grid grid-cols-1 lg:grid-cols-2 gap-x-8 gap-y-6">
<div class="space-y-6">
<div>
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">Nama Venue <span class="text-red-500">*</span></label>
<input type="text" id="name" name="name" value="{{ old('name', $venue->name) }}" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" required>
</div>
<div>
<label for="address" class="block text-sm font-medium text-gray-700 mb-2">Alamat <span class="text-red-500">*</span></label>
<textarea id="address" name="address" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" required>{{ old('address', $venue->address) }}</textarea>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label for="latitude" class="block text-sm font-medium text-gray-700 mb-2">Latitude</label>
<input type="text" id="latitude" name="latitude" value="{{ old('latitude', $venue->latitude) }}" class="w-full px-3 py-2 border border-gray-300 rounded-lg" placeholder="-8.123456">
</div>
<div>
<label for="longitude" class="block text-sm font-medium text-gray-700 mb-2">Longitude</label>
<input type="text" id="longitude" name="longitude" value="{{ old('longitude', $venue->longitude) }}" class="w-full px-3 py-2 border border-gray-300 rounded-lg" placeholder="113.123456">
</div>
</div>
<p class="text-xs text-gray-500 -mt-4">
*Cara mendapatkan: Buka Google Maps, klik kanan pada lokasi venue, lalu klik pada angka koordinat untuk menyalinnya.
</p>
<div>
<label for="phone" class="block text-sm font-medium text-gray-700 mb-2">Nomor Telepon</label>
<input type="text" id="phone" name="phone" value="{{ old('phone', $venue->phone) }}" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500">
</div>
<div class="pt-4 border-t">
<h3 class="text-base font-medium text-gray-800 mb-3">Link Media Sosial</h3>
<div class="space-y-4">
<div>
<label for="link_instagram" class="block text-sm font-medium text-gray-700 mb-2">Instagram</label>
<input type="url" id="link_instagram" name="link_instagram" value="{{ old('link_instagram', $venue->link_instagram) }}" class="w-full px-3 py-2 border border-gray-300 rounded-lg" placeholder="https://instagram.com/venueanda">
</div>
<div>
<label for="link_tiktok" class="block text-sm font-medium text-gray-700 mb-2">TikTok</label>
<input type="url" id="link_tiktok" name="link_tiktok" value="{{ old('link_tiktok', $venue->link_tiktok) }}" class="w-full px-3 py-2 border border-gray-300 rounded-lg" placeholder="https://tiktok.com/@venueanda">
</div>
<div>
<label for="link_facebook" class="block text-sm font-medium text-gray-700 mb-2">Facebook</label>
<input type="url" id="link_facebook" name="link_facebook" value="{{ old('link_facebook', $venue->link_facebook) }}" class="w-full px-3 py-2 border border-gray-300 rounded-lg" placeholder="https://facebook.com/venueanda">
</div>
<div>
<label for="link_x" class="block text-sm font-medium text-gray-700 mb-2">X (Twitter)</label>
<input type="url" id="link_x" name="link_x" value="{{ old('link_x', $venue->link_x) }}" class="w-full px-3 py-2 border border-gray-300 rounded-lg" placeholder="https://x.com/venueanda">
</div>
</div>
</div>
<div class="p-4 border border-gray-200 rounded-lg">
<h3 class="text-lg font-medium text-gray-900 mb-4">Jam Operasional Harian</h3>
<div class="space-y-4">
@php
$days = [1 => 'Senin', 2 => 'Selasa', 3 => 'Rabu', 4 => 'Kamis', 5 => 'Jumat', 6 => 'Sabtu', 7 => 'Minggu'];
// Siapkan data jam operasional yang ada agar mudah diakses
$hoursByDay = $venue->operatingHours->keyBy('day_of_week');
@endphp
@foreach($days as $dayNumber => $dayName)
@php
$hour = $hoursByDay->get($dayNumber);
@endphp
<div x-data="{ isClosed: {{ old('hours.'.$dayNumber.'.is_closed', $hour->is_closed ?? true) ? 'true' : 'false' }} }" class="grid grid-cols-1 sm:grid-cols-4 gap-3 items-center">
<div class="sm:col-span-1">
<label class="font-medium text-gray-700">{{ $dayName }}</label>
</div>
<div class="sm:col-span-3 flex items-center space-x-4">
<input type="checkbox" x-model="isClosed" name="hours[{{ $dayNumber }}][is_closed]" class="h-4 w-4 rounded">
<label class="text-sm text-gray-600">Tutup</label>
<div class="flex items-center space-x-2" x-show="!isClosed">
<input type="time" name="hours[{{ $dayNumber }}][open_time]" value="{{ old('hours.'.$dayNumber.'.open_time', $hour && $hour->open_time ? \Carbon\Carbon::parse($hour->open_time)->format('H:i') : '09:00') }}" :disabled="isClosed" class="w-full px-2 py-1 border border-gray-300 rounded-md text-sm">
<span>-</span>
<input type="time" name="hours[{{ $dayNumber }}][close_time]" value="{{ old('hours.'.$dayNumber.'.close_time', $hour && $hour->close_time ? \Carbon\Carbon::parse($hour->close_time)->format('H:i') : '22:00') }}" :disabled="isClosed" class="w-full px-2 py-1 border border-gray-300 rounded-md text-sm">
</div>
</div>
<input type="hidden" name="hours[{{ $dayNumber }}][day_of_week]" value="{{ $dayNumber }}">
</div>
@endforeach
</div>
</div>
<div>
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">Deskripsi Venue</label>
<textarea id="description" name="description" rows="5" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500">{{ old('description', $venue->description) }}</textarea>
</div>
</div>
<div class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Foto Sampul (Utama)</label>
<img id="current-image" src="{{ $venue->image ? asset('storage/' . $venue->image) : 'https://via.placeholder.com/300' }}" alt="{{ $venue->name }}" class="w-full h-48 object-cover rounded-lg bg-gray-100">
<input type="file" id="image" name="image" accept="image/*" class="w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 mt-2">
</div>
<div class="pt-6 border-t">
<h3 class="text-lg font-medium text-gray-900 mb-2">Galeri Venue</h3>
<div id="gallery-container" class="grid grid-cols-3 sm:grid-cols-4 gap-4">
@foreach($venue->images as $image)
<div id="gallery-image-{{ $image->id }}" class="relative group">
<img src="{{ asset('storage/' . $image->path) }}" alt="Gallery Image" class="w-full h-24 object-cover rounded-lg">
<button type="button" onclick="deleteImage({{ $image->id }}, event)"
class="absolute top-1 right-1 bg-red-600 text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity focus:outline-none">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
@endforeach
</div>
<p id="no-gallery-text" class="text-sm text-gray-500 mt-2 {{ $venue->images->isNotEmpty() ? 'hidden' : '' }}">Belum ada gambar di galeri.</p>
<div class="mt-4">
<label for="gallery_images" class="block text-sm font-medium text-gray-700 mb-2">Tambah Gambar ke Galeri</label>
<input type="file" id="gallery_images" name="gallery_images[]" accept="image/*" multiple class="w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-green-50 file:text-green-700 hover:file:bg-green-100">
</div>
</div>
</div>
</div>
<div class="flex items-center justify-end space-x-4 mt-8 pt-6 border-t border-gray-200">
<a href="{{ route('admin.venue.index') }}" class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg font-medium transition-colors">Batal</a>
<button type="submit" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors">Simpan Perubahan</button>
</div>
</form>
</div>
</div>
<script>
function deleteImage(imageId, event) {
event.preventDefault();
if (!confirm('Apakah Anda yakin ingin menghapus gambar ini?')) return;
fetch(`/admin/venue-image/${imageId}`, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json' // Memberitahu server kita ingin respons JSON
}
})
.then(response => {
// Cek jika respons dari server tidak OK (misal: error 403, 404, 500)
if (!response.ok) {
// Ubah respons menjadi error agar bisa ditangkap di .catch()
return response.json().then(err => { throw err; });
}
return response.json();
})
.then(data => {
if (data.success) {
const imageElement = document.getElementById(`gallery-image-${imageId}`);
imageElement.remove();
const galleryContainer = document.getElementById('gallery-container');
if (galleryContainer.children.length === 0) {
document.getElementById('no-gallery-text').classList.remove('hidden');
}
} else {
// Menampilkan pesan error dari server jika success = false
alert(data.message || 'Gagal menghapus gambar.');
}
})
.catch(error => {
// Menangkap error dari server (seperti 403, 500) atau error koneksi
console.error('Error:', error);
// Menampilkan pesan error yang lebih spesifik jika ada, jika tidak tampilkan pesan default
alert('Terjadi kesalahan: ' + (error.message || 'Periksa koneksi Anda.'));
});
}
</script>
@endsection

View File

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

View File

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

View File

@ -281,7 +281,7 @@
<hr class="dropdown-divider">
<a class="dropdown-item text-danger" href="{{ route('logout') }}"
onclick="event.preventDefault();
document.getElementById('logout-form').submit();">
document.getElementById('logout-form').submit();">
<i class="fas fa-sign-out-alt me-2"></i> {{ __('Logout') }}
</a>

View File

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Document</title>
<title>Cari Meja</title>
@vite('resources/css/app.css')
{{-- Font | Google Fonts --}}
<link rel="preconnect" href="https://fonts.googleapis.com">
@ -52,14 +52,29 @@ class="flex items-center space-x-2 text-sm font-medium text-gray-700 hover:text-
<div x-show="open" @click.away="open = false" x-transition
class="absolute right-0 mt-2 w-40 bg-white rounded-lg shadow-lg py-2 z-50">
<a href="{{ route('booking.history') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Riwayat Booking
</a>
<a href="{{ route('account.settings') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Pengaturan Akun
</a>
@if (Auth::user()->role === 'user')
<a href="{{ route('account.settings') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Pengaturan Akun
</a>
@elseif (Auth::user()->role === 'admin')
<a href="{{ url('/admin') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Halaman Admin
</a>
@elseif (Auth::user()->role === 'superadmin')
<a href="{{ url('/superadmin') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Halaman Superadmin
</a>
@endif
@if (Auth::user()->email_verified_at === null)
<a href="{{ route('verification.notice') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
@ -67,6 +82,7 @@ class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Verifikasi Email
</a>
@endif
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit"
@ -92,10 +108,22 @@ class="absolute top-full left-0 right-0 bg-white shadow-md mt-1 p-4 z-50">
class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Riwayat Booking
</a>
<a href="{{ route('account.settings') }}"
class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Pengaturan Akun
</a>
@if (Auth::user()->role === 'user')
<a href="{{ route('account.settings') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Pengaturan Akun
</a>
@elseif (Auth::user()->role === 'admin')
<a href="{{ url('/admin') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Halaman Admin
</a>
@elseif (Auth::user()->role === 'superadmin')
<a href="{{ url('/superadmin') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Halaman Superadmin
</a>
@endif
@if (Auth::user()->email_verified_at === null)
<a href="{{ route('verification.notice') }}"
class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
@ -141,7 +169,7 @@ class="absolute top-3 right-4 text-gray-500 hover:text-gray-700 text-xl">
<!-- Error message for login errors -->
@if(session('login_error') || ($errors->any() && old('email') && !old('name')))
<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">
@if(session('login_error'))
<p>{{ session('login_error') }}</p>
@if(str_contains(session('login_error'), 'belum diverifikasi'))
@ -243,7 +271,7 @@ class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 rounded">Daftar</but
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-4" role="alert">
<ul>
@foreach($errors->get('email') as $error)
<li>{{ $error }}</li>
<li>Email anda belum terdaftar.</li>
@endforeach
</ul>
</div>

View File

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

View File

@ -1,5 +1,46 @@
@extends('layouts.main') @section('content') <div class="min-h-96 mx-4 md:w-3/4 md:mx-auto py-8">
<h1 class="text-2xl font-bold mb-6">Riwayat Booking</h1>
@extends('layouts.main')
@section('content')
<div x-data="{
showModal: false,
qrCodeUrl: '',
venueName: '',
bookingDate: '',
bookingTime: ''
}"
x-on:keydown.escape.window="showModal = false; document.body.style.overflow = 'auto';"
class="min-h-screen bg-gray-100">
{{-- ======================================================= --}}
{{-- START: MODAL UNTUK MENAMPILKAN QR CODE --}}
{{-- ======================================================= --}}
<div x-show="showModal"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-end="opacity-0"
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-75 p-4"
x-cloak>
<div @click.away="showModal = false; document.body.style.overflow = 'auto';" class="bg-white rounded-lg shadow-xl p-6 max-w-sm w-full text-center">
<h3 class="text-lg font-bold text-gray-800" x-text="venueName"></h3>
<p class="text-sm text-gray-500 mb-4">
<span x-text="bookingDate"></span> - <span x-text="bookingTime"></span>
</p>
<div class="p-4 border rounded-lg bg-white">
<img :src="qrCodeUrl" alt="Booking QR Code" class="w-full h-auto">
</div>
<p class="text-xs text-gray-500 mt-4">Tunjukkan QR Code ini kepada kasir saat Anda tiba di venue.</p>
<div class="mt-6 flex space-x-4">
<button @click="showModal = false; document.body.style.overflow = 'auto';" class="w-full px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 font-medium">Tutup</button>
<a :href="qrCodeUrl" download="tiket-carimeja.svg" class="w-full flex items-center justify-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium">Download</a>
</div>
</div>
</div>
<div class="min-h-96 mx-4 md:w-3/4 md:mx-auto py-8">
<h1 class="text-2xl font-bold mb-6">Riwayat Bookingg</h1>
@if($bookings->isEmpty())
<div class="bg-white rounded-lg shadow-md p-6 text-center">
@ -64,17 +105,52 @@ class="px-3 py-1 rounded-full text-sm {{ $booking->start_time > now() ? 'bg-gree
@endif
</div>
@if($booking->start_time > now() && $booking->status == 'paid')
<div class="mt-4 flex justify-end space-x-4">
<a href="{{ route('venue', $booking->table->venue->name) }}"
class="text-blue-500 hover:underline">Lihat Venue</a>
<div class="border-t mt-4 pt-4 flex justify-end items-center space-x-4">
<a href="{{ route('venue', $booking->table->venue->name) }}" class="text-sm text-blue-600 hover:underline font-medium">
Lihat Venue
</a>
<a href="{{ route('booking.reschedule.form', $booking->id) }}"
class="text-orange-500 hover:underline">
Reschedule
</a>
</div>
@endif
{{-- Hanya tampilkan tombol jika status lunas --}}
@if($booking->status == 'paid')
{{-- Opsi untuk booking yang AKAN DATANG --}}
@if(now()->lt($booking->start_time))
{{-- Tombol Reschedule, dengan asumsi ada kolom reschedule_count di tabel bookings --}}
@if(isset($booking->reschedule_count) && $booking->reschedule_count < 1)
<a href="{{ route('booking.reschedule.form', $booking) }}" class="text-sm text-orange-600 hover:underline font-medium">
Reschedule
</a>
@endif
@endif
{{-- Opsi untuk booking yang SUDAH SELESAI --}}
@if(now()->gt($booking->end_time))
@if(!$booking->review)
<a href="{{ route('reviews.create', $booking) }}" class="text-sm text-green-600 hover:underline font-medium">
Beri Ulasan
</a>
@else
<span class="text-sm text-gray-500">Ulasan diberikan</span>
@endif
@endif
{{-- Tombol QR selalu muncul untuk booking lunas yang punya token --}}
@if($booking->validation_token)
<button @click="
showModal = true;
qrCodeUrl = '{{ route('booking.qrcode', $booking) }}';
venueName = '{{ addslashes($booking->table->venue->name) }}';
bookingDate = '{{ $booking->start_time->format("d M Y") }}';
bookingTime = '{{ $booking->start_time->format("H:i") }} - {{ $booking->end_time->format("H:i") }}';
document.body.style.overflow = 'hidden';
"
class="bg-blue-600 text-white text-sm font-bold px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors">
Lihat Tiket (QR)
</button>
@endif
@endif
</div>
</div>
</div>
@endforeach

View File

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

View File

@ -1,7 +1,7 @@
@extends('layouts.main')
@section('content')
<div class="min-h-96 mx-4 md:w-3/4 md:mx-auto py-8">
<!-- Notification Container -->
<div id="notification-container" class="fixed top-4 right-4 z-50 space-y-2"></div>
<h1 class="text-2xl font-bold mb-6">Reschedule Booking</h1>
@ -30,12 +30,9 @@
<p class="font-medium">{{ $duration }} Jam</p>
</div>
</div>
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<div class="flex items-start">
<div class="mr-3 text-yellow-500">
<i class="fa-solid fa-exclamation-circle text-xl"></i>
</div>
<div class="mr-3 text-yellow-500"><i class="fa-solid fa-exclamation-circle text-xl"></i></div>
<div>
<h3 class="font-semibold text-yellow-700">Perhatian</h3>
<p class="text-yellow-700 text-sm">
@ -54,14 +51,13 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Pilih Tanggal:</label>
<input type="date" x-model="selectedDate" class="w-full border p-2 rounded-lg"
:min="today" @change="dateChanged">
<label for="date-picker" class="block text-sm font-medium text-gray-700 mb-1">Tanggal Operasional:</label>
<input type="date" id="date-picker" x-model="selectedDate" class="w-full border p-2 rounded-lg bg-gray-100 cursor-not-allowed" disabled>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Pilih Meja:</label>
<select x-model="selectedTableId" class="w-full border p-2 rounded-lg" @change="tableChanged">
<label for="table-picker" class="block text-sm font-medium text-gray-700 mb-1">Pilih Meja:</label>
<select id="table-picker" x-model="selectedTableId" class="w-full border p-2 rounded-lg" @change="fetchSchedules">
<option value="">-- Pilih Meja --</option>
<template x-for="table in tables" :key="table.id">
<option :value="table.id" x-text="table.name + ' (' + table.brand + ')'"></option>
@ -72,264 +68,168 @@
<div class="mt-6" x-show="selectedDate && selectedTableId">
<label class="block text-sm font-medium text-gray-700 mb-1">Pilih Jam Mulai:</label>
<div class="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-2">
<div x-show="!isLoadingSchedules" class="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-2">
<template x-for="hour in availableHours" :key="hour">
<button
class="py-2 px-1 rounded-lg text-sm font-medium transition duration-150"
:class="isTimeSlotAvailable(hour) ?
(selectedStartHour === hour ? 'bg-blue-500 text-white' : 'bg-gray-100 hover:bg-gray-200 text-gray-800') :
'bg-gray-200 text-gray-400 cursor-not-allowed opacity-70'"
:disabled="!isTimeSlotAvailable(hour)"
@click="selectStartHour(hour)"
x-text="hour + ':00'">
</button>
<button class="py-2 px-1 rounded-lg text-sm font-medium transition duration-150"
:class="getSlotClass(hour)" :disabled="!isSlotAvailable(hour)" @click="selectedStartHour = hour"
x-text="hour + ':00'"></button>
</template>
</div>
<div class="mt-4" x-show="selectedStartHour">
<p class="text-sm text-gray-700 mb-2">
Jadwal reschedule: <span class="font-medium" x-text="formattedSchedule"></span>
</p>
<div x-show="isLoadingSchedules" class="text-center text-gray-500 py-4">
Memeriksa jadwal...
</div>
</div>
<div class="mt-8 flex justify-end">
<a href="{{ route('booking.history') }}" class="bg-gray-300 text-gray-700 px-4 py-2 rounded-lg mr-3">
Batal
</a>
<button @click="submitReschedule"
:disabled="!canSubmit || isSubmitting"
:class="canSubmit ? 'bg-green-500 hover:bg-green-600' : 'bg-green-300 cursor-not-allowed'"
class="text-white px-4 py-2 rounded-lg">
<a href="{{ route('booking.history') }}" class="bg-gray-200 text-gray-700 px-4 py-2 rounded-lg mr-3 hover:bg-gray-300">Batal</a>
<button @click="submitReschedule" :disabled="!canSubmit || isSubmitting"
class="text-white px-4 py-2 rounded-lg flex items-center justify-center" :class="canSubmit ? 'bg-green-600 hover:bg-green-700' : 'bg-green-300 cursor-not-allowed'">
<span x-show="!isSubmitting">Konfirmasi Reschedule</span>
<span x-show="isSubmitting">Memproses...</span>
<span x-show="isSubmitting">
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div> Memproses...
</span>
</button>
</div>
</div>
</div>
<script>
// Notification System
function showNotification(message, type = 'info', duration = 5000) {
const container = document.getElementById('notification-container');
const notification = document.createElement('div');
// Set notification styles based on type
let bgColor, textColor, icon;
switch(type) {
case 'success':
bgColor = 'bg-green-500';
textColor = 'text-white';
icon = 'fa-check-circle';
break;
case 'error':
bgColor = 'bg-red-500';
textColor = 'text-white';
icon = 'fa-exclamation-circle';
break;
case 'warning':
bgColor = 'bg-yellow-500';
textColor = 'text-white';
icon = 'fa-exclamation-triangle';
break;
default:
bgColor = 'bg-blue-500';
textColor = 'text-white';
icon = 'fa-info-circle';
}
notification.className = `${bgColor} ${textColor} px-6 py-4 rounded-lg shadow-lg transform transition-all duration-300 ease-in-out opacity-0 translate-x-full flex items-center space-x-3 max-w-md`;
notification.innerHTML = `
<i class="fas ${icon} text-lg"></i>
<div class="flex-1">
<p class="font-medium">${message}</p>
</div>
<button onclick="this.parentElement.remove()" class="text-white hover:text-gray-200 focus:outline-none">
<i class="fas fa-times"></i>
</button>
`;
container.appendChild(notification);
// Animate in
setTimeout(() => {
notification.classList.remove('opacity-0', 'translate-x-full');
notification.classList.add('opacity-100', 'translate-x-0');
}, 100);
// Auto remove after duration
setTimeout(() => {
notification.classList.add('opacity-0', 'translate-x-full');
setTimeout(() => {
if (notification.parentElement) {
notification.remove();
}
}, 300);
}, duration);
}
function showNotification(message, type = 'info') { /* ... fungsi notifikasi tidak berubah ... */ }
document.addEventListener('alpine:init', () => {
Alpine.data('rescheduleForm', () => ({
tables: @json($venue->tables),
// PERUBAHAN 2: Inisialisasi data yang bersih & benar
venue: @json($venue),
bookingId: {{ $booking->id }},
bookingDuration: {{ $duration }},
originalTableId: {{ $booking->table_id }},
originalStartTime: "{{ \Carbon\Carbon::parse($booking->start_time)->format('H:i') }}",
originalDate: "{{ \Carbon\Carbon::parse($booking->start_time)->format('Y-m-d') }}",
selectedDate: '',
selectedTableId: '',
tables: @json($venue->tables),
selectedDate: '{{ $operational_date_string }}',
selectedTableId: {{ $booking->table_id }},
selectedStartHour: null,
bookedSchedules: [],
availableHours: Array.from({length: 16}, (_, i) => (i + 9).toString().padStart(2, '0')),
isLoadingSchedules: true,
isSubmitting: false,
// PERUBAHAN 3: Fungsi init() disederhanakan
init() {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
this.today = `${year}-${month}-${day}`;
this.fetchSchedules();
},
this.selectedDate = this.originalDate;
this.selectedTableId = this.originalTableId;
// Semua fungsi di bawah ini sudah benar & tidak perlu diubah lagi
get openHour() { return parseInt(this.venue.open_time.split(':')[0]); },
get closeHour() { return parseInt(this.venue.close_time.split(':')[0]); },
get isOvernight() { return this.venue.is_overnight; },
this.checkBookedSchedules();
get availableHours() {
let hours = [];
if (this.isOvernight) {
for (let i = this.openHour; i < 24; i++) hours.push(i.toString().padStart(2, '0'));
for (let i = 0; i <= this.closeHour; i++) hours.push(i.toString().padStart(2, '0'));
} else {
for (let i = this.openHour; i <= this.closeHour; i++) hours.push(i.toString().padStart(2, '0'));
}
return hours;
},
get canSubmit() {
return this.selectedDate &&
this.selectedTableId &&
this.selectedStartHour !== null &&
(this.selectedDate !== this.originalDate ||
this.selectedTableId != this.originalTableId ||
this.selectedStartHour !== this.originalStartTime.split(':')[0]);
return this.selectedStartHour !== null && !this.isLoadingSchedules;
},
get formattedSchedule() {
if (!this.selectedStartHour) return '';
const startHour = parseInt(this.selectedStartHour);
const endHour = startHour + this.bookingDuration;
return `${this.selectedStartHour}:00 - ${endHour.toString().padStart(2, '0')}:00`;
},
async dateChanged() {
this.selectedStartHour = null;
await this.checkBookedSchedules();
},
async tableChanged() {
this.selectedStartHour = null;
await this.checkBookedSchedules();
},
async checkBookedSchedules() {
async fetchSchedules() {
if (!this.selectedDate || !this.selectedTableId) return;
this.isLoadingSchedules = true;
this.bookedSchedules = [];
this.selectedStartHour = null;
try {
const response = await fetch(`/booking/reschedule/check-availability?table_id=${this.selectedTableId}&date=${this.selectedDate}&booking_id=${this.bookingId}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
if (!response.ok) throw new Error('Failed to fetch schedules');
this.bookedSchedules = await response.json();
if (this.selectedDate === this.originalDate &&
parseInt(this.selectedTableId) === parseInt(this.originalTableId)) {
const originalHour = this.originalStartTime.split(':')[0];
if (this.isTimeSlotAvailable(originalHour)) {
this.selectedStartHour = originalHour;
}
}
} catch (error) {
console.error('Error checking booked schedules:', error);
showNotification('Terjadi kesalahan saat memeriksa jadwal. Silakan coba lagi.', 'error');
console.error(error);
} finally {
this.isLoadingSchedules = false;
}
},
isTimeSlotAvailable(hour) {
const hourInt = parseInt(hour);
const endHourInt = hourInt + this.bookingDuration;
if (endHourInt > 24) return false;
const selectedDate = new Date(this.selectedDate);
const today = new Date();
const isToday = selectedDate.toDateString() === today.toDateString();
if (isToday) {
const currentHour = today.getHours();
if (hourInt <= currentHour) {
return false;
}
isSlotAvailable(hour) {
const startHourInt = parseInt(hour);
const endHourInt = startHourInt + this.bookingDuration;
if (this.isOvernight) {
if (startHourInt >= this.openHour && endHourInt > (24 + this.closeHour)) return false;
if (startHourInt < this.openHour && endHourInt > this.closeHour) return false;
} else {
if (endHourInt > this.closeHour) return false;
}
const isOriginalTimeSlot = this.selectedDate === this.originalDate &&
parseInt(this.selectedTableId) === parseInt(this.originalTableId) &&
hour === this.originalStartTime.split(':')[0];
if (isOriginalTimeSlot) {
if (isToday) {
const currentHour = today.getHours();
return hourInt > currentHour;
}
return true;
const now = new Date();
const selectedDateTime = new Date(`${this.selectedDate}T${hour}:00:00`);
if (this.isOvernight && startHourInt < this.openHour) {
selectedDateTime.setDate(selectedDateTime.getDate() + 1);
}
return !this.bookedSchedules.some(schedule => {
if (selectedDateTime <= now) {
return false;
}
for (const schedule of this.bookedSchedules) {
const scheduleStart = parseInt(schedule.start.split(':')[0]);
const scheduleEnd = parseInt(schedule.end.split(':')[0]);
return (hourInt < scheduleEnd && endHourInt > scheduleStart);
});
const isOvernightBooking = scheduleEnd < scheduleStart;
if (isOvernightBooking) {
if ((startHourInt >= scheduleStart || startHourInt < scheduleEnd) && (endHourInt > scheduleStart || endHourInt <= scheduleEnd)) return false;
} else {
if (startHourInt < scheduleEnd && endHourInt > scheduleStart) return false;
}
}
return true;
},
selectStartHour(hour) {
if (this.isTimeSlotAvailable(hour)) {
this.selectedStartHour = hour;
getSlotClass(hour) {
if (!this.isSlotAvailable(hour)) {
return 'bg-gray-200 text-gray-400 cursor-not-allowed';
}
if (this.selectedStartHour === hour) {
return 'bg-blue-600 text-white shadow-md';
}
return 'bg-gray-100 hover:bg-gray-200 text-gray-800';
},
async submitReschedule() {
if (!this.canSubmit || this.isSubmitting) return;
if (!this.canSubmit) return;
this.isSubmitting = true;
const startHour = parseInt(this.selectedStartHour);
const endHour = startHour + this.bookingDuration;
const startTime = `${this.selectedDate} ${this.selectedStartHour}:00:00`;
const endTime = `${this.selectedDate} ${endHour.toString().padStart(2, '0')}:00:00`;
const startDateTime = new Date(`${this.selectedDate}T${this.selectedStartHour}:00:00`);
if (this.isOvernight && parseInt(this.selectedStartHour) < this.openHour) {
startDateTime.setDate(startDateTime.getDate() + 1);
}
const endDateTime = new Date(startDateTime.getTime());
endDateTime.setHours(endDateTime.getHours() + this.bookingDuration);
const formatForServer = (date) => {
const pad = (num) => num.toString().padStart(2, '0');
const year = date.getFullYear();
const month = pad(date.getMonth() + 1);
const day = pad(date.getDate());
const hours = pad(date.getHours());
const minutes = pad(date.getMinutes());
const seconds = pad(date.getSeconds());
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
try {
const response = await fetch(`/booking/${this.bookingId}/reschedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
},
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content },
body: JSON.stringify({
table_id: this.selectedTableId,
start_time: startTime,
end_time: endTime,
start_time: formatForServer(startDateTime),
end_time: formatForServer(endDateTime),
}),
});
const result = await response.json();
if (result.success) {
showNotification(result.message, 'success');
setTimeout(() => {
window.location.href = result.redirect;
}, 2000);
} else {
showNotification(result.message || 'Terjadi kesalahan saat memproses reschedule.', 'error');
this.isSubmitting = false;
}
if (response.ok && result.success) {
showNotification('Booking berhasil di-reschedule!', 'success');
setTimeout(() => window.location.href = result.redirect, 1500);
} else { throw new Error(result.message || 'Gagal reschedule.'); }
} catch (error) {
console.error('Error submitting reschedule:', error);
showNotification('Terjadi kesalahan. Silakan coba lagi.', 'error');
showNotification(error.message, 'error');
} finally {
this.isSubmitting = false;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,75 @@
@extends('layouts.main')
@section('content')
<style>
/* CSS untuk rating bintang */
.rating {
display: inline-block;
direction: rtl; /* Bintang dari kanan ke kiri */
}
.rating input {
display: none;
}
.rating label {
font-size: 2.5rem;
color: #ddd;
cursor: pointer;
padding: 0 0.1em;
}
.rating input:checked ~ label,
.rating label:hover,
.rating label:hover ~ label {
color: #f5b301;
}
</style>
<div class="min-h-screen container mx-auto px-4 py-8">
<div class="max-w-2xl mx-auto bg-white rounded-lg shadow-md p-8">
<h1 class="text-2xl font-bold text-gray-800 mb-2">Beri Ulasan Anda</h1>
<p class="text-gray-600 mb-6">
Untuk booking di **{{ $booking->table->venue->name }}** pada tanggal **{{ $booking->start_time->format('d M Y') }}**.
</p>
@if ($errors->any())
<div class="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-lg">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form action="{{ route('reviews.store') }}" method="POST">
@csrf
<input type="hidden" name="booking_id" value="{{ $booking->id }}">
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">Rating Anda *</label>
<div class="rating">
<input type="radio" id="star5" name="rating" value="5" required /><label for="star5"></label>
<input type="radio" id="star4" name="rating" value="4" /><label for="star4"></label>
<input type="radio" id="star3" name="rating" value="3" /><label for="star3"></label>
<input type="radio" id="star2" name="rating" value="2" /><label for="star2"></label>
<input type="radio" id="star1" name="rating" value="1" /><label for="star1"></label>
</div>
@error('rating') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
<div class="mb-6">
<label for="comment" class="block text-sm font-medium text-gray-700 mb-2">Ulasan Anda *</label>
<textarea name="comment" id="comment" rows="5"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="Bagaimana pengalaman Anda di venue ini? Ceritakan di sini...">{{ old('comment') }}</textarea>
@error('comment') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
<div class="flex justify-end">
<button type="submit" class="px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors">
Kirim Ulasan
</button>
</div>
</form>
</div>
</div>
@endsection

View File

@ -1,186 +1,188 @@
@extends('layouts.super-admin')
@section('content')
<div class="mb-6 flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold text-gray-800">Manajemen Admin</h1>
<p class="text-gray-600">Kelola admin untuk setiap venue</p>
<div class="p-6">
<div class="mb-6 flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold text-gray-800">Manajemen Admin</h1>
<p class="text-gray-600">Kelola admin untuk setiap venue</p>
</div>
<a href="{{ route('superadmin.admin.create') }}"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center">
<i class="fas fa-plus mr-2"></i>
Tambah Admin
</a>
</div>
<a href="{{ route('superadmin.admin.create') }}"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center">
<i class="fas fa-plus mr-2"></i>
Tambah Admin
</a>
</div>
@if(session('success'))
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 mb-6" role="alert">
<p>{{ session('success') }}</p>
</div>
@endif
@if(session('success'))
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 mb-6" role="alert">
<p>{{ session('success') }}</p>
</div>
@endif
<!-- Filter and Search -->
<div class="bg-white rounded-lg shadow mb-6 p-4">
<form action="{{ route('superadmin.admin.index') }}" method="GET">
<div class="flex flex-col md:flex-row gap-4">
<div class="flex-grow">
<label for="search" class="block text-sm font-medium text-gray-700 mb-1">Cari</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-search text-gray-400"></i>
<!-- Filter and Search -->
<div class="bg-white rounded-lg shadow mb-6 p-4">
<form action="{{ route('superadmin.admin.index') }}" method="GET">
<div class="flex flex-col md:flex-row gap-4">
<div class="flex-grow">
<label for="search" class="block text-sm font-medium text-gray-700 mb-1">Cari</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-search text-gray-400"></i>
</div>
<input type="text" id="search" name="search" value="{{ request('search') }}"
class="pl-10 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
placeholder="Cari nama atau email">
</div>
<input type="text" id="search" name="search" value="{{ request('search') }}"
class="pl-10 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
placeholder="Cari nama atau email">
</div>
<div class="md:w-64">
<label for="venue_filter" class="block text-sm font-medium text-gray-700 mb-1">Filter Venue</label>
<select id="venue_filter" name="venue_id"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
<option value="">Semua Venue</option>
@foreach($venues as $venue)
<option value="{{ $venue->id }}" {{ request('venue_id') == $venue->id ? 'selected' : '' }}>
{{ $venue->name }}
</option>
@endforeach
</select>
</div>
<div class="self-end">
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
Cari
</button>
<a href="{{ route('superadmin.admin.index') }}"
class="px-4 py-2 border border-gray-300 rounded-md bg-white hover:bg-gray-50 text-sm ml-2">
Reset Filter
</a>
</div>
</div>
<div class="md:w-64">
<label for="venue_filter" class="block text-sm font-medium text-gray-700 mb-1">Filter Venue</label>
<select id="venue_filter" name="venue_id"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
<option value="">Semua Venue</option>
@foreach($venues as $venue)
<option value="{{ $venue->id }}" {{ request('venue_id') == $venue->id ? 'selected' : '' }}>
{{ $venue->name }}
</option>
@endforeach
</select>
</div>
<div class="self-end">
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
Cari
</button>
<a href="{{ route('superadmin.admin.index') }}"
class="px-4 py-2 border border-gray-300 rounded-md bg-white hover:bg-gray-50 text-sm ml-2">
Reset Filter
</a>
</div>
</form>
</div>
<!-- Admin Table -->
<div class="bg-white rounded-lg shadow overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Nama
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Email
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Venue
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Terdaftar
</th>
<th scope="col"
class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Aksi
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@forelse($admins as $admin)
<tr>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div
class="flex-shrink-0 h-10 w-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600">
<i class="fas fa-user"></i>
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">{{ $admin->name }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">{{ $admin->email }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">
@if($admin->venue)
{{ $admin->venue->name }}
@else
<span class="text-gray-400">Tidak ada venue</span>
@endif
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ $admin->email_verified_at ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800' }}">
{{ $admin->email_verified_at ? 'Terverifikasi' : 'Belum Verifikasi' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ $admin->created_at->format('d M Y') }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button type="button" data-admin-id="{{ $admin->id }}" data-admin-name="{{ $admin->name }}"
class="text-red-600 hover:text-red-800 delete-admin-btn">
<i class="fas fa-trash-alt"></i>
</button>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-6 py-4 text-center text-gray-500">
Tidak ada data admin ditemukan
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</form>
</div>
<!-- Admin Table -->
<div class="bg-white rounded-lg shadow overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Nama
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Email
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Venue
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Terdaftar
</th>
<th scope="col"
class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Aksi
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@forelse($admins as $admin)
<tr>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div
class="flex-shrink-0 h-10 w-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600">
<i class="fas fa-user"></i>
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">{{ $admin->name }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">{{ $admin->email }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">
@if($admin->venue)
{{ $admin->venue->name }}
@else
<span class="text-gray-400">Tidak ada venue</span>
@endif
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ $admin->email_verified_at ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800' }}">
{{ $admin->email_verified_at ? 'Terverifikasi' : 'Belum Verifikasi' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ $admin->created_at->format('d M Y') }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button type="button" data-admin-id="{{ $admin->id }}" data-admin-name="{{ $admin->name }}"
class="text-red-600 hover:text-red-800 delete-admin-btn">
<i class="fas fa-trash-alt"></i>
</button>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-6 py-4 text-center text-gray-500">
Tidak ada data admin ditemukan
</td>
</tr>
@endforelse
</tbody>
</table>
<!-- Pagination -->
<div class="px-6 py-4 border-t border-gray-200">
{{ $admins->links() }}
</div>
</div>
<!-- Pagination -->
<div class="px-6 py-4 border-t border-gray-200">
{{ $admins->links() }}
</div>
</div>
<!-- Delete Admin Confirmation Modal -->
<div id="deleteAdminModal" tabindex="-1" aria-hidden="true"
class="fixed top-0 left-0 right-0 z-50 hidden w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-modal md:h-full">
<div class="relative w-full h-full max-w-md md:h-auto">
<div class="relative bg-white rounded-lg shadow">
<div class="flex items-center justify-between p-4 border-b">
<h3 class="text-xl font-semibold text-gray-900">
Konfirmasi Hapus
</h3>
<button type="button"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center"
data-modal-hide="deleteAdminModal">
<i class="fas fa-times"></i>
</button>
</div>
<div class="p-6 text-center">
<i class="fas fa-exclamation-triangle text-5xl text-yellow-400 mb-4"></i>
<h3 class="mb-5 text-lg font-normal text-gray-500">Apakah Anda yakin ingin menghapus admin <span
id="admin-name-to-delete" class="font-medium"></span>?</h3>
<form id="deleteAdminForm" method="POST" action="">
@csrf
@method('DELETE')
<button type="submit"
class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center mr-2">
Ya, saya yakin
<!-- Delete Admin Confirmation Modal -->
<div id="deleteAdminModal" tabindex="-1" aria-hidden="true"
class="fixed top-0 left-0 right-0 z-50 hidden w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-modal md:h-full">
<div class="relative w-full h-full max-w-md md:h-auto">
<div class="relative bg-white rounded-lg shadow">
<div class="flex items-center justify-between p-4 border-b">
<h3 class="text-xl font-semibold text-gray-900">
Konfirmasi Hapus
</h3>
<button type="button"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center"
data-modal-hide="deleteAdminModal">
<i class="fas fa-times"></i>
</button>
<button type="button" data-modal-hide="deleteAdminModal"
class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10">
Batal
</button>
</form>
</div>
<div class="p-6 text-center">
<i class="fas fa-exclamation-triangle text-5xl text-yellow-400 mb-4"></i>
<h3 class="mb-5 text-lg font-normal text-gray-500">Apakah Anda yakin ingin menghapus admin <span
id="admin-name-to-delete" class="font-medium"></span>?</h3>
<form id="deleteAdminForm" method="POST" action="">
@csrf
@method('DELETE')
<button type="submit"
class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center mr-2">
Ya, saya yakin
</button>
<button type="button" data-modal-hide="deleteAdminModal"
class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10">
Batal
</button>
</form>
</div>
</div>
</div>
</div>

View File

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

View File

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

View File

@ -1,217 +1,225 @@
@extends('layouts.super-admin')
@section('content')
<div class="mb-6 flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold text-gray-800">Manajemen Venue</h1>
<p class="text-gray-600">Kelola semua venue dalam sistem</p>
</div>
<a href="{{ route('superadmin.venue.create') }}"
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 flex items-center">
<i class="fas fa-plus mr-2"></i>
Tambah Venue
</a>
</div>
<!-- Flash Messages -->
@if(session('success'))
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 mb-6" role="alert">
<p>{{ session('success') }}</p>
</div>
@endif
@if(session('error'))
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6" role="alert">
<p>{{ session('error') }}</p>
</div>
@endif
<!-- Filter and Search -->
<div class="bg-white rounded-lg shadow mb-6 p-4">
<form action="{{ route('superadmin.venue.index') }}" method="GET">
<div class="flex flex-col md:flex-row gap-4">
<div class="flex-grow">
<label for="search" class="block text-sm font-medium text-gray-700 mb-1">Cari</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-search text-gray-400"></i>
</div>
<input type="text" id="search" name="search" value="{{ request('search') }}"
class="pl-10 block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring-green-500"
placeholder="Cari nama venue atau lokasi">
</div>
</div>
<div>
<label for="status" class="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select id="status" name="status"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring-green-500">
<option value="">Semua Status</option>
<option value="active" {{ request('status') == 'active' ? 'selected' : '' }}>Aktif</option>
<option value="inactive" {{ request('status') == 'inactive' ? 'selected' : '' }}>Tidak Aktif</option>
</select>
</div>
<div class="self-end">
<button type="submit" class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 text-sm">
Filter
</button>
<a href="{{ route('superadmin.venue.index') }}"
class="px-4 py-2 border border-gray-300 rounded-md bg-white hover:bg-gray-50 text-sm ml-2">
Reset
</a>
</div>
<div class="p-6">
<div class="mb-6 flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold text-gray-800">Manajemen Venue</h1>
<p class="text-gray-600">Kelola semua venue dalam sistem</p>
</div>
</form>
</div>
<a href="{{ route('superadmin.venue.create') }}"
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 flex items-center">
<i class="fas fa-plus mr-2"></i>
Tambah Venue
</a>
</div>
<!-- Venue Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@forelse($venues as $venue)
<div class="bg-white rounded-lg shadow overflow-hidden">
<div class="relative">
@php
// Tentukan path gambar yang akan ditampilkan
$imagePath = null;
<!-- Flash Messages -->
@if(session('success'))
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 mb-6" role="alert">
<p>{{ session('success') }}</p>
</div>
@endif
if ($venue->image) {
// Cek apakah file gambar ada di storage
if (Storage::disk('public')->exists($venue->image)) {
$imagePath = asset('storage/' . $venue->image);
}
// Cek apakah file gambar ada di public folder
elseif (file_exists(public_path($venue->image))) {
$imagePath = asset($venue->image);
}
// Cek jika path sudah lengkap dengan storage/
elseif (file_exists(public_path('storage/' . $venue->image))) {
$imagePath = asset('storage/' . $venue->image);
}
}
@if(session('error'))
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6" role="alert">
<p>{{ session('error') }}</p>
</div>
@endif
// Fallback ke placeholder jika gambar tidak ditemukan
if (!$imagePath) {
$imagePath = asset('images/venue-placeholder.jpg');
}
@endphp
<img src="{{ $imagePath }}" alt="{{ $venue->name }}" class="w-full h-48 object-cover"
onerror="this.src='{{ asset('images/venue-placeholder.jpg') }}'; this.onerror=null;">
<div class="absolute top-3 right-3 flex gap-2">
<a href="{{ route('superadmin.venue.edit', $venue->id) }}"
class="p-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200">
<i class="fas fa-edit"></i>
</a>
<button type="button" onclick="confirmDelete({{ $venue->id }})"
class="p-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors duration-200">
<i class="fas fa-trash-alt"></i>
<!-- Filter and Search -->
{{-- <div class="bg-white rounded-lg shadow mb-6 p-4">
<form action="{{ route('superadmin.venue.index') }}" method="GET">
<div class="flex flex-col md:flex-row gap-4">
<div class="flex-grow">
<label for="search" class="block text-sm font-medium text-gray-700 mb-1">Cari</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-search text-gray-400"></i>
</div>
<input type="text" id="search" name="search" value="{{ request('search') }}"
class="pl-10 block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring-green-500"
placeholder="Cari nama venue atau lokasi">
</div>
</div> --}}
{{-- <div>
<label for="status" class="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select id="status" name="status"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring-green-500">
<option value="">Semua Status</option>
<option value="open" {{ request('status')=='open' ? 'selected' : '' }}>Open</option>
<option value="close" {{ request('status')=='close' ? 'selected' : '' }}>Close
</option>
</select>
</div> --}}
{{-- <div class="self-end">
<button type="submit"
class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 text-sm">
Filter
</button>
<a href="{{ route('superadmin.venue.index') }}"
class="px-4 py-2 border border-gray-300 rounded-md bg-white hover:bg-gray-50 text-sm ml-2">
Reset
</a>
</div>
</div>
<div class="p-5">
<h3 class="text-xl font-semibold text-gray-900 mb-2">{{ $venue->name }}</h3>
<div class="flex items-center mb-2">
<i class="fas fa-map-marker-alt text-gray-500 mr-2"></i>
<span class="text-gray-600 truncate" title="{{ $venue->address }}">{{ $venue->address }}</span>
</div>
<div class="flex items-center mb-2">
<i class="fas fa-phone text-gray-500 mr-2"></i>
<span class="text-gray-600">{{ $venue->phone }}</span>
</div>
<div class="flex items-center mb-2">
<i class="fas fa-clock text-gray-500 mr-2"></i>
<span class="text-gray-600">
{{ $venue->open_time ?? '00:00' }} - {{ $venue->close_time ?? '23:59' }}
</span>
</div>
<div class="flex items-center mb-4">
<i class="fas fa-circle {{ $venue->status == 'active' ? 'text-green-500' : 'text-red-500' }} mr-2"></i>
<span class="text-sm font-medium {{ $venue->status == 'active' ? 'text-green-700' : 'text-red-700' }}">
{{ $venue->status == 'active' ? 'Aktif' : 'Tidak Aktif' }}
</span>
</div>
</form>
</div> --}}
@if($venue->description)
<!-- Venue Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@forelse($venues as $venue)
<div class="bg-white rounded-lg shadow overflow-hidden">
<div class="relative">
@php
// Tentukan path gambar yang akan ditampilkan
$imagePath = null;
if ($venue->image) {
// Cek apakah file gambar ada di storage
if (Storage::disk('public')->exists($venue->image)) {
$imagePath = asset('storage/' . $venue->image);
}
// Cek apakah file gambar ada di public folder
elseif (file_exists(public_path($venue->image))) {
$imagePath = asset($venue->image);
}
// Cek jika path sudah lengkap dengan storage/
elseif (file_exists(public_path('storage/' . $venue->image))) {
$imagePath = asset('storage/' . $venue->image);
}
}
// Fallback ke placeholder jika gambar tidak ditemukan
if (!$imagePath) {
$imagePath = asset('images/venue-placeholder.jpg');
}
@endphp
<img src="{{ $imagePath }}" alt="{{ $venue->name }}" class="w-full h-48 object-cover"
onerror="this.src='{{ asset('images/venue-placeholder.jpg') }}'; this.onerror=null;">
<div class="absolute top-3 right-3 flex gap-2">
{{-- <a href="{{ route('superadmin.venue.edit', $venue->id) }}"
class="p-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200">
<i class="fas fa-edit"></i>
</a> --}}
<button type="button" onclick="confirmDelete({{ $venue->id }})"
class="p-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors duration-200">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</div>
<div class="p-5">
<h3 class="text-xl font-semibold text-gray-900 mb-2">{{ $venue->name }}</h3>
{{-- <div class="flex items-center mb-2">
<i class="fas fa-map-marker-alt text-gray-500 mr-2"></i>
<span class="text-gray-600 truncate" title="{{ $venue->address }}">{{ $venue->address }}</span>
</div> --}}
<div class="flex items-center mb-4">
<i
class="fas fa-circle {{ $venue->status == 'open' ? 'text-green-500' : 'text-red-500' }} mr-2"></i>
<span
class="text-sm font-medium {{ $venue->status == 'open' ? 'text-green-700' : 'text-red-700' }}">
{{ $venue->status == 'open' ? 'Open' : 'Close' }}
</span>
</div>
<div class="flex items-center mb-2">
<i class="fas fa-clock text-gray-500 mr-2"></i>
<span class="text-gray-600">
{{ $venue->open_time ?? '00:00' }} - {{ $venue->close_time ?? '23:59' }}
</span>
</div>
<div class="flex items-center mb-2">
<i class="fas fa-phone text-gray-500 mr-2"></i>
<span class="text-gray-600">{{ $venue->phone }}</span>
</div>
{{-- @if($venue->description)
<div class="mb-4">
<p class="text-gray-600 text-sm line-clamp-2">{{ Str::limit($venue->description, 100) }}</p>
</div>
@endif
@endif --}}
<div class="border-t pt-4">
<div class="flex justify-between items-center">
<div class="text-sm text-gray-500">
<span class="font-medium">{{ $venue->created_at->format('d M Y') }}</span>
<div class="border-t pt-4">
<div class="flex justify-between items-center">
<div class="text-sm text-gray-500">
Dibuat: <span class="font-medium">{{ $venue->created_at->format('d M Y') }}</span>
</div>
<a href="{{ route('superadmin.venue.edit', $venue->id) }}"
class="text-green-600 hover:text-green-800 flex items-center text-sm transition-colors duration-200">
Detail
<i class="fas fa-arrow-right ml-1"></i>
</a>
</div>
<a href="{{ route('superadmin.venue.edit', $venue->id) }}"
class="text-green-600 hover:text-green-800 flex items-center text-sm transition-colors duration-200">
Detail
<i class="fas fa-arrow-right ml-1"></i>
</a>
</div>
</div>
</div>
</div>
@empty
<div class="col-span-3 bg-white rounded-lg shadow p-8 text-center">
<div class="max-w-sm mx-auto">
<i class="fas fa-building text-gray-300 text-6xl mb-4"></i>
<h3 class="text-xl font-semibold text-gray-900 mb-2">Belum ada venue</h3>
<p class="text-gray-500 mb-6">Mulai tambahkan venue baru untuk mengelola bisnis Anda dengan lebih baik</p>
<a href="{{ route('superadmin.venue.create') }}"
class="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 inline-flex items-center transition-colors duration-200">
<i class="fas fa-plus mr-2"></i>
Tambah Venue Pertama
</a>
@empty
<div class="col-span-3 bg-white rounded-lg shadow p-8 text-center">
<div class="max-w-sm mx-auto">
<i class="fas fa-building text-gray-300 text-6xl mb-4"></i>
<h3 class="text-xl font-semibold text-gray-900 mb-2">Belum ada venue</h3>
<p class="text-gray-500 mb-6">Mulai tambahkan venue baru untuk mengelola bisnis Anda dengan lebih baik
</p>
<a href="{{ route('superadmin.venue.create') }}"
class="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 inline-flex items-center transition-colors duration-200">
<i class="fas fa-plus mr-2"></i>
Tambah Venue Pertama
</a>
</div>
</div>
</div>
@endforelse
</div>
<!-- Pagination -->
@if($venues->hasPages())
<div class="mt-8">
<div class="bg-white px-4 py-3 border border-gray-200 rounded-lg">
{{ $venues->appends(request()->query())->links() }}
</div>
@endforelse
</div>
@endif
<!-- Delete Venue Confirmation Modal -->
<div id="deleteVenueModal" tabindex="-1" aria-hidden="true"
class="fixed top-0 left-0 right-0 z-50 hidden w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-modal md:h-full bg-gray-900 bg-opacity-50">
<div class="relative w-full h-full max-w-md md:h-auto mx-auto flex items-center justify-center min-h-screen">
<div class="relative bg-white rounded-lg shadow-xl max-w-md w-full">
<div class="flex items-center justify-between p-5 border-b border-gray-200">
<h3 class="text-xl font-semibold text-gray-900">
Konfirmasi Hapus
</h3>
<button type="button" onclick="closeDeleteModal()"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center transition-colors duration-200">
<i class="fas fa-times w-5 h-5"></i>
</button>
<!-- Pagination -->
@if($venues->hasPages())
<div class="mt-8">
<div class="bg-white px-4 py-3 border border-gray-200 rounded-lg">
{{ $venues->appends(request()->query())->links() }}
</div>
<div class="p-6 text-center">
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4">
<i class="fas fa-exclamation-triangle text-red-600 text-xl"></i>
</div>
@endif
<!-- Delete Venue Confirmation Modal -->
<div id="deleteVenueModal" tabindex="-1" aria-hidden="true"
class="fixed top-0 left-0 right-0 z-50 hidden w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-modal md:h-full bg-gray-900 bg-opacity-50">
<div class="relative w-full h-full max-w-md md:h-auto mx-auto flex items-center justify-center min-h-screen">
<div class="relative bg-white rounded-lg shadow-xl max-w-md w-full">
<div class="flex items-center justify-between p-5 border-b border-gray-200">
<h3 class="text-xl font-semibold text-gray-900">
Konfirmasi Hapus
</h3>
<button type="button" onclick="closeDeleteModal()"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center transition-colors duration-200">
<i class="fas fa-times w-5 h-5"></i>
</button>
</div>
<h3 class="mb-2 text-lg font-semibold text-gray-900">Hapus Venue</h3>
<p class="mb-6 text-sm text-gray-500">
Apakah Anda yakin ingin menghapus venue ini? Semua data terkait dengan venue ini akan ikut terhapus
dan tidak dapat dikembalikan.
</p>
<form id="deleteVenueForm" method="POST" action="" class="space-y-4">
@csrf
@method('DELETE')
<div class="flex justify-center space-x-3">
<button type="submit"
class="px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-lg hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 transition-colors duration-200">
Ya, Hapus
</button>
<button type="button" onclick="closeDeleteModal()"
class="px-4 py-2 bg-white text-gray-700 text-sm font-medium rounded-lg border border-gray-300 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 transition-colors duration-200">
Batal
</button>
<div class="p-6 text-center">
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4">
<i class="fas fa-exclamation-triangle text-red-600 text-xl"></i>
</div>
</form>
<h3 class="mb-2 text-lg font-semibold text-gray-900">Hapus Venue</h3>
<p class="mb-6 text-sm text-gray-500">
Apakah Anda yakin ingin menghapus venue ini? Semua data terkait dengan venue ini akan ikut
terhapus
dan tidak dapat dikembalikan.
</p>
<form id="deleteVenueForm" method="POST" action="" class="space-y-4">
@csrf
@method('DELETE')
<div class="flex justify-center space-x-3">
<button type="submit"
class="px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-lg hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 transition-colors duration-200">
Ya, Hapus
</button>
<button type="button" onclick="closeDeleteModal()"
class="px-4 py-2 bg-white text-gray-700 text-sm font-medium rounded-lg border border-gray-300 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 transition-colors duration-200">
Batal
</button>
</div>
</form>
</div>
</div>
</div>
</div>

View File

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