Kodingan terkini terfresh terbaru terbaik

This commit is contained in:
Stephen Gesityan 2025-05-07 02:01:58 +07:00
parent 5a13e481a0
commit 7c88680e65
31 changed files with 895 additions and 84 deletions

0
[address], Normal file
View File

0
[image], Normal file
View File

0
[location], Normal file
View File

0
[name], Normal file
View File

0
[price], Normal file
View File

View File

@ -27,7 +27,7 @@ class LoginController extends Controller
* *
* @var string * @var string
*/ */
protected $redirectTo = '/venue/capitano'; protected $redirectTo = '/home';
/** /**
* Create a new controller instance. * Create a new controller instance.
@ -47,6 +47,17 @@ public function logout(Request $request)
$request->session()->invalidate(); $request->session()->invalidate();
$request->session()->regenerateToken(); $request->session()->regenerateToken();
return redirect('/venue/das'); session()->flash('error', 'Berhasil logout!');
return redirect('/home');
} }
protected function authenticated(Request $request, $user)
{
session()->flash('success', 'Login berhasil!');
if ($user->role === 'admin') {
return redirect('/admin');
}
return redirect()->intended($this->redirectTo);
}
} }

View File

@ -28,7 +28,7 @@ class RegisterController extends Controller
* *
* @var string * @var string
*/ */
protected $redirectTo = '/'; protected $redirectTo = '/home';
/** /**
* Create a new controller instance. * Create a new controller instance.

View File

@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers\admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Venue;
use App\Models\Table;
use App\Models\Booking;
use Carbon\Carbon;
class AdminController extends Controller
{
public function index()
{
$venue = Venue::find(auth()->user()->venue_id);
$todayBookings = Booking::whereDate('created_at', now())
->whereHas('table', function ($query) use ($venue) {
$query->where('venue_id', $venue->id);
})
->count();
$totalTables = Table::where('venue_id', $venue->id)->count();
$usedTables = Table::where('venue_id', $venue->id)->where('status', 'booked')->count();
$availableTables = Table::where('venue_id', $venue->id)->where('status', 'available')->count();
$recentBookings = Booking::whereHas('table', function ($query) use ($venue) {
$query->where('venue_id', $venue->id);
})
->latest()
->take(5)
->with(['user', 'table'])
->get();
return view('admin.dashboard', compact(
'venue',
'todayBookings',
'totalTables',
'usedTables',
'availableTables',
'recentBookings'
));
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Http\Controllers\admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Booking;
class BookingsController extends Controller
{
public function index()
{
$bookings = Booking::with(['table', 'user'])
->orderBy('start_time', 'desc')
->paginate(10);
return view('admin.bookings.index', compact('bookings'));
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Http\Controllers\admin;
use App\Http\Controllers\Controller;
use App\Models\Table;
use Illuminate\Http\Request;
class TableController extends Controller
{
public function kelolaMeja()
{
$tables = Table::where('venue_id', auth()->user()->venue_id)->paginate(10);
return view('admin.tables.index', compact('tables'));
}
public function editTable($id)
{
$table = Table::findOrFail($id);
return view('admin.tables.edit', compact('table'));
}
public function updateTable(Request $request, $id)
{
$table = Table::findOrFail($id);
$table->update([
'name' => $request->name,
'brand' => $request->brand,
'status' => $request->status,
]);
return redirect()->route('admin.tables.index')->with('success', 'Data meja berhasil diperbarui.');
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Http\Controllers\pages;
use App\Http\Controllers\Controller;
use App\Models\Booking;
use App\Models\Table;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
class BookingController extends Controller
{
public function store(Request $request) {
$request->validate([
'table_id' => 'required|exists:tables,id',
'start_time' => 'required|date',
'end_time' => 'required|date|after:start_time',
]);
// Cek apakah meja sedang dibooking pada waktu tersebut
$conflict = Booking::where('table_id', $request->table_id)
->where(function($query) use ($request) {
$query->whereBetween('start_time', [$request->start_time, $request->end_time])
->orWhereBetween('end_time', [$request->start_time, $request->end_time])
->orWhere(function($query) use ($request) {
$query->where('start_time', '<', $request->start_time)
->where('end_time', '>', $request->end_time);
});
})
->where('status', '!=', 'cancelled') // skip booking yang dibatalkan
->exists();
if ($conflict) {
return response()->json(['message' => 'Meja sudah dibooking di jam tersebut'], 409);
}
Booking::create([
'table_id' => $request->table_id,
'user_id' => Auth::id(),
'start_time' => $request->start_time,
'end_time' => $request->end_time,
'status' => 'booked',
]);
return response()->json(['message' => 'Booking berhasil']);
}
}

View File

@ -7,10 +7,10 @@
class HomeController extends Controller class HomeController extends Controller
{ {
public function __construct() // public function __construct()
{ // {
$this->middleware('auth'); // $this->middleware('auth');
} // }
public function index() { public function index() {
return view('pages.home'); return view('pages.home');

View File

@ -3,67 +3,27 @@
namespace App\Http\Controllers\pages; namespace App\Http\Controllers\pages;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Venue; // Pastikan model Venue di-import
use Illuminate\Http\Request; use Illuminate\Http\Request;
class VenueController extends Controller class VenueController extends Controller
{ {
public function venue($venueName) { public function venue($venueName) {
$venues = [ // Mengambil venue berdasarkan nama yang diberikan
'capitano' => [ $venue = Venue::where('name', 'like', '%' . ucfirst($venueName) . '%')->first();
'name' => 'Capitano Billiard',
'location' => 'Genteng',
'address' => 'Jl. Hasanudin No.II, Dusun Krajan, Genteng Wetan, Kec. Genteng, Kabupaten Banyuwangi',
'price' => 30000,
'image' => 'images/billiard2.jpg',
'tables' => [
['name' => 'Table 1', 'brand' => 'Cosmic', 'status' => 'Available'],
['name' => 'Table 2', 'brand' => 'Cosmic', 'status' => 'Booked'],
['name' => 'Table 3', 'brand' => 'Cosmic', 'status' => 'Available'],
['name' => 'Table 4', 'brand' => 'Cosmic', 'status' => 'Available'],
['name' => 'Table 5', 'brand' => 'A Plus Premier', 'status' => 'Booked'],
['name' => 'Table 6', 'brand' => 'A Plus Premier', 'status' => 'Booked'],
['name' => 'Table 7', 'brand' => 'A Plus Premier', 'status' => 'Available'],
],
],
'osing' => [
'name' => 'Osing Billiard Center',
'location' => 'Lidah',
'address' => 'Dusun Krajan, Kalirejo, Kec. Kabat, Kabupaten Banyuwangi',
'price' => 25000,
'image' => 'images/billiard3.jpg',
'tables' => [
['name' => 'Table 1', 'brand' => 'Xingjue', 'status' => 'Booked'],
['name' => 'Table 2', 'brand' => 'Xingjue', 'status' => 'Booked'],
['name' => 'Table 3', 'brand' => 'Xingjue', 'status' => 'Available'],
['name' => 'Table 4', 'brand' => 'Xingjue', 'status' => 'Available'],
['name' => 'Table 5', 'brand' => 'Xingjue', 'status' => 'Booked'],
['name' => 'Table 6', 'brand' => 'Xingjue', 'status' => 'Available'],
['name' => 'Table 7', 'brand' => 'Xingjue', 'status' => 'Available'],
],
],
'das' => [
'name' => 'DAS Game & Billiard',
'location' => 'Jalen',
'address' => 'Jl. Samiran, Jalen Parungan, Setail, Kec. Genteng, Kabupaten Banyuwangi',
'price' => 20000,
'image' => 'images/billiard4.jpg',
'tables' => [
['name' => 'Table 1', 'brand' => 'Cosmic', 'status' => 'Available'],
['name' => 'Table 2', 'brand' => 'Cosmic', 'status' => 'Booked'],
['name' => 'Table 3', 'brand' => 'Cosmic', 'status' => 'Available'],
['name' => 'Table 4', 'brand' => 'Cosmic', 'status' => 'Booked'],
['name' => 'Table 5', 'brand' => 'Cosmic', 'status' => 'Booked'],
['name' => 'Table 6', 'brand' => 'Cosmic', 'status' => 'Available'],
['name' => 'Table 7', 'brand' => 'Cosmic', 'status' => 'Available'],
['name' => 'Table 8', 'brand' => 'Cosmic', 'status' => 'Booked'],
],
],
];
if (!isset($venues[$venueName])) { // Jika venue tidak ditemukan, tampilkan error 404
if (!$venue) {
abort(404); abort(404);
} }
return view('pages.venue', ['venue' => $venues[$venueName]]); // Ambil tabel-tabel terkait dengan venue
$tables = $venue->tables;
// Mengirim data venue dan tabel ke view
return view('pages.venue', [
'venue' => $venue,
'tables' => $tables
]);
} }
} }

View File

@ -64,5 +64,6 @@ class Kernel extends HttpKernel
'signed' => \App\Http\Middleware\ValidateSignature::class, 'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'is_admin' => \App\Http\Middleware\IsAdmin::class,
]; ];
} }

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class IsAdmin
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle($request, Closure $next)
{
if (auth()->check() && auth()->user()->role === 'admin') {
return $next($request);
}
abort(403); // atau redirect('/login')
}
}

23
app/Models/Booking.php Normal file
View File

@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Booking extends Model
{
use HasFactory;
protected $fillable = ['table_id', 'user_id', 'start_time', 'end_time', 'status'];
public function table()
{
return $this->belongsTo(Table::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}

23
app/Models/Table.php Normal file
View File

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

18
app/Models/Venue.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 Venue extends Model
{
use HasFactory;
protected $fillable = ['name', 'location', 'address', 'image'];
public function tables()
{
return $this->hasMany(Table::class);
}
}

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('venues', function (Blueprint $table) {
$table->id();
$table->string('name'); // Nama venue
$table->string('location'); // Lokasi venue (misalnya: Jakarta)
$table->string('address'); // Alamat venue
$table->integer('price');
$table->string('image')->nullable(); // Gambar venue (opsional)
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('venues');
}
};

View File

@ -0,0 +1,31 @@
<?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::create('tables', function (Blueprint $table) {
$table->id();
$table->foreignId('venue_id')->constrained(); // Referensi ke tabel venues
$table->string('name');
$table->string('brand');
$table->enum('status', ['Available', 'Booked'])->default('Available');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('tables');
}
};

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::create('bookings', function (Blueprint $table) {
$table->id();
$table->foreignId('table_id')->constrained(); // Referensi ke tabel tables
$table->foreignId('user_id')->constrained(); // Referensi ke tabel users
$table->dateTime('start_time');
$table->dateTime('end_time');
$table->enum('status', ['Booked', 'Cancelled', 'Completed'])->default('Booked');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('bookings');
}
};

View File

@ -0,0 +1,31 @@
<?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('users', function (Blueprint $table) {
// Hanya menambahkan kolom venue_id dengan foreign key
$table->unsignedBigInteger('venue_id')->nullable()->after('role');
$table->foreign('venue_id')->references('id')->on('venues')->onDelete('set null');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
// Menghapus foreign key dan kolom venue_id
if (Schema::hasColumn('users', 'venue_id')) {
$table->dropForeign(['venue_id']); // Menghapus foreign key constraint
$table->dropColumn('venue_id'); // Menghapus kolom venue_id
}
});
}
};

View File

@ -0,0 +1,81 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\Venue;
use App\Models\Table;
class VenueSeeder extends Seeder
{
public function run(): void
{
$venues = [
'capitano' => [
'name' => 'Capitano Billiard',
'location' => 'Genteng',
'address' => 'Jl. Hasanudin No.II, Dusun Krajan, Genteng Wetan, Kec. Genteng, Kabupaten Banyuwangi',
'price' => 30000,
'image' => 'images/billiard2.jpg',
'tables' => [
['name' => 'Table 1', 'brand' => 'Cosmic', 'status' => 'Available'],
['name' => 'Table 2', 'brand' => 'Cosmic', 'status' => 'Booked'],
['name' => 'Table 3', 'brand' => 'Cosmic', 'status' => 'Available'],
['name' => 'Table 4', 'brand' => 'Cosmic', 'status' => 'Available'],
['name' => 'Table 5', 'brand' => 'A Plus Premier', 'status' => 'Booked'],
['name' => 'Table 6', 'brand' => 'A Plus Premier', 'status' => 'Booked'],
],
],
'osing' => [
'name' => 'Osing Billiard Center',
'location' => 'Lidah',
'address' => 'Dusun Krajan, Kalirejo, Kec. Kabat, Kabupaten Banyuwangi',
'price' => 25000,
'image' => 'images/billiard3.jpg',
'tables' => [
['name' => 'Table 1', 'brand' => 'Xingjue', 'status' => 'Booked'],
['name' => 'Table 2', 'brand' => 'Xingjue', 'status' => 'Booked'],
['name' => 'Table 3', 'brand' => 'Xingjue', 'status' => 'Available'],
['name' => 'Table 4', 'brand' => 'Xingjue', 'status' => 'Available'],
['name' => 'Table 5', 'brand' => 'Xingjue', 'status' => 'Booked'],
['name' => 'Table 6', 'brand' => 'Xingjue', 'status' => 'Available'],
['name' => 'Table 7', 'brand' => 'Xingjue', 'status' => 'Available'],
],
],
'das' => [
'name' => 'DAS Game & Billiard',
'location' => 'Jalen',
'address' => 'Jl. Samiran, Jalen Parungan, Setail, Kec. Genteng, Kabupaten Banyuwangi',
'price' => 20000,
'image' => 'images/billiard4.jpg',
'tables' => [
['name' => 'Table 1', 'brand' => 'Cosmic', 'status' => 'Available'],
['name' => 'Table 2', 'brand' => 'Cosmic', 'status' => 'Booked'],
['name' => 'Table 3', 'brand' => 'Cosmic', 'status' => 'Available'],
['name' => 'Table 4', 'brand' => 'Cosmic', 'status' => 'Booked'],
['name' => 'Table 5', 'brand' => 'Cosmic', 'status' => 'Booked'],
['name' => 'Table 6', 'brand' => 'Cosmic', 'status' => 'Available'],
['name' => 'Table 7', 'brand' => 'Cosmic', 'status' => 'Available'],
['name' => 'Table 8', 'brand' => 'Cosmic', 'status' => 'Booked'],
],
],
];
foreach ($venues as $venueData) {
// Membuat venue baru
$venue = Venue::create([
'name' => $venueData['name'],
'location' => $venueData['location'],
'address' => $venueData['address'],
'price' => $venueData['price'],
'image' => $venueData['image'],
]);
// Menambahkan tabel untuk setiap venue
foreach ($venueData['tables'] as $tableData) {
$venue->tables()->create($tableData); // Menambahkan meja ke venue
}
}
}
}

View File

@ -0,0 +1,42 @@
@extends('layouts.admin')
@section('content')
<div class="p-4">
<h1 class="text-2xl font-bold mb-4">Daftar Booking</h1>
<div class="overflow-x-auto bg-white rounded shadow">
<table class="min-w-full table-auto">
<thead class="bg-gray-100">
<tr>
<th class="px-4 py-2 text-left">User</th>
<th class="px-4 py-2 text-left">Meja</th>
<th class="px-4 py-2 text-left">Mulai</th>
<th class="px-4 py-2 text-left">Selesai</th>
<th class="px-4 py-2 text-left">Status</th>
</tr>
</thead>
<tbody>
@forelse ($bookings as $booking)
<tr class="border-b">
<td class="px-4 py-2">{{ $booking->user->name }}</td>
<td class="px-4 py-2">{{ $booking->table->name }}</td>
<td class="px-4 py-2">{{ \Carbon\Carbon::parse($booking->start_time)->format('H:i d/m') }}</td>
<td class="px-4 py-2">{{ \Carbon\Carbon::parse($booking->end_time)->format('H:i d/m') }}</td>
<td class="px-4 py-2">
<span
class="text-sm px-2 py-1 rounded
{{ $booking->status === 'booked' ? 'bg-blue-200 text-blue-800' : ($booking->status === 'selesai' ? 'bg-green-200 text-green-800' : 'bg-red-200 text-red-800') }}">
{{ ucfirst($booking->status) }}
</span>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-4 py-2 text-center text-gray-500">Belum ada data booking.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
@endsection

View File

@ -0,0 +1,57 @@
@extends('layouts.admin')
@section('content')
<div class="p-4">
<h1 class="text-2xl font-bold mb-4">Admin {{ $venue->name }}</h1>
<p>Selamat datang, {{ auth()->user()->name }}!</p>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 my-6">
<a href="#" class="bg-blue-500 text-white p-4 rounded-lg">
<p class="text-sm">Jumlah Booking Hari Ini</p>
<p class="text-2xl font-bold">{{ $todayBookings }}</p>
</a>
<a href="#" class="bg-gray-600 text-white p-4 rounded-lg">
<p class="text-sm">Total Meja</p>
<p class="text-2xl font-bold">{{ $totalTables }}</p>
</a>
<a href="#" class="bg-red-600 text-white p-4 rounded-lg">
<p class="text-sm">Meja Sedang Digunakan</p>
<p class="text-2xl font-bold">{{ $usedTables }}</p>
</a>
<a href="#" class="bg-green-600 text-white p-4 rounded-lg">
<p class="text-sm">Meja Tersedia</p>
<p class="text-2xl font-bold">{{ $availableTables }}</p>
</a>
</div>
<h2 class="font-semibold text-lg mt-8 mb-2">Booking Terbaru</h2>
@if($recentBookings->isEmpty())
<p class="text-gray-500">Belum ada booking terbaru.</p>
@else
<div class="bg-white rounded shadow overflow-x-auto">
<table class="w-full table-auto">
<thead class="bg-gray-100">
<tr>
<th class="px-4 py-2 text-left">Nama User</th>
<th class="px-4 py-2 text-left">Meja</th>
<th class="px-4 py-2 text-left">Waktu</th>
<th class="px-4 py-2 text-left">Status</th>
</tr>
</thead>
<tbody>
@foreach($recentBookings as $booking)
<tr class="border-t">
<td class="px-4 py-2">{{ $booking->user->name }}</td>
<td class="px-4 py-2">{{ $booking->table->name }}</td>
<td class="px-4 py-2">{{ \Carbon\Carbon::parse($booking->start_time)->format('H:i') }} -
{{ \Carbon\Carbon::parse($booking->end_time)->format('H:i') }}</td>
<td class="px-4 py-2 capitalize">{{ $booking->status }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
</div>
@endsection

View File

@ -0,0 +1,34 @@
@extends('layouts.admin')
@section('content')
<div class="p-4">
<h1 class="text-2xl font-bold mb-4">Edit Meja: {{ $table->name }}</h1>
<form action="{{ route('admin.tables.update', $table->id) }}" method="POST" class="space-y-4">
@csrf
@method('PUT')
<div>
<label class="block text-sm font-medium text-gray-700">Nama Meja</label>
<input type="text" name="name" value="{{ $table->name }}" class="w-full border border-gray-300 p-2 rounded">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Merek</label>
<input type="text" name="brand" value="{{ $table->brand }}"
class="w-full border border-gray-300 p-2 rounded">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Status</label>
<select name="status" class="w-full border border-gray-300 p-2 rounded">
<option value="Available" {{ $table->status === 'Available' ? 'selected' : '' }}>Available</option>
<option value="Booked" {{ $table->status === 'Booked' ? 'selected' : '' }}>Booked</option>
<option value="Unavailable" {{ $table->status === 'Unavailable' ? 'selected' : '' }}>Unavailable</option>
</select>
</div>
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded">Simpan</button>
</form>
</div>
@endsection

View File

@ -0,0 +1,48 @@
@extends('layouts.admin')
@section('content')
<div class="p-4">
<h1 class="text-2xl font-bold mb-4">Kelola Meja</h1>
<div class="overflow-x-auto bg-white shadow rounded-lg">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-100">
<tr>
<th class="px-4 py-2 text-left font-semibold text-gray-600">Nama Meja</th>
<th class="px-4 py-2 text-left font-semibold text-gray-600">Merek</th>
<th class="px-4 py-2 text-left font-semibold text-gray-600">Status</th>
<th class="px-4 py-2 text-left font-semibold text-gray-600">Aksi</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@forelse ($tables as $table)
<tr class="hover:bg-gray-50">
<td class="px-4 py-2">{{ $table->name }}</td>
<td class="px-4 py-2">{{ $table->brand }}</td>
<td class="px-4 py-2">
<span class="px-2 py-1 rounded-full text-xs font-medium
{{ $table->status === 'Available' ? 'bg-green-100 text-green-800' :
($table->status === 'Booked' ? 'bg-yellow-100 text-yellow-800' :
'bg-red-100 text-red-800') }}">
{{ $table->status }}
</span>
</td>
<td class="px-4 py-2">
<a href="{{ route('admin.tables.edit', $table->id) }}"
class="text-blue-600 hover:underline">Edit</a>
</td>
</tr>
@empty
<tr>
<td colspan="4" class="px-4 py-4 text-center text-gray-500">Belum ada data meja.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-4">
{{ $tables->links() }}
</div>
</div>
@endsection

View File

@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Panel - {{ config('app.name') }}</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body x-data="{ sidebarOpen: true }" class="flex">
<!-- Sidebar -->
<div :class="sidebarOpen ? 'w-64' : 'w-16'" class="bg-white border-r h-screen transition-all duration-300">
<div class="flex justify-between items-center p-4">
<div class="flex items-center space-x-2">
<span class="font-bold text-lg" x-show="sidebarOpen">Admin Panel</span>
</div>
<button @click="sidebarOpen = !sidebarOpen" class="text-gray-600 focus:outline-none">
<svg x-show="sidebarOpen" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" 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>
<svg x-show="!sidebarOpen" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
<!-- Navigation -->
<nav class="flex flex-col justify-between h-full mt-4">
<ul>
<li><a href="{{ route('admin.dashboard') }}" class="block px-4 py-2 hover:bg-gray-100">📊 <span
x-show="sidebarOpen">Dashboard</span></a></li>
<li><a href="{{ route('admin.tables.index') }}" class="block px-4 py-2 hover:bg-gray-100">🪑 <span
x-show="sidebarOpen">Kelola
Meja</span></a></li>
<li><a href="{{ route('admin.bookings.index') }}" class="block px-4 py-2 hover:bg-gray-100">📅 <span
x-show="sidebarOpen">Daftar
Booking</span></a></li>
<li><a href="#" class="block px-4 py-2 hover:bg-gray-100">👥 <span x-show="sidebarOpen">Data
User</span></a></li>
<li><a href="#" class="block px-4 py-2 hover:bg-gray-100">🔔 <span
x-show="sidebarOpen">Notifikasi</span></a></li>
<li><a href="#" class="block px-4 py-2 hover:bg-gray-100">⚙️ <span
x-show="sidebarOpen">Pengaturan</span></a></li>
<li>
<div class="relative mt-4 px-4" x-data="{ open: false }">
<button @click="open = !open" class="flex items-center w-full text-left">
<span class="truncate" x-show="sidebarOpen">{{ auth()->user()->name }}</span>
<svg x-show="sidebarOpen" class="ml-1 h-4 w-4" fill="none" stroke="currentColor"
stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</button>
<div x-show="open" @click.outside="open = false"
class="absolute left-4 mt-2 w-48 bg-white border rounded shadow-md z-10" x-cloak>
<a href="#" class="block px-4 py-2 text-sm hover:bg-gray-100">Edit Profil</a>
<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>
</li>
</ul>
</nav>
<!-- User Dropdown -->
</div>
<!-- Main content -->
<div class="flex-1 p-6 bg-gray-50 min-h-screen">
@yield('content')
</div>
</body>
</html>

View File

@ -5,6 +5,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Document</title> <title>Document</title>
@vite('resources/css/app.css') @vite('resources/css/app.css')
{{-- Font | Google Fonts --}} {{-- Font | Google Fonts --}}
@ -148,11 +149,38 @@ class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 rounded">Daftar</but
<main class="pt-20"> <main class="pt-20">
@if(Auth::check()) @if (session('success') || session('error'))
<p>Halo, {{ Auth::user()->name }}</p> <div id="floating-alert" style="
@else position: fixed;
<p>Kamu belum login</p> top: 30px;
left: 50%;
transform: translateX(-50%);
background-color: {{ session('success') ? '#d1e7dd' : '#f8d7da' }};
color: {{ session('success') ? '#0f5132' : '#842029' }};
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
box-shadow: 0 3px 10px rgba(0,0,0,0.15);
z-index: 9999;
max-width: 300px;
text-align: center;
">
{{ session('success') ?? session('error') }}
</div>
<script>
setTimeout(() => {
const alert = document.getElementById('floating-alert');
if (alert) {
alert.style.transition = 'opacity 0.5s ease';
alert.style.opacity = '0';
setTimeout(() => alert.remove(), 500);
}
}, 3000);
</script>
@endif @endif
@yield('content') @yield('content')
</main> </main>

View File

@ -26,7 +26,7 @@ class="flex items-center bg-[url('/public/images/map.jpg')] bg-cover bg-center p
</div> </div>
</div> </div>
@foreach ($venue['tables'] as $table) @foreach ($venue['tables'] as $table)
<div x-data="{ open: false }" class="border rounded-lg shadow-md p-4 mb-4"> <div x-data="booking(@json(auth()->check()))" class="border rounded-lg shadow-md p-4 mb-4">
<div class="flex items-center justify-between cursor-pointer" @click="open = !open"> <div class="flex items-center justify-between cursor-pointer" @click="open = !open">
<div class="flex items-center"> <div class="flex items-center">
<img src="{{ asset('images/meja.jpg') }}" class="w-24"> <img src="{{ asset('images/meja.jpg') }}" class="w-24">
@ -47,37 +47,130 @@ class="flex items-center bg-[url('/public/images/map.jpg')] bg-cover bg-center p
<div x-show="open" x-collapse class="mt-4 p-4 border-t bg-gray-100 rounded-lg"> <div x-show="open" x-collapse class="mt-4 p-4 border-t bg-gray-100 rounded-lg">
<h4 class="font-semibold mb-2">Pilih Jam Booking:</h4> <h4 class="font-semibold mb-2">Pilih Jam Booking:</h4>
<select class="w-full border p-2 rounded-lg"> <select class="w-full border p-2 rounded-lg" x-model="selectedTime">
<option>10:00</option> <option value="">-- Pilih Jam --</option>
<option>11:00</option> <template x-for="hour in getHoursInRange(9, 22)" :key="hour">
<option>12:00</option> <option :value="hour + ':00'" x-text="hour + ':00'"></option>
<option>13:00</option> </template>
</select> </select>
<button class="mt-3 px-4 py-2 bg-green-500 text-white rounded-lg w-full">Confirm Booking</button>
<h4 class="font-semibold mb-2 mt-4">Pilih Durasi Main:</h4>
<select class="w-full border p-2 rounded-lg" x-model="selectedDuration">
<option value="">-- Pilih Durasi --</option>
<option value="1">1 Jam</option>
<option value="2">2 Jam</option>
<option value="3">3 Jam</option>
</select>
<button class="mt-3 px-4 py-2 bg-green-500 text-white rounded-lg w-full" :disabled="!selectedTime || !selectedDuration || isLoading"
@click="submitBooking('{{ $table['id'] }}', '{{ addslashes($table['name']) }}')">
<template x-if="isLoading">
<span>Loading...</span>
</template>
<template x-if="!isLoading">
<span>Confirm Booking</span>
</template>
</button>
</div> </div>
</div> </div>
@endforeach @endforeach
</div> </div>
</div> </div>
{{-- {{ dd($venue['location']) }} --}}
<script> <script>
function updateClock() { function updateClock() {
const now = new Date(); const now = new Date();
// Konversi ke WIB (GMT+7)
const options = { timeZone: 'Asia/Jakarta', hour12: false }; const options = { timeZone: 'Asia/Jakarta', hour12: false };
const timeFormatter = new Intl.DateTimeFormat('id-ID', { ...options, hour: '2-digit', minute: '2-digit', second: '2-digit' }); const timeFormatter = new Intl.DateTimeFormat('id-ID', { ...options, hour: '2-digit', minute: '2-digit', second: '2-digit' });
document.getElementById('realTimeClock').textContent = timeFormatter.format(now); document.getElementById('realTimeClock').textContent = timeFormatter.format(now);
} }
// Update setiap detik
setInterval(updateClock, 1000); setInterval(updateClock, 1000);
updateClock(); // Panggil sekali untuk langsung tampil updateClock();
document.addEventListener('alpine:init', () => {
Alpine.data('booking', (isLoggedIn) => ({
isLoggedIn,
open: false,
selectedTime: '',
selectedDuration: '',
isLoading: false,
getHoursInRange(startHour, endHour) {
let hours = [];
for (let i = startHour; i <= endHour; i++) {
hours.push(i);
}
return hours;
},
submitBooking(tableId, tableName) {
if (!this.isLoggedIn) {
alert('Silahkan login terlebih dahulu untuk melakukan booking.');
}
const selectedTime = this.selectedTime;
const selectedDuration = this.selectedDuration;
if (!selectedTime || !selectedDuration) {
alert('Please select both time and duration');
return;
}
// Validasi jam
const now = new Date();
const selectedDateTime = new Date();
const [selectedHour, selectedMinute] = selectedTime.split(':').map(Number);
selectedDateTime.setHours(selectedHour, selectedMinute, 0, 0);
if (selectedDateTime <= now) {
alert('Jam yang dipilih sudah lewat. Silakan pilih jam yang masih tersedia.');
return;
}
this.isLoading = true;
// Hitung end time
const bookingStart = new Date();
bookingStart.setHours(selectedHour, selectedMinute, 0, 0);
const bookingEnd = new Date(bookingStart);
bookingEnd.setHours(bookingEnd.getHours() + parseInt(selectedDuration));
const endTimeFormatted = ('0' + bookingEnd.getHours()).slice(-2) + ':' + ('0' + bookingEnd.getMinutes()).slice(-2);
const today = new Date().toISOString().split('T')[0];
const start_time = `${today} ${selectedTime}`;
const end_time = `${today} ${endTimeFormatted}`;
// Kirim ke backend
fetch('/booking', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
},
body: JSON.stringify({
table_id: tableId,
start_time: start_time,
end_time: end_time,
}),
})
.then(res => {
if (res.status === 409) throw new Error('Meja sudah dibooking.');
return res.json();
})
.then(data => {
alert(`Booking ${tableName} berhasil! Meja akan diblokir dari ${selectedTime} hingga ${endTimeFormatted}`);
location.reload(); // Reload untuk update status meja
})
.catch(err => {
alert('Gagal booking: ' + err.message);
})
.finally(() => {
this.isLoading = false;
});
}
}))
})
</script> </script>
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
@endsection @endsection

View File

@ -2,9 +2,23 @@
use App\Http\Controllers\pages\HomeController; use App\Http\Controllers\pages\HomeController;
use App\Http\Controllers\pages\VenueController; use App\Http\Controllers\pages\VenueController;
use App\Http\Controllers\pages\BookingController;
use App\Http\Controllers\admin\BookingsController;
use App\Http\Controllers\admin\TableController;
use App\Http\Controllers\admin\AdminController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
Auth::routes(); Auth::routes();
Route::get('/', [HomeController::class, "index"])->name('index'); Route::get('/home', [HomeController::class, "index"])->name('index');
Route::get('/venue/{venueName}', [VenueController::class, "venue"])->name('venue'); Route::get('/venue/{venueName}', [VenueController::class, "venue"])->name('venue');
Route::post('/booking', [BookingController::class, 'store'])->name('booking.store');
Route::middleware(['auth', '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('/tables', [TableController::class, 'kelolaMeja'])->name('admin.tables.index');
Route::get('/tables/{id}/edit', [TableController::class, 'editTable'])->name('admin.tables.edit');
Route::put('/tables/{id}', [TableController::class, 'updateTable'])->name('admin.tables.update');
});