# π Rule Web β Sistem Pembelian Tiket & Booking Tempat
## Pemandian Air Panas Banyu Biru Nganjuk
---
## π§± Tech Stack
| Komponen | Teknologi |
|---|---|
| Framework | Laravel 11 |
| Database | MySQL |
| Templating | Blade Templating (layouts, partials) |
| Styling | Bootstrap 5 (CDN) + Custom CSS |
| Icons | Font Awesome 6 (CDN) |
| Alert/Notifikasi | SweetAlert2 (CDN) |
| Tabel Admin | DataTables (CDN) |
| Grafik Dashboard | Chart.js (CDN) |
| QR Code Generate | `simplesoftwareio/simple-qrcode` |
| QR Code Scan | `instascan.js` atau `html5-qrcode` |
| PDF Generate | `barryvdh/laravel-dompdf` |
| Permission | `spatie/laravel-permission` |
| File Upload | Laravel Storage (drag & drop) |
---
## π¦ Paket Composer yang Dibutuhkan
```bash
composer require spatie/laravel-permission
composer require barryvdh/laravel-dompdf
composer require simplesoftwareio/simple-qrcode
```
---
## ποΈ Struktur Database & Relasi
### Tabel: `users`
```sql
id BIGINT PK AUTO_INCREMENT
name VARCHAR(100)
email VARCHAR(150) UNIQUE
password VARCHAR(255)
phone VARCHAR(20) NULLABLE
address TEXT NULLABLE
avatar VARCHAR(255) NULLABLE
email_verified_at TIMESTAMP NULLABLE
remember_token VARCHAR(100) NULLABLE
created_at TIMESTAMP
updated_at TIMESTAMP
```
### Tabel: `roles` (spatie)
```sql
id BIGINT PK
name VARCHAR(125) -- 'admin' | 'user'
guard_name VARCHAR(125)
created_at TIMESTAMP
updated_at TIMESTAMP
```
### Tabel: `model_has_roles` (spatie)
```sql
role_id BIGINT FK -> roles.id
model_type VARCHAR(255)
model_id BIGINT
```
### Tabel: `tickets`
```sql
id BIGINT PK AUTO_INCREMENT
name VARCHAR(100) -- Nama tiket (Dewasa, Anak-anak, dll)
description TEXT NULLABLE
price DECIMAL(10,2)
quota_per_day INT DEFAULT 100 -- Kuota harian
is_active BOOLEAN DEFAULT 1
created_at TIMESTAMP
updated_at TIMESTAMP
```
### Tabel: `ticket_orders`
```sql
id BIGINT PK AUTO_INCREMENT
user_id BIGINT FK -> users.id
order_code VARCHAR(20) UNIQUE -- Format: AT-XXXXX (random 5 digit)
visit_date DATE
total_qty INT
total_price DECIMAL(10,2)
payment_proof VARCHAR(255) NULLABLE
status ENUM('pending','confirmed','rejected') DEFAULT 'pending'
confirmed_at TIMESTAMP NULLABLE
confirmed_by BIGINT FK -> users.id NULLABLE
rejection_note TEXT NULLABLE
created_at TIMESTAMP
updated_at TIMESTAMP
```
### Tabel: `ticket_order_items`
```sql
id BIGINT PK AUTO_INCREMENT
ticket_order_id BIGINT FK -> ticket_orders.id
ticket_id BIGINT FK -> tickets.id
ticket_code VARCHAR(20) UNIQUE -- Format: AT-XXXXX per tiket individual
qr_code_path VARCHAR(255) -- Path file QR code image
qty INT
price DECIMAL(10,2) -- Harga saat beli (snapshot)
is_used BOOLEAN DEFAULT 0
used_at TIMESTAMP NULLABLE
created_at TIMESTAMP
updated_at TIMESTAMP
```
### Tabel: `places`
```sql
id BIGINT PK AUTO_INCREMENT
name VARCHAR(100) -- Nama kolam/tempat
description TEXT NULLABLE
capacity INT -- Kapasitas per booking
price_per_day DECIMAL(10,2)
image VARCHAR(255) NULLABLE
is_active BOOLEAN DEFAULT 1
created_at TIMESTAMP
updated_at TIMESTAMP
```
### Tabel: `bookings`
```sql
id BIGINT PK AUTO_INCREMENT
user_id BIGINT FK -> users.id
place_id BIGINT FK -> places.id
booking_code VARCHAR(20) UNIQUE -- Format: AB-XXXXX
booking_date DATE -- Tanggal booking (UNIQUE per place_id)
visitor_name VARCHAR(100)
visitor_phone VARCHAR(20)
visitor_address TEXT
num_persons INT
total_price DECIMAL(10,2)
payment_proof VARCHAR(255) NULLABLE
status ENUM('pending','confirmed','rejected') DEFAULT 'pending'
confirmed_at TIMESTAMP NULLABLE
confirmed_by BIGINT FK -> users.id NULLABLE
rejection_note TEXT NULLABLE
notes TEXT NULLABLE
created_at TIMESTAMP
updated_at TIMESTAMP
```
> **Constraint**: `UNIQUE(place_id, booking_date)` β satu tempat hanya bisa dibooking 1x per tanggal.
### Tabel: `bank_accounts`
```sql
id BIGINT PK AUTO_INCREMENT
bank_name VARCHAR(100) -- BRI, BNI, Mandiri, dll
account_number VARCHAR(50)
account_name VARCHAR(100)
is_active BOOLEAN DEFAULT 1
created_at TIMESTAMP
updated_at TIMESTAMP
```
### Tabel: `settings`
```sql
id BIGINT PK AUTO_INCREMENT
key VARCHAR(100) UNIQUE
value TEXT
created_at TIMESTAMP
updated_at TIMESTAMP
```
> Contoh key: `site_name`, `site_logo`, `ticket_note`, `booking_note`
---
## ποΈ Struktur Direktori Proyek
```
resources/
βββ views/
βββ layouts/
β βββ app.blade.php # Layout utama user (navbar, footer)
β βββ admin.blade.php # Layout admin (sidebar, topbar)
β βββ auth.blade.php # Layout halaman auth (login/register)
βββ partials/
β βββ navbar.blade.php
β βββ footer.blade.php
β βββ admin/
β β βββ sidebar.blade.php
β β βββ topbar.blade.php
β βββ alerts.blade.php # SweetAlert global
βββ auth/
β βββ login.blade.php
β βββ register.blade.php
βββ user/
β βββ dashboard.blade.php
β βββ profile.blade.php
β βββ tickets/
β β βββ index.blade.php # Pembelian tiket
β β βββ payment.blade.php # Input bukti bayar
β β βββ download.blade.php # Download tiket PDF
β βββ bookings/
β βββ index.blade.php # Form booking tempat
β βββ payment.blade.php # Input bukti bayar booking
β βββ status.blade.php # Status booking
βββ admin/
β βββ dashboard.blade.php
β βββ tickets/
β β βββ index.blade.php
β β βββ create.blade.php
β β βββ edit.blade.php
β βββ ticket-orders/
β β βββ index.blade.php
β β βββ show.blade.php
β βββ bookings/
β β βββ index.blade.php
β β βββ show.blade.php
β βββ admins/
β β βββ index.blade.php
β β βββ create.blade.php
β β βββ edit.blade.php
β βββ profile.blade.php
β βββ check/
β βββ ticket.blade.php # Check tiket (scan QR / input kode)
β βββ booking.blade.php # Check booking (input kode)
βββ pdf/
βββ ticket.blade.php # Template PDF tiket
```
---
## π Layouts & @stack Convention
### `layouts/app.blade.php` (User)
```html
@yield('title', 'Banyu Biru') - Pemandian Air Panas
@stack('styles')
@include('partials.navbar')
@yield('content')
@include('partials.footer')
@include('partials.alerts')
@stack('scripts')
```
### `layouts/admin.blade.php` (Admin)
```html
...CDN Links...
@stack('styles')
@include('partials.admin.sidebar')
@include('partials.admin.topbar')
@yield('content')
...CDN Scripts...
@stack('scripts')
```
### Penggunaan di halaman child:
```blade
@extends('layouts.admin')
@section('title', 'Dashboard Admin')
@push('styles')
@endpush
@section('content')
@endsection
@push('scripts')
@endpush
```
---
## π£οΈ Routes
```php
// routes/web.php
// βββ AUTH ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Route::middleware('guest')->group(function () {
Route::get('/login', [AuthController::class, 'showLogin'])->name('login');
Route::post('/login', [AuthController::class, 'login']);
Route::get('/register', [AuthController::class, 'showRegister'])->name('register');
Route::post('/register',[AuthController::class, 'register']);
});
Route::post('/logout', [AuthController::class, 'logout'])->name('logout')->middleware('auth');
// βββ USER βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Route::middleware(['auth', 'role:user'])->prefix('')->name('user.')->group(function () {
// Tiket
Route::get('/tiket', [TicketController::class, 'index'])->name('tickets.index');
Route::post('/tiket/order', [TicketController::class, 'order'])->name('tickets.order');
Route::get('/tiket/payment/{code}', [TicketController::class, 'payment'])->name('tickets.payment');
Route::post('/tiket/upload/{code}', [TicketController::class, 'uploadProof'])->name('tickets.upload');
Route::get('/tiket/download/{code}', [TicketController::class, 'download'])->name('tickets.download');
Route::get('/tiket/pdf/{itemId}', [TicketController::class, 'downloadPdf'])->name('tickets.pdf');
// Booking
Route::get('/booking', [BookingController::class, 'index'])->name('bookings.index');
Route::post('/booking/store', [BookingController::class, 'store'])->name('bookings.store');
Route::get('/booking/payment/{code}', [BookingController::class, 'payment'])->name('bookings.payment');
Route::post('/booking/upload/{code}', [BookingController::class, 'uploadProof'])->name('bookings.upload');
Route::get('/booking/status/{code}', [BookingController::class, 'status'])->name('bookings.status');
// API: cek ketersediaan tanggal booking
Route::get('/booking/check-date', [BookingController::class, 'checkDate'])->name('bookings.checkDate');
// Profil
Route::get('/profil', [ProfileController::class, 'index'])->name('profile.index');
Route::put('/profil', [ProfileController::class, 'update'])->name('profile.update');
});
// βββ ADMIN ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Route::middleware(['auth', 'role:admin'])->prefix('admin')->name('admin.')->group(function () {
Route::get('/dashboard', [Admin\DashboardController::class, 'index'])->name('dashboard');
// Manajemen Tiket
Route::resource('tickets', Admin\TicketController::class);
// Manajemen Penjualan Tiket
Route::get('ticket-orders', [Admin\TicketOrderController::class, 'index'])->name('ticket-orders.index');
Route::get('ticket-orders/{id}', [Admin\TicketOrderController::class, 'show'])->name('ticket-orders.show');
Route::post('ticket-orders/{id}/confirm', [Admin\TicketOrderController::class, 'confirm'])->name('ticket-orders.confirm');
Route::post('ticket-orders/{id}/reject', [Admin\TicketOrderController::class, 'reject'])->name('ticket-orders.reject');
// Manajemen Booking Tempat
Route::get('bookings', [Admin\BookingController::class, 'index'])->name('bookings.index');
Route::get('bookings/{id}', [Admin\BookingController::class, 'show'])->name('bookings.show');
Route::post('bookings/{id}/confirm', [Admin\BookingController::class, 'confirm'])->name('bookings.confirm');
Route::post('bookings/{id}/reject', [Admin\BookingController::class, 'reject'])->name('bookings.reject');
// Manajemen Admin
Route::resource('admins', Admin\AdminController::class)->except('show');
// Profil Admin
Route::get('profil', [Admin\ProfileController::class, 'index'])->name('profile.index');
Route::put('profil', [Admin\ProfileController::class, 'update'])->name('profile.update');
// Check Tiket & Booking
Route::get('check/ticket', [Admin\CheckController::class, 'ticketIndex'])->name('check.ticket');
Route::post('check/ticket', [Admin\CheckController::class, 'checkTicket'])->name('check.ticket.check');
Route::get('check/booking', [Admin\CheckController::class, 'bookingIndex'])->name('check.booking');
Route::post('check/booking', [Admin\CheckController::class, 'checkBooking'])->name('check.booking.check');
// Bank Account (opsional)
Route::resource('bank-accounts', Admin\BankAccountController::class)->except('show');
});
```
---
## π Alur Fitur
### β
Alur Pembelian Tiket
```
1. [GET] /tiket
β Halaman form pembelian tiket
β User isi: Nama, Email, No HP, Tanggal Kunjungan
β Pilih jenis tiket + quantity (tombol - dan +)
β Klik "Beli Sekarang"
β Jika belum login β SweetAlert "Silakan login terlebih dahulu"
2. [POST] /tiket/order
β Simpan ticket_orders + ticket_order_items
β Generate kode unik AT-XXXXX per item tiket
β Generate QR Code per item (simpan di storage)
β Redirect ke halaman payment
3. [GET] /tiket/payment/{order_code}
β Tampilkan total pembayaran
β Tampilkan nomor rekening + tombol salin (icon copy)
β Drag & drop upload bukti transfer (file input area)
4. [POST] /tiket/upload/{order_code}
β Upload payment_proof ke storage
β Update status order β 'pending'
β Redirect ke halaman download tiket
5. [GET] /tiket/download/{order_code}
β Tampilkan peringatan download segera (SweetAlert / Alert Bootstrap)
β Tampilkan card tiket per item (qty tiket = jumlah card)
β Setiap card: info tiket + tombol "Download PDF"
6. [GET] /tiket/pdf/{item_id}
β Generate PDF tiket (dompdf)
β Berisi: nama, tanggal kunjungan, jenis tiket, kode tiket, QR Code
```
### β
Alur Booking Tempat
```
1. [GET] /booking
β Form booking: Nama, No HP, Alamat, Pilih Tempat, Tanggal Booking, Jumlah Orang, Catatan
β Saat pilih tanggal β AJAX cek ketersediaan tanggal (/booking/check-date)
β Jika tanggal sudah dipakai β tampilkan SweetAlert "Tanggal sudah dibooking, pilih tanggal lain"
β Jika belum login β SweetAlert "Silakan login terlebih dahulu"
2. [POST] /booking/store
β Simpan booking ke tabel bookings
β Generate kode unik AB-XXXXX
β status = 'pending'
β Redirect ke halaman payment booking
3. [GET] /booking/payment/{booking_code}
β Tampilkan total pembayaran
β Tampilkan nomor rekening + tombol salin (icon copy)
β Drag & drop upload bukti transfer
4. [POST] /booking/upload/{booking_code}
β Upload payment_proof
β Redirect ke halaman status booking
5. [GET] /booking/status/{booking_code}
β Tampilkan status booking: pending / dikonfirmasi / ditolak
β Badge warna sesuai status
β Jika ditolak, tampilkan alasan penolakan
```
---
## π¨ UI/UX Rules
### Global
- Font: **Inter** atau **Poppins** dari Google Fonts
- Warna utama: `#1a6b4a` (hijau air / alam) + `#f0a500` (kuning aksen)
- Radius card: `12px`, shadow soft `box-shadow: 0 4px 20px rgba(0,0,0,0.08)`
- Semua notifikasi/konfirmasi/alert menggunakan **SweetAlert2**
- Tombol aksi menggunakan class Bootstrap + custom CSS gradient
### Komponen Quantity Tiket
```html
```
### Komponen Salin Nomor Rekening
```html
{{ $bank->bank_name }} β a/n {{ $bank->account_name }}
{{ $bank->account_number }}
```
### Komponen Drag & Drop Upload
```html
```
### Status Badge
```html
@switch($status)
@case('pending')
Menunggu
@break
@case('confirmed')
Dikonfirmasi
@break
@case('rejected')
Ditolak
@break
@endswitch
```
---
## π Autentikasi & Permission (Spatie)
### Setup
```php
// Seeder: DatabaseSeeder.php
Role::create(['name' => 'admin']);
Role::create(['name' => 'user']);
// Admin default
$admin = User::create([...]);
$admin->assignRole('admin');
```
### Middleware
```php
// Lindungi route admin
Route::middleware(['auth', 'role:admin'])
// Lindungi route user
Route::middleware(['auth', 'role:user'])
```
### Register (hanya untuk user)
```php
// AuthController@register
$user = User::create([...]);
$user->assignRole('user'); // selalu role user
```
### Redirect setelah login
```php
// AuthController@login
if ($user->hasRole('admin')) {
return redirect()->route('admin.dashboard');
}
return redirect()->route('user.tickets.index');
```
### Jika belum login (middleware redirect + SweetAlert)
```php
// Middleware atau di controller
if (!auth()->check()) {
session()->flash('swal_login_required', true);
return redirect()->route('login');
}
```
```blade
{{-- partials/alerts.blade.php --}}
@if(session('swal_login_required'))
@endif
```
---
## π₯οΈ Admin Dashboard
### Widget Statistik
```
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β Total Tiket β β Penjualan Tiket β β Total Booking β β Pendapatan Totalβ
β Terjual β β (bulan ini) β β (bulan ini) β β (bulan ini) β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
```
### Grafik (Chart.js)
1. **Line Chart** β Penjualan tiket per bulan (12 bulan terakhir)
2. **Bar Chart** β Booking tempat per bulan (12 bulan terakhir)
3. **Doughnut Chart** β Perbandingan status pesanan (pending/confirmed/rejected)
```javascript
// @push('scripts') di admin/dashboard.blade.php
const ctx = document.getElementById('salesChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: @json($months),
datasets: [{
label: 'Tiket Terjual',
data: @json($ticketData),
borderColor: '#1a6b4a',
backgroundColor: 'rgba(26,107,74,0.1)',
tension: 0.4,
fill: true
}]
},
options: { responsive: true, plugins: { legend: { position: 'top' } } }
});
```
---
## π Admin Tables (DataTables + Pagination)
### Contoh implementasi di setiap tabel admin:
```blade
{{-- @push('scripts') --}}
```
Semua tabel admin menggunakan:
- `id="tableXxx"` β diinisialisasi DataTables
- Fitur search bawaan DataTables
- Pagination otomatis DataTables
- Bahasa Indonesia (`i18n/id.json`)
---
## π« Template PDF Tiket
```blade
{{-- resources/views/pdf/ticket.blade.php --}}
BANYU BIRU
{!! QrCode::size(150)->generate($item->ticket_code) !!}
{{ $item->ticket_code }}
Nama Pemesan
{{ $order->user->name }}
Jenis Tiket
{{ $item->ticket->name }}
Tanggal Kunjungan
{{ \Carbon\Carbon::parse($order->visit_date)->isoFormat('dddd, D MMMM Y') }}
Harga
Rp {{ number_format($item->price, 0, ',', '.') }}
Tunjukkan tiket ini kepada petugas. Tiket hanya berlaku 1x.
```
---
## π Check Tiket & Booking (Admin)
### Check Tiket (`/admin/check/ticket`)
- Input manual kode format `AT-XXXXX`
- **ATAU** scan QR Code menggunakan kamera (`html5-qrcode`)
- Jika ditemukan β tampilkan detail tiket + status (valid/sudah dipakai)
- Konfirmasi "Tandai sebagai dipakai" β SweetAlert confirm β update `is_used = 1`
### Check Booking (`/admin/check/booking`)
- Input manual kode format `AB-XXXXX`
- Jika ditemukan β tampilkan detail booking + status
- Admin bisa konfirmasi/tolak dari halaman ini
### Format Kode Generator
```php
// Helper function
function generateUniqueCode(string $prefix, string $model): string
{
do {
$code = $prefix . '-' . strtoupper(substr(str_shuffle('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 5));
} while ($model::where('order_code', $code)->exists()
|| $model::where('ticket_code', $code)->exists());
return $code;
}
// Contoh: AT-A2B9C, AB-X7Y3Z
```
---
## π§ͺ SweetAlert Conventions
```javascript
// Konfirmasi aksi
Swal.fire({
title: 'Konfirmasi',
text: 'Apakah Anda yakin?',
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Ya, Lanjutkan',
cancelButtonText: 'Batal',
confirmButtonColor: '#1a6b4a',
cancelButtonColor: '#6c757d'
})
// Sukses
Swal.fire({ icon: 'success', title: 'Berhasil!', text: '...', timer: 2500, showConfirmButton: false })
// Error
Swal.fire({ icon: 'error', title: 'Gagal!', text: '...' })
// Toast notifikasi salin
Swal.fire({
toast: true, position: 'top-end', icon: 'success',
title: 'Nomor rekening berhasil disalin!',
showConfirmButton: false, timer: 2000
})
// Warning login
Swal.fire({ icon: 'warning', title: 'Login Diperlukan', text: 'Silakan login terlebih dahulu.' })
// Warning tanggal sudah dibooking
Swal.fire({ icon: 'warning', title: 'Tanggal Tidak Tersedia', text: 'Tanggal ini sudah dibooking. Silakan pilih tanggal lain.' })
```
---
## π Struktur Controller
```
app/Http/Controllers/
βββ AuthController.php
βββ User/
β βββ TicketController.php
β βββ BookingController.php
β βββ ProfileController.php
βββ Admin/
βββ DashboardController.php
βββ TicketController.php
βββ TicketOrderController.php
βββ BookingController.php
βββ AdminController.php
βββ ProfileController.php
βββ CheckController.php
```
---
## π± Seeder & Default Data
```php
// database/seeders/DatabaseSeeder.php
public function run(): void
{
// Roles
Role::create(['name' => 'admin', 'guard_name' => 'web']);
Role::create(['name' => 'user', 'guard_name' => 'web']);
// Admin default
$admin = User::create([
'name' => 'Administrator',
'email' => 'admin@banyubiru.com',
'password' => Hash::make('password'),
]);
$admin->assignRole('admin');
// Tiket default
Ticket::insert([
['name' => 'Dewasa', 'price' => 15000, 'quota_per_day' => 200],
]);
// Tempat default
Place::insert([
['name' => 'Pendopo', 'price_per_day' => 500000, 'capacity' => 50],
]);
// Bank account default
BankAccount::create([
'bank_name' => 'BRI',
'account_number' => '1234567890',
'account_name' => 'Banyu Biru Nganjuk',
'is_active' => 1,
]);
}
```
---
## βοΈ Environment & Config
```env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=wisata_web
DB_USERNAME=root
DB_PASSWORD=
```
```php
// config/app.php
'locale' => 'id',
'timezone' => 'Asia/Jakarta',
```
---
## π Catatan Penting
1. **Unique constraint booking**: `UNIQUE(place_id, booking_date)` diterapkan di database dan dicek ulang di Controller sebelum insert.
2. **QR Code** dibuat saat order dibuat (bukan saat download), disimpan di `storage/app/public/qrcodes/`.
3. **PDF Tiket** dibuat on-demand saat user klik "Download" menggunakan `dompdf`.
4. **File upload bukti transfer** disimpan di `storage/app/public/payment-proofs/` dan bisa diakses admin.
5. **Pagination DataTables** sepenuhnya client-side (data sudah di-load ke halaman). Untuk data sangat besar bisa beralih ke server-side DataTables.
6. **Role admin tidak bisa diregister** β hanya dibuat melalui seeder atau panel Manajemen Admin (oleh admin yang sudah ada).
7. **Salin nomor rekening** menggunakan `navigator.clipboard.writeText()` dengan fallback `document.execCommand('copy')`.
8. Seluruh **konfirmasi destruktif** (tolak, hapus, dll) harus melewati SweetAlert confirm dialog terlebih dahulu.